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