@@ -0,0 +1,445 @@
9+ "charm.land/bubbles/v2/help"
10+ "charm.land/bubbles/v2/key"
11+ "charm.land/bubbles/v2/textinput"
12+ tea "charm.land/bubbletea/v2"
13+ "charm.land/lipgloss/v2"
14+ "github.com/charmbracelet/catwalk/pkg/catwalk"
15+ "github.com/charmbracelet/crush/internal/config"
16+ "github.com/charmbracelet/crush/internal/ui/common"
17+ "github.com/charmbracelet/crush/internal/uiutil"
20+// ModelType represents the type of model to select.
24+ ModelTypeLarge ModelType = iota
28+// String returns the string representation of the [ModelType].
29+func (mt ModelType) String() string {
41+ largeModelInputPlaceholder = "Choose a model for large, complex tasks"
42+ smallModelInputPlaceholder = "Choose a model for small, simple tasks"
45+// ModelsID is the identifier for the model selection dialog.
46+const ModelsID = "models"
48+// Models represents a model selection dialog.
53+ providers []catwalk.Provider
66+ input textinput.Model
70+var _ Dialog = (*Models)(nil)
72+// NewModels creates a new Models dialog.
73+func NewModels(com *common.Common) (*Models, error) {
78+ help.Styles = t.DialogHelpStyles()
81+ m.list = NewModelsList(t)
83+ m.list.SetSelected(0)
85+ m.input = textinput.New()
86+ m.input.SetVirtualCursor(false)
87+ m.input.Placeholder = largeModelInputPlaceholder
88+ m.input.SetStyles(com.Styles.TextInput)
91+ m.keyMap.Tab = key.NewBinding(
92+ key.WithKeys("tab", "shift+tab"),
93+ key.WithHelp("tab", "toggle type"),
95+ m.keyMap.Select = key.NewBinding(
96+ key.WithKeys("enter", "ctrl+y"),
97+ key.WithHelp("enter", "confirm"),
99+ m.keyMap.UpDown = key.NewBinding(
100+ key.WithKeys("up", "down"),
101+ key.WithHelp("↑/↓", "choose"),
103+ m.keyMap.Next = key.NewBinding(
104+ key.WithKeys("down", "ctrl+n"),
105+ key.WithHelp("↓", "next item"),
107+ m.keyMap.Previous = key.NewBinding(
108+ key.WithKeys("up", "ctrl+p"),
109+ key.WithHelp("↑", "previous item"),
111+ m.keyMap.Close = CloseKey
113+ providers, err := getFilteredProviders(com.Config())
115+ return nil, fmt.Errorf("failed to get providers: %w", err)
118+ m.providers = providers
119+ if err := m.setProviderItems(); err != nil {
120+ return nil, fmt.Errorf("failed to set provider items: %w", err)
126+// SetSize sets the size of the dialog.
127+func (m *Models) SetSize(width, height int) {
131+ innerWidth := width - t.Dialog.View.GetHorizontalFrameSize()
132+ heightOffset := t.Dialog.Title.GetVerticalFrameSize() + 1 + // (1) title content
133+ t.Dialog.InputPrompt.GetVerticalFrameSize() + 1 + // (1) input content
134+ t.Dialog.HelpView.GetVerticalFrameSize() +
135+ t.Dialog.View.GetVerticalFrameSize()
136+ m.input.SetWidth(innerWidth - t.Dialog.InputPrompt.GetHorizontalFrameSize() - 1) // (1) cursor padding
137+ m.list.SetSize(innerWidth, height-heightOffset)
138+ m.help.SetWidth(width)
141+// ID implements Dialog.
142+func (m *Models) ID() string {
146+// Update implements Dialog.
147+func (m *Models) Update(msg tea.Msg) tea.Msg {
148+ switch msg := msg.(type) {
149+ case tea.KeyPressMsg:
151+ case key.Matches(msg, m.keyMap.Close):
153+ case key.Matches(msg, m.keyMap.Previous):
155+ if m.list.IsSelectedFirst() {
157+ m.list.ScrollToBottom()
161+ m.list.ScrollToSelected()
162+ case key.Matches(msg, m.keyMap.Next):
164+ if m.list.IsSelectedLast() {
165+ m.list.SelectFirst()
166+ m.list.ScrollToTop()
170+ m.list.ScrollToSelected()
171+ case key.Matches(msg, m.keyMap.Select):
172+ if selectedItem := m.list.SelectedItem(); selectedItem != nil {
173+ // TODO: Handle model selection confirmation.
175+ case key.Matches(msg, m.keyMap.Tab):
176+ if m.modelType == ModelTypeLarge {
177+ m.modelType = ModelTypeSmall
179+ m.modelType = ModelTypeLarge
181+ if err := m.setProviderItems(); err != nil {
182+ return uiutil.ReportError(err)
186+ m.input, cmd = m.input.Update(msg)
187+ value := m.input.Value()
188+ m.list.SetFilter(value)
189+ m.list.ScrollToSelected()
196+// Cursor returns the cursor for the dialog.
197+func (m *Models) Cursor() *tea.Cursor {
198+ return InputCursor(m.com.Styles, m.input.Cursor())
201+// modelTypeRadioView returns the radio view for model type selection.
202+func (m *Models) modelTypeRadioView() string {
204+ textStyle := t.HalfMuted
205+ largeRadioStyle := t.RadioOff
206+ smallRadioStyle := t.RadioOff
207+ if m.modelType == ModelTypeLarge {
208+ largeRadioStyle = t.RadioOn
210+ smallRadioStyle = t.RadioOn
213+ largeRadio := largeRadioStyle.Padding(0, 1).Render()
214+ smallRadio := smallRadioStyle.Padding(0, 1).Render()
216+ return fmt.Sprintf("%s%s %s%s",
217+ largeRadio, textStyle.Render(ModelTypeLarge.String()),
218+ smallRadio, textStyle.Render(ModelTypeSmall.String()))
221+// View implements Dialog.
222+func (m *Models) View() string {
224+ titleStyle := t.Dialog.Title
225+ dialogStyle := t.Dialog.View
227+ radios := m.modelTypeRadioView()
229+ headerOffset := lipgloss.Width(radios) + titleStyle.GetHorizontalFrameSize() +
230+ dialogStyle.GetHorizontalFrameSize()
232+ header := common.DialogTitle(t, "Switch Model", m.width-headerOffset) + radios
234+ return HeaderInputListHelpView(t, m.width, m.list.Height(), header,
235+ m.input.View(), m.list.Render(), m.help.View(m))
238+// ShortHelp returns the short help view.
239+func (m *Models) ShortHelp() []key.Binding {
240+ return []key.Binding{
248+// FullHelp returns the full help view.
249+func (m *Models) FullHelp() [][]key.Binding {
250+ return [][]key.Binding{
263+// setProviderItems sets the provider items in the list.
264+func (m *Models) setProviderItems() error {
266+ cfg := m.com.Config()
268+ selectedType := config.SelectedModelTypeLarge
269+ if m.modelType == ModelTypeLarge {
270+ selectedType = config.SelectedModelTypeLarge
272+ selectedType = config.SelectedModelTypeSmall
275+ var selectedItemID string
276+ currentModel := cfg.Models[selectedType]
277+ recentItems := cfg.RecentModels[selectedType]
279+ // Track providers already added to avoid duplicates
280+ addedProviders := make(map[string]bool)
282+ // Get a list of known providers to compare against
283+ knownProviders, err := config.Providers(cfg)
285+ return fmt.Errorf("failed to get providers: %w", err)
288+ containsProviderFunc := func(id string) func(p catwalk.Provider) bool {
289+ return func(p catwalk.Provider) bool {
290+ return p.ID == catwalk.InferenceProvider(id)
294+ // itemsMap contains the keys of added model items.
295+ itemsMap := make(map[string]*ModelItem)
296+ groups := []ModelGroup{}
297+ for id, p := range cfg.Providers.Seq2() {
302+ // Check if this provider is not in the known providers list
303+ if !slices.ContainsFunc(knownProviders, containsProviderFunc(id)) ||
304+ !slices.ContainsFunc(m.providers, containsProviderFunc(id)) {
305+ provider := p.ToProvider()
307+ // Add this unknown provider to the list
313+ addedProviders[id] = true
315+ group := NewModelGroup(t, name, true)
316+ for _, model := range p.Models {
317+ item := NewModelItem(t, provider, model)
318+ group.AppendItems(item)
319+ itemsMap[item.ID()] = item
320+ if model.ID == currentModel.Model && string(provider.ID) == currentModel.Provider {
321+ selectedItemID = item.ID()
327+ // Now add known providers from the predefined list
328+ for _, provider := range m.providers {
329+ providerID := string(provider.ID)
330+ if addedProviders[providerID] {
334+ providerConfig, providerConfigured := cfg.Providers.Get(providerID)
335+ if providerConfigured && providerConfig.Disable {
339+ displayProvider := provider
340+ if providerConfigured {
341+ displayProvider.Name = cmp.Or(providerConfig.Name, displayProvider.Name)
342+ modelIndex := make(map[string]int, len(displayProvider.Models))
343+ for i, model := range displayProvider.Models {
344+ modelIndex[model.ID] = i
346+ for _, model := range providerConfig.Models {
350+ if idx, ok := modelIndex[model.ID]; ok {
351+ if model.Name != "" {
352+ displayProvider.Models[idx].Name = model.Name
356+ if model.Name == "" {
357+ model.Name = model.ID
359+ displayProvider.Models = append(displayProvider.Models, model)
360+ modelIndex[model.ID] = len(displayProvider.Models) - 1
364+ name := displayProvider.Name
369+ group := NewModelGroup(t, name, providerConfigured)
370+ for _, model := range displayProvider.Models {
371+ item := NewModelItem(t, provider, model)
372+ group.AppendItems(item)
373+ itemsMap[item.ID()] = item
374+ if model.ID == currentModel.Model && string(provider.ID) == currentModel.Provider {
375+ selectedItemID = item.ID()
379+ groups = append(groups, group)
382+ if len(recentItems) > 0 {
383+ recentGroup := NewModelGroup(t, "Recently used", false)
385+ var validRecentItems []config.SelectedModel
386+ for _, recent := range recentItems {
387+ key := modelKey(recent.Provider, recent.Model)
388+ item, ok := itemsMap[key]
393+ validRecentItems = append(validRecentItems, recent)
394+ recentGroup.AppendItems(item)
395+ if recent.Model == currentModel.Model && recent.Provider == currentModel.Provider {
396+ selectedItemID = item.ID()
400+ if len(validRecentItems) != len(recentItems) {
401+ // FIXME: Does this need to be here? Is it mutating the config during a read?
402+ if err := cfg.SetConfigField(fmt.Sprintf("recent_models.%s", selectedType), validRecentItems); err != nil {
403+ return fmt.Errorf("failed to update recent models: %w", err)
407+ if len(recentGroup.Items) > 0 {
408+ groups = append([]ModelGroup{recentGroup}, groups...)
412+ // Set model groups in the list.
413+ m.list.SetGroups(groups...)
414+ m.list.SetSelectedItem(selectedItemID)
415+ // Update placeholder based on model type
416+ if m.modelType == ModelTypeLarge {
417+ m.input.Placeholder = largeModelInputPlaceholder
419+ m.input.Placeholder = smallModelInputPlaceholder
425+func getFilteredProviders(cfg *config.Config) ([]catwalk.Provider, error) {
426+ providers, err := config.Providers(cfg)
428+ return nil, fmt.Errorf("failed to get providers: %w", err)
430+ filteredProviders := []catwalk.Provider{}
431+ for _, p := range providers {
432+ hasAPIKeyEnv := strings.HasPrefix(p.APIKey, "$")
433+ if hasAPIKeyEnv && p.ID != catwalk.InferenceProviderAzure {
434+ filteredProviders = append(filteredProviders, p)
437+ return filteredProviders, nil
440+func modelKey(providerID, modelID string) string {
441+ if providerID == "" || modelID == "" {
444+ return providerID + ":" + modelID