From 4ff5ab8cdac2766379d85286b0f1aa7d8d9db77c Mon Sep 17 00:00:00 2001 From: Oerum <54005601+Oerum@users.noreply.github.com> Date: Mon, 9 Mar 2026 22:40:59 +0100 Subject: [PATCH] feat(tui): expose favorite model cycling with Alt+C/Alt+X keybinds - Set default keybinds for favorite model cycling to Alt+C and Alt+X. - Unhide favorite cycle commands in the command palette. - Update TUI tips to include the new shortcuts. - Update keybind display to use 'Option' modifier on macOS. - Add tests for new defaults and macOS display logic. Closes #16801 --- packages/opencode/src/cli/cmd/tui/app.tsx | 6 ++---- .../opencode/src/cli/cmd/tui/component/tips.tsx | 3 ++- packages/opencode/src/config/config.ts | 4 ++-- packages/opencode/src/util/keybind.ts | 5 +++-- packages/opencode/test/config/tui.test.ts | 13 +++++++++++++ packages/opencode/test/keybind.test.ts | 5 +++++ 6 files changed, 27 insertions(+), 9 deletions(-) diff --git a/packages/opencode/src/cli/cmd/tui/app.tsx b/packages/opencode/src/cli/cmd/tui/app.tsx index d5aef34f6e3..1af48f36006 100644 --- a/packages/opencode/src/cli/cmd/tui/app.tsx +++ b/packages/opencode/src/cli/cmd/tui/app.tsx @@ -443,21 +443,19 @@ function App() { }, }, { - title: "Favorite cycle", + title: "Favorite model cycle", value: "model.cycle_favorite", keybind: "model_cycle_favorite", category: "Agent", - hidden: true, onSelect: () => { local.model.cycleFavorite(1) }, }, { - title: "Favorite cycle reverse", + title: "Favorite model cycle reverse", value: "model.cycle_favorite_reverse", keybind: "model_cycle_favorite_reverse", category: "Agent", - hidden: true, onSelect: () => { local.model.cycleFavorite(-1) }, diff --git a/packages/opencode/src/cli/cmd/tui/component/tips.tsx b/packages/opencode/src/cli/cmd/tui/component/tips.tsx index 73d82248adb..14b9df289e6 100644 --- a/packages/opencode/src/cli/cmd/tui/component/tips.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/tips.tsx @@ -3,6 +3,7 @@ import { DEFAULT_THEMES, useTheme } from "@tui/context/theme" const themeCount = Object.keys(DEFAULT_THEMES).length const themeTip = `Use {highlight}/themes{/highlight} or {highlight}Ctrl+X T{/highlight} to switch between ${themeCount} built-in themes` +const meta = process.platform === "darwin" ? "Option" : "Alt" type TipPart = { text: string; highlight: boolean } @@ -69,7 +70,7 @@ const TIPS = [ "Press {highlight}Ctrl+P{/highlight} to see all available actions and commands", "Run {highlight}/connect{/highlight} to add API keys for 75+ supported LLM providers", "The leader key is {highlight}Ctrl+X{/highlight}; combine with other keys for quick actions", - "Press {highlight}F2{/highlight} to quickly switch between recently used models", + `Press {highlight}F2{/highlight} to quickly switch between recently used models, or {highlight}${meta}+C{/highlight}/{highlight}${meta}+X{/highlight} for favorite models`, "Press {highlight}Ctrl+X B{/highlight} to show/hide the sidebar panel", "Use {highlight}PageUp{/highlight}/{highlight}PageDown{/highlight} to navigate through conversation history", "Press {highlight}Ctrl+G{/highlight} or {highlight}Home{/highlight} to jump to the beginning of the conversation", diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts index 6b4242a225a..2aabf83edc2 100644 --- a/packages/opencode/src/config/config.ts +++ b/packages/opencode/src/config/config.ts @@ -807,8 +807,8 @@ export namespace Config { 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"), - model_cycle_favorite: z.string().optional().default("none").describe("Next favorite model"), - model_cycle_favorite_reverse: z.string().optional().default("none").describe("Previous favorite model"), + model_cycle_favorite: z.string().optional().default("alt+c").describe("Next favorite model"), + model_cycle_favorite_reverse: z.string().optional().default("alt+x").describe("Previous favorite 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/opencode/src/util/keybind.ts b/packages/opencode/src/util/keybind.ts index 83c7945ae19..f1d4f6721eb 100644 --- a/packages/opencode/src/util/keybind.ts +++ b/packages/opencode/src/util/keybind.ts @@ -32,12 +32,13 @@ export namespace Keybind { } } - export function toString(info: Info | undefined): string { + export function toString(info: Info | undefined, opts?: { platform?: NodeJS.Platform }): string { if (!info) return "" const parts: string[] = [] + const meta = (opts?.platform ?? process.platform) === "darwin" ? "option" : "alt" if (info.ctrl) parts.push("ctrl") - if (info.meta) parts.push("alt") + if (info.meta) parts.push(meta) if (info.super) parts.push("super") if (info.shift) parts.push("shift") if (info.name) { diff --git a/packages/opencode/test/config/tui.test.ts b/packages/opencode/test/config/tui.test.ts index f9de5b041b4..adb65dbf0d1 100644 --- a/packages/opencode/test/config/tui.test.ts +++ b/packages/opencode/test/config/tui.test.ts @@ -40,6 +40,19 @@ test("loads tui config with the same precedence order as server config paths", a }) }) +test("loads default favorite model cycle keybinds", async () => { + await using tmp = await tmpdir() + + await Instance.provide({ + directory: tmp.path, + fn: async () => { + const config = await TuiConfig.get() + expect(config.keybinds?.model_cycle_favorite).toBe("alt+c") + expect(config.keybinds?.model_cycle_favorite_reverse).toBe("alt+x") + }, + }) +}) + test("migrates tui-specific keys from opencode.json when tui.json does not exist", async () => { await using tmp = await tmpdir({ init: async (dir) => { diff --git a/packages/opencode/test/keybind.test.ts b/packages/opencode/test/keybind.test.ts index 4ca1f1697e2..6042bb81857 100644 --- a/packages/opencode/test/keybind.test.ts +++ b/packages/opencode/test/keybind.test.ts @@ -22,6 +22,11 @@ describe("Keybind.toString", () => { expect(Keybind.toString(info)).toBe("ctrl+alt+g") }) + test("should convert meta to option on macOS", () => { + const info: Keybind.Info = { ctrl: false, meta: true, shift: false, leader: false, name: "c" } + expect(Keybind.toString(info, { platform: "darwin" })).toBe("option+c") + }) + test("should convert all modifiers to string", () => { const info: Keybind.Info = { ctrl: true, meta: true, shift: true, leader: true, name: "h" } expect(Keybind.toString(info)).toBe(" ctrl+alt+shift+h")