Skip to content

Commit f9bcdb7

Browse files
Apply PR #26053: introduce opentui keymap as sole key/cmd engine
2 parents bd38e7f + 0307052 commit f9bcdb7

67 files changed

Lines changed: 3908 additions & 3026 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.opencode/plugins/tui-smoke.tsx

Lines changed: 386 additions & 274 deletions
Large diffs are not rendered by default.

.opencode/tui.json

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,20 @@
66
{
77
"enabled": false,
88
"label": "workspace",
9-
"keybinds": {
10-
"modal": "ctrl+alt+m",
11-
"screen": "ctrl+alt+o",
12-
"home": "escape,ctrl+shift+h",
13-
"dialog_close": "escape,q"
9+
"keymap": {
10+
"sections": {
11+
"global": {
12+
"plugin.smoke.modal": "ctrl+alt+m",
13+
"plugin.smoke.screen": "ctrl+alt+o"
14+
},
15+
"screen": {
16+
"plugin.smoke.screen.home": "escape,ctrl+shift+h",
17+
"plugin.smoke.screen.modal": "ctrl+alt+m"
18+
},
19+
"dialog": {
20+
"plugin.smoke.dialog.close": "escape,q"
21+
}
22+
}
1423
}
1524
}
1625
]

bun.lock

Lines changed: 19 additions & 12 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
"dev:storybook": "bun --cwd packages/storybook storybook",
1414
"lint": "oxlint",
1515
"typecheck": "bun turbo typecheck",
16+
"upgrade-opentui": "bun run script/upgrade-opentui.ts",
1617
"postinstall": "bun run --cwd packages/opencode fix-node-pty",
1718
"prepare": "husky",
1819
"random": "echo 'Random script'",
@@ -34,8 +35,9 @@
3435
"@types/cross-spawn": "6.0.6",
3536
"@octokit/rest": "22.0.0",
3637
"@hono/zod-validator": "0.4.2",
37-
"@opentui/core": "0.2.2",
38-
"@opentui/solid": "0.2.2",
38+
"@opentui/core": "0.0.0-20260506-6bb5353a",
39+
"@opentui/keymap": "0.0.0-20260506-6bb5353a",
40+
"@opentui/solid": "0.0.0-20260506-6bb5353a",
3941
"ulid": "3.0.1",
4042
"@kobalte/core": "0.13.11",
4143
"@types/luxon": "3.7.1",

packages/opencode/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@
1111
"test:ci": "mkdir -p .artifacts/unit && bun test --timeout 30000 --reporter=junit --reporter-outfile=.artifacts/unit/junit.xml",
1212
"build": "bun run script/build.ts",
1313
"fix-node-pty": "bun run script/fix-node-pty.ts",
14-
"upgrade-opentui": "bun run script/upgrade-opentui.ts",
1514
"dev": "bun run --conditions=browser ./src/index.ts",
1615
"dev:temporary": "bun run --conditions=browser ./src/temporary.ts",
1716
"db": "bun drizzle-kit"
@@ -125,6 +124,7 @@
125124
"@opentelemetry/sdk-trace-base": "2.6.1",
126125
"@opentelemetry/sdk-trace-node": "2.6.1",
127126
"@opentui/core": "catalog:",
127+
"@opentui/keymap": "catalog:",
128128
"@opentui/solid": "catalog:",
129129
"@parcel/watcher": "2.5.1",
130130
"@pierre/diffs": "catalog:",

packages/opencode/script/schema.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,5 +59,5 @@ await Bun.write(configFile, JSON.stringify(generate(Config.Info.zod), null, 2))
5959

6060
if (tuiFile) {
6161
console.log(tuiFile)
62-
await Bun.write(tuiFile, JSON.stringify(generate(TuiConfig.Info), null, 2))
62+
await Bun.write(tuiFile, JSON.stringify(generate(TuiConfig.JsonSchemaInfo), null, 2))
6363
}

packages/opencode/specs/tui-plugins.md

Lines changed: 31 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -53,13 +53,21 @@ Minimal module shape:
5353
import type { TuiPlugin, TuiPluginModule } from "@opencode-ai/plugin/tui"
5454

