Skip to content
This repository was archived by the owner on Apr 1, 2026. It is now read-only.

Commit d4cb47e

Browse files
committed
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.
1 parent 261ff41 commit d4cb47e

5 files changed

Lines changed: 75 additions & 10 deletions

File tree

packages/desktop/src/context/local.tsx

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,10 +162,32 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
162162

163163
const recent = createMemo(() => store.recent.map(find).filter(Boolean))
164164

165+
const cycle = (direction: 1 | -1) => {
166+
const recentList = recent()
167+
const current = currentModel()
168+
if (!current) return
169+
170+
const index = recentList.findIndex((x) => x?.provider.id === current.provider.id && x?.id === current.id)
171+
if (index === -1) return
172+
173+
let next = index + direction
174+
if (next < 0) next = recentList.length - 1
175+
if (next >= recentList.length) next = 0
176+
177+
const val = recentList[next]
178+
if (!val) return
179+
180+
model.set({
181+
providerID: val.provider.id,
182+
modelID: val.id,
183+
})
184+
}
185+
165186
return {
166187
current: currentModel,
167188
recent,
168189
list,
190+
cycle,
169191
set(model: ModelKey | undefined, options?: { recent?: boolean }) {
170192
batch(() => {
171193
setStore("model", agent.current().name, model ?? fallbackModel())

packages/opencode/src/cli/cmd/tui/app.tsx

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,24 @@ function App() {
172172
dialog.replace(() => <DialogModel />)
173173
},
174174
},
175+
{
176+
title: "Model cycle",
177+
value: "model.cycle_recent",
178+
keybind: "model_cycle_recent",
179+
category: "Agent",
180+
onSelect: () => {
181+
local.model.cycle(1)
182+
},
183+
},
184+
{
185+
title: "Model cycle reverse",
186+
value: "model.cycle_recent_reverse",
187+
keybind: "model_cycle_recent_reverse",
188+
category: "Agent",
189+
onSelect: () => {
190+
local.model.cycle(-1)
191+
},
192+
},
175193
{
176194
title: "Switch agent",
177195
value: "agent.list",

packages/opencode/src/cli/cmd/tui/context/local.tsx

Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -147,15 +147,6 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
147147
setModelStore("ready", true)
148148
})
149149

150-
createEffect(() => {
151-
Bun.write(
152-
file,
153-
JSON.stringify({
154-
recent: modelStore.recent,
155-
}),
156-
)
157-
})
158-
159150
const fallbackModel = createMemo(() => {
160151
if (sync.data.config.model) {
161152
const [providerID, modelID] = sync.data.config.model.split("/")
@@ -206,6 +197,21 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
206197
model: model.name ?? value.modelID,
207198
}
208199
}),
200+
cycle(direction: 1 | -1) {
201+
const current = currentModel()
202+
if (!current) return
203+
const recent = modelStore.recent
204+
const index = recent.findIndex(
205+
(x) => x.providerID === current.providerID && x.modelID === current.modelID,
206+
)
207+
if (index === -1) return
208+
let next = index + direction
209+
if (next < 0) next = recent.length - 1
210+
if (next >= recent.length) next = 0
211+
const val = recent[next]
212+
if (!val) return
213+
setModelStore("model", agent.current().name, { ...val })
214+
},
209215
set(model: { providerID: string; modelID: string }, options?: { recent?: boolean }) {
210216
batch(() => {
211217
if (!isModelValid(model)) {
@@ -216,12 +222,17 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
216222
})
217223
return
218224
}
219-
220225
setModelStore("model", agent.current().name, model)
221226
if (options?.recent) {
222227
const uniq = uniqueBy([model, ...modelStore.recent], (x) => x.providerID + x.modelID)
223228
if (uniq.length > 5) uniq.pop()
224229
setModelStore("recent", uniq)
230+
Bun.write(
231+
file,
232+
JSON.stringify({
233+
recent: modelStore.recent,
234+
}),
235+
)
225236
}
226237
})
227238
},

packages/opencode/src/config/config.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -453,6 +453,12 @@ export namespace Config {
453453
.default("<leader>h")
454454
.describe("Toggle code block concealment in messages"),
455455
model_list: z.string().optional().default("<leader>m").describe("List available models"),
456+
model_cycle_recent: z.string().optional().default("f2").describe("Next recently used model"),
457+
model_cycle_recent_reverse: z
458+
.string()
459+
.optional()
460+
.default("shift+f2")
461+
.describe("Previous recently used model"),
456462
command_list: z.string().optional().default("ctrl+p").describe("List available commands"),
457463
agent_list: z.string().optional().default("<leader>a").describe("List agents"),
458464
agent_cycle: z.string().optional().default("tab").describe("Next agent"),

packages/sdk/js/src/gen/types.gen.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,14 @@ export type KeybindsConfig = {
114114
* List available models
115115
*/
116116
model_list?: string
117+
/**
118+
* Next recently used model
119+
*/
120+
model_cycle_recent?: string
121+
/**
122+
* Previous recently used model
123+
*/
124+
model_cycle_recent_reverse?: string
117125
/**
118126
* List available commands
119127
*/

0 commit comments

Comments
 (0)