Skip to content

Commit 4627f75

Browse files
authored
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.
1 parent 9b69d17 commit 4627f75

28 files changed

Lines changed: 1778 additions & 165 deletions

AGENTS.md

Lines changed: 81 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ plannotator/
1111
│ │ ├── .claude-plugin/plugin.json
1212
│ │ ├── commands/ # Slash commands (plannotator-review.md, plannotator-annotate.md)
1313
│ │ ├── hooks/hooks.json # PermissionRequest hook config
14-
│ │ ├── server/index.ts # Entry point (plan + review + annotate subcommands)
14+
│ │ ├── server/index.ts # Entry point (plan + review + annotate + archive subcommands)
1515
│ │ └── dist/ # Built single-file apps (index.html, review.html)
1616
│ ├── opencode-plugin/ # OpenCode plugin
1717
│ │ ├── commands/ # Slash commands (plannotator-review.md, plannotator-annotate.md)
@@ -37,23 +37,28 @@ plannotator/
3737
│ │ ├── index.ts # startPlannotatorServer(), handleServerReady()
3838
│ │ ├── review.ts # startReviewServer(), handleReviewServerReady()
3939
│ │ ├── annotate.ts # startAnnotateServer(), handleAnnotateServerReady()
40-
│ │ ├── storage.ts # Plan saving to disk (getPlanDir, savePlan, etc.)
40+
│ │ ├── storage.ts # Re-exports from @plannotator/shared/storage
4141
│ │ ├── share-url.ts # Server-side share URL generation for remote sessions
4242
│ │ ├── remote.ts # isRemoteSession(), getServerPort()
4343
│ │ ├── browser.ts # openBrowser()
44-
│ │ ├── draft.ts # Annotation draft persistence (~/.plannotator/drafts/)
44+
│ │ ├── draft.ts # Re-exports from @plannotator/shared/draft
4545
│ │ ├── integrations.ts # Obsidian, Bear integrations
4646
│ │ ├── ide.ts # VS Code diff integration (openEditorDiff)
4747
│ │ ├── editor-annotations.ts # VS Code editor annotation endpoints
4848
│ │ └── project.ts # Project name detection for tags
49-
│ ├── ui/ # Shared React components
49+
│ ├── ui/ # Shared React components + theme
50+
│ │ ├── theme.css # Single source of truth for color tokens + Tailwind bridge
5051
│ │ ├── components/ # Viewer, Toolbar, Settings, etc.
5152
│ │ │ ├── plan-diff/ # PlanDiffBadge, PlanDiffViewer, clean/raw diff views
52-
│ │ │ └── sidebar/ # SidebarContainer, SidebarTabs, VersionBrowser
53+
│ │ │ └── sidebar/ # SidebarContainer, SidebarTabs, VersionBrowser, ArchiveBrowser
5354
│ │ ├── utils/ # parser.ts, sharing.ts, storage.ts, planSave.ts, agentSwitch.ts, planDiffEngine.ts
54-
│ │ ├── hooks/ # useSharing.ts, usePlanDiff.ts, useSidebar.ts, useLinkedDoc.ts, useAnnotationDraft.ts, useCodeAnnotationDraft.ts
55+
│ │ ├── hooks/ # useAnnotationHighlighter.ts, useSharing.ts, usePlanDiff.ts, useSidebar.ts, useLinkedDoc.ts, useAnnotationDraft.ts, useCodeAnnotationDraft.ts, useArchive.ts
5556
│ │ └── types.ts
56-
│ ├── shared/ # Cross-package types (EditorAnnotation)
57+
│ ├── ai/ # Provider-agnostic AI backbone (providers, sessions, endpoints)
58+
│ ├── shared/ # Shared types, utilities, and cross-runtime logic
59+
│ │ ├── storage.ts # Plan saving, version history, archive listing (node:fs only)
60+
│ │ ├── draft.ts # Annotation draft persistence (node:fs only)
61+
│ │ └── project.ts # Pure string helpers (sanitizeTag, extractRepoName, extractDirName)
5762
│ ├── editor/ # Plan review App.tsx
5863
│ └── review-editor/ # Code review UI
5964
│ ├── App.tsx # Main review app
@@ -64,6 +69,15 @@ plannotator/
6469
└── legacy/ # Old pre-monorepo code (reference only)
6570
```
6671

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+
6781
## Installation
6882

6983
**Via plugin marketplace** (when repo is public):
@@ -85,6 +99,7 @@ claude --plugin-dir ./apps/hook
8599
| `PLANNOTATOR_REMOTE` | Set to `1` or `true` for remote mode (devcontainer, SSH). Uses fixed port and skips browser open. |
86100
| `PLANNOTATOR_PORT` | Fixed port to use. Default: random locally, `19432` for remote sessions. |
87101
| `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. |
88103
| `PLANNOTATOR_SHARE_URL` | Custom base URL for share links (self-hosted portal). Default: `https://share.plannotator.ai`. |
89104
| `PLANNOTATOR_PASTE_URL` | Base URL of the paste service API for short URL sharing. Default: `https://plannotator-paste.plannotator.workers.dev`. |
90105

