From 92d4366a20b4499b8e4817df442d67d021949feb Mon Sep 17 00:00:00 2001 From: Yihui Khuu Date: Fri, 15 Aug 2025 21:20:07 +1000 Subject: [PATCH] feat(tui): support cycling recent models in reverse (#1953) --- packages/tui/internal/app/app.go | 22 ++++-- packages/tui/internal/commands/command.go | 86 ++++++++++++----------- packages/tui/internal/tui/tui.go | 4 ++ 3 files changed, 68 insertions(+), 44 deletions(-) diff --git a/packages/tui/internal/app/app.go b/packages/tui/internal/app/app.go index 023b799d..eff00f29 100644 --- a/packages/tui/internal/app/app.go +++ b/packages/tui/internal/app/app.go @@ -290,7 +290,7 @@ func (a *App) SwitchAgentReverse() (*App, tea.Cmd) { return a.cycleMode(false) } -func (a *App) CycleRecentModel() (*App, tea.Cmd) { +func (a *App) cycleRecentModel(forward bool) (*App, tea.Cmd) { recentModels := a.State.RecentlyUsedModels if len(recentModels) > 5 { recentModels = recentModels[:5] @@ -299,15 +299,21 @@ func (a *App) CycleRecentModel() (*App, tea.Cmd) { return a, toast.NewInfoToast("Need at least 2 recent models to cycle") } nextIndex := 0 + prevIndex := 0 for i, recentModel := range recentModels { if a.Provider != nil && a.Model != nil && recentModel.ProviderID == a.Provider.ID && recentModel.ModelID == a.Model.ID { nextIndex = (i + 1) % len(recentModels) + prevIndex = (i - 1 + len(recentModels)) % len(recentModels) break } } + targetIndex := nextIndex + if !forward { + targetIndex = prevIndex + } for range recentModels { - currentRecentModel := recentModels[nextIndex%len(recentModels)] + currentRecentModel := recentModels[targetIndex%len(recentModels)] provider, model := findModelByProviderAndModelID( a.Providers, currentRecentModel.ProviderID, @@ -327,8 +333,8 @@ func (a *App) CycleRecentModel() (*App, tea.Cmd) { ) } recentModels = append( - recentModels[:nextIndex%len(recentModels)], - recentModels[nextIndex%len(recentModels)+1:]...) + recentModels[:targetIndex%len(recentModels)], + recentModels[targetIndex%len(recentModels)+1:]...) if len(recentModels) < 2 { a.State.RecentlyUsedModels = recentModels return a, tea.Sequence( @@ -341,6 +347,14 @@ func (a *App) CycleRecentModel() (*App, tea.Cmd) { return a, toast.NewErrorToast("Recent model not found") } +func (a *App) CycleRecentModel() (*App, tea.Cmd) { + return a.cycleRecentModel(true) +} + +func (a *App) CycleRecentModelReverse() (*App, tea.Cmd) { + return a.cycleRecentModel(false) +} + func (a *App) SwitchToAgent(agentName string) (*App, tea.Cmd) { // Find the agent index by name for i, agent := range a.Agents { diff --git a/packages/tui/internal/commands/command.go b/packages/tui/internal/commands/command.go index fff54754..ebb468d2 100644 --- a/packages/tui/internal/commands/command.go +++ b/packages/tui/internal/commands/command.go @@ -107,45 +107,46 @@ func (r CommandRegistry) Matches(msg tea.KeyPressMsg, leader bool) []Command { } const ( - AppHelpCommand CommandName = "app_help" - SwitchAgentCommand CommandName = "switch_agent" - SwitchAgentReverseCommand CommandName = "switch_agent_reverse" - EditorOpenCommand CommandName = "editor_open" - SessionNewCommand CommandName = "session_new" - SessionListCommand CommandName = "session_list" - SessionShareCommand CommandName = "session_share" - SessionUnshareCommand CommandName = "session_unshare" - SessionInterruptCommand CommandName = "session_interrupt" - SessionCompactCommand CommandName = "session_compact" - SessionExportCommand CommandName = "session_export" - ToolDetailsCommand CommandName = "tool_details" - ThinkingBlocksCommand CommandName = "thinking_blocks" - ModelListCommand CommandName = "model_list" - AgentListCommand CommandName = "agent_list" - ModelCycleRecentCommand CommandName = "model_cycle_recent" - ThemeListCommand CommandName = "theme_list" - FileListCommand CommandName = "file_list" - FileCloseCommand CommandName = "file_close" - FileSearchCommand CommandName = "file_search" - FileDiffToggleCommand CommandName = "file_diff_toggle" - ProjectInitCommand CommandName = "project_init" - InputClearCommand CommandName = "input_clear" - InputPasteCommand CommandName = "input_paste" - InputSubmitCommand CommandName = "input_submit" - InputNewlineCommand CommandName = "input_newline" - MessagesPageUpCommand CommandName = "messages_page_up" - MessagesPageDownCommand CommandName = "messages_page_down" - MessagesHalfPageUpCommand CommandName = "messages_half_page_up" - MessagesHalfPageDownCommand CommandName = "messages_half_page_down" - MessagesPreviousCommand CommandName = "messages_previous" - MessagesNextCommand CommandName = "messages_next" - MessagesFirstCommand CommandName = "messages_first" - MessagesLastCommand CommandName = "messages_last" - MessagesLayoutToggleCommand CommandName = "messages_layout_toggle" - MessagesCopyCommand CommandName = "messages_copy" - MessagesUndoCommand CommandName = "messages_undo" - MessagesRedoCommand CommandName = "messages_redo" - AppExitCommand CommandName = "app_exit" + AppHelpCommand CommandName = "app_help" + SwitchAgentCommand CommandName = "switch_agent" + SwitchAgentReverseCommand CommandName = "switch_agent_reverse" + EditorOpenCommand CommandName = "editor_open" + SessionNewCommand CommandName = "session_new" + SessionListCommand CommandName = "session_list" + SessionShareCommand CommandName = "session_share" + SessionUnshareCommand CommandName = "session_unshare" + SessionInterruptCommand CommandName = "session_interrupt" + SessionCompactCommand CommandName = "session_compact" + SessionExportCommand CommandName = "session_export" + ToolDetailsCommand CommandName = "tool_details" + ThinkingBlocksCommand CommandName = "thinking_blocks" + ModelListCommand CommandName = "model_list" + AgentListCommand CommandName = "agent_list" + ModelCycleRecentCommand CommandName = "model_cycle_recent" + ModelCycleRecentReverseCommand CommandName = "model_cycle_recent_reverse" + ThemeListCommand CommandName = "theme_list" + FileListCommand CommandName = "file_list" + FileCloseCommand CommandName = "file_close" + FileSearchCommand CommandName = "file_search" + FileDiffToggleCommand CommandName = "file_diff_toggle" + ProjectInitCommand CommandName = "project_init" + InputClearCommand CommandName = "input_clear" + InputPasteCommand CommandName = "input_paste" + InputSubmitCommand CommandName = "input_submit" + InputNewlineCommand CommandName = "input_newline" + MessagesPageUpCommand CommandName = "messages_page_up" + MessagesPageDownCommand CommandName = "messages_page_down" + MessagesHalfPageUpCommand CommandName = "messages_half_page_up" + MessagesHalfPageDownCommand CommandName = "messages_half_page_down" + MessagesPreviousCommand CommandName = "messages_previous" + MessagesNextCommand CommandName = "messages_next" + MessagesFirstCommand CommandName = "messages_first" + MessagesLastCommand CommandName = "messages_last" + MessagesLayoutToggleCommand CommandName = "messages_layout_toggle" + MessagesCopyCommand CommandName = "messages_copy" + MessagesUndoCommand CommandName = "messages_undo" + MessagesRedoCommand CommandName = "messages_redo" + AppExitCommand CommandName = "app_exit" ) func (k Command) Matches(msg tea.KeyPressMsg, leader bool) bool { @@ -266,9 +267,14 @@ func LoadFromConfig(config *opencode.Config) CommandRegistry { }, { Name: ModelCycleRecentCommand, - Description: "cycle recent models", + Description: "next recent model", Keybindings: parseBindings("f2"), }, + { + Name: ModelCycleRecentReverseCommand, + Description: "previous recent model", + Keybindings: parseBindings("shift+f2"), + }, { Name: ThemeListCommand, Description: "list themes", diff --git a/packages/tui/internal/tui/tui.go b/packages/tui/internal/tui/tui.go index f9a014dd..dcbdd2b5 100644 --- a/packages/tui/internal/tui/tui.go +++ b/packages/tui/internal/tui/tui.go @@ -1190,6 +1190,10 @@ func (a Model) executeCommand(command commands.Command) (tea.Model, tea.Cmd) { updated, cmd := a.app.CycleRecentModel() a.app = updated cmds = append(cmds, cmd) + case commands.ModelCycleRecentReverseCommand: + updated, cmd := a.app.CycleRecentModelReverse() + a.app = updated + cmds = append(cmds, cmd) case commands.ThemeListCommand: themeDialog := dialog.NewThemeDialog() a.modal = themeDialog