Skip to content

Add plugin response contract for structured output and annotations - #198

Merged
C-Hipple merged 2 commits into
mainfrom
claude/plugin-response-contract-wg4fxs
Jul 30, 2026
Merged

Add plugin response contract for structured output and annotations#198
C-Hipple merged 2 commits into
mainfrom
claude/plugin-response-contract-wg4fxs

Conversation

@C-Hipple

Copy link
Copy Markdown
Owner

Summary

Implements a plugin response contract that allows plugins to emit structured JSON declaring how their output should be rendered and to attach line-level annotations to PR diffs. Plugins can now specify markdown or html body types and include annotations anchored to specific files and lines. The system maintains full backwards compatibility—plain text and non-conforming JSON output continues to work as before, wrapped as markdown.

Changes

  • Backend (server/plugin_contract.go): Added ParsePluginOutput() to parse plugin stdout against the response contract, with fallback handling for legacy output. Added collectPluginAnnotations() to aggregate annotations from successful plugins and parsePluginOutputs() to prepare results for clients. Annotations without a filename or positive line number are dropped.

  • Server integration (server/server.go): Extended PRPayload to include Annotations field populated from collectPluginAnnotations(), making plugin annotations available in PR responses.

  • Frontend utilities (bun_client/frontend/src/plugin_utils.ts): Added contract types (PluginBody, PluginAnnotation, PluginResult, PRAnnotation) and helper functions: resolvePluginBody() to select which body to render (parsed or fallback), sortedAnnotations() to order annotations by file and line, annotationSeverityVariant() to map severity strings to UI variants, and totalAnnotationCount() for summary counts.

  • Frontend components:

    • PluginBodyView.tsx: Renders plugin bodies as markdown inline or HTML in a sandboxed iframe (with theme variable inheritance and responsive sizing).
    • PluginAnnotations.tsx: Lists line-level annotations with severity badges and file:line anchors.
    • Updated PluginOutput.tsx and PluginsPanel.tsx to use the new contract helpers and display annotation counts.
  • Documentation (docs/plugins.md, docs/protocol.md): Documented the response contract with field descriptions, backwards compatibility guarantees, and updated protocol docs to include PRAnnotation in GetPRStruct replies.

  • Tests: Comprehensive test coverage for parsing (conforming/non-conforming JSON, legacy fallback, annotation filtering) and annotation collection (successful plugins only, deterministic ordering).

Test Plan

  • go build ./... passes
  • go test ./... passes (new tests in server/plugin_contract_test.go cover parsing, fallback behavior, and annotation collection)
  • bun run test passes (new tests in bun_client/frontend/src/plugin_utils.test.ts cover body resolution, annotation sorting, and severity mapping)

https://claude.ai/code/session_01CM8QkxAUerhjRFyJ2NT3wY

claude added 2 commits July 29, 2026 21:58
Plugins can now emit JSON with a body object ({body_type: markdown|html,
body_content}) and a list of annotations anchoring remarks to a filename
and line in the PR diff. Output that doesn't match the contract — plain
text, invalid JSON, a missing body, or an unknown body_type — is wrapped
verbatim as a markdown body with no annotations, so existing plugins keep
working unchanged.

Parsing happens when results are served, not when they're stored: the raw
stdout stays in the DB and is still returned in the `result` field, while
GetPluginOutput/RerunPlugins replies gain parsed `body` and `annotations`
fields. Annotations from every successfully executed plugin are also
aggregated into a new `annotations` field on the PR reply payload (GetPR
and friends), each tagged with its source plugin, ready for follow-up
work rendering them into the diff.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CM8QkxAUerhjRFyJ2NT3wY
The plugin output page rendered whatever the plugin wrote to stdout as
preformatted text, so a plugin speaking the response contract showed its
raw JSON. Both plugin surfaces — the standalone output page and the review
view's drawer — now render the parsed body according to its declared
body_type and list the plugin's annotations beneath it.

plugin_utils owns the client's reading of the contract: resolvePluginBody
trusts a body the server sends (an empty body with annotations attached is
a legitimate annotations-only response) and falls back to the raw stdout as
markdown when the body is missing or carries an unknown type, so an older
server keeps working.

An html body renders in a sandboxed frame rather than via innerHTML.
Plugin bodies are frequently LLM-generated text derived from a PR's diff
and description, so they aren't trusted markup; the frame withholds
allow-scripts, which blocks scripts and inline handlers, and keeps
allow-same-origin only to measure content for sizing. It inherits the
active theme's variables and refits when the theme changes, collapsing
before measuring so a shorter re-run doesn't leave blank space.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CM8QkxAUerhjRFyJ2NT3wY
@C-Hipple
C-Hipple merged commit 5bd1baa into main Jul 30, 2026
7 checks passed
@C-Hipple
C-Hipple deleted the claude/plugin-response-contract-wg4fxs branch July 30, 2026 13:57
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.

2 participants