on main help sign in
chore: add api key step
99747b8Kujtim Hoxha
internal/config/config.go+50−2
@@ -211,8 +211,9 @@ type Config struct {
211 // TODO: most likely remove this concept when I come back to it
211 // TODO: most likely remove this concept when I come back to it
212 Agents map[string]Agent `json:"-"`
212 Agents map[string]Agent `json:"-"`
213 // TODO: find a better way to do this this should probably not be part of the config
213 // TODO: find a better way to do this this should probably not be part of the config
214− resolver VariableResolver
214+ resolver VariableResolver
215− dataConfigDir string `json:"-"`
215+ dataConfigDir string `json:"-"`
216+ knownProviders []provider.Provider `json:"-"`
216}
217}
217
218
218func (c *Config) WorkingDir() string {
219func (c *Config) WorkingDir() string {
@@ -323,3 +324,50 @@ func (c *Config) SetConfigField(key string, value any) error {
323 }
324 }
324 return nil
325 return nil
325}
326}
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) {
65 if err != nil || len(providers) == 0 {
65 if err != nil || len(providers) == 0 {
66 return nil, fmt.Errorf("failed to load providers: %w", err)
66 return nil, fmt.Errorf("failed to load providers: %w", err)
67 }
67 }
68+ cfg.knownProviders = providers
68
69
69 env := env.New()
70 env := env.New()
70 // Configure providers
71 // Configure providers
internal/tui/components/chat/splash/keys.go+6−1
@@ -11,7 +11,8 @@ type KeyMap struct {
11 Yes,
11 Yes,
12 No,
12 No,
13 Tab,
13 Tab,
14− LeftRight key.Binding
14+ LeftRight,
15+ Back key.Binding
15}
16}
16
17
17func DefaultKeyMap() KeyMap {
18func DefaultKeyMap() KeyMap {
@@ -44,5 +45,9 @@ func DefaultKeyMap() KeyMap {
44 key.WithKeys("left", "right"),
45 key.WithKeys("left", "right"),
45 key.WithHelp("←/→", "switch"),
46 key.WithHelp("←/→", "switch"),
46 ),
47 ),
48+ Back: key.NewBinding(
49+ key.WithKeys("esc"),
50+ key.WithHelp("esc", "back"),
51+ ),
47 }
52 }
48}
53}
internal/tui/components/chat/splash/splash.go+113−13
@@ -2,6 +2,7 @@ package splash
2
2
3import (
3import (
4 "fmt"
4 "fmt"
5+ "log/slog"
5 "slices"
6 "slices"
6
7
7 "github.com/charmbracelet/bubbles/v2/key"
8 "github.com/charmbracelet/bubbles/v2/key"
@@ -48,10 +49,12 @@ type splashCmp struct {
48 // State
49 // State
49 isOnboarding bool
50 isOnboarding bool
50 needsProjectInit bool
51 needsProjectInit bool
52+ needsAPIKey bool
51 selectedNo bool
53 selectedNo bool
52
54
53− modelList *models.ModelListComponent
55+ modelList *models.ModelListComponent
54− cursorRow, cursorCol int
56+ apiKeyInput *models.APIKeyInput
57+ selectedModel *models.ModelOption
55}
58}
56
59
57func New() Splash {
60func New() Splash {
@@ -69,12 +72,15 @@ func New() Splash {
69 t := styles.CurrentTheme()
72 t := styles.CurrentTheme()
70 inputStyle := t.S().Base.Padding(0, 1, 0, 1)
73 inputStyle := t.S().Base.Padding(0, 1, 0, 1)
71 modelList := models.NewModelListComponent(listKeyMap, inputStyle, "Find your fave")
74 modelList := models.NewModelListComponent(listKeyMap, inputStyle, "Find your fave")
75+ apiKeyInput := models.NewAPIKeyInput()
76+
72 return &splashCmp{
77 return &splashCmp{
73 width: 0,
78 width: 0,
74 height: 0,
79 height: 0,
75 keyMap: keyMap,
80 keyMap: keyMap,
76 logoRendered: "",
81 logoRendered: "",
77 modelList: modelList,
82 modelList: modelList,
83+ apiKeyInput: apiKeyInput,
78 selectedNo: false,
84 selectedNo: false,
79 }
85 }
80}
86}
@@ -114,7 +120,7 @@ func (s *splashCmp) GetSize() (int, int) {
114
120
115// Init implements SplashPage.
121// Init implements SplashPage.
116func (s *splashCmp) Init() tea.Cmd {
122func (s *splashCmp) Init() tea.Cmd {
117− return s.modelList.Init()
123+ return tea.Batch(s.modelList.Init(), s.apiKeyInput.Init())
118}
124}
119
125
120// SetSize implements SplashPage.
126// SetSize implements SplashPage.
@@ -125,8 +131,6 @@ func (s *splashCmp) SetSize(width int, height int) tea.Cmd {
125 listHeigh := min(40, height-(SplashScreenPaddingY*2)-lipgloss.Height(s.logoRendered)-2) // -1 for the title
131 listHeigh := min(40, height-(SplashScreenPaddingY*2)-lipgloss.Height(s.logoRendered)-2) // -1 for the title
126 listWidth := min(60, width-(SplashScreenPaddingX*2))
132 listWidth := min(60, width-(SplashScreenPaddingX*2))
127
133
128− // Calculate the cursor position based on the height and logo size
129− s.cursorRow = height - listHeigh
130 return s.modelList.SetSize(listWidth, listHeigh)
134 return s.modelList.SetSize(listWidth, listHeigh)
131}
135}
132
136
@@ -137,8 +141,16 @@ func (s *splashCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
137 return s, s.SetSize(msg.Width, msg.Height)
141 return s, s.SetSize(msg.Width, msg.Height)
138 case tea.KeyPressMsg:
142 case tea.KeyPressMsg:
139 switch {
143 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+ }
140 case key.Matches(msg, s.keyMap.Select):
152 case key.Matches(msg, s.keyMap.Select):
141− if s.isOnboarding {
153+ if s.isOnboarding && !s.needsAPIKey {
142 modelInx := s.modelList.SelectedIndex()
154 modelInx := s.modelList.SelectedIndex()
143 items := s.modelList.Items()
155 items := s.modelList.Items()
144 selectedItem := items[modelInx].(completions.CompletionItem).Value().(models.ModelOption)
156 selectedItem := items[modelInx].(completions.CompletionItem).Value().(models.ModelOption)
@@ -146,6 +158,18 @@ func (s *splashCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
146 cmd := s.setPreferredModel(selectedItem)
158 cmd := s.setPreferredModel(selectedItem)
147 s.isOnboarding = false
159 s.isOnboarding = false
148 return s, tea.Batch(cmd, util.CmdHandler(OnboardingCompleteMsg{}))
160 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)
149 }
173 }
150 } else if s.needsProjectInit {
174 } else if s.needsProjectInit {
151 return s, s.initializeProject()
175 return s, s.initializeProject()
@@ -165,16 +189,50 @@ func (s *splashCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
165 return s, util.CmdHandler(OnboardingCompleteMsg{})
189 return s, util.CmdHandler(OnboardingCompleteMsg{})
166 }
190 }
167 default:
191 default:
192+ if s.needsAPIKey {
193+ u, cmd := s.apiKeyInput.Update(msg)
194+ s.apiKeyInput = u.(*models.APIKeyInput)
195+ return s, cmd
168− if s.isOnboarding {
196+ } else if s.isOnboarding {
169 u, cmd := s.modelList.Update(msg)
197 u, cmd := s.modelList.Update(msg)
170 s.modelList = u
198 s.modelList = u
171 return s, cmd
199 return s, cmd
172 }
200 }
173 }
201 }
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+ }
174 }
212 }
175 return s, nil
213 return s, nil
176}
214}
177
215
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+
178func (s *splashCmp) initializeProject() tea.Cmd {
236func (s *splashCmp) initializeProject() tea.Cmd {
179 s.needsProjectInit = false
237 s.needsProjectInit = false
180 prompt := `Please analyze this codebase and create a CRUSH.md file containing:
238 prompt := `Please analyze this codebase and create a CRUSH.md file containing:
@@ -283,7 +341,21 @@ func (s *splashCmp) View() string {
283 t := styles.CurrentTheme()
341 t := styles.CurrentTheme()
284
342
285 var content string
343 var content string
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+ )
286− if s.isOnboarding {
358+ } else if s.isOnboarding {
287 remainingHeight := s.height - lipgloss.Height(s.logoRendered) - (SplashScreenPaddingY * 2)
359 remainingHeight := s.height - lipgloss.Height(s.logoRendered) - (SplashScreenPaddingY * 2)
288 modelListView := s.modelList.View()
360 modelListView := s.modelList.View()
289 modelSelector := t.S().Base.AlignVertical(lipgloss.Bottom).Height(remainingHeight).Render(
361 modelSelector := t.S().Base.AlignVertical(lipgloss.Bottom).Height(remainingHeight).Render(
@@ -363,7 +435,12 @@ func (s *splashCmp) View() string {
363}
435}
364
436
365func (s *splashCmp) Cursor() *tea.Cursor {
437func (s *splashCmp) Cursor() *tea.Cursor {
438+ if s.needsAPIKey {
439+ cursor := s.apiKeyInput.Cursor()
440+ if cursor != nil {
441+ return s.moveCursor(cursor)
442+ }
366− if s.isOnboarding {
443+ } else if s.isOnboarding {
367 cursor := s.modelList.Cursor()
444 cursor := s.modelList.Cursor()
368 if cursor != nil {
445 if cursor != nil {
369 return s.moveCursor(cursor)
446 return s.moveCursor(cursor)
@@ -391,15 +468,38 @@ func (m *splashCmp) moveCursor(cursor *tea.Cursor) *tea.Cursor {
391 if cursor == nil {
468 if cursor == nil {
392 return nil
469 return nil
393 }
470 }
394− offset := m.cursorRow
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())
395− cursor.Y += offset
480+ cursor.Y += offset
481+ // API key input already includes prompt in its cursor positioning
396− cursor.X = cursor.X + 3 // 3 for padding
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+
397 return cursor
492 return cursor
398}
493}
399
494
400// Bindings implements SplashPage.
495// Bindings implements SplashPage.
401func (s *splashCmp) Bindings() []key.Binding {
496func (s *splashCmp) Bindings() []key.Binding {
497+ if s.needsAPIKey {
498+ return []key.Binding{
499+ s.keyMap.Select,
500+ s.keyMap.Back,
501+ }
402− if s.isOnboarding {
502+ } else if s.isOnboarding {
403 return []key.Binding{
503 return []key.Binding{
404 s.keyMap.Select,
504 s.keyMap.Select,
405 s.keyMap.Next,
505 s.keyMap.Next,
internal/tui/components/dialogs/models/apikey.go+13−7
@@ -12,9 +12,10 @@ import (
12)
12)
13
13
14type APIKeyInput struct {
14type APIKeyInput struct {
15− input textinput.Model
15+ input textinput.Model
16− width int
16+ width int
17− height int
17+ height int
18+ providerName string
18}
19}
19
20
20func NewAPIKeyInput() *APIKeyInput {
21func NewAPIKeyInput() *APIKeyInput {
@@ -29,11 +30,16 @@ func NewAPIKeyInput() *APIKeyInput {
29 ti.Focus()
30 ti.Focus()
30
31
31 return &APIKeyInput{
32 return &APIKeyInput{
32− input: ti,
33+ input: ti,
33− width: 60,
34+ width: 60,
35+ providerName: "Provider",
34 }
36 }
35}
37}
36
38
39+func (a *APIKeyInput) SetProviderName(name string) {
40+ a.providerName = name
41+}
42+
37func (a *APIKeyInput) Init() tea.Cmd {
43func (a *APIKeyInput) Init() tea.Cmd {
38 return textinput.Blink
44 return textinput.Blink
39}
45}
@@ -54,9 +60,9 @@ func (a *APIKeyInput) View() string {
54 t := styles.CurrentTheme()
60 t := styles.CurrentTheme()
55
61
56 title := t.S().Base.
62 title := t.S().Base.
57− Foreground(t.Secondary).
63+ Foreground(t.Primary).
58 Bold(true).
64 Bold(true).
59− Render("Enter your Anthropic API Key")
65+ Render(fmt.Sprintf("Enter your %s API Key", a.providerName))
60
66
61 inputView := a.input.View()
67 inputView := a.input.View()
62
68
internal/tui/page/chat/chat.go+21−5
@@ -256,7 +256,9 @@ func (p *chatPage) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
256 p.changeFocus()
256 p.changeFocus()
257 return p, nil
257 return p, nil
258 case key.Matches(msg, p.keyMap.Cancel):
258 case key.Matches(msg, p.keyMap.Cancel):
259+ if p.session.ID != "" && p.app.CoderAgent.IsBusy() {
259− return p, p.cancel()
260+ return p, p.cancel()
261+ }
260 case key.Matches(msg, p.keyMap.Details):
262 case key.Matches(msg, p.keyMap.Details):
261 p.showDetails()
263 p.showDetails()
262 return p, nil
264 return p, nil
@@ -276,6 +278,24 @@ func (p *chatPage) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
276 p.splash = u.(splash.Splash)
278 p.splash = u.(splash.Splash)
277 cmds = append(cmds, cmd)
279 cmds = append(cmds, cmd)
278 }
280 }
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+ }
279 }
299 }
280 return p, tea.Batch(cmds...)
300 return p, tea.Batch(cmds...)
281}
301}
@@ -479,10 +499,6 @@ func (p *chatPage) changeFocus() {
479}
499}
480
500
481func (p *chatPage) cancel() tea.Cmd {
501func (p *chatPage) cancel() tea.Cmd {
482− if p.session.ID == "" || !p.app.CoderAgent.IsBusy() {
483− return nil
484− }
485−
486 if p.isCanceling {
502 if p.isCanceling {
487 p.isCanceling = false
503 p.isCanceling = false
488 p.app.CoderAgent.Cancel(p.session.ID)
504 p.app.CoderAgent.Cancel(p.session.ID)