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