@@ -148,6 +163,22 @@ User annotates markdown, provides feedback
148163
Send Annotations → feedback sent to agent session
149164
```
150165

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/
172+
173+
Browser opens read-only archive viewer (sharing disabled)
174+
175+
User browses saved plan decisions with approved/denied badges
176+
177+
Done → POST /api/done closes the browser
178+
```
179+
180+
During normal plan review, an Archive sidebar tab provides the same browsing via linked doc overlay without leaving the current session.
181+
151182
## Server API
152183

153184
### Plan Server (`packages/server/index.ts`)
@@ -172,12 +203,17 @@ Send Annotations → feedback sent to agent session
172203
| `/api/draft` | GET/POST/DELETE | Auto-save annotation drafts to survive server crashes |
173204
| `/api/editor-annotations` | GET | List editor annotations (VS Code only) |
174205
| `/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 |
175211

176212
### Review Server (`packages/server/review.ts`)
177213

178214
| Endpoint | Method | Purpose |
179215
| --------------------- | ------ | ------------------------------------------ |
180-
| `/api/diff` | GET | Returns `{ rawPatch, gitRef, origin }` |
216+
| `/api/diff` | GET | Returns `{ rawPatch, gitRef, origin, diffType, gitContext }` |
181217
| `/api/file-content` | GET | Returns `{ oldContent, newContent }` for expandable diff context |
182218
| `/api/git-add` | POST | Stage/unstage a file (body: `{ filePath, undo? }`) |
183219
| `/api/feedback` | POST | Submit review (body: feedback, annotations, agentSwitch) |
@@ -186,6 +222,17 @@ Send Annotations → feedback sent to agent session
186222
| `/api/draft` | GET/POST/DELETE | Auto-save annotation drafts to survive server crashes |
187223
| `/api/editor-annotations` | GET | List editor annotations (VS Code only) |
188224
| `/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 |
189236

190237
### Annotate Server (`packages/server/annotate.ts`)
191238

