You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
feat: external annotations API with real-time SSE (#400)
Adds a general-purpose External Annotations API that allows external programs (linters, AI tools, security scanners) to push annotations into a live Plannotator session via HTTP, with real-time delivery over SSE.
## What's included
- **Shared core** (`packages/shared/external-annotation.ts`): types, in-memory store, input validation, SSE serialization
- **Server handlers**: Bun + Pi implementations with full CRUD (GET/POST/PATCH/DELETE) + SSE streaming
- **Client hook** (`useExternalAnnotations`): EventSource with polling fallback, optimistic updates
- **Editor integration**: two-array state model (local + external), content-aware dedup, ID-based routing
- **Persistence**: source field preserved through share URLs and crash-recovery drafts
- **Docs**: new Integrations category with API overview page, updated API reference
## API surface
All three servers (plan, review, annotate) expose:
- `GET /api/external-annotations/stream` - SSE stream
- `GET /api/external-annotations` - JSON snapshot (polling fallback)
- `POST /api/external-annotations` - Add annotations (single or batch)
- `PATCH /api/external-annotations?id=` - Update fields
- `DELETE /api/external-annotations` - Remove by id, source, or clear all
For provenance purposes, this commit was AI assisted.
└── legacy/ # Old pre-monorepo code (reference only)
65
70
```
66
71
72
+
## Server Runtimes
73
+
74
+
There are two separate server implementations with the same API surface:
75
+
76
+
-**Bun server** (`packages/server/`) — used by both Claude Code (`apps/hook/`) and OpenCode (`apps/opencode-plugin/`). These plugins import directly from `@plannotator/server`.
77
+
-**Pi server** (`apps/pi-extension/server/`) — a standalone Node.js server for the Pi extension. It mirrors the Bun server's API but uses `node:http` primitives instead of Bun's `Request`/`Response` APIs.
78
+
79
+
When adding or modifying server endpoints, both implementations must be updated. Runtime-agnostic logic (store, validation, types) lives in `packages/shared/` and is imported by both.
80
+
67
81
## Installation
68
82
69
83
**Via plugin marketplace** (when repo is public):
@@ -85,6 +99,7 @@ claude --plugin-dir ./apps/hook
85
99
|`PLANNOTATOR_REMOTE`| Set to `1` or `true` for remote mode (devcontainer, SSH). Uses fixed port and skips browser open. |
86
100
|`PLANNOTATOR_PORT`| Fixed port to use. Default: random locally, `19432` for remote sessions. |
87
101
|`PLANNOTATOR_BROWSER`| Custom browser to open plans in. macOS: app name or path. Linux/Windows: executable path. |
102
+
|`PLANNOTATOR_SHARE`| Set to `disabled` to turn off URL sharing entirely. Default: enabled. |
88
103
|`PLANNOTATOR_SHARE_URL`| Custom base URL for share links (self-hosted portal). Default: `https://share.plannotator.ai`. |
89
104
|`PLANNOTATOR_PASTE_URL`| Base URL of the paste service API for short URL sharing. Default: `https://plannotator-paste.plannotator.workers.dev`. |
90
105
@@ -148,6 +163,22 @@ User annotates markdown, provides feedback
148
163
Send Annotations → feedback sent to agent session
149
164
```
150
165
166
+
## Archive Flow
167
+
168
+
```
169
+
User runs plannotator archive (CLI) or /plannotator-archive (Pi)
170
+
↓
171
+
Server starts in mode:"archive", reads ~/.plannotator/plans/
|`/api/file-content`| GET | Returns `{ oldContent, newContent }` for expandable diff context |
182
218
|`/api/git-add`| POST | Stage/unstage a file (body: `{ filePath, undo? }`) |
183
219
|`/api/feedback`| POST | Submit review (body: feedback, annotations, agentSwitch) |
@@ -186,6 +222,17 @@ Send Annotations → feedback sent to agent session
186
222
|`/api/draft`| GET/POST/DELETE | Auto-save annotation drafts to survive server crashes |
187
223
|`/api/editor-annotations`| GET | List editor annotations (VS Code only) |
188
224
|`/api/editor-annotation`| POST/DELETE | Add or remove an editor annotation (VS Code only) |
225
+
|`/api/ai/capabilities`| GET | Check if AI features are available |
226
+
|`/api/ai/session`| POST | Create or fork an AI session |
227
+
|`/api/ai/query`| POST | Send a message and stream the response (SSE) |
228
+
|`/api/ai/abort`| POST | Abort the current query |
229
+
|`/api/ai/permission`| POST | Respond to a permission request |
230
+
|`/api/ai/sessions`| GET | List active sessions |
231
+
|`/api/external-annotations/stream`| GET | SSE stream for real-time external annotations |
232
+
|`/api/external-annotations`| GET | Snapshot of external annotations (polling fallback, `?since=N` for version gating) |
233
+
|`/api/external-annotations`| POST | Add external annotations (single or batch `{ annotations: [...] }`) |
234
+
|`/api/external-annotations`| PATCH | Update fields on a single annotation (`?id=`) |
235
+
|`/api/external-annotations`| DELETE | Remove by `?id=`, `?source=`, or clear all |
189
236
190
237
### Annotate Server (`packages/server/annotate.ts`)
191
238
@@ -196,6 +243,11 @@ Send Annotations → feedback sent to agent session
196
243
|`/api/image`| GET | Serve image by path query param |
197
244
|`/api/upload`| POST | Upload image, returns `{ path, originalName }`|
198
245
|`/api/draft`| GET/POST/DELETE | Auto-save annotation drafts to survive server crashes |
246
+
|`/api/external-annotations/stream`| GET | SSE stream for real-time external annotations |
247
+
|`/api/external-annotations`| GET | Snapshot of external annotations (polling fallback, `?since=N` for version gating) |
248
+
|`/api/external-annotations`| POST | Add external annotations (single or batch `{ annotations: [...] }`) |
249
+
|`/api/external-annotations`| PATCH | Update fields on a single annotation (`?id=`) |
250
+
|`/api/external-annotations`| DELETE | Remove by `?id=`, `?source=`, or clear all |
199
251
200
252
All servers use random ports locally or fixed port (`19432`) in remote mode.
201
253
@@ -228,7 +280,11 @@ When a user denies a plan and Claude resubmits, the UI shows what changed betwee
228
280
229
281
**State** (`packages/ui/hooks/usePlanDiff.ts`): Manages base version selection, diff computation, and version fetching. The server sends `previousPlan` with the initial `/api/plan` response; the hook auto-diffs against it. Users can select any prior version from the sidebar Version Browser.
230
282
231
-
**Sidebar** (`packages/ui/hooks/useSidebar.ts`): Shared left sidebar with two tabs — Table of Contents and Version Browser. The "Auto-open Sidebar" setting controls whether it opens on load (TOC tab only).
283
+
**Diff annotations:** The clean diff view supports block-level annotation — hover over added/removed/modified sections to annotate entire blocks. Annotations carry a `diffContext` field (`added`/`removed`/`modified`). Exported feedback includes `[In diff content]` labels.
284
+
285
+
**Annotation hook** (`packages/ui/hooks/useAnnotationHighlighter.ts`): Annotation infrastructure used by `Viewer.tsx`. Manages web-highlighter lifecycle, toolbar/popover state, annotation creation, text-based restoration, and scroll-to-selected. The diff view uses its own block-level hover system instead.
286
+
287
+
**Sidebar** (`packages/ui/hooks/useSidebar.ts`): Shared left sidebar with three tabs — Table of Contents, Version Browser, and Archive. The "Auto-open Sidebar" setting controls whether it opens on load (TOC tab only). In archive mode, the sidebar opens to the Archive tab automatically.
232
288
233
289
## Data Types
234
290
@@ -237,8 +293,6 @@ When a user denies a plan and Claude resubmits, the UI shows what changed betwee
237
293
```typescript
238
294
enumAnnotationType {
239
295
DELETION="DELETION",
240
-
INSERTION="INSERTION",
241
-
REPLACEMENT="REPLACEMENT",
242
296
COMMENT="COMMENT",
243
297
GLOBAL_COMMENT="GLOBAL_COMMENT",
244
298
}
@@ -254,11 +308,13 @@ interface Annotation {
254
308
startOffset:number;
255
309
endOffset:number;
256
310
type:AnnotationType;
257
-
text?:string; // For comment/replacement/insertion
311
+
text?:string; // For comment
258
312
originalText:string; // The selected text
259
313
createdA:number; // Timestamp
260
314
author?:string; // Tater identity
261
315
images?:ImageAttachment[]; // Attached images with names
316
+
source?:string; // External tool identifier (e.g., "eslint") — set when annotation comes from external API
317
+
diffContext?:'added'|'removed'|'modified'; // Set when annotation created in plan diff view
`exportAnnotations(blocks, annotations, globalAttachments)` generates human-readable feedback for Claude. Images are referenced by name: `[image-name] /tmp/path...`.
346
+
`exportAnnotations(blocks, annotations, globalAttachments)` generates human-readable feedback for Claude. Images are referenced by name: `[image-name] /tmp/path...`. Annotations with `diffContext` include `[In diff content]` labels.
291
347
292
348
## Annotation System
293
349
@@ -312,13 +368,12 @@ interface SharePayload {
312
368
p:string; // Plan markdown
313
369
a:ShareableAnnotation[]; // Compact annotations
314
370
g?:ShareableImage[]; // Global attachments
371
+
d?: (string|null)[]; // diffContext per annotation, parallel to `a`
@@ -378,10 +433,19 @@ bun run package:vscode # Package .vsix for marketplace
378
433
bun run build # Build hook + opencode (main targets)
379
434
```
380
435
381
-
**Important:** The OpenCode plugin copies pre-built HTML from `apps/hook/dist/` and `apps/review/dist/`. When making UI changes (in `packages/ui/`, `packages/editor/`, or `packages/review-editor/`), you must rebuild the hook/review first:
436
+
**Important: Build order matters.** The hook build (`build:hook`) copies pre-built HTML from `apps/review/dist/`. If you change UI code in `packages/ui/`, `packages/editor/`, or `packages/review-editor/`, you **must** rebuild the review app first, then the hook:
437
+
438
+
```bash
439
+
bun run --cwd apps/review build && bun run build:hook # For review UI changes
440
+
bun run build:hook # For plan UI changes only
441
+
bun run build:hook && bun run build:opencode # For OpenCode plugin
442
+
```
443
+
444
+
Running only `build:hook` after review-editor changes will copy stale HTML files. When testing locally with a compiled binary, the full sequence is:
382
445
383
446
```bash
384
-
bun run build:hook && bun run build:opencode # For UI changes
447
+
bun run --cwd apps/review build && bun run build:hook && \
448
+
bun build apps/hook/server/index.ts --compile --outfile ~/.local/bin/plannotator
385
449
```
386
450
387
451
Running only `build:opencode` will copy stale HTML files.
Copy file name to clipboardExpand all lines: CLAUDE.md
+26-5Lines changed: 26 additions & 5 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -69,6 +69,15 @@ plannotator/
69
69
└── legacy/ # Old pre-monorepo code (reference only)
70
70
```
71
71
72
+
## Server Runtimes
73
+
74
+
There are two separate server implementations with the same API surface:
75
+
76
+
-**Bun server** (`packages/server/`) — used by both Claude Code (`apps/hook/`) and OpenCode (`apps/opencode-plugin/`). These plugins import directly from `@plannotator/server`.
77
+
-**Pi server** (`apps/pi-extension/server/`) — a standalone Node.js server for the Pi extension. It mirrors the Bun server's API but uses `node:http` primitives instead of Bun's `Request`/`Response` APIs.
78
+
79
+
When adding or modifying server endpoints, both implementations must be updated. Runtime-agnostic logic (store, validation, types) lives in `packages/shared/` and is imported by both.
80
+
72
81
## Installation
73
82
74
83
**Via plugin marketplace** (when repo is public):
@@ -194,6 +203,11 @@ During normal plan review, an Archive sidebar tab provides the same browsing via
194
203
|`/api/draft`| GET/POST/DELETE | Auto-save annotation drafts to survive server crashes |
195
204
|`/api/editor-annotations`| GET | List editor annotations (VS Code only) |
196
205
|`/api/editor-annotation`| POST/DELETE | Add or remove an editor annotation (VS Code only) |
206
+
|`/api/external-annotations/stream`| GET | SSE stream for real-time external annotations |
207
+
|`/api/external-annotations`| GET | Snapshot of external annotations (polling fallback, `?since=N` for version gating) |
208
+
|`/api/external-annotations`| POST | Add external annotations (single or batch `{ annotations: [...] }`) |
209
+
|`/api/external-annotations`| PATCH | Update fields on a single annotation (`?id=`) |
210
+
|`/api/external-annotations`| DELETE | Remove by `?id=`, `?source=`, or clear all |
197
211
198
212
### Review Server (`packages/server/review.ts`)
199
213
@@ -214,6 +228,11 @@ During normal plan review, an Archive sidebar tab provides the same browsing via
214
228
|`/api/ai/abort`| POST | Abort the current query |
215
229
|`/api/ai/permission`| POST | Respond to a permission request |
216
230
|`/api/ai/sessions`| GET | List active sessions |
231
+
|`/api/external-annotations/stream`| GET | SSE stream for real-time external annotations |
232
+
|`/api/external-annotations`| GET | Snapshot of external annotations (polling fallback, `?since=N` for version gating) |
233
+
|`/api/external-annotations`| POST | Add external annotations (single or batch `{ annotations: [...] }`) |
234
+
|`/api/external-annotations`| PATCH | Update fields on a single annotation (`?id=`) |
235
+
|`/api/external-annotations`| DELETE | Remove by `?id=`, `?source=`, or clear all |
217
236
218
237
### Annotate Server (`packages/server/annotate.ts`)
219
238
@@ -224,6 +243,11 @@ During normal plan review, an Archive sidebar tab provides the same browsing via
224
243
|`/api/image`| GET | Serve image by path query param |
225
244
|`/api/upload`| POST | Upload image, returns `{ path, originalName }`|
226
245
|`/api/draft`| GET/POST/DELETE | Auto-save annotation drafts to survive server crashes |
246
+
|`/api/external-annotations/stream`| GET | SSE stream for real-time external annotations |
247
+
|`/api/external-annotations`| GET | Snapshot of external annotations (polling fallback, `?since=N` for version gating) |
248
+
|`/api/external-annotations`| POST | Add external annotations (single or batch `{ annotations: [...] }`) |
249
+
|`/api/external-annotations`| PATCH | Update fields on a single annotation (`?id=`) |
250
+
|`/api/external-annotations`| DELETE | Remove by `?id=`, `?source=`, or clear all |
227
251
228
252
All servers use random ports locally or fixed port (`19432`) in remote mode.
229
253
@@ -269,8 +293,6 @@ When a user denies a plan and Claude resubmits, the UI shows what changed betwee
269
293
```typescript
270
294
enumAnnotationType {
271
295
DELETION="DELETION",
272
-
INSERTION="INSERTION",
273
-
REPLACEMENT="REPLACEMENT",
274
296
COMMENT="COMMENT",
275
297
GLOBAL_COMMENT="GLOBAL_COMMENT",
276
298
}
@@ -286,11 +308,12 @@ interface Annotation {
286
308
startOffset:number;
287
309
endOffset:number;
288
310
type:AnnotationType;
289
-
text?:string; // For comment/replacement/insertion
311
+
text?:string; // For comment
290
312
originalText:string; // The selected text
291
313
createdA:number; // Timestamp
292
314
author?:string; // Tater identity
293
315
images?:ImageAttachment[]; // Attached images with names
316
+
source?:string; // External tool identifier (e.g., "eslint") — set when annotation comes from external API
294
317
diffContext?:'added'|'removed'|'modified'; // Set when annotation created in plan diff view
0 commit comments