Skip to content

feat(tui): expose stable prompt action commands to plugins #40953

Open

Description

@kitlangton

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, while useCommands() 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

  1. mustfsb commented on Aug 7, 2026

    @mustfsb

    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?

  2. opencodejr commented on Oct 28, 2026

    @opencodejr
    Contributor

    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 / createLayer register commands that have an id as named keymap commands. Those are the only ones keymap.commands() and dispatch(id) can see. Commands with only a bind are registered on purpose as anonymous bindings (id: undefined).

    The prompt actions all fall into that second group:

    • In routes/session/form.tsx: option up/down and j/k, Return, Space, the number keys, Tab and Escape.
    • In routes/session/permission.tsx: left/right and Return.

    The only named entries in those layers are prompt.paste, prompt.clear, app.exit and permission.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.next and permission.action, as you propose. The components would keep their existing handlers and the IDs would only be reachable while their layer is active.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions