Skip to content

feat(opencode): allow skipping the agent turn after a command - #46579

Closed
renaudcerrato wants to merge 1 commit into
anomalyco:devfrom
renaudcerrato:feat/command-noreply
Closed

renaudcerrato wants to merge 1 commit into
anomalyco:devfrom
renaudcerrato:feat/command-noreply

Conversation

@renaudcerrato

@renaudcerrato renaudcerrato commented Sep 1, 2026 •

Copy link
Copy Markdown

Issue for this PR

Closes #28292 (the noReply half; custom dialogs are a separate surface)

Type of change

  • New feature

What does this PR do?

Slash commands always run an agent LLM turn after execution, even when the command needs no LLM involvement. prompt() already short-circuits on noReply (returns the user message without running the loop), but the flag was unreachable from the command path: CommandInput had no noReply field, and the command.execute.before hook could only mutate parts.

This PR plumbs noReply through the command path:

  • CommandInput gains an optional noReply field, so server API callers (POST /session/:id/command) can skip the turn
  • The command.execute.before hook output gains a noReply flag (default false) that plugins can set, forwarded to prompt() alongside the client-provided value
  • Semantics are OR: either party can suppress the turn; neither can force a turn the other explicitly declined

Real-world usage (from our own plugin and others in #28292):

  • Config-management commands. A plugin slash command that lists/sets/deletes entries in a JSON file, validated against the provider list — pure text I/O, no LLM needed. Every invocation cost a full LLM turn. Worse: run right after an edit turn, the model received an effectively empty user message plus stale context and re-emitted the previous turn's tool calls — redoing edits the user had already made. The plugin-side workaround (splicing a synthetic anchor part instructing the model to reply with a one-line confirmation) stops the redo hazard but still burns an LLM call per command; only server-side noReply fixes the cost.
  • Auto-review toggle (opencode-review, @Svtter in the thread): "toggle auto-review without LLM round-trip".
  • Queue commands (@mirsella in the thread): a queue plugin handling /queue commands entirely locally.
  • The thread also documents why the ignored: true parts workaround fails: the turn still fires, still costs tokens, and suppression is probabilistic.

How did you verify your code works?

  • bun run typecheck clean in packages/opencode, packages/plugin, packages/sdk/js
  • bun test test/session/prompt.test.ts: 61 pass, 0 fail, including three new tests:
    • client noReply: true → user message returned, zero LLM calls
    • plugin sets output.noReply = true in command.execute.before → same, plus plugin-pushed parts land in the persisted message
    • plugin sets output.noReply = false while client sent noReply: true → turn still skipped (a hook cannot override the client's suppression)
  • SDK regen produces only the expected noReply additions, no unrelated drift

Screenshots / recordings

Not a UI change.

Checklist

  • I have tested my changes locally
  • I have not included unrelated changes in this PR

Critical changes: the command.execute.before hook output type changes from { parts } to { parts, noReply } — additive; existing plugins that only touch parts are unaffected (structural typing; runtime always initializes noReply: false). Command.Event.Executed's messageID is the user message when noReply is set (vs the assistant message otherwise); noted in the SDK docs row. The only in-repo consumer reads only data.name and is unaffected.

Commands always ran an agent loop after execution: CommandInput had no
noReply field and the command.execute.before hook could only mutate
parts, so neither clients nor plugins could opt out of the LLM turn.
prompt() already short-circuits on noReply — it just was not reachable
from the command path.

Plumb noReply through:
- CommandInput gains an optional noReply field (server API callers)
- command.execute.before hook output gains a noReply flag (plugins),
  forwarded to prompt() alongside the client-provided value

Semantics are OR: either party can suppress the turn, neither can force
a turn the other explicitly declined.

This lets text-only commands (config management, toggles, lookups)
complete without a wasted LLM call and without the model emitting a
spurious reply to an effectively empty user turn.

Tests: client noReply skips the loop; hook noReply skips the loop; a
hook cannot force a turn the client suppressed. SDK types regenerated,
docs updated.
@github-actions github-actions Bot added needs:compliance This means the issue will auto-close after 2 hours. and removed needs:compliance This means the issue will auto-close after 2 hours. labels Sep 1, 2026
@github-actions

github-actions Bot commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

Thanks for updating your PR! It now meets our contributing guidelines. 👍

@thdxr

thdxr commented Oct 10, 2026

Copy link
Copy Markdown
Member

This branch has been upgraded to V2. We're no longer taking PRs for V1. If you think this is still relevant, please port it over and open it for V2.

— from 𝕺𝖕𝖊𝖓𝕮𝖔𝖉𝖊

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

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

2 participants