Skip to content

[FEATURE]:Allow plugins to intercept slash commands and return results directly (skip LLM), plus register custom dialogs #28292

@graoke

Description

@graoke

Feature hasn't been suggested before.

  • I have verified this feature I'm about to request hasn't been suggested before.

Describe the enhancement you want to request

Feature Request: Allow plugins to intercept slash commands and return results directly (skip LLM), plus register custom dialogs

Problem
Currently, plugins can hook into slash command execution via command.execute.before, but they have no way to prevent the command from being sent to the LLM after the hook runs. The only workaround is throwing an error in the hook, which is hacky and produces a poor UX.
Additionally, external plugins cannot register TUI-level palette commands with custom dialogs (like /models does with DialogModel). Dialog-based interactions — browsing, selecting, confirming — are exclusive to opencode's built-in commands.
These limitations make it unnecessarily hard to build responsive plugin experiences. For example:

  • A /todo list plugin that queries a local file and displays results instantly
  • A /branch switch plugin that shows a picker of git branches
  • A /focus goal plugin that updates session state and shows a confirmation without waiting for an LLM round-trip

Feature 1: noReply field on command.execute.before output
Allow the command.execute.before hook to signal that the LLM call should be skipped and the hook's output parts shown directly to the user.
Current hook signature (packages/plugin/src/index.ts lines 261-264):

"command.execute.before"?: (
  input: { command: string; sessionID: string; arguments: string },
  output: { parts: Part[] },
) => Promise<void>

Proposed change — add an optional noReply field to output:

"command.execute.before"?: (
  input: { command: string; sessionID: string; arguments: string },
  output: { parts: Part[]; noReply?: boolean },
) => Promise<void>

When output.noReply = true is set by the hook, the command should:

  1. Save the user message to the session (so it appears in chat history)
  2. Skip the LLM loop entirely
  3. Display the output.parts directly as the assistant's response (or as a toast/status)
    The noReply parameter already exists on the prompt() function (packages/opencode/src/session/prompt.ts line 1631), but there is currently no way for a plugin hook to set it.
    Implementation sketch (packages/opencode/src/session/prompt.ts around line 1984):
const hookOutput = yield* plugin.trigger(
  "command.execute.before",
  { command: input.command, sessionID: input.sessionID, arguments: input.arguments },
  { parts },
)
if (hookOutput?.noReply) {
  const result = yield* prompt({
    ...input,
    parts: hookOutput.parts,
    noReply: true,
  })
  yield* bus.publish(Command.Event.Executed, { ... })
  return result
}

Feature 2: Plugin API for registering palette commands with custom dialogs
Allow external plugins (loaded via opencode.json's plugin field) to register TUI-level palette commands that open custom dialogs — the same capability that built-in commands like /models (DialogModel) and /sessions (DialogSession) have.
Currently, api.keymap.registerLayer() is only accessible to internal system plugins. External plugins only have access to server-side hooks. There's no bridge for an external plugin to say "I want this slash command to open a dialog."
Possible approaches:
(a) Expose api.ui.dialog.replace() and api.keymap.registerLayer() to external plugins, so they can register commands with custom dialog components (even if limited to simple form/select/list components).
(b) Add a new hook (e.g., "command.dialog") that receives the command name and arguments, and returns a dialog descriptor that the TUI can render:

"command.dialog"?: (
  input: { command: string; arguments: string },
  output: { dialog: { type: "select" | "confirm" | "prompt"; options?: string[]; message: string } },
) => Promise<void>

(c) Allow plugins to provide pre-built dialog components via a plugin manifest, loaded and rendered by the TUI at runtime.
Use Cases

  1. Instant feedback commands: /f-goal (set a focus goal) → plugin saves to file, shows "✅ Goal: old → new" in the UI immediately, no LLM wait
  2. Selection dialogs: /branch → plugin lists git branches, shows a selectable list, user picks one, plugin checks it out
  3. Status queries: /status → plugin reads working tree state, renders a compact summary in a dialog
  4. Toggle switches: /debug on → plugin toggles a config flag, shows confirmation toast
  5. Composable workflows: /deploy staging → plugin runs pre-checks, shows progress, all without consuming LLM context window
    Why this matters
    These changes would unlock a new class of low-latency, utility-focused plugins that don't need LLM involvement for simple CRUD operations. This is especially valuable for:

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type
No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions