@@ -291,6 +291,8 @@ type Config struct {
// We currently only support large/small as values here.
Models map[SelectedModelType]SelectedModel `json:"models,omitempty" jsonschema:"description=Model configurations for different model types,example={\"large\":{\"model\":\"gpt-4o\",\"provider\":\"openai\"}}"`
+ // Recently used models stored in the data directory config.
+ RecentModels map[SelectedModelType][]SelectedModel `json:"recent_models,omitempty" jsonschema:"description=Recently used models sorted by most recent first"`
// The providers that are configured
Providers *csync.Map[string, ProviderConfig] `json:"providers,omitempty" jsonschema:"description=AI provider configurations"`
@@ -400,6 +402,9 @@ func (c *Config) UpdatePreferredModel(modelType SelectedModelType, model Selecte
if err := c.SetConfigField(fmt.Sprintf("models.%s", modelType), model); err != nil {
return fmt.Errorf("failed to update preferred model: %w", err)
}
+ if err := c.recordRecentModel(modelType, model); err != nil {
+ return err
+ }
return nil
}
@@ -467,6 +472,49 @@ func (c *Config) SetProviderAPIKey(providerID, apiKey string) error {
return nil
}
+const maxRecentModelsPerType = 5
+
+func (c *Config) recordRecentModel(modelType SelectedModelType, model SelectedModel) error {
+ if model.Provider == "" || model.Model == "" {
+ return nil
+ }
+
+ if c.RecentModels == nil {
+ c.RecentModels = make(map[SelectedModelType][]SelectedModel)
+ }
+
+ eq := func(a, b SelectedModel) bool {
+ return a.Provider == b.Provider && a.Model == b.Model
+ }
+
+ entry := SelectedModel{
+ Provider: model.Provider,
+ Model: model.Model,
+ }
+
+ current := c.RecentModels[modelType]
+ withoutCurrent := slices.DeleteFunc(slices.Clone(current), func(existing SelectedModel) bool {
+ return eq(existing, entry)
+ })
+
+ updated := append([]SelectedModel{entry}, withoutCurrent...)
+ if len(updated) > maxRecentModelsPerType {
+ updated = updated[:maxRecentModelsPerType]
+ }
+
+ if slices.EqualFunc(current, updated, eq) {
+ return nil
+ }
+
+ c.RecentModels[modelType] = updated
+
+ if err := c.SetConfigField(fmt.Sprintf("recent_models.%s", modelType), updated); err != nil {
+ return fmt.Errorf("failed to persist recent models: %w", err)
+ }
+
+ return nil
+}
+
func allToolNames() []string {
return []string{
"agent",
@@ -22,6 +22,13 @@ type ModelListComponent struct {
providers []catwalk.Provider
}
+func modelKey(providerID, modelID string) string {
+ if providerID == "" || modelID == "" {
+ return ""
+ }
+ return providerID + ":" + modelID
+}
+
func NewModelListComponent(keyMap list.KeyMap, inputPlaceholder string, shouldResize bool) *ModelListComponent {
t := styles.CurrentTheme()
inputStyle := t.S().Base.PaddingLeft(1).PaddingBottom(1)
@@ -104,14 +111,19 @@ func (m *ModelListComponent) SetModelType(modelType int) tea.Cmd {
var groups []list.Group[list.CompletionItem[ModelOption]]
// first none section
selectedItemID := ""
+ itemsByKey := make(map[string]list.CompletionItem[ModelOption])
cfg := config.Get()
var currentModel config.SelectedModel
+ selectedType := config.SelectedModelTypeLarge
if m.modelType == LargeModelType {
currentModel = cfg.Models[config.SelectedModelTypeLarge]
+ selectedType = config.SelectedModelTypeLarge
} else {
currentModel = cfg.Models[config.SelectedModelTypeSmall]
+ selectedType = config.SelectedModelTypeSmall
}
+ recentItems := cfg.RecentModels[selectedType]
configuredIcon := t.S().Base.Foreground(t.Success).Render(styles.CheckIcon)
configured := fmt.Sprintf("%s %s", configuredIcon, t.S().Subtle.Render("Configured"))
@@ -169,14 +181,17 @@ func (m *ModelListComponent) SetModelType(modelType int) tea.Cmd {
Section: section,
}
for _, model := range configProvider.Models {
- item := list.NewCompletionItem(model.Name, ModelOption{
+ modelOption := ModelOption{
Provider: configProvider,
Model: model,
- },
- list.WithCompletionID(
- fmt.Sprintf("%s:%s", providerConfig.ID, model.ID),
- ),
+ }
+ key := modelKey(string(configProvider.ID), model.ID)
+ item := list.NewCompletionItem(
+ model.Name,
+ modelOption,
+ list.WithCompletionID(key),
)
+ itemsByKey[key] = item
group.Items = append(group.Items, item)
if model.ID == currentModel.Model && string(configProvider.ID) == currentModel.Provider {
@@ -239,14 +254,17 @@ func (m *ModelListComponent) SetModelType(modelType int) tea.Cmd {
Section: section,
}
for _, model := range displayProvider.Models {
- item := list.NewCompletionItem(model.Name, ModelOption{
+ modelOption := ModelOption{
Provider: displayProvider,
Model: model,
- },
- list.WithCompletionID(
- fmt.Sprintf("%s:%s", displayProvider.ID, model.ID),
- ),
+ }
+ key := modelKey(string(displayProvider.ID), model.ID)
+ item := list.NewCompletionItem(
+ model.Name,
+ modelOption,
+ list.WithCompletionID(key),
)
+ itemsByKey[key] = item
group.Items = append(group.Items, item)
if model.ID == currentModel.Model && string(displayProvider.ID) == currentModel.Provider {
selectedItemID = item.ID()
@@ -255,6 +273,35 @@ func (m *ModelListComponent) SetModelType(modelType int) tea.Cmd {
groups = append(groups, group)
}
+ if len(recentItems) > 0 {
+ recentSection := list.NewItemSection("Recently used")
+ recentGroup := list.Group[list.CompletionItem[ModelOption]]{
+ Section: recentSection,
+ }
+ for _, recent := range recentItems {
+ key := modelKey(recent.Provider, recent.Model)
+ option, ok := itemsByKey[key]
+ if !ok {
+ continue
+ }
+ recentID := fmt.Sprintf("recent::%s", key)
+ item := list.NewCompletionItem(
+ option.Text(),
+ option.Value(),
+ list.WithCompletionID(recentID),
+ )
+ recentGroup.Items = append(recentGroup.Items, item)
+ if recent.Model == currentModel.Model && recent.Provider == currentModel.Provider {
+ selectedItemID = recentID
+ }
+ }
+
+ if len(recentGroup.Items) > 0 {
+ recentGroup.Section.SetInfo(t.S().Base.Foreground(t.FgHalfMuted).Render("Most recent selections"))
+ groups = append([]list.Group[list.CompletionItem[ModelOption]]{recentGroup}, groups...)
+ }
+ }
+
var cmds []tea.Cmd
cmd := m.list.SetGroups(groups)
@@ -225,7 +225,10 @@ func (a *appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
return a, util.ReportWarn("Agent is busy, please wait...")
}
- config.Get().UpdatePreferredModel(msg.ModelType, msg.Model)
+ cfg := config.Get()
+ if err := cfg.UpdatePreferredModel(msg.ModelType, msg.Model); err != nil {
+ return a, util.ReportError(err)
+ }
go a.app.UpdateAgentModel(context.TODO())