key_input.go

v0.7.0
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 API variants, show model picker via live fetch.
101
			// Kimi Code (key "kimi") has no listable models endpoint on its
102
			// Anthropic gateway, so it falls through to the static model picker.
103
			if m.selected == "kimi-api-cn" || m.selected == "kimi-api-intl" {
104
				reg := provider.Registry[m.selected]
105
				return m.startFetch(kimiFetcher{apiKey: key, baseURL: reg.BaseURL, provKey: m.selected})
106
			}
107
			// For providers with plans, show plan picker after entering key.
108
			if reg, ok := provider.Registry[m.selected]; ok && len(reg.Plans) > 0 {
109
				return m.showPlanPicker(reg.Plans), nil
110
			}
111
			// For providers with static model lists, show model picker after entering key.
112
			if models, ok := provider.StaticModels[m.selected]; ok {
113
				return m.showStaticModelPicker(models), nil
114
			}
115
			m.phase = phaseDone
116
			m.launchAfterConfig = true
117
			return m, tea.Quit
118
		}
119
	}
120
121
	var cmd tea.Cmd
122
	m.input, cmd = m.input.Update(msg)
123
	return m, cmd
124
}
125
126
// selectedEntryName returns the display name for the currently selected entry,
127
// accounting for toggle state.
128
func (m Model) selectedEntryName() string {
129
	if m.cursor >= len(m.entries) {
130
		return ""
131
	}
132
	e := m.entries[m.cursor]
133
	if e.Key == "ollama" && e.AltKey != "" {
134
		if m.ollamaCloud {
135
			return "Ollama (cloud)"
136
		}
137
		return "Ollama (local)"
138
	}
139
	if e.Key == "zai" && e.AltKey != "" {
140
		if m.zaiCoding {
141
			return "z.AI (coding)"
142
		}
143
		return "z.AI (api)"
144
	}
145
	if e.Key == "kimi" && len(e.AltKeys) == 2 {
146
		switch m.kimiVariant {
147
		case 0:
148
			return "Kimi (code)"
149
		case 1:
150
			return "Kimi (api-cn)"
151
		case 2:
152
			return "Kimi (api-intl)"
153
		}
154
	}
155
	return e.Name
156
}
157
158
func (m Model) viewInput() string {
159
	var b strings.Builder
160
	b.WriteString(logoStyle.Render(logo))
161
	b.WriteString("\n\n")
162
	b.WriteString(promptStyle.Render(fmt.Sprintf(" API Key for %s", m.selectedEntryName())))
163
	b.WriteString("\n\n ")
164
	b.WriteString(m.input.View())
165
	b.WriteString("\n\n")
166
	b.WriteString(hintStyle.Render(" enter confirm • esc back"))
167
168
	content := b.String()
169
	if m.width > 0 && m.height > 0 {
170
		return lipgloss.Place(m.width, m.height, lipgloss.Center, lipgloss.Center, content)
171
	}
172
	return content
173
}
174

Source Files