on main help sign in
feat(ui): model dialog: implement model selection handling
commit90491b4b8ae5be3605cd92d77418edfd5aa4f3e1 authorAyman Bagabas <ayman.bagabas@gmail.com> dateMon 5 Jan 2026, 12:45 (1 year ago) parent5df4696
5 files changed+81−24
internal/config/config.go+5 internal/ui/dialog/messages.go+3−3 internal/ui/dialog/models.go+31−17 internal/ui/dialog/models_item.go+21−3 internal/ui/model/ui.go+21−1
internal/config/config.go+5
@@ -54,6 +54,11 @@ var defaultContextPaths = []string{
5454
5555type SelectedModelType string
5656
57+// String returns the string representation of the [SelectedModelType].
58+func (s SelectedModelType) String() string {
59+ return string(s)
60+}
61+
5762const (
5863 SelectedModelTypeLarge SelectedModelType = "large"
5964 SelectedModelTypeSmall SelectedModelType = "small"
internal/ui/dialog/messages.go+3−3
@@ -2,7 +2,7 @@ package dialog
22
33import (
44 tea "charm.land/bubbletea/v2"
5− "github.com/charmbracelet/catwalk/pkg/catwalk"
5+ "github.com/charmbracelet/crush/internal/config"
66 "github.com/charmbracelet/crush/internal/session"
77)
88
@@ -24,8 +24,8 @@ type SessionSelectedMsg struct {
2424
2525// ModelSelectedMsg is a message indicating a model has been selected.
2626type ModelSelectedMsg struct {
27− Provider catwalk.Provider
28− Model catwalk.Model
27+ Model config.SelectedModel
28+ ModelType config.SelectedModelType
2929}
3030
3131// Messages for commands
internal/ui/dialog/models.go+31−17
@@ -37,6 +37,30 @@ func (mt ModelType) String() string {
3737 }
3838}
3939
40+// Config returns the corresponding config model type.
41+func (mt ModelType) Config() config.SelectedModelType {
42+ switch mt {
43+ case ModelTypeLarge:
44+ return config.SelectedModelTypeLarge
45+ case ModelTypeSmall:
46+ return config.SelectedModelTypeSmall
47+ default:
48+ return ""
49+ }
50+}
51+
52+// Placeholder returns the input placeholder for the model type.
53+func (mt ModelType) Placeholder() string {
54+ switch mt {
55+ case ModelTypeLarge:
56+ return largeModelInputPlaceholder
57+ case ModelTypeSmall:
58+ return smallModelInputPlaceholder
59+ default:
60+ return ""
61+ }
62+}
63+
4064const (
4165 largeModelInputPlaceholder = "Choose a model for large, complex tasks"
4266 smallModelInputPlaceholder = "Choose a model for small, simple tasks"
@@ -180,8 +204,8 @@ func (m *Models) Update(msg tea.Msg) tea.Msg {
180204 }
181205
182206 return ModelSelectedMsg{
183− Provider: modelItem.prov,
184− Model: modelItem.model,
207+ Model: modelItem.SelectedModel(),
208+ ModelType: modelItem.SelectedModelType(),
185209 }
186210 case key.Matches(msg, m.keyMap.Tab):
187211 if m.modelType == ModelTypeLarge {
@@ -276,14 +300,8 @@ func (m *Models) setProviderItems() error {
276300 t := m.com.Styles
277301 cfg := m.com.Config()
278302
279− selectedType := config.SelectedModelTypeLarge
280− if m.modelType == ModelTypeLarge {
281− selectedType = config.SelectedModelTypeLarge
282− } else {
283− selectedType = config.SelectedModelTypeSmall
284− }
285−
286303 var selectedItemID string
304+ selectedType := m.modelType.Config()
287305 currentModel := cfg.Models[selectedType]
288306 recentItems := cfg.RecentModels[selectedType]
289307
@@ -325,7 +343,7 @@ func (m *Models) setProviderItems() error {
325343
326344 group := NewModelGroup(t, name, true)
327345 for _, model := range p.Models {
328− item := NewModelItem(t, provider, model, false)
346+ item := NewModelItem(t, provider, model, m.modelType, false)
329347 group.AppendItems(item)
330348 itemsMap[item.ID()] = item
331349 if model.ID == currentModel.Model && string(provider.ID) == currentModel.Provider {
@@ -379,7 +397,7 @@ func (m *Models) setProviderItems() error {
379397
380398 group := NewModelGroup(t, name, providerConfigured)
381399 for _, model := range displayProvider.Models {
382− item := NewModelItem(t, provider, model, false)
400+ item := NewModelItem(t, provider, model, m.modelType, false)
383401 group.AppendItems(item)
384402 itemsMap[item.ID()] = item
385403 if model.ID == currentModel.Model && string(provider.ID) == currentModel.Provider {
@@ -402,7 +420,7 @@ func (m *Models) setProviderItems() error {
402420 }
403421
404422 // Show provider for recent items
405− item = NewModelItem(t, item.prov, item.model, true)
423+ item = NewModelItem(t, item.prov, item.model, m.modelType, true)
406424 item.showProvider = true
407425
408426 validRecentItems = append(validRecentItems, recent)
@@ -429,11 +447,7 @@ func (m *Models) setProviderItems() error {
429447 m.list.SetSelectedItem(selectedItemID)
430448
431449 // Update placeholder based on model type
432− if m.modelType == ModelTypeLarge {
433− m.input.Placeholder = largeModelInputPlaceholder
434− } else {
435− m.input.Placeholder = smallModelInputPlaceholder
436− }
450+ m.input.Placeholder = m.modelType.Placeholder()
437451
438452 return nil
439453}
internal/ui/dialog/models_item.go+21−3
@@ -3,6 +3,7 @@ package dialog
33import (
44 "charm.land/lipgloss/v2"
55 "github.com/charmbracelet/catwalk/pkg/catwalk"
6+ "github.com/charmbracelet/crush/internal/config"
67 "github.com/charmbracelet/crush/internal/ui/common"
78 "github.com/charmbracelet/crush/internal/ui/styles"
89 "github.com/charmbracelet/x/ansi"
@@ -49,8 +50,9 @@ func (m *ModelGroup) Render(width int) string {
4950
5051// ModelItem represents a list item for a model type.
5152type ModelItem struct {
52− prov catwalk.Provider
53− model catwalk.Model
53+ prov catwalk.Provider
54+ model catwalk.Model
55+ modelType ModelType
5456
5557 cache map[int]string
5658 t *styles.Styles
@@ -59,13 +61,29 @@ type ModelItem struct {
5961 showProvider bool
6062}
6163
64+// SelectedModel returns this model item as a [config.SelectedModel] instance.
65+func (m *ModelItem) SelectedModel() config.SelectedModel {
66+ return config.SelectedModel{
67+ Model: m.model.ID,
68+ Provider: string(m.prov.ID),
69+ ReasoningEffort: m.model.DefaultReasoningEffort,
70+ MaxTokens: m.model.DefaultMaxTokens,
71+ }
72+}
73+
74+// SelectedModelType returns the type of model represented by this item.
75+func (m *ModelItem) SelectedModelType() config.SelectedModelType {
76+ return m.modelType.Config()
77+}
78+
6279var _ ListItem = &ModelItem{}
6380
6481// NewModelItem creates a new ModelItem.
65−func NewModelItem(t *styles.Styles, prov catwalk.Provider, model catwalk.Model, showProvider bool) *ModelItem {
82+func NewModelItem(t *styles.Styles, prov catwalk.Provider, model catwalk.Model, typ ModelType, showProvider bool) *ModelItem {
6683 return &ModelItem{
6784 prov: prov,
6885 model: model,
86+ modelType: typ,
6987 t: t,
7088 cache: make(map[int]string),
7189 showProvider: showProvider,
internal/ui/model/ui.go+21−1
@@ -574,7 +574,27 @@ func (m *UI) handleKeyPressMsg(msg tea.KeyPressMsg) tea.Cmd {
574574 case dialog.QuitMsg:
575575 cmds = append(cmds, tea.Quit)
576576 case dialog.ModelSelectedMsg:
577− // TODO: Handle model switching
577+ if m.com.App.AgentCoordinator.IsBusy() {
578+ cmds = append(cmds, uiutil.ReportWarn("Agent is busy, please wait..."))
579+ break
580+ }
581+
582+ cfg := m.com.Config()
583+ if cfg == nil {
584+ cmds = append(cmds, uiutil.ReportError(errors.New("configuration not found")))
585+ break
586+ }
587+
588+ if err := cfg.UpdatePreferredModel(msg.ModelType, msg.Model); err != nil {
589+ cmds = append(cmds, uiutil.ReportError(err))
590+ }
591+
592+ // XXX: Should this be in a separate goroutine?
593+ go m.com.App.UpdateAgentModel(context.TODO())
594+
595+ modelMsg := fmt.Sprintf("%s model changed to %s", msg.ModelType, msg.Model.Model)
596+ cmds = append(cmds, uiutil.ReportInfo(modelMsg))
597+ m.dialog.CloseDialog(dialog.ModelsID)
578598 }
579599
580600 return tea.Batch(cmds...)