key_input.go

v0.6.1
Doc Versions Source
1
package tui
2
3
import (
4
	"fmt"
5
	"regexp"
6
	"strings"
7
8
	tea "github.com/charmbracelet/bubbletea"
9
	"github.com/charmbracelet/lipgloss"
10
	"sourcecraft.dev/bigbes/claudio/provider"
11
)
12
13
// SanitizeAPIKey extracts just the API key value if the input contains YAML-like content.
14
// This handles cases where the user accidentally pastes config content like:
15
// "zai: api_key: xxx model: yyy"
16
func SanitizeAPIKey(input string) string {
17
	input = strings.TrimSpace(input)
18
	if input == "" {
19
		return ""
20
	}
21
22
	// If input contains "api_key:" pattern, extract just the key value
23
	if strings.Contains(input, "api_key:") {
24
		// Pattern: anything followed by "api_key:" then the actual key
25
		re := regexp.MustCompile(`api_key:\s*(\S+)`)
26
		matches := re.FindStringSubmatch(input)
27
		if len(matches) > 1 {
28
			return strings.TrimSpace(matches[1])
29
		}
30
	}
31
32
	// Remove common YAML prefixes that might have been accidentally included
33
	input = regexp.MustCompile(`^\w+:\s*`).ReplaceAllString(input, "")
34
35
	return strings.TrimSpace(input)
36
}
37
38
func (m Model) updateInput(msg tea.Msg) (tea.Model, tea.Cmd) {
39
	switch msg := msg.(type) {
40
	case tea.KeyMsg:
41
		switch msg.String() {
42
		case "ctrl+c":
43
			m.quitting = true
44
			return m, tea.Quit
45
		case "esc":
46
			m.phase = phaseSelect
47
			return m, nil
48
		case "enter":
49
			rawKey := strings.TrimSpace(m.input.Value())
50
			// Sanitize: if the key contains YAML-like content, extract just the key
51
			key := SanitizeAPIKey(rawKey)
52
			if key == "" {
53
				return m, nil
54
			}
55
			// Warn if we had to sanitize (indicates possible copy-paste error)
56
			if key != rawKey {
57
				m.statusMsg = "Note: API key was auto-corrected"
58
			}
59
			m.cfg.ActiveProvider = m.selected
60
			pc := m.cfg.Providers[m.selected]
61
			pc.APIKey = key
62
			m.cfg.Providers[m.selected] = pc
63
			// For zai variants, save the API key to both zai and zai-coding.
64
			if m.selected == "zai" || m.selected == "zai-coding" {
65
				otherKey := "zai"
66
				if m.selected == "zai" {
67
					otherKey = "zai-coding"
68
				}
69
				otherPC := m.cfg.Providers[otherKey]
70
				otherPC.APIKey = key
71
				m.cfg.Providers[otherKey] = otherPC
72
			}
73
			// For kimi variants, save the API key to all variants.
74
			if m.selected == "kimi" || m.selected == "kimi-api-cn" || m.selected == "kimi-api-intl" {
75
				for _, kimiKey := range []string{"kimi", "kimi-api-cn", "kimi-api-intl"} {
76
					if kimiKey != m.selected {
77
						kimiPC := m.cfg.Providers[kimiKey]
78
						kimiPC.APIKey = key
79
						m.cfg.Providers[kimiKey] = kimiPC
80
					}
81
				}
82
			}
83
			// For openrouter, show model picker after entering key.
84
			if m.selected == "openrouter" {
85
				return m.startFetch(openrouterFetcher{apiKey: key})
86
			}
87
			// For nvidia, show model picker after entering key.
88
			if m.selected == "nvidia" {
89
				return m.startFetch(nvidiaFetcher{apiKey: key})
90
			}
91
			// For mistral, show model picker after entering key.
92
			if m.selected == "mistral" {
93
				return m.startFetch(mistralFetcher{apiKey: key})
94
			}
95
			// For ollama-cloud, show model picker after entering key.
96
			if m.selected == "ollama-cloud" {
97
				reg := provider.Registry[m.selected]
98
				return m.startFetch(ollamaFetcher{apiKey: key, baseURL: reg.BaseURL, provKey: m.selected})
99
			}
100
			// For kimi variants, show model picker after entering key.
101
			if m.selected == "kimi" || m.selected == "kimi-api-cn" || m.selected == "kimi-api-intl" {
102
				reg := provider.Registry[m.selected]
103
				return m.startFetch(kimiFetcher{apiKey: key, baseURL: reg.BaseURL, provKey: m.selected})
104
			}
105
			// For providers with plans, show plan picker after entering key.
106
			if reg, ok := provider.Registry[m.selected]; ok && len(reg.Plans) > 0 {
107
				return m.showPlanPicker(reg.Plans), nil
108
			}
109
			// For providers with static model lists, show model picker after entering key.
110
			if models, ok := provider.StaticModels[m.selected]; ok {
111
				return m.showStaticModelPicker(models), nil
112
			}
113
			m.phase = phaseDone
114
			m.launchAfterConfig = true
115
			return m, tea.Quit
116
		}
117
	}
118
119
	var cmd tea.Cmd
120
	m.input, cmd = m.input.Update(msg)
121
	return m, cmd
122
}
123
124
// selectedEntryName returns the display name for the currently selected entry,
125
// accounting for toggle state.
126
func (m Model) selectedEntryName() string {
127
	if m.cursor >= len(m.entries) {
128
		return ""
129
	}
130
	e := m.entries[m.cursor]
131
	if e.Key == "ollama" && e.AltKey != "" {
132
		if m.ollamaCloud {
133
			return "Ollama (cloud)"
134
		}
135
		return "Ollama (local)"
136
	}
137
	if e.Key == "zai" && e.AltKey != "" {
138
		if m.zaiCoding {
139
			return "z.AI (coding)"
140
		}
141
		return "z.AI (api)"
142
	}
143
	if e.Key == "kimi" && len(e.AltKeys) == 2 {
144
		switch m.kimiVariant {
145
		case 0:
146
			return "Kimi (code)"
147
		case 1:
148
			return "Kimi (api-cn)"
149
		case 2:
150
			return "Kimi (api-intl)"
151
		}
152
	}
153
	return e.Name
154
}
155
156
func (m Model) viewInput() string {
157
	var b strings.Builder
158
	b.WriteString(logoStyle.Render(logo))
159
	b.WriteString("\n\n")
160
	b.WriteString(promptStyle.Render(fmt.Sprintf(" API Key for %s", m.selectedEntryName())))
161
	b.WriteString("\n\n ")
162
	b.WriteString(m.input.View())
163
	b.WriteString("\n\n")
164
	b.WriteString(hintStyle.Render(" enter confirm • esc back"))
165
166
	content := b.String()
167
	if m.width > 0 && m.height > 0 {
168
		return lipgloss.Place(m.width, m.height, lipgloss.Center, lipgloss.Center, content)
169
	}
170
	return content
171
}
172

Source Files