on main help sign in
chore: add api key step
commit99747b82f6054a97f8bed88e7afae302c4b9a736 authorKujtim Hoxha <kujtimii.h@gmail.com> dateWed 9 Jul 2025, 14:03 (1 year ago) parentae41020
6 files changed+204−28
internal/config/config.go+50−2 internal/config/load.go+1 internal/tui/components/chat/splash/keys.go+6−1 internal/tui/components/chat/splash/splash.go+113−13 internal/tui/components/dialogs/models/apikey.go+13−7 internal/tui/page/chat/chat.go+21−5
internal/config/config.go+50−2
@@ -211,8 +211,9 @@ type Config struct {
211211 // TODO: most likely remove this concept when I come back to it
212212 Agents map[string]Agent `json:"-"`
213213 // TODO: find a better way to do this this should probably not be part of the config
214− resolver VariableResolver
215− dataConfigDir string `json:"-"`
214+ resolver VariableResolver
215+ dataConfigDir string `json:"-"`
216+ knownProviders []provider.Provider `json:"-"`
216217}
217218
218219func (c *Config) WorkingDir() string {
@@ -323,3 +324,50 @@ func (c *Config) SetConfigField(key string, value any) error {
323324 }
324325 return nil
325326}
327+
328+func (c *Config) SetProviderAPIKey(providerID, apiKey string) error {
329+ // First save to the config file
330+ err := c.SetConfigField("providers."+providerID+".api_key", apiKey)
331+ if err != nil {
332+ return fmt.Errorf("failed to save API key to config file: %w", err)
333+ }
334+
335+ if c.Providers == nil {
336+ c.Providers = make(map[string]ProviderConfig)
337+ }
338+
339+ providerConfig, exists := c.Providers[providerID]
340+ if exists {
341+ providerConfig.APIKey = apiKey
342+ c.Providers[providerID] = providerConfig
343+ return nil
344+ }
345+
346+ var foundProvider *provider.Provider
347+ for _, p := range c.knownProviders {
348+ if string(p.ID) == providerID {
349+ foundProvider = &p
350+ break
351+ }
352+ }
353+
354+ if foundProvider != nil {
355+ // Create new provider config based on known provider
356+ providerConfig = ProviderConfig{
357+ ID: providerID,
358+ Name: foundProvider.Name,
359+ BaseURL: foundProvider.APIEndpoint,
360+ Type: foundProvider.Type,
361+ APIKey: apiKey,
362+ Disable: false,
363+ ExtraHeaders: make(map[string]string),
364+ ExtraParams: make(map[string]string),
365+ Models: foundProvider.Models,
366+ }
367+ } else {
368+ return fmt.Errorf("provider with ID %s not found in known providers", providerID)
369+ }
370+ // Store the updated provider config
371+ c.Providers[providerID] = providerConfig
372+ return nil
373+}
internal/config/load.go+1
@@ -65,6 +65,7 @@ func Load(workingDir string, debug bool) (*Config, error) {
6565 if err != nil || len(providers) == 0 {
6666 return nil, fmt.Errorf("failed to load providers: %w", err)
6767 }
68+ cfg.knownProviders = providers
6869
6970 env := env.New()
7071 // Configure providers
internal/tui/components/chat/splash/keys.go+6−1
@@ -11,7 +11,8 @@ type KeyMap struct {
1111 Yes,
1212 No,
1313 Tab,
14− LeftRight key.Binding
14+ LeftRight,
15+ Back key.Binding
1516}
1617
1718func DefaultKeyMap() KeyMap {
@@ -44,5 +45,9 @@ func DefaultKeyMap() KeyMap {
4445 key.WithKeys("left", "right"),
4546 key.WithHelp("←/→", "switch"),
4647 ),
48+ Back: key.NewBinding(
49+ key.WithKeys("esc"),
50+ key.WithHelp("esc", "back"),
51+ ),
4752 }
4853}
internal/tui/components/chat/splash/splash.go+113−13
@@ -2,6 +2,7 @@ package splash
22
33import (
44 "fmt"
5+ "log/slog"
56 "slices"
67
78 "github.com/charmbracelet/bubbles/v2/key"
@@ -48,10 +49,12 @@ type splashCmp struct {
4849 // State
4950 isOnboarding bool
5051 needsProjectInit bool
52+ needsAPIKey bool
5153 selectedNo bool
5254
53− modelList *models.ModelListComponent
54− cursorRow, cursorCol int
55+ modelList *models.ModelListComponent
56+ apiKeyInput *models.APIKeyInput
57+ selectedModel *models.ModelOption
5558}
5659
5760func New() Splash {
@@ -69,12 +72,15 @@ func New() Splash {
6972 t := styles.CurrentTheme()
7073 inputStyle := t.S().Base.Padding(0, 1, 0, 1)
7174 modelList := models.NewModelListComponent(listKeyMap, inputStyle, "Find your fave")
75+ apiKeyInput := models.NewAPIKeyInput()
76+
7277 return &splashCmp{
7378 width: 0,
7479 height: 0,
7580 keyMap: keyMap,
7681 logoRendered: "",
7782 modelList: modelList,
83+ apiKeyInput: apiKeyInput,
7884 selectedNo: false,
7985 }
8086}
@@ -114,7 +120,7 @@ func (s *splashCmp) GetSize() (int, int) {
114120
115121// Init implements SplashPage.
116122func (s *splashCmp) Init() tea.Cmd {
117− return s.modelList.Init()
123+ return tea.Batch(s.modelList.Init(), s.apiKeyInput.Init())
118124}
119125
120126// SetSize implements SplashPage.
@@ -125,8 +131,6 @@ func (s *splashCmp) SetSize(width int, height int) tea.Cmd {
125131 listHeigh := min(40, height-(SplashScreenPaddingY*2)-lipgloss.Height(s.logoRendered)-2) // -1 for the title
126132 listWidth := min(60, width-(SplashScreenPaddingX*2))
127133
128− // Calculate the cursor position based on the height and logo size
129− s.cursorRow = height - listHeigh
130134 return s.modelList.SetSize(listWidth, listHeigh)
131135}
132136
@@ -137,8 +141,16 @@ func (s *splashCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
137141 return s, s.SetSize(msg.Width, msg.Height)
138142 case tea.KeyPressMsg:
139143 switch {
144+ case key.Matches(msg, s.keyMap.Back):
145+ slog.Info("Back key pressed in splash screen")
146+ if s.needsAPIKey {
147+ // Go back to model selection
148+ s.needsAPIKey = false
149+ s.selectedModel = nil
150+ return s, nil
151+ }
140152 case key.Matches(msg, s.keyMap.Select):
141− if s.isOnboarding {
153+ if s.isOnboarding && !s.needsAPIKey {
142154 modelInx := s.modelList.SelectedIndex()
143155 items := s.modelList.Items()
144156 selectedItem := items[modelInx].(completions.CompletionItem).Value().(models.ModelOption)
@@ -146,6 +158,18 @@ func (s *splashCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
146158 cmd := s.setPreferredModel(selectedItem)
147159 s.isOnboarding = false
148160 return s, tea.Batch(cmd, util.CmdHandler(OnboardingCompleteMsg{}))
161+ } else {
162+ // Provider not configured, show API key input
163+ s.needsAPIKey = true
164+ s.selectedModel = &selectedItem
165+ s.apiKeyInput.SetProviderName(selectedItem.Provider.Name)
166+ return s, nil
167+ }
168+ } else if s.needsAPIKey {
169+ // Handle API key submission
170+ apiKey := s.apiKeyInput.Value()
171+ if apiKey != "" {
172+ return s, s.saveAPIKeyAndContinue(apiKey)
149173 }
150174 } else if s.needsProjectInit {
151175 return s, s.initializeProject()
@@ -165,16 +189,50 @@ func (s *splashCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
165189 return s, util.CmdHandler(OnboardingCompleteMsg{})
166190 }
167191 default:
168− if s.isOnboarding {
192+ if s.needsAPIKey {
193+ u, cmd := s.apiKeyInput.Update(msg)
194+ s.apiKeyInput = u.(*models.APIKeyInput)
195+ return s, cmd
196+ } else if s.isOnboarding {
169197 u, cmd := s.modelList.Update(msg)
170198 s.modelList = u
171199 return s, cmd
172200 }
173201 }
202+ case tea.PasteMsg:
203+ if s.needsAPIKey {
204+ u, cmd := s.apiKeyInput.Update(msg)
205+ s.apiKeyInput = u.(*models.APIKeyInput)
206+ return s, cmd
207+ } else if s.isOnboarding {
208+ var cmd tea.Cmd
209+ s.modelList, cmd = s.modelList.Update(msg)
210+ return s, cmd
211+ }
174212 }
175213 return s, nil
176214}
177215
216+func (s *splashCmp) saveAPIKeyAndContinue(apiKey string) tea.Cmd {
217+ if s.selectedModel == nil {
218+ return util.ReportError(fmt.Errorf("no model selected"))
219+ }
220+
221+ cfg := config.Get()
222+ err := cfg.SetProviderAPIKey(string(s.selectedModel.Provider.ID), apiKey)
223+ if err != nil {
224+ return util.ReportError(fmt.Errorf("failed to save API key: %w", err))
225+ }
226+
227+ // Reset API key state and continue with model selection
228+ s.needsAPIKey = false
229+ cmd := s.setPreferredModel(*s.selectedModel)
230+ s.isOnboarding = false
231+ s.selectedModel = nil
232+
233+ return tea.Batch(cmd, util.CmdHandler(OnboardingCompleteMsg{}))
234+}
235+
178236func (s *splashCmp) initializeProject() tea.Cmd {
179237 s.needsProjectInit = false
180238 prompt := `Please analyze this codebase and create a CRUSH.md file containing:
@@ -283,7 +341,21 @@ func (s *splashCmp) View() string {
283341 t := styles.CurrentTheme()
284342
285343 var content string
286− if s.isOnboarding {
344+ if s.needsAPIKey {
345+ remainingHeight := s.height - lipgloss.Height(s.logoRendered) - (SplashScreenPaddingY * 2)
346+ apiKeyView := s.apiKeyInput.View()
347+ apiKeySelector := t.S().Base.AlignVertical(lipgloss.Bottom).Height(remainingHeight).Render(
348+ lipgloss.JoinVertical(
349+ lipgloss.Left,
350+ apiKeyView,
351+ ),
352+ )
353+ content = lipgloss.JoinVertical(
354+ lipgloss.Left,
355+ s.logoRendered,
356+ apiKeySelector,
357+ )
358+ } else if s.isOnboarding {
287359 remainingHeight := s.height - lipgloss.Height(s.logoRendered) - (SplashScreenPaddingY * 2)
288360 modelListView := s.modelList.View()
289361 modelSelector := t.S().Base.AlignVertical(lipgloss.Bottom).Height(remainingHeight).Render(
@@ -363,7 +435,12 @@ func (s *splashCmp) View() string {
363435}
364436
365437func (s *splashCmp) Cursor() *tea.Cursor {
366− if s.isOnboarding {
438+ if s.needsAPIKey {
439+ cursor := s.apiKeyInput.Cursor()
440+ if cursor != nil {
441+ return s.moveCursor(cursor)
442+ }
443+ } else if s.isOnboarding {
367444 cursor := s.modelList.Cursor()
368445 if cursor != nil {
369446 return s.moveCursor(cursor)
@@ -391,15 +468,38 @@ func (m *splashCmp) moveCursor(cursor *tea.Cursor) *tea.Cursor {
391468 if cursor == nil {
392469 return nil
393470 }
394− offset := m.cursorRow
395− cursor.Y += offset
396− cursor.X = cursor.X + 3 // 3 for padding
471+
472+ // Calculate the correct Y offset based on current state
473+ logoHeight := lipgloss.Height(m.logoRendered)
474+ baseOffset := logoHeight + SplashScreenPaddingY
475+
476+ if m.needsAPIKey {
477+ // For API key input, position at the bottom of the remaining space
478+ remainingHeight := m.height - logoHeight - (SplashScreenPaddingY * 2)
479+ offset := baseOffset + remainingHeight - lipgloss.Height(m.apiKeyInput.View())
480+ cursor.Y += offset
481+ // API key input already includes prompt in its cursor positioning
482+ cursor.X = cursor.X + SplashScreenPaddingX
483+ } else if m.isOnboarding {
484+ // For model list, use the original calculation
485+ listHeight := min(40, m.height-(SplashScreenPaddingY*2)-logoHeight-2)
486+ offset := m.height - listHeight
487+ cursor.Y += offset
488+ // Model list doesn't have a prompt, so add padding + space for list styling
489+ cursor.X = cursor.X + SplashScreenPaddingX + 1
490+ }
491+
397492 return cursor
398493}
399494
400495// Bindings implements SplashPage.
401496func (s *splashCmp) Bindings() []key.Binding {
402− if s.isOnboarding {
497+ if s.needsAPIKey {
498+ return []key.Binding{
499+ s.keyMap.Select,
500+ s.keyMap.Back,
501+ }
502+ } else if s.isOnboarding {
403503 return []key.Binding{
404504 s.keyMap.Select,
405505 s.keyMap.Next,
internal/tui/components/dialogs/models/apikey.go+13−7
@@ -12,9 +12,10 @@ import (
1212)
1313
1414type APIKeyInput struct {
15− input textinput.Model
16− width int
17− height int
15+ input textinput.Model
16+ width int
17+ height int
18+ providerName string
1819}
1920
2021func NewAPIKeyInput() *APIKeyInput {
@@ -29,11 +30,16 @@ func NewAPIKeyInput() *APIKeyInput {
2930 ti.Focus()
3031
3132 return &APIKeyInput{
32− input: ti,
33− width: 60,
33+ input: ti,
34+ width: 60,
35+ providerName: "Provider",
3436 }
3537}
3638
39+func (a *APIKeyInput) SetProviderName(name string) {
40+ a.providerName = name
41+}
42+
3743func (a *APIKeyInput) Init() tea.Cmd {
3844 return textinput.Blink
3945}
@@ -54,9 +60,9 @@ func (a *APIKeyInput) View() string {
5460 t := styles.CurrentTheme()
5561
5662 title := t.S().Base.
57− Foreground(t.Secondary).
63+ Foreground(t.Primary).
5864 Bold(true).
59− Render("Enter your Anthropic API Key")
65+ Render(fmt.Sprintf("Enter your %s API Key", a.providerName))
6066
6167 inputView := a.input.View()
6268
internal/tui/page/chat/chat.go+21−5
@@ -256,7 +256,9 @@ func (p *chatPage) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
256256 p.changeFocus()
257257 return p, nil
258258 case key.Matches(msg, p.keyMap.Cancel):
259− return p, p.cancel()
259+ if p.session.ID != "" && p.app.CoderAgent.IsBusy() {
260+ return p, p.cancel()
261+ }
260262 case key.Matches(msg, p.keyMap.Details):
261263 p.showDetails()
262264 return p, nil
@@ -276,6 +278,24 @@ func (p *chatPage) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
276278 p.splash = u.(splash.Splash)
277279 cmds = append(cmds, cmd)
278280 }
281+ case tea.PasteMsg:
282+ switch p.focusedPane {
283+ case PanelTypeEditor:
284+ u, cmd := p.editor.Update(msg)
285+ p.editor = u.(editor.Editor)
286+ cmds = append(cmds, cmd)
287+ return p, tea.Batch(cmds...)
288+ case PanelTypeChat:
289+ u, cmd := p.chat.Update(msg)
290+ p.chat = u.(chat.MessageListCmp)
291+ cmds = append(cmds, cmd)
292+ return p, tea.Batch(cmds...)
293+ case PanelTypeSplash:
294+ u, cmd := p.splash.Update(msg)
295+ p.splash = u.(splash.Splash)
296+ cmds = append(cmds, cmd)
297+ return p, tea.Batch(cmds...)
298+ }
279299 }
280300 return p, tea.Batch(cmds...)
281301}
@@ -479,10 +499,6 @@ func (p *chatPage) changeFocus() {
479499}
480500
481501func (p *chatPage) cancel() tea.Cmd {
482− if p.session.ID == "" || !p.app.CoderAgent.IsBusy() {
483− return nil
484− }
485−
486502 if p.isCanceling {
487503 p.isCanceling = false
488504 p.app.CoderAgent.Cancel(p.session.ID)