From 1c0d505983c7e440d11849b283a1ddc34ba0adea Mon Sep 17 00:00:00 2001 From: Michael Ramos Date: Wed, 13 May 2026 05:10:42 -0700 Subject: [PATCH] fix(opencode): intercept annotate/review/archive commands before LLM (#713) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit OpenCode's command dispatcher appends `arguments` to the `.md` body and runs `resolvePromptParts()` over the combined string, which auto-attaches any file path it finds as a `FilePart`. With `/plannotator-annotate /path/to/huge.md`, that meant the agent received the file's content as a user message before the annotation UI even opened — blowing the context on large files (GLM-5 auto-compact reported in #713). Move `plannotator-annotate`, `plannotator-review`, and `plannotator-archive` from the post-hoc `event` handler to `command.execute.before`, matching the pattern `plannotator-last` already used. The hook clears `output.parts` in place so the agent never receives the command turn; handlers then run the UI and inject feedback via `client.session.prompt` as a separate turn. Empty the bodies of the three `.md` files for defense in depth — only the frontmatter is needed for OpenCode to register the slash command. Also fixes a latent bug in the `plannotator-last` path: `output.parts = []` reassigns the throwaway wrapper object's property but doesn't touch the `parts` array the caller in `prompt.ts:1944` uses directly. Switched to `output.parts.length = 0` to mutate in place. `plannotator-last` only escaped notice because its parts array was always a single benign text part. --- .../commands/plannotator-annotate.md | 3 - .../commands/plannotator-archive.md | 3 - .../commands/plannotator-review.md | 3 - apps/opencode-plugin/index.ts | 94 +++++++++---------- 4 files changed, 43 insertions(+), 60 deletions(-) diff --git a/apps/opencode-plugin/commands/plannotator-annotate.md b/apps/opencode-plugin/commands/plannotator-annotate.md index f2c7d2208..6a9c58fb2 100644 --- a/apps/opencode-plugin/commands/plannotator-annotate.md +++ b/apps/opencode-plugin/commands/plannotator-annotate.md @@ -1,6 +1,3 @@ --- description: Open interactive annotation UI for a markdown file, HTML file, or URL --- - -The Plannotator Annotate UI has been triggered. Opening the annotation UI... -Acknowledge "Opening annotation UI..." and wait for the user's feedback. diff --git a/apps/opencode-plugin/commands/plannotator-archive.md b/apps/opencode-plugin/commands/plannotator-archive.md index b9899be8a..2dfff75cc 100644 --- a/apps/opencode-plugin/commands/plannotator-archive.md +++ b/apps/opencode-plugin/commands/plannotator-archive.md @@ -1,6 +1,3 @@ --- description: Browse saved plan decisions in the archive --- - -The Plannotator Archive has been triggered. Opening the archive browser... -Acknowledge "Opening plan archive..." and wait for the user to finish browsing. diff --git a/apps/opencode-plugin/commands/plannotator-review.md b/apps/opencode-plugin/commands/plannotator-review.md index 172904be4..bd150d01d 100644 --- a/apps/opencode-plugin/commands/plannotator-review.md +++ b/apps/opencode-plugin/commands/plannotator-review.md @@ -1,6 +1,3 @@ --- description: Open interactive code review for current changes or a PR URL; pass --git to force Git in JJ workspaces --- - -The Plannotator Code Review has been triggered. Opening the review UI... -Acknowledge "Opening code review..." and wait for the user's feedback. diff --git a/apps/opencode-plugin/index.ts b/apps/opencode-plugin/index.ts index 5e576853a..b9ebb9fb8 100644 --- a/apps/opencode-plugin/index.ts +++ b/apps/opencode-plugin/index.ts @@ -383,11 +383,26 @@ The user will review your plan in a visual UI where they can annotate, approve, Do NOT proceed with implementation until your plan is approved.`); }, - // Intercept plannotator-last before the agent sees the command + // Intercept plannotator commands before the agent sees them. + // Clearing output.parts in place suppresses the .md body + appended + // args so the agent never receives the command — without this, OpenCode + // calls resolvePromptParts() on " ", which auto-attaches + // any file path it finds as a FilePart. On a large file that blows the + // context before the annotation UI even opens (#713). + // + // Must mutate in place (length = 0), not reassign (= []). The caller + // holds a reference to the parts array directly and ignores any new + // array assigned to output.parts. "command.execute.before": async (input, output) => { - if (input.command !== "plannotator-last") return; + const cmd = input.command; + if ( + cmd !== "plannotator-last" && + cmd !== "plannotator-annotate" && + cmd !== "plannotator-review" && + cmd !== "plannotator-archive" + ) return; - output.parts = []; + output.parts.length = 0; const deps: CommandDeps = { client: ctx.client, @@ -398,58 +413,35 @@ Do NOT proceed with implementation until your plan is approved.`); getPasteApiUrl, directory: ctx.directory, }; + // input.arguments is the raw tail string from OpenCode's command dispatcher — + // needed so --gate / --json reach the handlers' parseAnnotateArgs (#570). + const event = { + properties: { sessionID: input.sessionID, arguments: input.arguments }, + }; - const feedback = await handleAnnotateLastCommand( - // input.arguments is the raw tail string from OpenCode's command dispatcher — - // needed so --gate / --json reach handleAnnotateLastCommand's parseAnnotateArgs (#570). - { properties: { sessionID: input.sessionID, arguments: input.arguments } }, - deps - ); - - if (feedback) { - try { - await ctx.client.session.prompt({ - path: { id: input.sessionID }, - body: { - parts: [{ - type: "text", - text: getAnnotateMessageFeedbackPrompt("opencode", undefined, { feedback }), - }], - }, - }); - } catch { - // Session may not be available + if (cmd === "plannotator-last") { + const feedback = await handleAnnotateLastCommand(event, deps); + if (feedback) { + try { + await ctx.client.session.prompt({ + path: { id: input.sessionID }, + body: { + parts: [{ + type: "text", + text: getAnnotateMessageFeedbackPrompt("opencode", undefined, { feedback }), + }], + }, + }); + } catch { + // Session may not be available + } } + return; } - }, - - // Listen for slash commands (review + annotate) - event: async ({ event }) => { - const isCommandEvent = - event.type === "command.executed" || - event.type === "tui.command.execute"; - - if (!isCommandEvent) return; - - // @ts-ignore - Event structure varies - const commandName = event.properties?.name || event.command || event.payload?.name; - - const deps: CommandDeps = { - client: ctx.client, - htmlContent: getPlanHtml(), - reviewHtmlContent: getReviewHtml(), - getSharingEnabled, - getShareBaseUrl, - getPasteApiUrl, - directory: ctx.directory, - }; - if (commandName === "plannotator-review") - return handleReviewCommand(event, deps); - if (commandName === "plannotator-annotate") - return handleAnnotateCommand(event, deps); - if (commandName === "plannotator-archive") - return handleArchiveCommand(event, deps); + if (cmd === "plannotator-annotate") return handleAnnotateCommand(event, deps); + if (cmd === "plannotator-review") return handleReviewCommand(event, deps); + if (cmd === "plannotator-archive") return handleArchiveCommand(event, deps); }, };