1package splash
2
3import (
4 "fmt"
5 "strings"
6 "time"
7
8 "github.com/charmbracelet/bubbles/v2/key"
9 "github.com/charmbracelet/bubbles/v2/spinner"
10 tea "github.com/charmbracelet/bubbletea/v2"
11 "github.com/charmbracelet/catwalk/pkg/catwalk"
12 "github.com/charmbracelet/crush/internal/client"
13 "github.com/charmbracelet/crush/internal/config"
14 "github.com/charmbracelet/crush/internal/home"
15 "github.com/charmbracelet/crush/internal/llm/prompt"
16 "github.com/charmbracelet/crush/internal/tui/components/chat"
17 "github.com/charmbracelet/crush/internal/tui/components/core"
18 "github.com/charmbracelet/crush/internal/tui/components/core/layout"
19 "github.com/charmbracelet/crush/internal/tui/components/dialogs/models"
20 "github.com/charmbracelet/crush/internal/tui/components/logo"
21 lspcomponent "github.com/charmbracelet/crush/internal/tui/components/lsp"
22 "github.com/charmbracelet/crush/internal/tui/components/mcp"
23 "github.com/charmbracelet/crush/internal/tui/exp/list"
24 "github.com/charmbracelet/crush/internal/tui/styles"
25 "github.com/charmbracelet/crush/internal/tui/util"
26 "github.com/charmbracelet/crush/internal/version"
27 "github.com/charmbracelet/lipgloss/v2"
28)
29
30type Splash interface {
31 util.Model
32 layout.Sizeable
33 layout.Help
34 Cursor() *tea.Cursor
35 // SetOnboarding controls whether the splash shows model selection UI
36 SetOnboarding(bool)
37 // SetProjectInit controls whether the splash shows project initialization prompt
38 SetProjectInit(bool)
39
40 // Showing API key input
41 IsShowingAPIKey() bool
42
43 // IsAPIKeyValid returns whether the API key is valid
44 IsAPIKeyValid() bool
45}
46
47const (
48 SplashScreenPaddingY = 1 // Padding Y for the splash screen
49
50 LogoGap = 6
51)
52
53// OnboardingCompleteMsg is sent when onboarding is complete
54type (
55 OnboardingCompleteMsg struct{}
56 SubmitAPIKeyMsg struct{}
57)
58
59type splashCmp struct {
60 width, height int
61 keyMap KeyMap
62 logoRendered string
63
64 // State
65 isOnboarding bool
66 needsProjectInit bool
67 needsAPIKey bool
68 selectedNo bool
69
70 listHeight int
71 modelList *models.ModelListComponent
72 apiKeyInput *models.APIKeyInput
73 selectedModel *models.ModelOption
74 isAPIKeyValid bool
75 apiKeyValue string
76
77 client *client.Client
78 cfg *config.Config
79}
80
81func New(c *client.Client, cfg *config.Config) Splash {
82 keyMap := DefaultKeyMap()
83 listKeyMap := list.DefaultKeyMap()
84 listKeyMap.Down.SetEnabled(false)
85 listKeyMap.Up.SetEnabled(false)
86 listKeyMap.HalfPageDown.SetEnabled(false)
87 listKeyMap.HalfPageUp.SetEnabled(false)
88 listKeyMap.Home.SetEnabled(false)
89 listKeyMap.End.SetEnabled(false)
90 listKeyMap.DownOneItem = keyMap.Next
91 listKeyMap.UpOneItem = keyMap.Previous
92
93 modelList := models.NewModelListComponent(cfg, listKeyMap, "Find your fave", false)
94 apiKeyInput := models.NewAPIKeyInput()
95
96 return &splashCmp{
97 client: c,
98 cfg: cfg,
99 width: 0,
100 height: 0,
101 keyMap: keyMap,
102 logoRendered: "",
103 modelList: modelList,
104 apiKeyInput: apiKeyInput,
105 selectedNo: false,
106 }
107}
108
109func (s *splashCmp) SetOnboarding(onboarding bool) {
110 s.isOnboarding = onboarding
111}
112
113func (s *splashCmp) SetProjectInit(needsInit bool) {
114 s.needsProjectInit = needsInit
115}
116
117// GetSize implements SplashPage.
118func (s *splashCmp) GetSize() (int, int) {
119 return s.width, s.height
120}
121
122// Init implements SplashPage.
123func (s *splashCmp) Init() tea.Cmd {
124 return tea.Batch(s.modelList.Init(), s.apiKeyInput.Init())
125}
126
127// SetSize implements SplashPage.
128func (s *splashCmp) SetSize(width int, height int) tea.Cmd {
129 wasSmallScreen := s.isSmallScreen()
130 rerenderLogo := width != s.width
131 s.height = height
132 s.width = width
133 if rerenderLogo || wasSmallScreen != s.isSmallScreen() {
134 s.logoRendered = s.logoBlock()
135 }
136 // remove padding, logo height, gap, title space
137 s.listHeight = s.height - lipgloss.Height(s.logoRendered) - (SplashScreenPaddingY * 2) - s.logoGap() - 2
138 listWidth := min(60, width)
139 s.apiKeyInput.SetWidth(width - 2)
140 return s.modelList.SetSize(listWidth, s.listHeight)
141}
142
143// Update implements SplashPage.
144func (s *splashCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
145 switch msg := msg.(type) {
146 case tea.WindowSizeMsg:
147 return s, s.SetSize(msg.Width, msg.Height)
148 case models.APIKeyStateChangeMsg:
149 u, cmd := s.apiKeyInput.Update(msg)
150 s.apiKeyInput = u.(*models.APIKeyInput)
151 if msg.State == models.APIKeyInputStateVerified {
152 return s, tea.Tick(5*time.Second, func(t time.Time) tea.Msg {
153 return SubmitAPIKeyMsg{}
154 })
155 }
156 return s, cmd
157 case SubmitAPIKeyMsg:
158 if s.isAPIKeyValid {
159 return s, s.saveAPIKeyAndContinue(s.apiKeyValue)
160 }
161 case tea.KeyPressMsg:
162 switch {
163 case key.Matches(msg, s.keyMap.Back):
164 if s.isAPIKeyValid {
165 return s, nil
166 }
167 if s.needsAPIKey {
168 // Go back to model selection
169 s.needsAPIKey = false
170 s.selectedModel = nil
171 s.isAPIKeyValid = false
172 s.apiKeyValue = ""
173 s.apiKeyInput.Reset()
174 return s, nil
175 }
176 case key.Matches(msg, s.keyMap.Select):
177 if s.isAPIKeyValid {
178 return s, s.saveAPIKeyAndContinue(s.apiKeyValue)
179 }
180 if s.isOnboarding && !s.needsAPIKey {
181 selectedItem := s.modelList.SelectedModel()
182 if selectedItem == nil {
183 return s, nil
184 }
185 if s.isProviderConfigured(string(selectedItem.Provider.ID)) {
186 cmd := s.setPreferredModel(*selectedItem)
187 s.isOnboarding = false
188 return s, tea.Batch(cmd, util.CmdHandler(OnboardingCompleteMsg{}))
189 } else {
190 // Provider not configured, show API key input
191 s.needsAPIKey = true
192 s.selectedModel = selectedItem
193 s.apiKeyInput.SetProviderName(selectedItem.Provider.Name)
194 return s, nil
195 }
196 } else if s.needsAPIKey {
197 // Handle API key submission
198 s.apiKeyValue = strings.TrimSpace(s.apiKeyInput.Value())
199 if s.apiKeyValue == "" {
200 return s, nil
201 }
202
203 provider, err := s.getProvider(s.selectedModel.Provider.ID)
204 if err != nil || provider == nil {
205 return s, util.ReportError(fmt.Errorf("provider %s not found", s.selectedModel.Provider.ID))
206 }
207 providerConfig := config.ProviderConfig{
208 ID: string(s.selectedModel.Provider.ID),
209 Name: s.selectedModel.Provider.Name,
210 APIKey: s.apiKeyValue,
211 Type: provider.Type,
212 BaseURL: provider.APIEndpoint,
213 }
214 return s, tea.Sequence(
215 util.CmdHandler(models.APIKeyStateChangeMsg{
216 State: models.APIKeyInputStateVerifying,
217 }),
218 func() tea.Msg {
219 start := time.Now()
220 err := providerConfig.TestConnection(s.cfg.Resolver())
221 // intentionally wait for at least 750ms to make sure the user sees the spinner
222 elapsed := time.Since(start)
223 if elapsed < 750*time.Millisecond {
224 time.Sleep(750*time.Millisecond - elapsed)
225 }
226 if err == nil {
227 s.isAPIKeyValid = true
228 return models.APIKeyStateChangeMsg{
229 State: models.APIKeyInputStateVerified,
230 }
231 }
232 return models.APIKeyStateChangeMsg{
233 State: models.APIKeyInputStateError,
234 }
235 },
236 )
237 } else if s.needsProjectInit {
238 return s, s.initializeProject()
239 }
240 case key.Matches(msg, s.keyMap.Tab, s.keyMap.LeftRight):
241 if s.needsAPIKey {
242 u, cmd := s.apiKeyInput.Update(msg)
243 s.apiKeyInput = u.(*models.APIKeyInput)
244 return s, cmd
245 }
246 if s.needsProjectInit {
247 s.selectedNo = !s.selectedNo
248 return s, nil
249 }
250 case key.Matches(msg, s.keyMap.Yes):
251 if s.needsAPIKey {
252 u, cmd := s.apiKeyInput.Update(msg)
253 s.apiKeyInput = u.(*models.APIKeyInput)
254 return s, cmd
255 }
256 if s.isOnboarding {
257 u, cmd := s.modelList.Update(msg)
258 s.modelList = u
259 return s, cmd
260 }
261 if s.needsProjectInit {
262 s.selectedNo = false
263 return s, s.initializeProject()
264 }
265 case key.Matches(msg, s.keyMap.No):
266 if s.needsAPIKey {
267 u, cmd := s.apiKeyInput.Update(msg)
268 s.apiKeyInput = u.(*models.APIKeyInput)
269 return s, cmd
270 }
271 if s.isOnboarding {
272 u, cmd := s.modelList.Update(msg)
273 s.modelList = u
274 return s, cmd
275 }
276 if s.needsProjectInit {
277 s.selectedNo = true
278 return s, s.initializeProject()
279 }
280 default:
281 if s.needsAPIKey {
282 u, cmd := s.apiKeyInput.Update(msg)
283 s.apiKeyInput = u.(*models.APIKeyInput)
284 return s, cmd
285 } else if s.isOnboarding {
286 u, cmd := s.modelList.Update(msg)
287 s.modelList = u
288 return s, cmd
289 }
290 }
291 case tea.PasteMsg:
292 if s.needsAPIKey {
293 u, cmd := s.apiKeyInput.Update(msg)
294 s.apiKeyInput = u.(*models.APIKeyInput)
295 return s, cmd
296 } else if s.isOnboarding {
297 var cmd tea.Cmd
298 s.modelList, cmd = s.modelList.Update(msg)
299 return s, cmd
300 }
301 case spinner.TickMsg:
302 u, cmd := s.apiKeyInput.Update(msg)
303 s.apiKeyInput = u.(*models.APIKeyInput)
304 return s, cmd
305 }
306 return s, nil
307}
308
309func (s *splashCmp) saveAPIKeyAndContinue(apiKey string) tea.Cmd {
310 if s.selectedModel == nil {
311 return nil
312 }
313
314 err := s.cfg.SetProviderAPIKey(string(s.selectedModel.Provider.ID), apiKey)
315 if err != nil {
316 return util.ReportError(fmt.Errorf("failed to save API key: %w", err))
317 }
318
319 // Reset API key state and continue with model selection
320 s.needsAPIKey = false
321 cmd := s.setPreferredModel(*s.selectedModel)
322 s.isOnboarding = false
323 s.selectedModel = nil
324 s.isAPIKeyValid = false
325
326 return tea.Batch(cmd, util.CmdHandler(OnboardingCompleteMsg{}))
327}
328
329func (s *splashCmp) initializeProject() tea.Cmd {
330 s.needsProjectInit = false
331
332 if err := config.MarkProjectInitialized(s.cfg); err != nil {
333 return util.ReportError(err)
334 }
335 var cmds []tea.Cmd
336
337 cmds = append(cmds, util.CmdHandler(OnboardingCompleteMsg{}))
338 if !s.selectedNo {
339 cmds = append(cmds,
340 util.CmdHandler(chat.SessionClearedMsg{}),
341 util.CmdHandler(chat.SendMsg{
342 Text: prompt.Initialize(),
343 }),
344 )
345 }
346 return tea.Sequence(cmds...)
347}
348
349func (s *splashCmp) setPreferredModel(selectedItem models.ModelOption) tea.Cmd {
350 model := s.cfg.GetModel(string(selectedItem.Provider.ID), selectedItem.Model.ID)
351 if model == nil {
352 return util.ReportError(fmt.Errorf("model %s not found for provider %s", selectedItem.Model.ID, selectedItem.Provider.ID))
353 }
354
355 selectedModel := config.SelectedModel{
356 Model: selectedItem.Model.ID,
357 Provider: string(selectedItem.Provider.ID),
358 ReasoningEffort: model.DefaultReasoningEffort,
359 MaxTokens: model.DefaultMaxTokens,
360 }
361
362 err := s.cfg.UpdatePreferredModel(config.SelectedModelTypeLarge, selectedModel)
363 if err != nil {
364 return util.ReportError(err)
365 }
366
367 // Now lets automatically setup the small model
368 knownProvider, err := s.getProvider(selectedItem.Provider.ID)
369 if err != nil {
370 return util.ReportError(err)
371 }
372 if knownProvider == nil {
373 // for local provider we just use the same model
374 err = s.cfg.UpdatePreferredModel(config.SelectedModelTypeSmall, selectedModel)
375 if err != nil {
376 return util.ReportError(err)
377 }
378 } else {
379 smallModel := knownProvider.DefaultSmallModelID
380 model := s.cfg.GetModel(string(selectedItem.Provider.ID), smallModel)
381 // should never happen
382 if model == nil {
383 err = s.cfg.UpdatePreferredModel(config.SelectedModelTypeSmall, selectedModel)
384 if err != nil {
385 return util.ReportError(err)
386 }
387 return nil
388 }
389 smallSelectedModel := config.SelectedModel{
390 Model: smallModel,
391 Provider: string(selectedItem.Provider.ID),
392 ReasoningEffort: model.DefaultReasoningEffort,
393 MaxTokens: model.DefaultMaxTokens,
394 }
395 err = s.cfg.UpdatePreferredModel(config.SelectedModelTypeSmall, smallSelectedModel)
396 if err != nil {
397 return util.ReportError(err)
398 }
399 }
400 s.cfg.SetupAgents()
401 return nil
402}
403
404func (s *splashCmp) getProvider(providerID catwalk.InferenceProvider) (*catwalk.Provider, error) {
405 providers, err := config.Providers(s.cfg)
406 if err != nil {
407 return nil, err
408 }
409 for _, p := range providers {
410 if p.ID == providerID {
411 return &p, nil
412 }
413 }
414 return nil, nil
415}
416
417func (s *splashCmp) isProviderConfigured(providerID string) bool {
418 if _, ok := s.cfg.Providers.Get(providerID); ok {
419 return true
420 }
421 return false
422}
423
424func (s *splashCmp) View() string {
425 t := styles.CurrentTheme()
426 var content string
427 if s.needsAPIKey {
428 remainingHeight := s.height - lipgloss.Height(s.logoRendered) - (SplashScreenPaddingY * 2)
429 apiKeyView := t.S().Base.PaddingLeft(1).Render(s.apiKeyInput.View())
430 apiKeySelector := t.S().Base.AlignVertical(lipgloss.Bottom).Height(remainingHeight).Render(
431 lipgloss.JoinVertical(
432 lipgloss.Left,
433 apiKeyView,
434 ),
435 )
436 content = lipgloss.JoinVertical(
437 lipgloss.Left,
438 s.logoRendered,
439 apiKeySelector,
440 )
441 } else if s.isOnboarding {
442 modelListView := s.modelList.View()
443 remainingHeight := s.height - lipgloss.Height(s.logoRendered) - (SplashScreenPaddingY * 2)
444 modelSelector := t.S().Base.AlignVertical(lipgloss.Bottom).Height(remainingHeight).Render(
445 lipgloss.JoinVertical(
446 lipgloss.Left,
447 t.S().Base.PaddingLeft(1).Foreground(t.Primary).Render("Choose a Model"),
448 "",
449 modelListView,
450 ),
451 )
452 content = lipgloss.JoinVertical(
453 lipgloss.Left,
454 s.logoRendered,
455 modelSelector,
456 )
457 } else if s.needsProjectInit {
458 titleStyle := t.S().Base.Foreground(t.FgBase)
459 pathStyle := t.S().Base.Foreground(t.Success).PaddingLeft(2)
460 bodyStyle := t.S().Base.Foreground(t.FgMuted)
461 shortcutStyle := t.S().Base.Foreground(t.Success)
462
463 initText := lipgloss.JoinVertical(
464 lipgloss.Left,
465 titleStyle.Render("Would you like to initialize this project?"),
466 "",
467 pathStyle.Render(s.cwd()),
468 "",
469 bodyStyle.Render("When I initialize your codebase I examine the project and put the"),
470 bodyStyle.Render("result into a CRUSH.md file which serves as general context."),
471 "",
472 bodyStyle.Render("You can also initialize anytime via ")+shortcutStyle.Render("ctrl+p")+bodyStyle.Render("."),
473 "",
474 bodyStyle.Render("Would you like to initialize now?"),
475 )
476
477 yesButton := core.SelectableButton(core.ButtonOpts{
478 Text: "Yep!",
479 UnderlineIndex: 0,
480 Selected: !s.selectedNo,
481 })
482
483 noButton := core.SelectableButton(core.ButtonOpts{
484 Text: "Nope",
485 UnderlineIndex: 0,
486 Selected: s.selectedNo,
487 })
488
489 buttons := lipgloss.JoinHorizontal(lipgloss.Left, yesButton, " ", noButton)
490 remainingHeight := s.height - lipgloss.Height(s.logoRendered) - (SplashScreenPaddingY * 2)
491
492 initContent := t.S().Base.AlignVertical(lipgloss.Bottom).PaddingLeft(1).Height(remainingHeight).Render(
493 lipgloss.JoinVertical(
494 lipgloss.Left,
495 initText,
496 "",
497 buttons,
498 ),
499 )
500
501 content = lipgloss.JoinVertical(
502 lipgloss.Left,
503 s.logoRendered,
504 "",
505 initContent,
506 )
507 } else {
508 parts := []string{
509 s.logoRendered,
510 s.infoSection(),
511 }
512 content = lipgloss.JoinVertical(lipgloss.Left, parts...)
513 }
514
515 return t.S().Base.
516 Width(s.width).
517 Height(s.height).
518 PaddingTop(SplashScreenPaddingY).
519 PaddingBottom(SplashScreenPaddingY).
520 Render(content)
521}
522
523func (s *splashCmp) Cursor() *tea.Cursor {
524 if s.needsAPIKey {
525 cursor := s.apiKeyInput.Cursor()
526 if cursor != nil {
527 return s.moveCursor(cursor)
528 }
529 } else if s.isOnboarding {
530 cursor := s.modelList.Cursor()
531 if cursor != nil {
532 return s.moveCursor(cursor)
533 }
534 } else {
535 return nil
536 }
537 return nil
538}
539
540func (s *splashCmp) isSmallScreen() bool {
541 // Consider a screen small if either the width is less than 40 or if the
542 // height is less than 20
543 return s.width < 55 || s.height < 20
544}
545
546func (s *splashCmp) infoSection() string {
547 t := styles.CurrentTheme()
548 infoStyle := t.S().Base.PaddingLeft(2)
549 if s.isSmallScreen() {
550 infoStyle = infoStyle.MarginTop(1)
551 }
552 return infoStyle.Render(
553 lipgloss.JoinVertical(
554 lipgloss.Left,
555 s.cwdPart(),
556 "",
557 s.currentModelBlock(),
558 "",
559 lipgloss.JoinHorizontal(lipgloss.Left, s.lspBlock(), s.mcpBlock()),
560 "",
561 ),
562 )
563}
564
565func (s *splashCmp) logoBlock() string {
566 t := styles.CurrentTheme()
567 logoStyle := t.S().Base.Padding(0, 2).Width(s.width)
568 if s.isSmallScreen() {
569 // If the width is too small, render a smaller version of the logo
570 // NOTE: 20 is not correct because [splashCmp.height] is not the
571 // *actual* window height, instead, it is the height of the splash
572 // component and that depends on other variables like compact mode and
573 // the height of the editor.
574 return logoStyle.Render(
575 logo.SmallRender(s.width - logoStyle.GetHorizontalFrameSize()),
576 )
577 }
578 return logoStyle.Render(
579 logo.Render(version.Version, false, logo.Opts{
580 FieldColor: t.Primary,
581 TitleColorA: t.Secondary,
582 TitleColorB: t.Primary,
583 CharmColor: t.Secondary,
584 VersionColor: t.Primary,
585 Width: s.width - logoStyle.GetHorizontalFrameSize(),
586 }),
587 )
588}
589
590func (s *splashCmp) moveCursor(cursor *tea.Cursor) *tea.Cursor {
591 if cursor == nil {
592 return nil
593 }
594 // Calculate the correct Y offset based on current state
595 logoHeight := lipgloss.Height(s.logoRendered)
596 if s.needsAPIKey {
597 infoSectionHeight := lipgloss.Height(s.infoSection())
598 baseOffset := logoHeight + SplashScreenPaddingY + infoSectionHeight
599 remainingHeight := s.height - baseOffset - lipgloss.Height(s.apiKeyInput.View()) - SplashScreenPaddingY
600 offset := baseOffset + remainingHeight
601 cursor.Y += offset
602 cursor.X = cursor.X + 1
603 } else if s.isOnboarding {
604 offset := logoHeight + SplashScreenPaddingY + s.logoGap() + 2
605 cursor.Y += offset
606 cursor.X = cursor.X + 1
607 }
608
609 return cursor
610}
611
612func (s *splashCmp) logoGap() int {
613 if s.height > 35 {
614 return LogoGap
615 }
616 return 0
617}
618
619// Bindings implements SplashPage.
620func (s *splashCmp) Bindings() []key.Binding {
621 if s.needsAPIKey {
622 return []key.Binding{
623 s.keyMap.Select,
624 s.keyMap.Back,
625 }
626 } else if s.isOnboarding {
627 return []key.Binding{
628 s.keyMap.Select,
629 s.keyMap.Next,
630 s.keyMap.Previous,
631 }
632 } else if s.needsProjectInit {
633 return []key.Binding{
634 s.keyMap.Select,
635 s.keyMap.Yes,
636 s.keyMap.No,
637 s.keyMap.Tab,
638 s.keyMap.LeftRight,
639 }
640 }
641 return []key.Binding{}
642}
643
644func (s *splashCmp) getMaxInfoWidth() int {
645 return min(s.width-2, 90) // 2 for left padding
646}
647
648func (s *splashCmp) cwdPart() string {
649 t := styles.CurrentTheme()
650 maxWidth := s.getMaxInfoWidth()
651 return t.S().Muted.Width(maxWidth).Render(s.cwd())
652}
653
654func (s *splashCmp) cwd() string {
655 return home.Short(s.cfg.WorkingDir())
656}
657
658func LSPList(c *client.Client, cfg *config.Config, maxWidth int) []string {
659 return lspcomponent.RenderLSPList(c, cfg, lspcomponent.RenderOptions{
660 MaxWidth: maxWidth,
661 ShowSection: false,
662 })
663}
664
665func (s *splashCmp) lspBlock() string {
666 t := styles.CurrentTheme()
667 maxWidth := s.getMaxInfoWidth() / 2
668 section := t.S().Subtle.Render("LSPs")
669 lspList := append([]string{section, ""}, LSPList(s.client, s.cfg, maxWidth-1)...)
670 return t.S().Base.Width(maxWidth).PaddingRight(1).Render(
671 lipgloss.JoinVertical(
672 lipgloss.Left,
673 lspList...,
674 ),
675 )
676}
677
678func MCPList(cfg *config.Config, maxWidth int) []string {
679 return mcp.RenderMCPList(cfg, mcp.RenderOptions{
680 MaxWidth: maxWidth,
681 ShowSection: false,
682 })
683}
684
685func (s *splashCmp) mcpBlock() string {
686 t := styles.CurrentTheme()
687 maxWidth := s.getMaxInfoWidth() / 2
688 section := t.S().Subtle.Render("MCPs")
689 mcpList := append([]string{section, ""}, MCPList(s.cfg, maxWidth-1)...)
690 return t.S().Base.Width(maxWidth).PaddingRight(1).Render(
691 lipgloss.JoinVertical(
692 lipgloss.Left,
693 mcpList...,
694 ),
695 )
696}
697
698func (s *splashCmp) currentModelBlock() string {
699 agentCfg := s.cfg.Agents["coder"]
700 model := s.cfg.GetModelByType(agentCfg.Model)
701 if model == nil {
702 return ""
703 }
704 t := styles.CurrentTheme()
705 modelIcon := t.S().Base.Foreground(t.FgSubtle).Render(styles.ModelIcon)
706 modelName := t.S().Text.Render(model.Name)
707 modelInfo := fmt.Sprintf("%s %s", modelIcon, modelName)
708 parts := []string{
709 modelInfo,
710 }
711
712 return lipgloss.JoinVertical(
713 lipgloss.Left,
714 parts...,
715 )
716}
717
718func (s *splashCmp) IsShowingAPIKey() bool {
719 return s.needsAPIKey
720}
721
722func (s *splashCmp) IsAPIKeyValid() bool {
723 return s.isAPIKeyValid
724}