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
*/