@@ -196,6 +243,11 @@ Send Annotations → feedback sent to agent session
196243
| `/api/image` | GET | Serve image by path query param |
197244
| `/api/upload` | POST | Upload image, returns `{ path, originalName }` |
198245
| `/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 |
199251

200252
All servers use random ports locally or fixed port (`19432`) in remote mode.
201253

@@ -228,7 +280,11 @@ When a user denies a plan and Claude resubmits, the UI shows what changed betwee
228280

229281
**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.
230282

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.
232288

233289
## Data Types
234290

@@ -237,8 +293,6 @@ When a user denies a plan and Claude resubmits, the UI shows what changed betwee
237293
```typescript
238294
enum AnnotationType {
239295
DELETION = "DELETION",
240-
INSERTION = "INSERTION",
241-
REPLACEMENT = "REPLACEMENT",
242296
COMMENT = "COMMENT",
243297
GLOBAL_COMMENT = "GLOBAL_COMMENT",
244298
}
@@ -254,11 +308,13 @@ interface Annotation {
254308
startOffset: number;
255309
endOffset: number;
256310
type: AnnotationType;
257-
text?: string; // For comment/replacement/insertion
311+
text?: string; // For comment
258312
originalText: string; // The selected text
259313
createdA: number; // Timestamp
260314
author?: string; // Tater identity
261315
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
262318
startMeta?: { parentTagName; parentIndex; textOffset };
263319
endMeta?: { parentTagName; parentIndex; textOffset };
264320
}
@@ -287,7 +343,7 @@ interface Block {
287343
- Horizontal rules (`---`)
288344
- Paragraphs (default)
289345

290-
`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.
291347

292348
## Annotation System
293349

@@ -312,13 +368,12 @@ interface SharePayload {
312368
p: string; // Plan markdown
313369
a: ShareableAnnotation[]; // Compact annotations
314370
g?: ShareableImage[]; // Global attachments
371+
d?: (string | null)[]; // diffContext per annotation, parallel to `a`
315372
}
316373

317374
type ShareableAnnotation =
318375
| ["D", string, string | null, ShareableImage[]?] // [type, original, author, images?]
319-
| ["R", string, string, string | null, ShareableImage[]?] // [type, original, replacement, author, images?]
320376
| ["C", string, string, string | null, ShareableImage[]?] // [type, original, comment, author, images?]
321-
| ["I", string, string, string | null, ShareableImage[]?] // [type, context, newText, author, images?]
322377
| ["G", string, string | null, ShareableImage[]?]; // [type, comment, author, images?]
323378
```
324379

@@ -378,10 +433,19 @@ bun run package:vscode # Package .vsix for marketplace
378433
bun run build # Build hook + opencode (main targets)
379434
```
380435

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:
382445

383446
```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
385449
```
386450

387451
Running only `build:opencode` will copy stale HTML files.

CLAUDE.md

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,15 @@ plannotator/
6969
└── legacy/ # Old pre-monorepo code (reference only)
7070
```
7171

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+
7281
## Installation
7382

7483
**Via plugin marketplace** (when repo is public):
@@ -194,6 +203,11 @@ During normal plan review, an Archive sidebar tab provides the same browsing via
194203
| `/api/draft` | GET/POST/DELETE | Auto-save annotation drafts to survive server crashes |
195204
| `/api/editor-annotations` | GET | List editor annotations (VS Code only) |
196205
| `/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 |
197211

198212
### Review Server (`packages/server/review.ts`)
199213

@@ -214,6 +228,11 @@ During normal plan review, an Archive sidebar tab provides the same browsing via
214228
| `/api/ai/abort` | POST | Abort the current query |
215229
| `/api/ai/permission` | POST | Respond to a permission request |
216230
| `/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 |
217236

218237
### Annotate Server (`packages/server/annotate.ts`)
219238

@@ -224,6 +243,11 @@ During normal plan review, an Archive sidebar tab provides the same browsing via
224243
| `/api/image` | GET | Serve image by path query param |
225244
| `/api/upload` | POST | Upload image, returns `{ path, originalName }` |
226245
| `/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 |
227251

228252
All servers use random ports locally or fixed port (`19432`) in remote mode.
229253

@@ -269,8 +293,6 @@ When a user denies a plan and Claude resubmits, the UI shows what changed betwee
269293
```typescript
270294
enum AnnotationType {
271295
DELETION = "DELETION",
272-
INSERTION = "INSERTION",
273-
REPLACEMENT = "REPLACEMENT",
274296
COMMENT = "COMMENT",
275297
GLOBAL_COMMENT = "GLOBAL_COMMENT",
276298
}
@@ -286,11 +308,12 @@ interface Annotation {
286308
startOffset: number;
287309
endOffset: number;
288310
type: AnnotationType;
289-
text?: string; // For comment/replacement/insertion
311+
text?: string; // For comment
290312
originalText: string; // The selected text
291313
createdA: number; // Timestamp
292314
author?: string; // Tater identity
293315
images?: ImageAttachment[]; // Attached images with names
316+
source?: string; // External tool identifier (e.g., "eslint") — set when annotation comes from external API
294317
diffContext?: 'added' | 'removed' | 'modified'; // Set when annotation created in plan diff view
295318
startMeta?: { parentTagName; parentIndex; textOffset };
296319
endMeta?: { parentTagName; parentIndex; textOffset };
@@ -350,9 +373,7 @@ interface SharePayload {
350373

351374
type ShareableAnnotation =
352375
| ["D", string, string | null, ShareableImage[]?] // [type, original, author, images?]
353-
| ["R", string, string, string | null, ShareableImage[]?] // [type, original, replacement, author, images?]
354376
| ["C", string, string, string | null, ShareableImage[]?] // [type, original, comment, author, images?]
355-
| ["I", string, string, string | null, ShareableImage[]?] // [type, context, newText, author, images?]
356377
| ["G", string, string | null, ShareableImage[]?]; // [type, comment, author, images?]
357378
```
358379

0 commit comments

Comments
 (0)