Add plugin response contract for structured output and annotations - #198
Merged
Conversation
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
markdownorhtmlbody 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): AddedParsePluginOutput()to parse plugin stdout against the response contract, with fallback handling for legacy output. AddedcollectPluginAnnotations()to aggregate annotations from successful plugins andparsePluginOutputs()to prepare results for clients. Annotations without a filename or positive line number are dropped.Server integration (
server/server.go): ExtendedPRPayloadto includeAnnotationsfield populated fromcollectPluginAnnotations(), 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, andtotalAnnotationCount()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.PluginOutput.tsxandPluginsPanel.tsxto 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 includePRAnnotationinGetPRStructreplies.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 ./...passesgo test ./...passes (new tests inserver/plugin_contract_test.gocover parsing, fallback behavior, and annotation collection)bun run testpasses (new tests inbun_client/frontend/src/plugin_utils.test.tscover body resolution, annotation sorting, and severity mapping)https://claude.ai/code/session_01CM8QkxAUerhjRFyJ2NT3wY