Explorar el Código

feat(tui): add keymap to remove entries from recently used models (#1019)

Timo Clasen hace 7 meses
padre
commit
f707fb3f8d

+ 35 - 1
packages/tui/internal/components/dialog/models.go

@@ -23,6 +23,7 @@ const (
 	numVisibleModels = 10
 	numVisibleModels = 10
 	minDialogWidth   = 40
 	minDialogWidth   = 40
 	maxDialogWidth   = 80
 	maxDialogWidth   = 80
+	maxRecentModels  = 5
 )
 )
 
 
 // ModelDialog interface for the model selection dialog
 // ModelDialog interface for the model selection dialog
@@ -122,6 +123,17 @@ func (m *modelDialog) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 	case SearchCancelledMsg:
 	case SearchCancelledMsg:
 		return m, util.CmdHandler(modal.CloseModalMsg{})
 		return m, util.CmdHandler(modal.CloseModalMsg{})
 
 
+	case SearchRemoveItemMsg:
+		if item, ok := msg.Item.(modelItem); ok {
+			if m.isModelInRecentSection(item.model, msg.Index) {
+				m.app.State.RemoveModelFromRecentlyUsed(item.model.Provider.ID, item.model.Model.ID)
+				m.app.SaveState()
+				items := m.buildDisplayList(m.searchDialog.GetQuery())
+				m.searchDialog.SetItems(items)
+			}
+		}
+		return m, nil
+
 	case SearchQueryChangedMsg:
 	case SearchQueryChangedMsg:
 		// Update the list based on search query
 		// Update the list based on search query
 		items := m.buildDisplayList(msg.Query)
 		items := m.buildDisplayList(msg.Query)
@@ -307,7 +319,7 @@ func (m *modelDialog) buildGroupedResults() []list.Item {
 	var items []list.Item
 	var items []list.Item
 
 
 	// Add Recent section
 	// Add Recent section
-	recentModels := m.getRecentModels(5)
+	recentModels := m.getRecentModels(maxRecentModels)
 	if len(recentModels) > 0 {
 	if len(recentModels) > 0 {
 		items = append(items, list.HeaderItem("Recent"))
 		items = append(items, list.HeaderItem("Recent"))
 		for _, model := range recentModels {
 		for _, model := range recentModels {
@@ -398,6 +410,28 @@ func (m *modelDialog) getRecentModels(limit int) []ModelWithProvider {
 	return recentModels
 	return recentModels
 }
 }
 
 
+func (m *modelDialog) isModelInRecentSection(model ModelWithProvider, index int) bool {
+	// Only check if we're in grouped mode (no search query)
+	if m.searchDialog.GetQuery() != "" {
+		return false
+	}
+
+	recentModels := m.getRecentModels(maxRecentModels)
+	if len(recentModels) == 0 {
+		return false
+	}
+
+	// Index 0 is the "Recent" header, so recent models are at indices 1 to len(recentModels)
+	if index >= 1 && index <= len(recentModels) {
+		if index-1 < len(recentModels) {
+			recentModel := recentModels[index-1]
+			return recentModel.Provider.ID == model.Provider.ID && recentModel.Model.ID == model.Model.ID
+		}
+	}
+
+	return false
+}
+
 func (m *modelDialog) Render(background string) string {
 func (m *modelDialog) Render(background string) string {
 	return m.modal.Render(m.View(), background)
 	return m.modal.Render(m.View(), background)
 }
 }

+ 18 - 0
packages/tui/internal/components/dialog/search.go

@@ -24,6 +24,12 @@ type SearchSelectionMsg struct {
 // SearchCancelledMsg is emitted when the search is cancelled
 // SearchCancelledMsg is emitted when the search is cancelled
 type SearchCancelledMsg struct{}
 type SearchCancelledMsg struct{}
 
 
+// SearchRemoveItemMsg is emitted when Ctrl+X is pressed to remove an item
+type SearchRemoveItemMsg struct {
+	Item  any
+	Index int
+}
+
 // SearchDialog is a reusable component that combines a text input with a list
 // SearchDialog is a reusable component that combines a text input with a list
 type SearchDialog struct {
 type SearchDialog struct {
 	textInput textinput.Model
 	textInput textinput.Model
@@ -38,6 +44,7 @@ type searchKeyMap struct {
 	Down   key.Binding
 	Down   key.Binding
 	Enter  key.Binding
 	Enter  key.Binding
 	Escape key.Binding
 	Escape key.Binding
+	Remove key.Binding
 }
 }
 
 
 var searchKeys = searchKeyMap{
 var searchKeys = searchKeyMap{
@@ -57,6 +64,10 @@ var searchKeys = searchKeyMap{
 		key.WithKeys("esc"),
 		key.WithKeys("esc"),
 		key.WithHelp("esc", "cancel"),
 		key.WithHelp("esc", "cancel"),
 	),
 	),
+	Remove: key.NewBinding(
+		key.WithKeys("ctrl+x"),
+		key.WithHelp("ctrl+x", "remove from recent"),
+	),
 }
 }
 
 
 // NewSearchDialog creates a new SearchDialog
 // NewSearchDialog creates a new SearchDialog
@@ -148,6 +159,13 @@ func (s *SearchDialog) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 				}
 				}
 			}
 			}
 
 
+		case key.Matches(msg, searchKeys.Remove):
+			if selectedItem, idx := s.list.GetSelectedItem(); idx != -1 {
+				return s, func() tea.Msg {
+					return SearchRemoveItemMsg{Item: selectedItem, Index: idx}
+				}
+			}
+
 		case key.Matches(msg, searchKeys.Up):
 		case key.Matches(msg, searchKeys.Up):
 			var cmd tea.Cmd
 			var cmd tea.Cmd
 			listModel, cmd := s.list.Update(msg)
 			listModel, cmd := s.list.Update(msg)

+ 9 - 0
packages/tui/internal/config/config.go

@@ -69,6 +69,15 @@ func (s *State) UpdateModelUsage(providerID, modelID string) {
 	}
 	}
 }
 }
 
 
+func (s *State) RemoveModelFromRecentlyUsed(providerID, modelID string) {
+	for i, usage := range s.RecentlyUsedModels {
+		if usage.ProviderID == providerID && usage.ModelID == modelID {
+			s.RecentlyUsedModels = append(s.RecentlyUsedModels[:i], s.RecentlyUsedModels[i+1:]...)
+			return
+		}
+	}
+}
+
 // SaveState writes the provided Config struct to the specified TOML file.
 // SaveState writes the provided Config struct to the specified TOML file.
 // It will create the file if it doesn't exist, or overwrite it if it does.
 // It will create the file if it doesn't exist, or overwrite it if it does.
 func SaveState(filePath string, state *State) error {
 func SaveState(filePath string, state *State) error {