Skip to content

Commit 19da27e

Browse files
authored
internal which-key plugin, inactive by default (#26337)
1 parent 4a73749 commit 19da27e

34 files changed

Lines changed: 908 additions & 63 deletions

packages/opencode/specs/tui-plugins.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ Example:
3636
- `plugin_enabled` is keyed by plugin id, not by plugin spec.
3737
- For file plugins, that id must come from the plugin module's exported `id`. For npm plugins, it is the exported `id` or the package name if `id` is omitted.
3838
- Plugins are enabled by default. `plugin_enabled` is only for explicit overrides, usually to disable a plugin with `false`.
39+
- Internal plugins can declare `enabled: false` to be registered but inactive by default; `plugin_enabled` and runtime KV can still enable them by id.
3940
- `plugin_enabled` is merged across config layers.
4041
- Runtime enable/disable state is also stored in KV under `plugin_enabled`; that KV state overrides config on startup.
4142

@@ -227,6 +228,7 @@ Top-level API groups exposed to `tui(api, options, meta)`:
227228
- 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.
228229
- Use `api.keymap.dispatchCommand(name)` for user-style execution semantics and `api.keymap.runCommand(name)` only for forced programmatic execution.
229230
- 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.
231+
- Built-in which-key shortcuts are resolved from `keymap.sections.which_key`, not plugin options.
230232

231233
### Keys
232234

@@ -314,6 +316,7 @@ Theme install behavior:
314316
Current host slot names:
315317

316318
- `app`
319+
- `app_bottom`
317320
- `home_logo`
318321
- `home_prompt` with props `{ workspace_id?, ref? }`
319322
- `home_prompt_right` with props `{ workspace_id? }`
@@ -332,7 +335,8 @@ Slot notes:
332335
- `api.slots.register(plugin)` does not return an unregister function.
333336
- Returned ids are `pluginId`, `pluginId:1`, `pluginId:2`, and so on.
334337
- Plugin-provided `id` is not allowed.
335-
- The current host renders `home_logo`, `home_prompt`, and `session_prompt` with `replace`, `home_footer`, `sidebar_title`, and `sidebar_footer` with `single_winner`, and `app`, `home_prompt_right`, `session_prompt_right`, `home_bottom`, and `sidebar_content` with the slot library default mode.
338+
- The current host renders `home_logo`, `home_prompt`, and `session_prompt` with `replace`, `home_footer`, `sidebar_title`, and `sidebar_footer` with `single_winner`, and `app`, `app_bottom`, `home_prompt_right`, `session_prompt_right`, `home_bottom`, and `sidebar_content` with the slot library default mode.
339+
- `app_bottom` is rendered in normal layout flow below the active route, while `app` is rendered afterward for global app-level UI.
336340
- Plugins can define custom slot names in `api.slots.register(...)` and render them from plugin UI with `ui.Slot`.
337341

338342
### Plugin control and lifecycle

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

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -399,6 +399,7 @@ function App(props: { onSnapshot?: () => Promise<string[]> }) {
399399
{
400400
name: "command.palette.show",
401401
title: "Show command palette",
402+
category: "System",
402403
hidden: true,
403404
run: () => {
404405
command.show()
@@ -852,6 +853,7 @@ function App(props: { onSnapshot?: () => Promise<string[]> }) {
852853
<box
853854
width={dimensions().width}
854855
height={dimensions().height}
856+
flexDirection="column"
855857
backgroundColor={theme.background}
856858
onMouseDown={(evt) => {
857859
if (!Flag.OPENCODE_EXPERIMENTAL_DISABLE_COPY_ON_SELECT) return
@@ -867,17 +869,22 @@ function App(props: { onSnapshot?: () => Promise<string[]> }) {
867869
<TimeToFirstDraw />
868870
</Show>
869871
<Show when={ready()}>
870-
<Switch>
871-
<Match when={route.data.type === "home"}>
872-
<Home />
873-
</Match>
874-
<Match when={route.data.type === "session"}>
875-
<Session />
876-
</Match>
877-
</Switch>
872+
<box flexGrow={1} minHeight={0} flexDirection="column">
873+
<Switch>
874+
<Match when={route.data.type === "home"}>
875+
<Home />
876+
</Match>
877+
<Match when={route.data.type === "session"}>
878+
<Session />
879+
</Match>
880+
</Switch>
881+
{plugin()}
882+
</box>
883+
<box flexShrink={0}>
884+
<TuiPluginRuntime.Slot name="app_bottom" />
885+
</box>
886+
<TuiPluginRuntime.Slot name="app" />
878887
</Show>
879-
{plugin()}
880-
<TuiPluginRuntime.Slot name="app" />
881888
<StartupLoading ready={ready} />
882889
</box>
883890
)

packages/opencode/src/cli/cmd/tui/component/dialog-provider.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -243,6 +243,8 @@ function AutoMethod(props: AutoMethodProps) {
243243
bindings: [
244244
{
245245
key: "c",
246+
desc: "Copy provider code",
247+
group: "Dialog",
246248
cmd: () => {
247249
const code =
248250
props.authorization.instructions.match(/[A-Z0-9]{4}-[A-Z0-9]{4,5}/)?.[0] ?? props.authorization.url

packages/opencode/src/cli/cmd/tui/component/dialog-retry-action.tsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,18 +48,26 @@ export function DialogRetryAction(props: DialogRetryActionProps) {
4848
bindings: [
4949
{
5050
key: "left",
51+
desc: "Previous retry option",
52+
group: "Dialog",
5153
cmd: () => setSelected((value) => (value === "action" ? "dismiss" : "action")),
5254
},
5355
{
5456
key: "right",
57+
desc: "Next retry option",
58+
group: "Dialog",
5559
cmd: () => setSelected((value) => (value === "action" ? "dismiss" : "action")),
5660
},
5761
{
5862
key: "tab",
63+
desc: "Next retry option",
64+
group: "Dialog",
5965
cmd: () => setSelected((value) => (value === "action" ? "dismiss" : "action")),
6066
},
6167
{
6268
key: "return",
69+
desc: "Confirm retry option",
70+
group: "Dialog",
6371
cmd: () => {
6472
if (selected() === "action") runAction(props, dialog)
6573
else dismiss(props, dialog)

packages/opencode/src/cli/cmd/tui/component/dialog-session-delete-failed.tsx

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -42,11 +42,11 @@ export function DialogSessionDeleteFailed(props: {
4242

4343
useBindings(() => ({
4444
bindings: [
45-
{ key: "return", cmd: () => void confirm() },
46-
{ key: "left", cmd: () => setStore("active", "delete") },
47-
{ key: "up", cmd: () => setStore("active", "delete") },
48-
{ key: "right", cmd: () => setStore("active", "restore") },
49-
{ key: "down", cmd: () => setStore("active", "restore") },
45+
{ key: "return", desc: "Confirm recovery option", group: "Dialog", cmd: () => void confirm() },
46+
{ key: "left", desc: "Delete broken session", group: "Dialog", cmd: () => setStore("active", "delete") },
47+
{ key: "up", desc: "Delete broken session", group: "Dialog", cmd: () => setStore("active", "delete") },
48+
{ key: "right", desc: "Restore broken session", group: "Dialog", cmd: () => setStore("active", "restore") },
49+
{ key: "down", desc: "Restore broken session", group: "Dialog", cmd: () => setStore("active", "restore") },
5050
],
5151
}))
5252

packages/opencode/src/cli/cmd/tui/component/dialog-workspace-unavailable.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,9 @@ export function DialogWorkspaceUnavailable(props: { onRestore?: () => boolean |
2525

2626
useBindings(() => ({
2727
bindings: [
28-
{ key: "return", cmd: () => void confirm() },
29-
{ key: "left", cmd: () => setStore("active", "cancel") },
30-
{ key: "right", cmd: () => setStore("active", "restore") },
28+
{ key: "return", desc: "Confirm workspace option", group: "Dialog", cmd: () => void confirm() },
29+
{ key: "left", desc: "Cancel workspace restore", group: "Dialog", cmd: () => setStore("active", "cancel") },
30+
{ key: "right", desc: "Restore workspace", group: "Dialog", cmd: () => setStore("active", "restore") },
3131
],
3232
}))
3333

packages/opencode/src/cli/cmd/tui/component/prompt/autocomplete.tsx

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -528,32 +528,42 @@ export function Autocomplete(props: {
528528
commands: [
529529
{
530530
name: "prompt.autocomplete.prev",
531+
title: "Previous autocomplete item",
532+
category: "Autocomplete",
531533
run() {
532534
setStore("input", "keyboard")
533535
move(-1)
534536
},
535537
},
536538
{
537539
name: "prompt.autocomplete.next",
540+
title: "Next autocomplete item",
541+
category: "Autocomplete",
538542
run() {
539543
setStore("input", "keyboard")
540544
move(1)
541545
},
542546
},
543547
{
544548
name: "prompt.autocomplete.hide",
549+
title: "Hide autocomplete",
550+
category: "Autocomplete",
545551
run() {
546552
hide()
547553
},
548554
},
549555
{
550556
name: "prompt.autocomplete.select",
557+
title: "Select autocomplete item",
558+
category: "Autocomplete",
551559
run() {
552560
select()
553561
},
554562
},
555563
{
556564
name: "prompt.autocomplete.complete",
565+
title: "Complete autocomplete item",
566+
category: "Autocomplete",
557567
run() {
558568
const selected = options()[store.selected]
559569
if (selected?.isDirectory) {

packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -892,6 +892,8 @@ export function Prompt(props: PromptProps) {
892892
bindings: [
893893
{
894894
key: "!",
895+
desc: "Shell mode",
896+
group: "Prompt",
895897
cmd: () => {
896898
setStore("placeholder", randomIndex(shell().length))
897899
setStore("mode", "shell")
@@ -905,7 +907,7 @@ export function Prompt(props: PromptProps) {
905907
return {
906908
target: inputTarget,
907909
enabled: inputTarget() !== undefined && store.mode === "shell",
908-
bindings: [{ key: "escape", cmd: () => setStore("mode", "normal") }],
910+
bindings: [{ key: "escape", desc: "Exit shell mode", group: "Prompt", cmd: () => setStore("mode", "normal") }],
909911
}
910912
})
911913

@@ -916,7 +918,7 @@ export function Prompt(props: PromptProps) {
916918
cursorVersion()
917919
return inputTarget() !== undefined && store.mode === "shell" && input?.visualCursor.offset === 0
918920
})(),
919-
bindings: [{ key: "backspace", cmd: () => setStore("mode", "normal") }],
921+
bindings: [{ key: "backspace", desc: "Exit shell mode", group: "Prompt", cmd: () => setStore("mode", "normal") }],
920922
}
921923
})
922924

@@ -936,6 +938,8 @@ export function Prompt(props: PromptProps) {
936938
commands: [
937939
{
938940
name: "prompt.history.previous",
941+
title: "Previous prompt history",
942+
category: "Prompt",
939943
run() {
940944
if (input.cursorOffset !== 0) {
941945
input.cursorOffset = 0
@@ -972,6 +976,8 @@ export function Prompt(props: PromptProps) {
972976
commands: [
973977
{
974978
name: "prompt.history.next",
979+
title: "Next prompt history",
980+
category: "Prompt",
975981
run() {
976982
if (input.cursorOffset !== input.plainText.length) {
977983
input.cursorOffset = input.plainText.length

packages/opencode/src/cli/cmd/tui/config/tui-schema.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,20 @@ const GlobalKeymapSection = {
8888
"terminal.title.toggle": keymapBinding("none"),
8989
}
9090

91+
const WhichKeyKeymapSection = {
92+
"tui-which-key.toggle": keymapBinding("ctrl+alt+k"),
93+
"tui-which-key.layout.toggle": keymapBinding("ctrl+alt+shift+k"),
94+
"tui-which-key.pending.toggle": keymapBinding("ctrl+alt+shift+p"),
95+
"tui-which-key.group.previous": keymapBinding("ctrl+alt+left,ctrl+alt+["),
96+
"tui-which-key.group.next": keymapBinding("ctrl+alt+right,ctrl+alt+]"),
97+
"tui-which-key.scroll.up": keymapBinding("ctrl+alt+up,ctrl+alt+p"),
98+
"tui-which-key.scroll.down": keymapBinding("ctrl+alt+down,ctrl+alt+n"),
99+
"tui-which-key.page.up": keymapBinding("ctrl+alt+pageup"),
100+
"tui-which-key.page.down": keymapBinding("ctrl+alt+pagedown"),
101+
"tui-which-key.home": keymapBinding("ctrl+alt+home"),
102+
"tui-which-key.end": keymapBinding("ctrl+alt+end"),
103+
}
104+
91105
const SessionKeymapSection = {
92106
"session.share": keymapBinding("none"),
93107
"session.rename": keymapBinding("ctrl+r"),
@@ -231,6 +245,7 @@ const HomeTipsKeymapSection = {
231245

232246
const KeymapSectionsShape = {
233247
global: keymapSection(GlobalKeymapSection),
248+
which_key: keymapSection(WhichKeyKeymapSection),
234249
session: keymapSection(SessionKeymapSection),
235250
prompt: keymapSection(PromptKeymapSection),
236251
autocomplete: keymapSection(AutocompleteKeymapSection),
@@ -246,6 +261,7 @@ const KeymapSectionsShape = {
246261

247262
const KeymapSectionsInputShape = {
248263
global: keymapSectionInput(GlobalKeymapSection).optional(),
264+
which_key: keymapSectionInput(WhichKeyKeymapSection).optional(),
249265
session: keymapSectionInput(SessionKeymapSection).optional(),
250266
prompt: keymapSectionInput(PromptKeymapSection).optional(),
251267
autocomplete: keymapSectionInput(AutocompleteKeymapSection).optional(),
@@ -271,6 +287,7 @@ export type KeymapInfo = {
271287

272288
export const KeymapSectionGroups = {
273289
global: "Global",
290+
which_key: "System",
274291
session: "Session",
275292
prompt: "Prompt",
276293
autocomplete: "Autocomplete",

packages/opencode/src/cli/cmd/tui/feature-plugins/home/footer.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import type { TuiPlugin, TuiPluginApi, TuiPluginModule } from "@opencode-ai/plugin/tui"
1+
import type { TuiPlugin, TuiPluginApi } from "@opencode-ai/plugin/tui"
2+
import type { InternalTuiPlugin } from "../../plugin/internal"
23
import { createMemo, Match, Show, Switch } from "solid-js"
34
import { Global } from "@opencode-ai/core/global"
45

@@ -85,7 +86,7 @@ const tui: TuiPlugin = async (api) => {
8586
})
8687
}
8788

88-
const plugin: TuiPluginModule & { id: string } = {
89+
const plugin: InternalTuiPlugin = {
8990
id,
9091
tui,
9192
}

0 commit comments

Comments
 (0)