5555
const tui: TuiPlugin = async (api, options, meta) => {
56-
api.command.register(() => [
57-
{
58-
title: "Demo",
59-
value: "demo.open",
60-
onSelect: () => api.route.navigate("demo"),
61-
},
62-
])
56+
api.keymap.registerLayer({
57+
commands: [
58+
{
59+
name: "demo.open",
60+
title: "Demo",
61+
category: "Plugin",
62+
namespace: "palette",
63+
slashName: "demo",
64+
run() {
65+
api.route.navigate("demo")
66+
},
67+
},
68+
],
69+
bindings: [{ key: "ctrl+shift+m", cmd: "demo.open", desc: "Open demo" }],
70+
})
6371

6472
api.route.register([
6573
{
@@ -194,10 +202,10 @@ That is what makes local config-scoped plugins able to import `@opencode-ai/plug
194202
Top-level API groups exposed to `tui(api, options, meta)`:
195203

196204
- `api.app.version`
197-
- `api.command.register(cb)` / `api.command.trigger(value)` / `api.command.show()`
205+
- `api.keys.formatSequence(parts)`, `formatBindings(bindings)`
206+
- `api.keymap`
198207
- `api.route.register(routes)` / `api.route.navigate(name, params?)` / `api.route.current`
199208
- `api.ui.Dialog`, `DialogAlert`, `DialogConfirm`, `DialogPrompt`, `DialogSelect`, `Slot`, `Prompt`, `ui.toast`, `ui.dialog`
200-
- `api.keybind.match`, `print`, `create`
201209
- `api.tuiConfig`
202210
- `api.kv.get`, `set`, `ready`
203211
- `api.state`
@@ -209,23 +217,23 @@ Top-level API groups exposed to `tui(api, options, meta)`:
209217
- `api.plugins.list()`, `activate(id)`, `deactivate(id)`, `add(spec)`, `install(spec, options?)`
210218
- `api.lifecycle.signal`, `api.lifecycle.onDispose(fn)`
211219

212-
### Commands
220+
### Keymap
213221

214-
`api.command.register` returns an unregister function. Command rows support:
222+
- `api.keymap` exposes the raw `Keymap<Renderable, KeyEvent>` instance from the host.
223+
- The host already installs the default OpenTUI bundle (`default keys`, metadata fields, and enabled fields) plus OpenCode's comma bindings, leader token, base layout fallback, pending-sequence helpers, and managed textarea layer.
224+
- Register commands with `api.keymap.registerLayer({ commands: [...] })`.
225+
- Register key bindings with `bindings: [{ key, cmd, desc }]` in the same layer or a separate layer.
226+
- Use `api.keymap.acquireResource(...)` for shared plugin addon setup that should ref-count against the host keymap.
227+
- To surface a command in the host command palette, set `namespace: "palette"` and provide metadata such as `title`, `category`, `desc`, `suggested`, `hidden`, `enabled`, `slashName`, and `slashAliases` on the command.
228+
- Use `api.keymap.dispatchCommand(name)` for user-style execution semantics and `api.keymap.runCommand(name)` only for forced programmatic execution.
229+
- Disposers returned by `api.keymap` registrations and `acquireResource(...)` are automatically cleaned up when the plugin deactivates. You do not need to add those disposers to `api.lifecycle.onDispose(...)` yourself.
215230

216-
- `title`, `value`
217-
- `description`, `category`
218-
- `keybind`
219-
- `suggested`, `hidden`, `enabled`
220-
- `slash: { name, aliases? }`
221-
- `onSelect`
231+
### Keys
222232

223-
Command behavior:
224-
225-
- Registrations are reactive.
226-
- Later registrations win for duplicate `value` and for keybind handling.
227-
- Hidden commands are removed from the command dialog and slash list, but still respond to keybinds and `command.trigger(value)` if `enabled !== false`.
228-
- `api.command.show()` opens the host command dialog directly.
233+
- `api.keys` exposes host-formatted shortcut display helpers for plugin UI.
234+
- `formatSequence(parts)` formats parsed key sequence parts using the host's display policy.
235+
- `formatBindings(bindings)` formats binding lists and returns `undefined` when there is nothing to show.
236+
- For generic config-to-bindings helpers, import `resolveBindingSections` from `@opencode-ai/plugin/tui`.
229237

230238
### Routes
231239

@@ -252,13 +260,6 @@ Command behavior:
252260
- `setSize("medium" | "large" | "xlarge")`
253261
- readonly `size`, `depth`, `open`
254262

255-
### Keybinds
256-
257-
- `api.keybind.match(key, evt)` and `print(key)` use the host keybind parser/printer.
258-
- `api.keybind.create(defaults, overrides?)` builds a plugin-local keybind set.
259-
- Only missing, blank, or non-string overrides are ignored. Key syntax is not validated.
260-
- Returned keybind set exposes `all`, `get(name)`, `match(name, evt)`, `print(name)`.
261-
262263
### KV, state, client, events
263264

264265
- `api.kv` is the shared app KV store backed by `state/kv.json`. It is not plugin-namespaced.

packages/opencode/specs/v2/keymappings.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,29 @@ Make it `keymappings`, closer to neovim. Can be layered like `<leader>abc`. Comm
88
99
_Why_
1010
Currently its keybindings that have an `id` like `message_redo` and then a command can use that or define it's own binding. While some keybindings are just used with `.match` in arbitrary key handlers and there is no info what the key is used for, except the binding id maybe. It also is unknown in which context/scope what binding is active, so a plugin like `which-key` is nearly impossible to get right.
11+
12+
## OpenTUI Keymap Migration
13+
14+
The v2 TUI uses `@opentui/keymap` as the key/cmd engine. The remaining legacy compatibility is config-only and exists to migrate users from `keybinds` to `keymap`:
15+
16+
- `packages/opencode/src/config/keybinds.ts`: old `keybinds` schema, defaults, and legacy key names.
17+
- `packages/opencode/src/cli/cmd/tui/config/legacy-keymap-transform.ts`: transforms parsed legacy `keybinds` into OpenTUI `keymap` sections.
18+
- `packages/opencode/src/cli/cmd/tui/config/tui-migrate.ts`: migrates legacy TUI keys from `opencode.json` into `tui.json`, including `theme`, `keybinds`, and nested `tui`.
19+
- `packages/opencode/src/cli/cmd/tui/config/tui-schema.ts`: still accepts deprecated `keybinds` via `KeybindOverride` and marks it as deprecated. This file also contains the new `keymap` config schema.
20+
- `packages/opencode/src/cli/cmd/tui/config/tui.ts`: parses legacy `keybinds`, applies the Windows `terminal_suspend`/`input_undo` adjustment, and uses `LegacyKeymapTransform.create(...)` as the fallback when no `keymap` section is configured.
21+
- `packages/plugin/src/tui.ts`: plugin-facing `tuiConfig` still includes `keybinds` through `PluginConfig`; this should be removed when the public plugin API no longer exposes legacy config.
22+
23+
The transform must stay while users are migrating. It lets users upgrade without first rewriting their existing `keybinds` config. If `keymap` is configured, `keybinds` are ignored for keymap resolution. If `keymap` is missing, `legacy-keymap-transform.ts` turns legacy `keybinds` into the resolved `keymap` consumed by OpenTUI.
24+
25+
## Removing Legacy Later
26+
27+
When switching fully to the new config style, remove legacy support with these exact changes:
28+
29+
- Delete `packages/opencode/src/config/keybinds.ts`.
30+
- Delete `packages/opencode/src/cli/cmd/tui/config/legacy-keymap-transform.ts`.
31+
- Delete `packages/opencode/src/cli/cmd/tui/config/tui-migrate.ts`.
32+
- In `packages/opencode/src/cli/cmd/tui/config/tui-schema.ts`, remove the `ConfigKeybinds` import, remove `KeybindOverride`, and delete the deprecated `keybinds` field from `TuiInfo`.
33+
- In `packages/opencode/src/cli/cmd/tui/config/tui.ts`, remove `migrateTuiConfig(...)`, remove `ConfigKeybinds`, remove the Windows legacy keybind adjustment, remove `LegacyKeymapTransform.create(...)`, and require/default `keymap` through the new config path instead.
34+
- In `packages/opencode/src/cli/cmd/tui/config/tui.ts`, remove `keybinds` from `Resolved`; resolved TUI config should expose `keymap` only.
35+
- In `packages/plugin/src/tui.ts`, remove `keybinds` from plugin-facing `TuiConfigView`.
36+
- Remove or rewrite tests that write or assert `keybinds`, especially in `packages/opencode/test/config/tui.test.ts`, `packages/opencode/test/fixture/tui-runtime.ts`, and TUI plugin loader tests.

0 commit comments

Comments
 (0)