diff --git a/internal/tui/components/chat/splash/splash.go b/internal/tui/components/chat/splash/splash.go index 7cbc7b5fd371aa9db9cec3613ca40377a70c5d77..50912b283b05b6e2479fa82793e16e726a1cac8b 100644 --- a/internal/tui/components/chat/splash/splash.go +++ b/internal/tui/components/chat/splash/splash.go @@ -59,7 +59,7 @@ func New() Splash { t := styles.CurrentTheme() inputStyle := t.S().Base.Padding(0, 1, 0, 1) - modelList := models.NewModelListComponent(listKeyMap, inputStyle) + modelList := models.NewModelListComponent(listKeyMap, inputStyle, "Find your fave") return &splashCmp{ width: 0, height: 0, diff --git a/internal/tui/components/core/helpers.go b/internal/tui/components/core/helpers.go index 1c2c05a6229b98222d870694e726069bfc9c6e92..659ffd88c6b72b60933f9a19e3712093376a29bf 100644 --- a/internal/tui/components/core/helpers.go +++ b/internal/tui/components/core/helpers.go @@ -23,6 +23,22 @@ func Section(text string, width int) string { return text } +func SectionWithInfo(text string, width int, info string) string { + t := styles.CurrentTheme() + char := "─" + length := lipgloss.Width(text) + 1 + remainingWidth := width - length + + if info != "" { + remainingWidth -= lipgloss.Width(info) + 1 // 1 for the space before info + } + lineStyle := t.S().Base.Foreground(t.Border) + if remainingWidth > 0 { + text = text + " " + lineStyle.Render(strings.Repeat(char, remainingWidth)) + " " + info + } + return text +} + func Title(title string, width int) string { t := styles.CurrentTheme() char := "╱" diff --git a/internal/tui/components/core/list/list.go b/internal/tui/components/core/list/list.go index f0887ee8aed5df7d3fcb34fe282cf916fad5920a..96127193ca62e3c4d19943dfd5e58d99f87e0d47 100644 --- a/internal/tui/components/core/list/list.go +++ b/internal/tui/components/core/list/list.go @@ -41,6 +41,7 @@ type ListModel interface { SelectedIndex() int // Get the index of the currently selected item SetSelected(int) tea.Cmd // Set the selected item by index and scroll to it Filter(string) tea.Cmd // Filter items based on a search term + SetFilterPlaceholder(string) // Set the placeholder text for the filter input } // HasAnim interface identifies items that support animation. @@ -1355,3 +1356,7 @@ func (m *model) Focus() tea.Cmd { func (m *model) IsFocused() bool { return m.isFocused } + +func (m *model) SetFilterPlaceholder(placeholder string) { + m.input.Placeholder = placeholder +} diff --git a/internal/tui/components/dialogs/commands/item.go b/internal/tui/components/dialogs/commands/item.go index a89a884472cff75a0051d89b637ae4f55feba527..fa385f67f9e8fb76804df147153b068434ac58fd 100644 --- a/internal/tui/components/dialogs/commands/item.go +++ b/internal/tui/components/dialogs/commands/item.go @@ -14,11 +14,12 @@ type ItemSection interface { util.Model layout.Sizeable list.SectionHeader + SetInfo(info string) } type itemSectionModel struct { - width int - title string - noPadding bool // No padding for the section header + width int + title string + info string } func NewItemSection(title string) ItemSection { @@ -40,7 +41,14 @@ func (m *itemSectionModel) View() tea.View { title := ansi.Truncate(m.title, m.width-2, "…") style := t.S().Base.Padding(1, 1, 0, 1) title = t.S().Muted.Render(title) - return tea.NewView(style.Render(core.Section(title, m.width-2))) + section := "" + if m.info != "" { + section = core.SectionWithInfo(title, m.width-2, m.info) + } else { + section = core.Section(title, m.width-2) + } + + return tea.NewView(style.Render(section)) } func (m *itemSectionModel) GetSize() (int, int) { @@ -55,3 +63,7 @@ func (m *itemSectionModel) SetSize(width int, height int) tea.Cmd { func (m *itemSectionModel) IsSectionHeader() bool { return true } + +func (m *itemSectionModel) SetInfo(info string) { + m.info = info +} diff --git a/internal/tui/components/dialogs/models/list.go b/internal/tui/components/dialogs/models/list.go index bb489c587e2e2d77256a4737083c0cbb58300618..bbb23300ae218830cff76daaa8418ab1a75ff15e 100644 --- a/internal/tui/components/dialogs/models/list.go +++ b/internal/tui/components/dialogs/models/list.go @@ -1,6 +1,7 @@ package models import ( + "fmt" "slices" tea "github.com/charmbracelet/bubbletea/v2" @@ -9,6 +10,7 @@ import ( "github.com/charmbracelet/crush/internal/tui/components/completions" "github.com/charmbracelet/crush/internal/tui/components/core/list" "github.com/charmbracelet/crush/internal/tui/components/dialogs/commands" + "github.com/charmbracelet/crush/internal/tui/styles" "github.com/charmbracelet/crush/internal/tui/util" "github.com/charmbracelet/lipgloss/v2" ) @@ -18,11 +20,12 @@ type ModelListComponent struct { modelType int } -func NewModelListComponent(keyMap list.KeyMap, inputStyle lipgloss.Style) *ModelListComponent { +func NewModelListComponent(keyMap list.KeyMap, inputStyle lipgloss.Style, inputPlaceholder string) *ModelListComponent { modelList := list.New( list.WithFilterable(true), list.WithKeyMap(keyMap), list.WithInputStyle(inputStyle), + list.WithFilterPlaceholder(inputPlaceholder), list.WithWrapNavigation(true), ) @@ -59,6 +62,7 @@ func (m *ModelListComponent) SelectedIndex() int { } func (m *ModelListComponent) SetModelType(modelType int) tea.Cmd { + t := styles.CurrentTheme() m.modelType = modelType providers, err := config.Providers() @@ -77,6 +81,9 @@ func (m *ModelListComponent) SetModelType(modelType int) tea.Cmd { currentModel = cfg.Models[config.SelectedModelTypeSmall] } + configuredIcon := t.S().Base.Foreground(t.Success).Render(styles.CheckIcon) + configured := fmt.Sprintf("%s %s", configuredIcon, t.S().Subtle.Render("Configured")) + // Create a map to track which providers we've already added addedProviders := make(map[string]bool) @@ -120,7 +127,9 @@ func (m *ModelListComponent) SetModelType(modelType int) tea.Cmd { if name == "" { name = string(configProvider.ID) } - modelItems = append(modelItems, commands.NewItemSection(name)) + section := commands.NewItemSection(name) + section.SetInfo(configured) + modelItems = append(modelItems, section) for _, model := range configProvider.Models { modelItems = append(modelItems, completions.NewCompletionItem(model.Name, ModelOption{ Provider: configProvider, @@ -150,7 +159,12 @@ func (m *ModelListComponent) SetModelType(modelType int) tea.Cmd { if name == "" { name = string(provider.ID) } - modelItems = append(modelItems, commands.NewItemSection(name)) + + section := commands.NewItemSection(name) + if _, ok := cfg.Providers[string(provider.ID)]; ok { + section.SetInfo(configured) + } + modelItems = append(modelItems, section) for _, model := range provider.Models { modelItems = append(modelItems, completions.NewCompletionItem(model.Name, ModelOption{ Provider: provider, @@ -169,3 +183,7 @@ func (m *ModelListComponent) SetModelType(modelType int) tea.Cmd { func (m *ModelListComponent) GetModelType() int { return m.modelType } + +func (m *ModelListComponent) SetInputPlaceholder(placeholder string) { + m.list.SetFilterPlaceholder(placeholder) +} diff --git a/internal/tui/components/dialogs/models/models.go b/internal/tui/components/dialogs/models/models.go index a88086131476f8843a607b726675b86c9242ed03..6b23746251f01645c32da28972d060c64bf1f2a0 100644 --- a/internal/tui/components/dialogs/models/models.go +++ b/internal/tui/components/dialogs/models/models.go @@ -24,6 +24,9 @@ const ( const ( LargeModelType int = iota SmallModelType + + largeModelInputPlaceholder = "Choose a model for large, complex tasks" + smallModelInputPlaceholder = "Choose a model for small, simple tasks" ) // ModelSelectedMsg is sent when a model is selected @@ -71,7 +74,7 @@ func NewModelDialogCmp() ModelDialog { t := styles.CurrentTheme() inputStyle := t.S().Base.Padding(0, 1, 0, 1) - modelList := NewModelListComponent(listKeyMap, inputStyle) + modelList := NewModelListComponent(listKeyMap, inputStyle, "Choose a model for large, complex tasks") help := help.New() help.Styles = t.S().Help @@ -122,8 +125,10 @@ func (m *modelDialogCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) { ) case key.Matches(msg, m.keyMap.Tab): if m.modelList.GetModelType() == LargeModelType { + m.modelList.SetInputPlaceholder(smallModelInputPlaceholder) return m, m.modelList.SetModelType(SmallModelType) } else { + m.modelList.SetInputPlaceholder(largeModelInputPlaceholder) return m, m.modelList.SetModelType(LargeModelType) } case key.Matches(msg, m.keyMap.Close):