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