From f342d61cef67686ff818c6d67f18f74839bc0f35 Mon Sep 17 00:00:00 2001 From: Sebastian Herrlinger Date: Tue, 26 May 2026 23:53:54 +0200 Subject: [PATCH] global keybinds --- packages/opencode/src/cli/cmd/tui/app.tsx | 13 +++- .../opencode/test/cli/tui/keymap.test.tsx | 65 ++++++++++++++++++- 2 files changed, 74 insertions(+), 4 deletions(-) diff --git a/packages/opencode/src/cli/cmd/tui/app.tsx b/packages/opencode/src/cli/cmd/tui/app.tsx index b61ee80446..b88b79a069 100644 --- a/packages/opencode/src/cli/cmd/tui/app.tsx +++ b/packages/opencode/src/cli/cmd/tui/app.tsx @@ -79,10 +79,8 @@ import { import type { EventSource } from "./context/sdk" import { DialogVariant } from "./component/dialog-variant" -const appBindingCommands = [ - "command.palette.show", +const appGlobalBindingCommands = [ "session.list", - "session.new", "session.quick_switch.1", "session.quick_switch.2", "session.quick_switch.3", @@ -92,6 +90,11 @@ const appBindingCommands = [ "session.quick_switch.7", "session.quick_switch.8", "session.quick_switch.9", +] as const + +const appBindingCommands = [ + "command.palette.show", + "session.new", "model.list", "model.cycle_recent", "model.cycle_recent_reverse", @@ -929,6 +932,10 @@ function App(props: { onSnapshot?: () => Promise }) { bindings: tuiConfig.keybinds.gather("app", appBindingCommands), })) + useBindings(() => ({ + bindings: tuiConfig.keybinds.gather("app.global", appGlobalBindingCommands), + })) + useBindings(() => ({ mode: OPENCODE_BASE_MODE, enabled: () => { diff --git a/packages/opencode/test/cli/tui/keymap.test.tsx b/packages/opencode/test/cli/tui/keymap.test.tsx index 82cd72d6c8..9090676635 100644 --- a/packages/opencode/test/cli/tui/keymap.test.tsx +++ b/packages/opencode/test/cli/tui/keymap.test.tsx @@ -4,7 +4,12 @@ import { testRender, useRenderer } from "@opentui/solid" import { expect, test } from "bun:test" import { onCleanup } from "solid-js" import { createTuiResolvedConfig } from "../../fixture/tui-runtime" -import { OpencodeKeymapProvider, registerOpencodeKeymap } from "@/cli/cmd/tui/keymap" +import { + getOpencodeModeStack, + OPENCODE_BASE_MODE, + OpencodeKeymapProvider, + registerOpencodeKeymap, +} from "@/cli/cmd/tui/keymap" test("legacy page key aliases compile as page keys", async () => { const sequences: Record = {} @@ -52,3 +57,61 @@ test("legacy page key aliases compile as page keys", async () => { app.renderer.destroy() } }) + +test("mode-less bindings stay active when opencode mode changes", async () => { + const counts: Record> = {} + + function Harness() { + const renderer = useRenderer() + const keymap = createDefaultOpenTuiKeymap(renderer) + const config = createTuiResolvedConfig() + const offKeymap = registerOpencodeKeymap(keymap, renderer, config) + const offGlobal = keymap.registerLayer({ + commands: [{ name: "session.list", run() {} }], + bindings: config.keybinds.gather("test.global", ["session.list"]), + }) + const offBase = keymap.registerLayer({ + mode: OPENCODE_BASE_MODE, + commands: [{ name: "session.new", run() {} }], + bindings: config.keybinds.gather("test.base", ["session.new"]), + }) + const activeCounts = () => + Object.fromEntries( + Array.from( + keymap.getCommandBindings({ visibility: "active", commands: ["session.list", "session.new"] }), + ([command, bindings]) => [command, bindings.length], + ), + ) + + counts.base = activeCounts() + const popQuestion = getOpencodeModeStack(keymap).push("question") + counts.question = activeCounts() + popQuestion() + const popAutocomplete = getOpencodeModeStack(keymap).push("autocomplete") + counts.autocomplete = activeCounts() + popAutocomplete() + + onCleanup(() => { + offBase() + offGlobal() + offKeymap() + }) + + return ( + + + + ) + } + + const app = await testRender(() => ) + try { + expect(counts).toEqual({ + base: { "session.list": 1, "session.new": 1 }, + question: { "session.list": 1, "session.new": 0 }, + autocomplete: { "session.list": 1, "session.new": 0 }, + }) + } finally { + app.renderer.destroy() + } +})