diff --git a/go.mod b/go.mod index 0e3f432a3e3262516cc910f1e3c0c309b05d0e8a..7795febc9cf458a4830ac5ecfe849c3557024786 100644 --- a/go.mod +++ b/go.mod @@ -24,6 +24,7 @@ require ( github.com/fsnotify/fsnotify v1.8.0 github.com/go-logfmt/logfmt v0.6.0 github.com/google/uuid v1.6.0 + github.com/invopop/jsonschema v0.13.0 github.com/mark3labs/mcp-go v0.17.0 github.com/muesli/termenv v0.16.0 github.com/ncruces/go-sqlite3 v0.25.0 @@ -42,7 +43,6 @@ require ( require ( github.com/bahlo/generic-list-go v0.2.0 // indirect github.com/buger/jsonparser v1.1.1 // indirect - github.com/invopop/jsonschema v0.13.0 // indirect github.com/mailru/easyjson v0.7.7 // indirect github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect ) diff --git a/internal/tui/components/dialogs/models/keys.go b/internal/tui/components/dialogs/models/keys.go index f9c3c549c30c6d95282e88818c247ec80a2f0e4c..dd34f82860b21a98a6ae4a5eea57aa052b383f33 100644 --- a/internal/tui/components/dialogs/models/keys.go +++ b/internal/tui/components/dialogs/models/keys.go @@ -8,13 +8,14 @@ type KeyMap struct { Select, Next, Previous, + Tab, Close key.Binding } func DefaultKeyMap() KeyMap { return KeyMap{ Select: key.NewBinding( - key.WithKeys("enter", "tab", "ctrl+y"), + key.WithKeys("enter", "ctrl+y"), key.WithHelp("enter", "confirm"), ), Next: key.NewBinding( @@ -25,6 +26,10 @@ func DefaultKeyMap() KeyMap { key.WithKeys("up", "ctrl+p"), key.WithHelp("↑", "previous item"), ), + Tab: key.NewBinding( + key.WithKeys("tab"), + key.WithHelp("tab", "toggle model type"), + ), Close: key.NewBinding( key.WithKeys("esc"), key.WithHelp("esc", "cancel"), @@ -38,6 +43,7 @@ func (k KeyMap) KeyBindings() []key.Binding { k.Select, k.Next, k.Previous, + k.Tab, k.Close, } } @@ -57,10 +63,10 @@ func (k KeyMap) FullHelp() [][]key.Binding { func (k KeyMap) ShortHelp() []key.Binding { return []key.Binding{ key.NewBinding( - key.WithKeys("down", "up"), key.WithHelp("↑↓", "choose"), ), + k.Tab, k.Select, k.Close, } diff --git a/internal/tui/components/dialogs/models/models.go b/internal/tui/components/dialogs/models/models.go index 730cc5d5693dd06821c1ff30cb26398b14325c62..8e81777d7e3e6c45628cb8839b52c484e47f5640 100644 --- a/internal/tui/components/dialogs/models/models.go +++ b/internal/tui/components/dialogs/models/models.go @@ -22,9 +22,15 @@ const ( defaultWidth = 60 ) +const ( + LargeModelType int = iota + SmallModelType +) + // ModelSelectedMsg is sent when a model is selected type ModelSelectedMsg struct { - Model config.PreferredModel + Model config.PreferredModel + ModelType config.ModelType } // CloseModelDialogMsg is sent when a model is selected @@ -42,12 +48,13 @@ type ModelOption struct { type modelDialogCmp struct { width int - wWidth int // Width of the terminal window - wHeight int // Height of the terminal window + wWidth int + wHeight int modelList list.ListModel keyMap KeyMap help help.Model + modelType int } func NewModelDialogCmp() ModelDialog { @@ -80,34 +87,13 @@ func NewModelDialogCmp() ModelDialog { width: defaultWidth, keyMap: DefaultKeyMap(), help: help, + modelType: LargeModelType, } } func (m *modelDialogCmp) Init() tea.Cmd { - providers := config.Providers() - - modelItems := []util.Model{} - selectIndex := 0 - agentModel := config.GetAgentModel(config.AgentCoder) - agentProvider := config.GetAgentProvider(config.AgentCoder) - for _, provider := range providers { - name := provider.Name - if name == "" { - name = string(provider.ID) - } - modelItems = append(modelItems, commands.NewItemSection(name)) - for _, model := range provider.Models { - modelItems = append(modelItems, completions.NewCompletionItem(model.Name, ModelOption{ - Provider: provider, - Model: model, - })) - if model.ID == agentModel.ID && provider.ID == agentProvider.ID { - selectIndex = len(modelItems) - 1 // Set the selected index to the current model - } - } - } - - return tea.Sequence(m.modelList.Init(), m.modelList.SetItems(modelItems), m.modelList.SetSelected(selectIndex)) + m.SetModelType(m.modelType) + return m.modelList.Init() } func (m *modelDialogCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) { @@ -115,24 +101,41 @@ func (m *modelDialogCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) { case tea.WindowSizeMsg: m.wWidth = msg.Width m.wHeight = msg.Height + m.SetModelType(m.modelType) return m, m.modelList.SetSize(m.listWidth(), m.listHeight()) case tea.KeyPressMsg: switch { case key.Matches(msg, m.keyMap.Select): selectedItemInx := m.modelList.SelectedIndex() if selectedItemInx == list.NoSelection { - return m, nil // No item selected, do nothing + return m, nil } items := m.modelList.Items() selectedItem := items[selectedItemInx].(completions.CompletionItem).Value().(ModelOption) + var modelType config.ModelType + if m.modelType == LargeModelType { + modelType = config.LargeModel + } else { + modelType = config.SmallModel + } + return m, tea.Sequence( util.CmdHandler(dialogs.CloseDialogMsg{}), - util.CmdHandler(ModelSelectedMsg{Model: config.PreferredModel{ - ModelID: selectedItem.Model.ID, - Provider: selectedItem.Provider.ID, - }}), + util.CmdHandler(ModelSelectedMsg{ + Model: config.PreferredModel{ + ModelID: selectedItem.Model.ID, + Provider: selectedItem.Provider.ID, + }, + ModelType: modelType, + }), ) + case key.Matches(msg, m.keyMap.Tab): + if m.modelType == LargeModelType { + return m, m.SetModelType(SmallModelType) + } else { + return m, m.SetModelType(LargeModelType) + } case key.Matches(msg, m.keyMap.Close): return m, util.CmdHandler(dialogs.CloseDialogMsg{}) default: @@ -147,9 +150,10 @@ func (m *modelDialogCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) { func (m *modelDialogCmp) View() tea.View { t := styles.CurrentTheme() listView := m.modelList.View() + radio := m.modelTypeRadio() content := lipgloss.JoinVertical( lipgloss.Left, - t.S().Base.Padding(0, 1, 1, 1).Render(core.Title("Switch Model", m.width-4)), + t.S().Base.Padding(0, 1, 1, 1).Render(core.Title("Switch Model", m.width-lipgloss.Width(radio)-5)+" "+radio), listView.String(), "", t.S().Base.Width(m.width-2).PaddingLeft(1).AlignHorizontal(lipgloss.Left).Render(m.help.View(m.keyMap)), @@ -197,3 +201,49 @@ func (m *modelDialogCmp) moveCursor(cursor *tea.Cursor) *tea.Cursor { func (m *modelDialogCmp) ID() dialogs.DialogID { return ModelsDialogID } + +func (m *modelDialogCmp) modelTypeRadio() string { + t := styles.CurrentTheme() + choices := []string{"Large", "Small"} + iconSelected := "◉" + iconUnselected := "○" + if m.modelType == LargeModelType { + return t.S().Base.Foreground(t.FgHalfMuted).Render(iconSelected + " " + choices[0] + " " + iconUnselected + " " + choices[1]) + } + return t.S().Base.Foreground(t.FgHalfMuted).Render(iconUnselected + " " + choices[0] + " " + iconSelected + " " + choices[1]) +} + +func (m *modelDialogCmp) SetModelType(modelType int) tea.Cmd { + m.modelType = modelType + + providers := config.Providers() + modelItems := []util.Model{} + selectIndex := 0 + + cfg := config.Get() + var currentModel config.PreferredModel + if m.modelType == LargeModelType { + currentModel = cfg.Models.Large + } else { + currentModel = cfg.Models.Small + } + + for _, provider := range providers { + name := provider.Name + if name == "" { + name = string(provider.ID) + } + modelItems = append(modelItems, commands.NewItemSection(name)) + for _, model := range provider.Models { + modelItems = append(modelItems, completions.NewCompletionItem(model.Name, ModelOption{ + Provider: provider, + Model: model, + })) + if model.ID == currentModel.ModelID && provider.ID == currentModel.Provider { + selectIndex = len(modelItems) - 1 // Set the selected index to the current model + } + } + } + + return tea.Sequence(m.modelList.SetItems(modelItems), m.modelList.SetSelected(selectIndex)) +} diff --git a/internal/tui/tui.go b/internal/tui/tui.go index e3c974ca002529ce1ac90f420afcc5eedf2a45fd..46a3f3c2a4ee2a6f4b2441a3d10e98f7e46eca2a 100644 --- a/internal/tui/tui.go +++ b/internal/tui/tui.go @@ -172,7 +172,7 @@ func (a *appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { // Model Switch case models.ModelSelectedMsg: - config.UpdatePreferredModel(config.LargeModel, msg.Model) + config.UpdatePreferredModel(msg.ModelType, msg.Model) // Update the agent with the new model/provider configuration if err := a.app.UpdateAgentModel(); err != nil { @@ -180,7 +180,11 @@ func (a *appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return a, util.ReportError(fmt.Errorf("model changed to %s but failed to update agent: %v", msg.Model.ModelID, err)) } - return a, util.ReportInfo(fmt.Sprintf("Model changed to %s", msg.Model.ModelID)) + modelTypeName := "large" + if msg.ModelType == config.SmallModel { + modelTypeName = "small" + } + return a, util.ReportInfo(fmt.Sprintf("%s model changed to %s", modelTypeName, msg.Model.ModelID)) // File Picker case chat.OpenFilePickerMsg: