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