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