From d4cb47eadc515646f9f42679100a075ff9c9d458 Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Fri, 31 Oct 2025 19:42:27 -0400 Subject: [PATCH] tui: add keyboard shortcuts to cycle through recently used models Users can now press F2 to cycle forward and Shift+F2 to cycle backward through their recently used models, making it faster to switch between commonly used AI models without opening the model selection dialog. --- packages/desktop/src/context/local.tsx | 22 +++++++++++++ packages/opencode/src/cli/cmd/tui/app.tsx | 18 +++++++++++ .../src/cli/cmd/tui/context/local.tsx | 31 +++++++++++++------ packages/opencode/src/config/config.ts | 6 ++++ packages/sdk/js/src/gen/types.gen.ts | 8 +++++ 5 files changed, 75 insertions(+), 10 deletions(-) diff --git a/packages/desktop/src/context/local.tsx b/packages/desktop/src/context/local.tsx index 4607a184..2b844536 100644 --- a/packages/desktop/src/context/local.tsx +++ b/packages/desktop/src/context/local.tsx @@ -162,10 +162,32 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({ const recent = createMemo(() => store.recent.map(find).filter(Boolean)) + const cycle = (direction: 1 | -1) => { + const recentList = recent() + const current = currentModel() + if (!current) return + + const index = recentList.findIndex((x) => x?.provider.id === current.provider.id && x?.id === current.id) + if (index === -1) return + + let next = index + direction + if (next < 0) next = recentList.length - 1 + if (next >= recentList.length) next = 0 + + const val = recentList[next] + if (!val) return + + model.set({ + providerID: val.provider.id, + modelID: val.id, + }) + } + return { current: currentModel, recent, list, + cycle, set(model: ModelKey | undefined, options?: { recent?: boolean }) { batch(() => { setStore("model", agent.current().name, model ?? fallbackModel()) diff --git a/packages/opencode/src/cli/cmd/tui/app.tsx b/packages/opencode/src/cli/cmd/tui/app.tsx index 65f90067..2427432e 100644 --- a/packages/opencode/src/cli/cmd/tui/app.tsx +++ b/packages/opencode/src/cli/cmd/tui/app.tsx @@ -172,6 +172,24 @@ function App() { dialog.replace(() => ) }, }, + { + title: "Model cycle", + value: "model.cycle_recent", + keybind: "model_cycle_recent", + category: "Agent", + onSelect: () => { + local.model.cycle(1) + }, + }, + { + title: "Model cycle reverse", + value: "model.cycle_recent_reverse", + keybind: "model_cycle_recent_reverse", + category: "Agent", + onSelect: () => { + local.model.cycle(-1) + }, + }, { title: "Switch agent", value: "agent.list", diff --git a/packages/opencode/src/cli/cmd/tui/context/local.tsx b/packages/opencode/src/cli/cmd/tui/context/local.tsx index e8f11a35..2485cd01 100644 --- a/packages/opencode/src/cli/cmd/tui/context/local.tsx +++ b/packages/opencode/src/cli/cmd/tui/context/local.tsx @@ -147,15 +147,6 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({ setModelStore("ready", true) }) - createEffect(() => { - Bun.write( - file, - JSON.stringify({ - recent: modelStore.recent, - }), - ) - }) - const fallbackModel = createMemo(() => { if (sync.data.config.model) { const [providerID, modelID] = sync.data.config.model.split("/") @@ -206,6 +197,21 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({ model: model.name ?? value.modelID, } }), + cycle(direction: 1 | -1) { + const current = currentModel() + if (!current) return + const recent = modelStore.recent + const index = recent.findIndex( + (x) => x.providerID === current.providerID && x.modelID === current.modelID, + ) + if (index === -1) return + let next = index + direction + if (next < 0) next = recent.length - 1 + if (next >= recent.length) next = 0 + const val = recent[next] + if (!val) return + setModelStore("model", agent.current().name, { ...val }) + }, set(model: { providerID: string; modelID: string }, options?: { recent?: boolean }) { batch(() => { if (!isModelValid(model)) { @@ -216,12 +222,17 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({ }) return } - setModelStore("model", agent.current().name, model) if (options?.recent) { const uniq = uniqueBy([model, ...modelStore.recent], (x) => x.providerID + x.modelID) if (uniq.length > 5) uniq.pop() setModelStore("recent", uniq) + Bun.write( + file, + JSON.stringify({ + recent: modelStore.recent, + }), + ) } }) }, diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts index f84cca73..2a283a88 100644 --- a/packages/opencode/src/config/config.ts +++ b/packages/opencode/src/config/config.ts @@ -453,6 +453,12 @@ export namespace Config { .default("h") .describe("Toggle code block concealment in messages"), model_list: z.string().optional().default("m").describe("List available models"), + model_cycle_recent: z.string().optional().default("f2").describe("Next recently used model"), + model_cycle_recent_reverse: z + .string() + .optional() + .default("shift+f2") + .describe("Previous recently used model"), command_list: z.string().optional().default("ctrl+p").describe("List available commands"), agent_list: z.string().optional().default("a").describe("List agents"), agent_cycle: z.string().optional().default("tab").describe("Next agent"), diff --git a/packages/sdk/js/src/gen/types.gen.ts b/packages/sdk/js/src/gen/types.gen.ts index 994fa824..63673e76 100644 --- a/packages/sdk/js/src/gen/types.gen.ts +++ b/packages/sdk/js/src/gen/types.gen.ts @@ -114,6 +114,14 @@ export type KeybindsConfig = { * List available models */ model_list?: string + /** + * Next recently used model + */ + model_cycle_recent?: string + /** + * Previous recently used model + */ + model_cycle_recent_reverse?: string /** * List available commands */