Repository navigation
Description
Problem
OpenCode V2 form and permission prompts own the correct selection, validation, and submission behavior, but their navigation and primary actions are registered as anonymous inline key bindings.
TUI plugins can dispatch named commands through context.keymap.dispatch(id) and inspect reachable named commands through context.keymap.commands(). Inline bindings do not become command entries, so plugins cannot invoke the active prompt semantically.
A concrete TP-7 hardware integration attempted to map its reel to option navigation and its Plus button to selection/confirmation. Looking for reachable Form or Permission bindings in keymap.commands() always returned nothing, so the plugin fell back to normal scrolling.
Relevant code:
packages/tui/src/routes/session/form.tsx: form option navigation and Return actions are inline bindings.packages/tui/src/routes/session/permission.tsx: permission option navigation and Return are inline bindings.packages/tui/src/context/keymap.tsx: inline bindings are registered directly, whileuseCommands()exposes named command entries only.
Proposed Seam
Give prompt actions stable semantic command IDs while keeping prompt components authoritative for state and behavior. For example:
form.option.previous
form.option.next
form.action
permission.option.previous
permission.option.next
permission.action
The exact names are open to refinement. The important property is that the active prompt layer exposes reachable named commands that plugins can dispatch. Existing keyboard bindings should continue to invoke the same component-owned handlers.
A plugin could then select only a reachable action and preserve fallback behavior outside prompts:
const command = context.keymap.commands().find((item) =>
item.id === "form.option.next" || item.id === "permission.option.next"
)
if (command?.id) context.keymap.dispatch(command.id)
else context.keymap.dispatch("session.line.down")Why This Seam
The prompt component should continue to own:
- Current selection and field state
- Conditional fields and multiselect behavior
- Validation
- External-action acknowledgement
- Reply, cancellation, and submission
Hardware integrations and other plugins should express semantic intent through keymap commands. They should not read prompt data and maintain duplicate UI state, call form APIs directly, search commands by display title, or inject synthetic keyboard events.
This also supports accessibility adapters, macros, and future hardware controllers without coupling them to form internals.
Acceptance Criteria
- Active form option navigation is reachable and dispatchable through stable named command IDs.
- The active form primary Return action is reachable and dispatchable through a stable named command ID across option, review, external, and text-edit states.
- Active permission option navigation and selection are reachable and dispatchable through stable named command IDs.
- Commands are reachable only when their owning prompt layer is active.
- Existing keyboard behavior and bindings remain unchanged.
- Tests prove a plugin-facing command lookup can discover and invoke these actions.
Activity
mustfsb commentedon Aug 7, 2026
Hi, I’d like to work on this.
My plan is to expose stable command IDs for form and permission option navigation and primary actions, while keeping the existing prompt-owned handlers and keyboard behavior unchanged. I’ll also add focused coverage proving that the commands are only reachable while the relevant prompt is active and can be discovered and dispatched through the plugin-facing keymap API.
Would this approach and the proposed command IDs be acceptable?
opencodejr commentedon Oct 28, 2026
Thanks for the detailed write-up. I checked this against the code on
dev(b7992598), and what you describe is how the keymap is designed to work today, so this reads as a feature request rather than a bug.In
packages/tui/src/context/keymap.tsx,groupCommands/createLayerregister commands that have anidas named keymap commands. Those are the only oneskeymap.commands()anddispatch(id)can see. Commands with only abindare registered on purpose as anonymous bindings (id: undefined).The prompt actions all fall into that second group:
routes/session/form.tsx: option up/down andj/k, Return, Space, the number keys, Tab and Escape.routes/session/permission.tsx: left/right and Return.The only named entries in those layers are
prompt.paste,prompt.clear,app.exitandpermission.prompt.fullscreen.So a plugin looking for Form or Permission commands finds nothing, as you saw. Getting that to work means adding stable IDs such as
form.option.nextandpermission.action, as you propose. The components would keep their existing handlers and the IDs would only be reachable while their layer is active.