Detailed changes
@@ -134,13 +134,23 @@ func Status(t *styles.Styles, opts StatusOpts, width int) string {
// Section renders a section header with a title and a horizontal line filling
// the remaining width.
-func Section(t *styles.Styles, text string, width int) string {
+func Section(t *styles.Styles, text string, width int, info ...string) string {
char := styles.SectionSeparator
length := lipgloss.Width(text) + 1
remainingWidth := width - length
+
+ var infoText string
+ if len(info) > 0 {
+ infoText = strings.Join(info, " ")
+ if len(infoText) > 0 {
+ infoText = " " + infoText
+ remainingWidth -= lipgloss.Width(infoText)
+ }
+ }
+
text = t.Section.Title.Render(text)
if remainingWidth > 0 {
- text = text + " " + t.Section.Line.Render(strings.Repeat(char, remainingWidth))
+ text = text + " " + t.Section.Line.Render(strings.Repeat(char, remainingWidth)) + infoText
}
return text
}
@@ -197,22 +197,23 @@ func (c *Commands) Cursor() *tea.Cursor {
return InputCursor(c.com.Styles, c.input.Cursor())
}
-// radioView generates the command type selector radio buttons.
-func radioView(t *styles.Styles, selected uicmd.CommandType, hasUserCmds bool, hasMCPPrompts bool) string {
+// commandsRadioView generates the command type selector radio buttons.
+func commandsRadioView(sty *styles.Styles, selected uicmd.CommandType, hasUserCmds bool, hasMCPPrompts bool) string {
if !hasUserCmds && !hasMCPPrompts {
return ""
}
selectedFn := func(t uicmd.CommandType) string {
if t == selected {
- return " ◉ " + t.String()
+ return sty.RadioOn.Padding(0, 1).Render() + sty.HalfMuted.Render(t.String())
}
- return " ○ " + t.String()
+ return sty.RadioOff.Padding(0, 1).Render() + sty.HalfMuted.Render(t.String())
}
parts := []string{
selectedFn(uicmd.SystemCommands),
}
+
if hasUserCmds {
parts = append(parts, selectedFn(uicmd.UserCommands))
}
@@ -220,14 +221,13 @@ func radioView(t *styles.Styles, selected uicmd.CommandType, hasUserCmds bool, h
parts = append(parts, selectedFn(uicmd.MCPPrompts))
}
- radio := strings.Join(parts, " ")
- return t.Dialog.Commands.CommandTypeSelector.Render(radio)
+ return strings.Join(parts, " ")
}
// View implements [Dialog].
func (c *Commands) View() string {
t := c.com.Styles
- radio := radioView(t, c.selected, len(c.userCmds) > 0, c.mcpPrompts.Len() > 0)
+ radio := commandsRadioView(t, c.selected, len(c.userCmds) > 0, c.mcpPrompts.Len() > 0)
titleStyle := t.Dialog.Title
dialogStyle := t.Dialog.View.Width(c.width)
headerOffset := lipgloss.Width(radio) + titleStyle.GetHorizontalFrameSize() + dialogStyle.GetHorizontalFrameSize()
@@ -0,0 +1,445 @@
+package dialog
+
+import (
+ "cmp"
+ "fmt"
+ "slices"
+ "strings"
+
+ "charm.land/bubbles/v2/help"
+ "charm.land/bubbles/v2/key"
+ "charm.land/bubbles/v2/textinput"
+ tea "charm.land/bubbletea/v2"
+ "charm.land/lipgloss/v2"
+ "github.com/charmbracelet/catwalk/pkg/catwalk"
+ "github.com/charmbracelet/crush/internal/config"
+ "github.com/charmbracelet/crush/internal/ui/common"
+ "github.com/charmbracelet/crush/internal/uiutil"
+)
+
+// ModelType represents the type of model to select.
+type ModelType int
+
+const (
+ ModelTypeLarge ModelType = iota
+ ModelTypeSmall
+)
+
+// String returns the string representation of the [ModelType].
+func (mt ModelType) String() string {
+ switch mt {
+ case ModelTypeLarge:
+ return "Large Task"
+ case ModelTypeSmall:
+ return "Small Task"
+ default:
+ return "Unknown"
+ }
+}
+
+const (
+ largeModelInputPlaceholder = "Choose a model for large, complex tasks"
+ smallModelInputPlaceholder = "Choose a model for small, simple tasks"
+)
+
+// ModelsID is the identifier for the model selection dialog.
+const ModelsID = "models"
+
+// Models represents a model selection dialog.
+type Models struct {
+ com *common.Common
+
+ modelType ModelType
+ providers []catwalk.Provider
+
+ width, height int
+
+ keyMap struct {
+ Tab key.Binding
+ UpDown key.Binding
+ Select key.Binding
+ Next key.Binding
+ Previous key.Binding
+ Close key.Binding
+ }
+ list *ModelsList
+ input textinput.Model
+ help help.Model
+}
+
+var _ Dialog = (*Models)(nil)
+
+// NewModels creates a new Models dialog.
+func NewModels(com *common.Common) (*Models, error) {
+ t := com.Styles
+ m := &Models{}
+ m.com = com
+ help := help.New()
+ help.Styles = t.DialogHelpStyles()
+
+ m.help = help
+ m.list = NewModelsList(t)
+ m.list.Focus()
+ m.list.SetSelected(0)
+
+ m.input = textinput.New()
+ m.input.SetVirtualCursor(false)
+ m.input.Placeholder = largeModelInputPlaceholder
+ m.input.SetStyles(com.Styles.TextInput)
+ m.input.Focus()
+
+ m.keyMap.Tab = key.NewBinding(
+ key.WithKeys("tab", "shift+tab"),
+ key.WithHelp("tab", "toggle type"),
+ )
+ m.keyMap.Select = key.NewBinding(
+ key.WithKeys("enter", "ctrl+y"),
+ key.WithHelp("enter", "confirm"),
+ )
+ m.keyMap.UpDown = key.NewBinding(
+ key.WithKeys("up", "down"),
+ key.WithHelp("↑/↓", "choose"),
+ )
+ m.keyMap.Next = key.NewBinding(
+ key.WithKeys("down", "ctrl+n"),
+ key.WithHelp("↓", "next item"),
+ )
+ m.keyMap.Previous = key.NewBinding(
+ key.WithKeys("up", "ctrl+p"),
+ key.WithHelp("↑", "previous item"),
+ )
+ m.keyMap.Close = CloseKey
+
+ providers, err := getFilteredProviders(com.Config())
+ if err != nil {
+ return nil, fmt.Errorf("failed to get providers: %w", err)
+ }
+
+ m.providers = providers
+ if err := m.setProviderItems(); err != nil {
+ return nil, fmt.Errorf("failed to set provider items: %w", err)
+ }
+
+ return m, nil
+}
+
+// SetSize sets the size of the dialog.
+func (m *Models) SetSize(width, height int) {
+ t := m.com.Styles
+ m.width = width
+ m.height = height
+ innerWidth := width - t.Dialog.View.GetHorizontalFrameSize()
+ heightOffset := t.Dialog.Title.GetVerticalFrameSize() + 1 + // (1) title content
+ t.Dialog.InputPrompt.GetVerticalFrameSize() + 1 + // (1) input content
+ t.Dialog.HelpView.GetVerticalFrameSize() +
+ t.Dialog.View.GetVerticalFrameSize()
+ m.input.SetWidth(innerWidth - t.Dialog.InputPrompt.GetHorizontalFrameSize() - 1) // (1) cursor padding
+ m.list.SetSize(innerWidth, height-heightOffset)
+ m.help.SetWidth(width)
+}
+
+// ID implements Dialog.
+func (m *Models) ID() string {
+ return ModelsID
+}
+
+// Update implements Dialog.
+func (m *Models) Update(msg tea.Msg) tea.Msg {
+ switch msg := msg.(type) {
+ case tea.KeyPressMsg:
+ switch {
+ case key.Matches(msg, m.keyMap.Close):
+ return CloseMsg{}
+ case key.Matches(msg, m.keyMap.Previous):
+ m.list.Focus()
+ if m.list.IsSelectedFirst() {
+ m.list.SelectLast()
+ m.list.ScrollToBottom()
+ break
+ }
+ m.list.SelectPrev()
+ m.list.ScrollToSelected()
+ case key.Matches(msg, m.keyMap.Next):
+ m.list.Focus()
+ if m.list.IsSelectedLast() {
+ m.list.SelectFirst()
+ m.list.ScrollToTop()
+ break
+ }
+ m.list.SelectNext()
+ m.list.ScrollToSelected()
+ case key.Matches(msg, m.keyMap.Select):
+ if selectedItem := m.list.SelectedItem(); selectedItem != nil {
+ // TODO: Handle model selection confirmation.
+ }
+ case key.Matches(msg, m.keyMap.Tab):
+ if m.modelType == ModelTypeLarge {
+ m.modelType = ModelTypeSmall
+ } else {
+ m.modelType = ModelTypeLarge
+ }
+ if err := m.setProviderItems(); err != nil {
+ return uiutil.ReportError(err)
+ }
+ default:
+ var cmd tea.Cmd
+ m.input, cmd = m.input.Update(msg)
+ value := m.input.Value()
+ m.list.SetFilter(value)
+ m.list.ScrollToSelected()
+ return cmd
+ }
+ }
+ return nil
+}
+
+// Cursor returns the cursor for the dialog.
+func (m *Models) Cursor() *tea.Cursor {
+ return InputCursor(m.com.Styles, m.input.Cursor())
+}
+
+// modelTypeRadioView returns the radio view for model type selection.
+func (m *Models) modelTypeRadioView() string {
+ t := m.com.Styles
+ textStyle := t.HalfMuted
+ largeRadioStyle := t.RadioOff
+ smallRadioStyle := t.RadioOff
+ if m.modelType == ModelTypeLarge {
+ largeRadioStyle = t.RadioOn
+ } else {
+ smallRadioStyle = t.RadioOn
+ }
+
+ largeRadio := largeRadioStyle.Padding(0, 1).Render()
+ smallRadio := smallRadioStyle.Padding(0, 1).Render()
+
+ return fmt.Sprintf("%s%s %s%s",
+ largeRadio, textStyle.Render(ModelTypeLarge.String()),
+ smallRadio, textStyle.Render(ModelTypeSmall.String()))
+}
+
+// View implements Dialog.
+func (m *Models) View() string {
+ t := m.com.Styles
+ titleStyle := t.Dialog.Title
+ dialogStyle := t.Dialog.View
+
+ radios := m.modelTypeRadioView()
+
+ headerOffset := lipgloss.Width(radios) + titleStyle.GetHorizontalFrameSize() +
+ dialogStyle.GetHorizontalFrameSize()
+
+ header := common.DialogTitle(t, "Switch Model", m.width-headerOffset) + radios
+
+ return HeaderInputListHelpView(t, m.width, m.list.Height(), header,
+ m.input.View(), m.list.Render(), m.help.View(m))
+}
+
+// ShortHelp returns the short help view.
+func (m *Models) ShortHelp() []key.Binding {
+ return []key.Binding{
+ m.keyMap.UpDown,
+ m.keyMap.Tab,
+ m.keyMap.Select,
+ m.keyMap.Close,
+ }
+}
+
+// FullHelp returns the full help view.
+func (m *Models) FullHelp() [][]key.Binding {
+ return [][]key.Binding{
+ {
+ m.keyMap.Select,
+ m.keyMap.Next,
+ m.keyMap.Previous,
+ m.keyMap.Tab,
+ },
+ {
+ m.keyMap.Close,
+ },
+ }
+}
+
+// setProviderItems sets the provider items in the list.
+func (m *Models) setProviderItems() error {
+ t := m.com.Styles
+ cfg := m.com.Config()
+
+ selectedType := config.SelectedModelTypeLarge
+ if m.modelType == ModelTypeLarge {
+ selectedType = config.SelectedModelTypeLarge
+ } else {
+ selectedType = config.SelectedModelTypeSmall
+ }
+
+ var selectedItemID string
+ currentModel := cfg.Models[selectedType]
+ recentItems := cfg.RecentModels[selectedType]
+
+ // Track providers already added to avoid duplicates
+ addedProviders := make(map[string]bool)
+
+ // Get a list of known providers to compare against
+ knownProviders, err := config.Providers(cfg)
+ if err != nil {
+ return fmt.Errorf("failed to get providers: %w", err)
+ }
+
+ containsProviderFunc := func(id string) func(p catwalk.Provider) bool {
+ return func(p catwalk.Provider) bool {
+ return p.ID == catwalk.InferenceProvider(id)
+ }
+ }
+
+ // itemsMap contains the keys of added model items.
+ itemsMap := make(map[string]*ModelItem)
+ groups := []ModelGroup{}
+ for id, p := range cfg.Providers.Seq2() {
+ if p.Disable {
+ continue
+ }
+
+ // Check if this provider is not in the known providers list
+ if !slices.ContainsFunc(knownProviders, containsProviderFunc(id)) ||
+ !slices.ContainsFunc(m.providers, containsProviderFunc(id)) {
+ provider := p.ToProvider()
+
+ // Add this unknown provider to the list
+ name := p.Name
+ if name == "" {
+ name = id
+ }
+
+ addedProviders[id] = true
+
+ group := NewModelGroup(t, name, true)
+ for _, model := range p.Models {
+ item := NewModelItem(t, provider, model)
+ group.AppendItems(item)
+ itemsMap[item.ID()] = item
+ if model.ID == currentModel.Model && string(provider.ID) == currentModel.Provider {
+ selectedItemID = item.ID()
+ }
+ }
+ }
+ }
+
+ // Now add known providers from the predefined list
+ for _, provider := range m.providers {
+ providerID := string(provider.ID)
+ if addedProviders[providerID] {
+ continue
+ }
+
+ providerConfig, providerConfigured := cfg.Providers.Get(providerID)
+ if providerConfigured && providerConfig.Disable {
+ continue
+ }
+
+ displayProvider := provider
+ if providerConfigured {
+ displayProvider.Name = cmp.Or(providerConfig.Name, displayProvider.Name)
+ modelIndex := make(map[string]int, len(displayProvider.Models))
+ for i, model := range displayProvider.Models {
+ modelIndex[model.ID] = i
+ }
+ for _, model := range providerConfig.Models {
+ if model.ID == "" {
+ continue
+ }
+ if idx, ok := modelIndex[model.ID]; ok {
+ if model.Name != "" {
+ displayProvider.Models[idx].Name = model.Name
+ }
+ continue
+ }
+ if model.Name == "" {
+ model.Name = model.ID
+ }
+ displayProvider.Models = append(displayProvider.Models, model)
+ modelIndex[model.ID] = len(displayProvider.Models) - 1
+ }
+ }
+
+ name := displayProvider.Name
+ if name == "" {
+ name = providerID
+ }
+
+ group := NewModelGroup(t, name, providerConfigured)
+ for _, model := range displayProvider.Models {
+ item := NewModelItem(t, provider, model)
+ group.AppendItems(item)
+ itemsMap[item.ID()] = item
+ if model.ID == currentModel.Model && string(provider.ID) == currentModel.Provider {
+ selectedItemID = item.ID()
+ }
+ }
+
+ groups = append(groups, group)
+ }
+
+ if len(recentItems) > 0 {
+ recentGroup := NewModelGroup(t, "Recently used", false)
+
+ var validRecentItems []config.SelectedModel
+ for _, recent := range recentItems {
+ key := modelKey(recent.Provider, recent.Model)
+ item, ok := itemsMap[key]
+ if !ok {
+ continue
+ }
+
+ validRecentItems = append(validRecentItems, recent)
+ recentGroup.AppendItems(item)
+ if recent.Model == currentModel.Model && recent.Provider == currentModel.Provider {
+ selectedItemID = item.ID()
+ }
+ }
+
+ if len(validRecentItems) != len(recentItems) {
+ // FIXME: Does this need to be here? Is it mutating the config during a read?
+ if err := cfg.SetConfigField(fmt.Sprintf("recent_models.%s", selectedType), validRecentItems); err != nil {
+ return fmt.Errorf("failed to update recent models: %w", err)
+ }
+ }
+
+ if len(recentGroup.Items) > 0 {
+ groups = append([]ModelGroup{recentGroup}, groups...)
+ }
+ }
+
+ // Set model groups in the list.
+ m.list.SetGroups(groups...)
+ m.list.SetSelectedItem(selectedItemID)
+ // Update placeholder based on model type
+ if m.modelType == ModelTypeLarge {
+ m.input.Placeholder = largeModelInputPlaceholder
+ } else {
+ m.input.Placeholder = smallModelInputPlaceholder
+ }
+
+ return nil
+}
+
+func getFilteredProviders(cfg *config.Config) ([]catwalk.Provider, error) {
+ providers, err := config.Providers(cfg)
+ if err != nil {
+ return nil, fmt.Errorf("failed to get providers: %w", err)
+ }
+ filteredProviders := []catwalk.Provider{}
+ for _, p := range providers {
+ hasAPIKeyEnv := strings.HasPrefix(p.APIKey, "$")
+ if hasAPIKeyEnv && p.ID != catwalk.InferenceProviderAzure {
+ filteredProviders = append(filteredProviders, p)
+ }
+ }
+ return filteredProviders, nil
+}
+
+func modelKey(providerID, modelID string) string {
+ if providerID == "" || modelID == "" {
+ return ""
+ }
+ return providerID + ":" + modelID
+}
@@ -0,0 +1,95 @@
+package dialog
+
+import (
+ "charm.land/lipgloss/v2"
+ "github.com/charmbracelet/catwalk/pkg/catwalk"
+ "github.com/charmbracelet/crush/internal/ui/common"
+ "github.com/charmbracelet/crush/internal/ui/styles"
+ "github.com/charmbracelet/x/ansi"
+ "github.com/sahilm/fuzzy"
+)
+
+// ModelGroup represents a group of model items.
+type ModelGroup struct {
+ Title string
+ Items []*ModelItem
+ configured bool
+ t *styles.Styles
+}
+
+// NewModelGroup creates a new ModelGroup.
+func NewModelGroup(t *styles.Styles, title string, configured bool, items ...*ModelItem) ModelGroup {
+ return ModelGroup{
+ Title: title,
+ Items: items,
+ t: t,
+ }
+}
+
+// AppendItems appends [ModelItem]s to the group.
+func (m *ModelGroup) AppendItems(items ...*ModelItem) {
+ m.Items = append(m.Items, items...)
+}
+
+// Render implements [list.Item].
+func (m *ModelGroup) Render(width int) string {
+ var configured string
+ if m.configured {
+ configuredIcon := m.t.ToolCallSuccess.Render()
+ configuredText := m.t.Subtle.Render("Configured")
+ configured = configuredIcon + " " + configuredText
+ }
+
+ title := " " + m.Title + " "
+ title = ansi.Truncate(title, max(0, width-lipgloss.Width(configured)-1), "…")
+
+ return common.Section(m.t, title, width, configured)
+}
+
+// ModelItem represents a list item for a model type.
+type ModelItem struct {
+ prov catwalk.Provider
+ model catwalk.Model
+
+ cache map[int]string
+ t *styles.Styles
+ m fuzzy.Match
+ focused bool
+}
+
+var _ ListItem = &ModelItem{}
+
+// NewModelItem creates a new ModelItem.
+func NewModelItem(t *styles.Styles, prov catwalk.Provider, model catwalk.Model) *ModelItem {
+ return &ModelItem{
+ prov: prov,
+ model: model,
+ t: t,
+ cache: make(map[int]string),
+ }
+}
+
+// Filter implements ListItem.
+func (m *ModelItem) Filter() string {
+ return m.model.Name
+}
+
+// ID implements ListItem.
+func (m *ModelItem) ID() string {
+ return modelKey(string(m.prov.ID), m.model.ID)
+}
+
+// Render implements ListItem.
+func (m *ModelItem) Render(width int) string {
+ return renderItem(m.t, m.model.Name, 0, m.focused, width, m.cache, &m.m)
+}
+
+// SetFocused implements ListItem.
+func (m *ModelItem) SetFocused(focused bool) {
+ m.focused = focused
+}
+
+// SetMatch implements ListItem.
+func (m *ModelItem) SetMatch(fm fuzzy.Match) {
+ m.m = fm
+}
@@ -0,0 +1,163 @@
+package dialog
+
+import (
+ "slices"
+
+ "github.com/charmbracelet/crush/internal/ui/list"
+ "github.com/charmbracelet/crush/internal/ui/styles"
+ "github.com/sahilm/fuzzy"
+)
+
+// ModelsList is a list specifically for model items and groups.
+type ModelsList struct {
+ *list.List
+ groups []ModelGroup
+ items []list.Item
+ query string
+ t *styles.Styles
+}
+
+// NewModelsList creates a new list suitable for model items and groups.
+func NewModelsList(sty *styles.Styles, groups ...ModelGroup) *ModelsList {
+ f := &ModelsList{
+ List: list.NewList(),
+ groups: groups,
+ t: sty,
+ }
+ return f
+}
+
+// SetGroups sets the model groups and updates the list items.
+func (f *ModelsList) SetGroups(groups ...ModelGroup) {
+ f.groups = groups
+}
+
+// SetFilter sets the filter query and updates the list items.
+func (f *ModelsList) SetFilter(q string) {
+ f.query = q
+}
+
+// SetSelectedItem sets the selected item in the list by item ID.
+func (f *ModelsList) SetSelectedItem(itemID string) {
+ count := 0
+ for _, g := range f.groups {
+ for _, item := range g.Items {
+ if item.ID() == itemID {
+ f.List.SetSelected(count)
+ return
+ }
+ count++
+ }
+ }
+}
+
+// SelectNext selects the next selectable item in the list.
+func (f *ModelsList) SelectNext() bool {
+ for f.List.SelectNext() {
+ if _, ok := f.List.SelectedItem().(*ModelItem); ok {
+ return true
+ }
+ }
+ return false
+}
+
+// SelectPrev selects the previous selectable item in the list.
+func (f *ModelsList) SelectPrev() bool {
+ for f.List.SelectPrev() {
+ if _, ok := f.List.SelectedItem().(*ModelItem); ok {
+ return true
+ }
+ }
+ return false
+}
+
+// VisibleItems returns the visible items after filtering.
+func (f *ModelsList) VisibleItems() []list.Item {
+ if len(f.query) == 0 {
+ // No filter, return all items with group headers
+ items := []list.Item{}
+ for _, g := range f.groups {
+ items = append(items, &g)
+ for _, item := range g.Items {
+ items = append(items, item)
+ }
+ // Add a space separator after each provider section
+ items = append(items, list.NewSpacerItem(1))
+ }
+ return items
+ }
+
+ groupItems := map[int][]*ModelItem{}
+ filterableItems := []list.FilterableItem{}
+ for i, g := range f.groups {
+ for _, item := range g.Items {
+ filterableItems = append(filterableItems, item)
+ groupItems[i] = append(groupItems[i], item)
+ }
+ }
+
+ matches := fuzzy.FindFrom(f.query, list.FilterableItemsSource(filterableItems))
+ for _, match := range matches {
+ item := filterableItems[match.Index]
+ if ms, ok := item.(list.MatchSettable); ok {
+ ms.SetMatch(match)
+ item = ms.(list.FilterableItem)
+ }
+ filterableItems = append(filterableItems, item)
+ }
+
+ items := []list.Item{}
+ visitedGroups := map[int]bool{}
+
+ // Reconstruct groups with matched items
+ for _, match := range matches {
+ item := filterableItems[match.Index]
+ // Find which group this item belongs to
+ for gi, g := range f.groups {
+ if slices.Contains(groupItems[gi], item.(*ModelItem)) {
+ if !visitedGroups[gi] {
+ // Add section header
+ items = append(items, &g)
+ visitedGroups[gi] = true
+ }
+ // Add the matched item
+ if ms, ok := item.(list.MatchSettable); ok {
+ ms.SetMatch(match)
+ item = ms.(list.FilterableItem)
+ }
+ // Add a space separator after each provider section
+ items = append(items, item, list.NewSpacerItem(1))
+ break
+ }
+ }
+ }
+
+ return items
+}
+
+// Render renders the filterable list.
+func (f *ModelsList) Render() string {
+ f.List.SetItems(f.VisibleItems()...)
+ return f.List.Render()
+}
+
+type modelGroups []ModelGroup
+
+func (m modelGroups) Len() int {
+ n := 0
+ for _, g := range m {
+ n += len(g.Items)
+ }
+ return n
+}
+
+func (m modelGroups) String(i int) string {
+ count := 0
+ for _, g := range m {
+ if i < count+len(g.Items) {
+ return g.Items[i-count].Filter()
+ }
+ count += len(g.Items)
+ }
+ return ""
+}
@@ -494,7 +494,9 @@ func (m *UI) handleKeyPressMsg(msg tea.KeyPressMsg) tea.Cmd {
}
return true
case key.Matches(msg, m.keyMap.Models):
- // TODO: Implement me
+ if cmd := m.openModelsDialog(); cmd != nil {
+ cmds = append(cmds, cmd)
+ }
return true
case key.Matches(msg, m.keyMap.Sessions):
if m.dialog.ContainsDialog(dialog.SessionsID) {
@@ -1375,6 +1377,25 @@ func (m *UI) openQuitDialog() tea.Cmd {
return nil
}
+// openModelsDialog opens the models dialog.
+func (m *UI) openModelsDialog() tea.Cmd {
+ if m.dialog.ContainsDialog(dialog.ModelsID) {
+ // Bring to front
+ m.dialog.BringToFront(dialog.ModelsID)
+ return nil
+ }
+
+ modelsDialog, err := dialog.NewModels(m.com)
+ if err != nil {
+ return uiutil.ReportError(err)
+ }
+
+ modelsDialog.SetSize(min(60, m.width-8), 30)
+ m.dialog.OpenDialog(modelsDialog)
+
+ return nil
+}
+
// openCommandsDialog opens the commands dialog.
func (m *UI) openCommandsDialog() tea.Cmd {
if m.dialog.ContainsDialog(dialog.CommandsID) {
@@ -32,6 +32,9 @@ const (
ToolSuccess string = "✓"
ToolError string = "×"
+ RadioOn string = "◉"
+ RadioOff string = "○"
+
BorderThin string = "│"
BorderThick string = "▌"
@@ -51,9 +54,10 @@ type Styles struct {
WindowTooSmall lipgloss.Style
// Reusable text styles
- Base lipgloss.Style
- Muted lipgloss.Style
- Subtle lipgloss.Style
+ Base lipgloss.Style
+ Muted lipgloss.Style
+ HalfMuted lipgloss.Style
+ Subtle lipgloss.Style
// Tags
TagBase lipgloss.Style
@@ -123,6 +127,10 @@ type Styles struct {
EditorPromptYoloDotsFocused lipgloss.Style
EditorPromptYoloDotsBlurred lipgloss.Style
+ // Radio
+ RadioOn lipgloss.Style
+ RadioOff lipgloss.Style
+
// Background
Background color.Color
@@ -291,9 +299,7 @@ type Styles struct {
List lipgloss.Style
- Commands struct {
- CommandTypeSelector lipgloss.Style
- }
+ Commands struct{}
}
}
@@ -917,6 +923,7 @@ func DefaultStyles() Styles {
// text presets
s.Base = lipgloss.NewStyle().Foreground(fgBase)
s.Muted = lipgloss.NewStyle().Foreground(fgMuted)
+ s.HalfMuted = lipgloss.NewStyle().Foreground(fgHalfMuted)
s.Subtle = lipgloss.NewStyle().Foreground(fgSubtle)
s.WindowTooSmall = s.Muted
@@ -1008,6 +1015,9 @@ func DefaultStyles() Styles {
s.EditorPromptYoloDotsFocused = lipgloss.NewStyle().MarginRight(1).Foreground(charmtone.Zest).SetString(":::")
s.EditorPromptYoloDotsBlurred = s.EditorPromptYoloDotsFocused.Foreground(charmtone.Squid)
+ s.RadioOn = s.HalfMuted.SetString(RadioOn)
+ s.RadioOff = s.HalfMuted.SetString(RadioOff)
+
// Logo colors
s.LogoFieldColor = primary
s.LogoTitleColorA = secondary
@@ -1095,8 +1105,6 @@ func DefaultStyles() Styles {
s.Dialog.List = base.Margin(0, 0, 1, 0)
- s.Dialog.Commands.CommandTypeSelector = base.Foreground(fgHalfMuted)
-
return s
}