* feat(shared): add isSameProject, PR stack types, and PR list provider Extends PRRef/PRMetadata with defaultBranch, PRStackInfo, PRStackTree, PRStackNode, PRDiffScope, and PRListItem types. Adds isSameProject() for owner/repo validation on PR switching. Adds fetchPRStack() and fetchPRList() dispatch functions (GitHub-only for now, GitLab stubs). Includes 9 new tests for isSameProject covering GitHub, GitLab, and cross-platform scenarios. For provenance purposes, this commit was AI assisted. * feat(shared): add GitHub PR stack tree walking and PR list fetching Implements fetchGhPRStack() which walks up/down the PR stack via GraphQL, resolving numbers and titles for each node in the chain. Collapses queryPRsByHead/queryPRsByBase into a single queryPRsByRef helper. Adds fetchGhPRList() using gh pr list. Fixes GHE support by removing hostnameArgs from fetchGhPRList (--repo already handles GHE). Filters jq "null" string from defaultBranch detection. For provenance purposes, this commit was AI assisted. * feat(shared): fetch defaultBranch for GitLab MRs Queries the project's default_branch via glab API so getPRStackInfo can detect stacked MRs on GitLab. Best-effort — caught errors fall back to undefined. For provenance purposes, this commit was AI assisted. * feat(shared): add PR stack detection and full-stack diff module New pr-stack module with: - getPRStackInfo(): detects stacked PRs from baseBranch vs defaultBranch - getPRDiffScopeOptions(): generates layer/full-stack scope options - runPRFullStackDiff(): computes diff from default branch to HEAD - resolvePRFullStackBaseRef(): resolves origin/main or local main - checkoutPRHead(): fetches and checks out a PR head in a worktree - buildMinimalStackTree(): builds UI tree from stack info Includes 13 tests covering ref resolution, branch fallbacks, and GitLab ref formats. For provenance purposes, this commit was AI assisted. * feat(shared): add worktree pool for per-PR agent isolation Creates a session-scoped pool of git worktrees — each PR visited during a stacked review gets its own isolated checkout. Agents run in their PR's worktree undisturbed by PR switches. Handles deduplication of concurrent ensure() calls for the same PR. Includes 11 tests covering caching, cross-repo restrictions, GitLab ref formats, and cleanup. For provenance purposes, this commit was AI assisted. * feat(shared): add diffScope/prUrl to agent jobs, branch diff type Adds prUrl and diffScope optional fields to AgentJobInfo so agent findings carry the PR and scope context they were launched under. Exports new pr-stack and worktree-pool modules from package.json. Adds 'branch' to DefaultDiffType union for branch diff as default. For provenance purposes, this commit was AI assisted. * feat(ui): add PR annotation fields, Popover, and SearchableSelect Extends CodeAnnotation with prUrl, prNumber, prTitle, prRepo, and diffScope fields for stacked PR attribution. Adds shared Popover wrapper around radix-ui. Adds SearchableSelect for filterable dropdown lists (used by PR selector). For provenance purposes, this commit was AI assisted. * feat(ui): add branch diff as default option, new Git settings tab Adds 'Branch' as a fourth default diff type option in both the first-run dialog and settings panel. Moves the default diff type setting from the Display tab to a new Git tab in review mode. Updates config store validators to accept 'branch'. For provenance purposes, this commit was AI assisted. * feat(server): add prUrl/diffScope plumbing to agent jobs and prompts Threads prUrl and diffScope through the agent job lifecycle so findings carry the PR and scope they were generated under. Adds full-stack prompt branch to codex-review and tour-review — when in full-stack mode, the diff is inlined in the prompt instead of telling the agent to run git diff. Re-exports isSameProject and new PR provider functions from server/pr.ts. For provenance purposes, this commit was AI assisted. * feat(server): add stacked PR support to Bun review server Adds PR switching, layer/full-stack scope toggling, PR list caching, worktree pool integration, and multi-PR platform posting to the Bun review server. Key additions: - /api/pr-diff-scope: switch between layer and full-stack diffs - /api/pr-list: cached PR list for the current repo - /api/pr-switch: in-place navigation between PRs in a stack - /api/pr-action: targetPrUrl support for multi-PR posting - /api/file-content: full-stack branch for hunk expansion - prSwitchCache/prStackTreeCache for session-scoped caching - diffScope tagging on agent job completion - Scope guard: returns 400 on full-stack diff failure instead of overwriting the working diff with empty content For provenance purposes, this commit was AI assisted. * feat(ai): pass cwd to Claude agent SDK for worktree support Forwards the working directory to the Claude agent provider so agents run in the correct worktree when reviewing stacked PRs. For provenance purposes, this commit was AI assisted. * feat(pi): add stacked PR support to Pi server (Bun parity) Mirrors all stacked PR features from the Bun server: - PR switching, scope toggling, PR list, multi-PR posting - prSwitchCache/prStackTreeCache with initial PR seeding - diffScope/prUrl plumbing in agent jobs - Worktree pool creation and lifecycle - Full-stack file-content resolution matching Bun's guard structure - targetPrUrl support on /api/pr-action Hoists worktreePool declaration to outer scope in plannotator-browser to fix TS18004 scoping error. Updates vendor.sh for new shared modules. For provenance purposes, this commit was AI assisted. * feat(hook): create worktree pool for PR review sessions Creates a worktree pool when opening a PR review with --local, seeding it with the initial PR's checkout. Integrates pool cleanup into server shutdown. Passes the pool to startReviewServer for agent isolation during PR switching. For provenance purposes, this commit was AI assisted. * feat(review-editor): add hooks for PR stack, context, and annotations - useAnnotationFactory: stamps prUrl/prNumber/prTitle/prRepo/diffScope onto annotations, only when viewing a stacked PR - usePRStack: handles scope selection and PR switching with loading state - usePRContext: adds URL-change detection to prevent stale-fetch race when switching PRs (discards in-flight responses for previous PR) For provenance purposes, this commit was AI assisted. * feat(review-editor): add stacked PR UI components - PRSelector: searchable dropdown for switching between PRs in a repo - PRSwitchOverlay: loading animation during PR switch - StackedPRLabel: stack tree popover with scope selector and PR navigation - ReviewSubmissionDialog: multi-PR submission dialog with per-target status, orphaned findings section with copy-as-markdown, and partial failure retry For provenance purposes, this commit was AI assisted. * feat(review-editor): multi-PR export with heading hierarchy Updates exportReviewFeedback for multi-PR sessions: - Groups annotations by prUrl, then by file within each PR - Uses proper heading hierarchy (## for files, ### for annotations in multi-PR mode) - Detects single-PR mismatch (annotations from a different PR than the current view) and uses annotation-level PR context - Adds diffScope labels per PR group when present Includes 5 new tests: multi-PR headings, single-PR mismatch, diffScope labels, and non-stacked annotation handling. For provenance purposes, this commit was AI assisted. * feat(review-editor): integrate stacked PR into sidebar, diff panel, and agents - ReviewSidebar: groups annotations by PR in multi-PR sessions, shows PR headers with annotation counts - ReviewDiffPanel: filters annotations by prUrl and diffScope so only matching annotations appear in the diff gutter - ReviewStateContext: adds prDiffScope to shared review state - ReviewAgentJobDetailPanel: shows diffScope in job detail - PRSummaryTab: shows stack info in PR summary - index.css: PR switch shimmer and overlay animations For provenance purposes, this commit was AI assisted. * feat(review-editor): wire stacked PR into main review app Integrates all stacked PR features into the review editor: - PR stack state management (prStackInfo, prStackTree, prDiffScope) - applyPRResponse: shared handler for PR switch and scope toggle, preserves active file index on scope changes - Multi-PR platform posting via Promise.allSettled with parallel requests, partial failure retry, and per-target status tracking - ReviewSubmissionDialog replaces inline dialog JSX - useAnnotationFactory stamps PR context onto annotations - keepalive on /api/feedback to survive tab closure - Proper try/catch/finally on handlePlatformAction For provenance purposes, this commit was AI assisted. * docs: add stacked PR review documentation Updates AGENTS.md, code-review command docs, and AI code review guide with stacked PR review capabilities. For provenance purposes, this commit was AI assisted. * fix(server): stamp prNumber/prTitle/prRepo on agent findings Agent annotations only had prUrl and diffScope, missing prNumber, prTitle, and prRepo. When agent findings were the only annotations for a PR target in the submission dialog, the target rendered as #0 with no title. Now resolves full PR context from prSwitchCache at job completion and stamps all five fields. Both Bun and Pi. For provenance purposes, this commit was AI assisted. * feat(ui): rename diff options — "Committed" replaces "Branch" / "Current PR Diff" Consolidates two confusing committed-diff options into one: - Settings/first-run: "Committed" — "Everything you've committed on this branch" - Mid-session switcher: "Committed changes" (replaces both "vs main" and "Current PR Diff") Uses merge-base under the hood (matches GitHub PR behavior). Removes the two-dot branch diff from the UI — it stays in the runtime DiffType union for backwards compat. Old "branch" values in config/cookies are silently upgraded to merge-base. Git settings tab now uses radio cards with descriptions instead of a cramped segmented control. First-run dialog descriptions rewritten in plain language — no git commands. For provenance purposes, this commit was AI assisted. * fix(review-editor): rename client-side "PR Diff" labels to "Committed changes" DiffTypePicker.tsx had a hardcoded "PR Diff" override for merge-base when the base picker is present. exportFeedback.ts also used "PR Diff" in export labels. Both now say "Committed changes" to match the server-side label and settings UI. For provenance purposes, this commit was AI assisted. * fix(server): discover stack UI for root PRs targeting the default branch Root PRs (baseBranch === defaultBranch) were excluded from stack detection because getPRStackInfo returned null. Now the server always fetches the stack tree in PR mode. If the tree reveals descendant PRs, prStackInfo is retroactively set with source "tree-discovered", enabling the stack UI, scope selector, and PR navigation from the root of a stack. Both Bun and Pi servers updated. Adds "tree-discovered" to the PRStackInfo source union. For provenance purposes, this commit was AI assisted. * fix(review-editor): derive diff scope from annotations, not UI state The export function now reads diffScope from annotations instead of the prReviewScope parameter. Fixes two issues: 1. Agent job "Copy All" showed the wrong scope when the user switched between layer/full-stack after launching the agent 2. Mixed-scope annotations produced a confusing "layer, full-stack" comma-joined label instead of grouping by scope Extracts renderScopedGroups helper for scope-aware grouping — used by both single-PR and multi-PR export paths. When annotations share one scope, it appears in the header. When mixed, annotations are grouped under ## Layer / ## Full-stack headings. Includes 4 new tests: uniform scope derivation, mixed scope grouping, single scope header, and prReviewScope override prevention. For provenance purposes, this commit was AI assisted. * fix(server): add tree-discovered stack fallback to pr-switch handler The initial-load path upgrades prStackInfo for root PRs when the stack tree reveals descendants, but the pr-switch handler was missing this logic. The server now sends correct prStackInfo after switching to a root-of-stack PR. Both Bun and Pi. Also removes stale prReviewScope dependency from agent job panel's copyAllText useMemo. For provenance purposes, this commit was AI assisted. * fix: extract resolveStackInfo helper, fix stack UI on non-stacked PRs Extracts the tree-discovered stack fallback into resolveStackInfo() in pr-stack.ts — eliminates 4 copies of the same logic across Bun startup, Bun pr-switch, Pi startup, and Pi pr-switch. Fixes StackedPRLabel showing on every PR: the check now counts non-default-branch nodes (> 1) instead of all nodes (> 1). Without this, every PR showed a "Stack (1 PR)" popover because the tree always has at least [defaultBranch, currentPR]. For provenance purposes, this commit was AI assisted. * fix(review-editor): don't re-open already-succeeded PR tabs on retry On partial failure retry, openUrls was pre-seeded with URLs from previously succeeded targets, causing those PR pages to re-open in the browser alongside newly succeeded ones. Now starts empty — only URLs from the current attempt are opened. For provenance purposes, this commit was AI assisted. * refactor(review-editor): extract PR session state into usePRSession hook Consolidates 5 independent useState calls (prMetadata, prStackInfo, prStackTree, prDiffScope, prDiffScopeOptions) into a single usePRSession hook with atomic updatePRSession callback. Replaces two identical 5-line setter blocks (initial load and applyPRResponse) with single updatePRSession calls. All ~60 consumer sites unchanged — same variable names via destructuring. For provenance purposes, this commit was AI assisted.
27 KiB
Plannotator
A plan review UI for Claude Code that intercepts ExitPlanMode via hooks, letting users approve or request changes with annotated feedback. Also provides code review for git diffs and annotation of arbitrary markdown files.
Project Structure
plannotator/
├── apps/
│ ├── hook/ # Claude Code plugin
│ │ ├── .claude-plugin/plugin.json
│ │ ├── commands/ # Slash commands (plannotator-review.md, plannotator-annotate.md)
│ │ ├── hooks/hooks.json # PermissionRequest hook config
│ │ ├── server/index.ts # Entry point (plan + review + annotate + archive subcommands)
│ │ └── dist/ # Built single-file apps (index.html, review.html)
│ ├── opencode-plugin/ # OpenCode plugin
│ │ ├── commands/ # Slash commands (plannotator-review.md, plannotator-annotate.md)
│ │ ├── index.ts # Plugin entry with submit_plan tool + review/annotate event handlers
│ │ ├── plannotator.html # Built plan review app
│ │ └── review-editor.html # Built code review app
│ ├── marketing/ # Marketing site, docs, and blog (plannotator.ai)
│ │ └── astro.config.mjs # Astro 5 static site with content collections
│ ├── paste-service/ # Paste service for short URL sharing
│ │ ├── core/ # Platform-agnostic logic (handler, storage interface, cors)
│ │ ├── stores/ # Storage backends (fs, kv, s3)
│ │ └── targets/ # Deployment entries (bun.ts, cloudflare.ts)
│ ├── review/ # Standalone review server (for development)
│ │ ├── index.html
│ │ ├── index.tsx
│ │ └── vite.config.ts
│ └── vscode-extension/ # VS Code extension — opens plans in editor tabs
│ ├── bin/ # Router scripts (open-in-vscode, xdg-open)
│ ├── src/ # extension.ts, cookie-proxy.ts, ipc-server.ts, panel-manager.ts, editor-annotations.ts, vscode-theme.ts
│ └── package.json # Extension manifest (publisher: backnotprop)
├── packages/
│ ├── server/ # Shared server implementation
│ │ ├── index.ts # startPlannotatorServer(), handleServerReady()
│ │ ├── review.ts # startReviewServer(), handleReviewServerReady()
│ │ ├── annotate.ts # startAnnotateServer(), handleAnnotateServerReady()
│ │ ├── storage.ts # Re-exports from @plannotator/shared/storage
│ │ ├── share-url.ts # Server-side share URL generation for remote sessions
│ │ ├── remote.ts # isRemoteSession(), getServerPort()
│ │ ├── browser.ts # openBrowser()
│ │ ├── draft.ts # Re-exports from @plannotator/shared/draft
│ │ ├── integrations.ts # Obsidian, Bear integrations
│ │ ├── ide.ts # VS Code diff integration (openEditorDiff)
│ │ ├── editor-annotations.ts # VS Code editor annotation endpoints
│ │ └── project.ts # Project name detection for tags
│ ├── ui/ # Shared React components + theme
│ │ ├── theme.css # Single source of truth for color tokens + Tailwind bridge
│ │ ├── components/ # Viewer, Toolbar, Settings, etc.
│ │ │ ├── icons/ # Shared SVG icon components (themeIcons, etc.)
│ │ │ ├── plan-diff/ # PlanDiffBadge, PlanDiffViewer, clean/raw diff views
│ │ │ └── sidebar/ # SidebarContainer, SidebarTabs, VersionBrowser, ArchiveBrowser
│ │ ├── utils/ # parser.ts, sharing.ts, storage.ts, planSave.ts, agentSwitch.ts, planDiffEngine.ts, planAgentInstructions.ts
│ │ ├── hooks/ # useAnnotationHighlighter.ts, useSharing.ts, usePlanDiff.ts, useSidebar.ts, useLinkedDoc.ts, useAnnotationDraft.ts, useCodeAnnotationDraft.ts, useArchive.ts
│ │ └── types.ts
│ ├── ai/ # Provider-agnostic AI backbone (providers, sessions, endpoints)
│ ├── shared/ # Shared types, utilities, and cross-runtime logic
│ │ ├── storage.ts # Plan saving, version history, archive listing (node:fs only)
│ │ ├── draft.ts # Annotation draft persistence (node:fs only)
│ │ └── project.ts # Pure string helpers (sanitizeTag, extractRepoName, extractDirName)
│ ├── editor/ # Plan review App.tsx
│ └── review-editor/ # Code review UI
│ ├── App.tsx # Main review app
│ ├── components/ # DiffViewer, FileTree, ReviewSidebar
│ ├── dock/ # Dockview center panel infrastructure
│ ├── demoData.ts # Demo diff for standalone mode
│ └── index.css # Review-specific styles
├── .claude-plugin/marketplace.json # For marketplace install
└── legacy/ # Old pre-monorepo code (reference only)
Server Runtimes
There are two separate server implementations with the same API surface:
- Bun server (
packages/server/) — used by both Claude Code (apps/hook/) and OpenCode (apps/opencode-plugin/). These plugins import directly from@plannotator/server. - Pi server (
apps/pi-extension/server/) — a standalone Node.js server for the Pi extension. It mirrors the Bun server's API but usesnode:httpprimitives instead of Bun'sRequest/ResponseAPIs.
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.
Installation
Via plugin marketplace (when repo is public):
/plugin marketplace add backnotprop/plannotator
Local testing:
claude --plugin-dir ./apps/hook
Environment Variables
| Variable | Description |
|---|---|
PLANNOTATOR_REMOTE |
Set to 1 / true for remote mode, 0 / false for local mode, or leave unset for SSH auto-detection. Uses a fixed port in remote mode; browser-opening behavior depends on the environment. |
PLANNOTATOR_PORT |
Fixed port to use. Default: random locally, 19432 for remote sessions. |
PLANNOTATOR_BROWSER |
Custom browser to open plans in. macOS: app name or path. Linux/Windows: executable path. |
PLANNOTATOR_SHARE |
Set to disabled to turn off URL sharing entirely. Default: enabled. |
PLANNOTATOR_SHARE_URL |
Custom base URL for share links (self-hosted portal). Default: https://share.plannotator.ai. |
PLANNOTATOR_PASTE_URL |
Base URL of the paste service API for short URL sharing. Default: https://plannotator-paste.plannotator.workers.dev. |
PLANNOTATOR_ORIGIN |
Explicit agent-origin override at the top of the detection chain. Valid values: claude-code, opencode, codex, copilot-cli, gemini-cli. Invalid values silently fall through to env-based detection. Unset by default. |
PLANNOTATOR_JINA |
Set to 0 / false to disable Jina Reader for URL annotation, or 1 / true to enable. Default: enabled. Can also be set via ~/.plannotator/config.json ({ "jina": false }) or per-invocation via --no-jina. |
JINA_API_KEY |
Optional Jina Reader API key for higher rate limits (500 RPM vs 20 RPM unauthenticated). Free keys include 10M tokens. |
PLANNOTATOR_VERIFY_ATTESTATION |
Read by the install scripts only, not by the runtime binary. Set to 1 / true to have scripts/install.sh / install.ps1 / install.cmd run gh attestation verify on every install. Off by default. Can also be set persistently via ~/.plannotator/config.json ({ "verifyAttestation": true }) or per-invocation via --verify-attestation. Requires gh installed and authenticated. |
Legacy: SSH_TTY and SSH_CONNECTION are still detected when PLANNOTATOR_REMOTE is unset. Set PLANNOTATOR_REMOTE=1 / true to force remote mode or 0 / false to force local mode.
Devcontainer/SSH usage:
export PLANNOTATOR_REMOTE=1
export PLANNOTATOR_PORT=9999
Plan Review Flow
Claude calls ExitPlanMode
↓
PermissionRequest hook fires
↓
Bun server reads plan from stdin JSON (tool_input.plan)
↓
Server starts on random port, opens browser
↓
User reviews plan, optionally adds annotations
↓
Approve → stdout: {"hookSpecificOutput":{"decision":{"behavior":"allow"}}}
Deny → stdout: {"hookSpecificOutput":{"decision":{"behavior":"deny","message":"..."}}}
Code Review Flow
User runs /plannotator-review command
↓
Claude Code: plannotator review subcommand runs
OpenCode: event handler intercepts command
↓
git diff captures unstaged changes
↓
Review server starts, opens browser with diff viewer
↓
User annotates code, provides feedback
↓
Send Feedback → feedback sent to agent session
Approve → "LGTM" sent to agent session
Annotate Flow
User runs /plannotator-annotate <file.md | file.html | https://... | folder/>
↓
Claude Code: plannotator annotate subcommand runs
OpenCode/Pi: event handler intercepts command
↓
Input type detected:
.md/.mdx → file read from disk
.html/.htm → file read, converted to markdown via Turndown
https:// → fetched via Jina Reader (default) or fetch+Turndown (--no-jina)
folder/ → file browser opened, files converted on demand
↓
Annotate server starts (reuses plan editor HTML with mode:"annotate")
↓
User annotates content, provides feedback
↓
Send Annotations → feedback sent to agent session
Archive Flow
User runs plannotator archive (CLI) or /plannotator-archive (Pi)
↓
Server starts in mode:"archive", reads ~/.plannotator/plans/
↓
Browser opens read-only archive viewer (sharing disabled)
↓
User browses saved plan decisions with approved/denied badges
↓
Done → POST /api/done closes the browser
During normal plan review, an Archive sidebar tab provides the same browsing via linked doc overlay without leaving the current session.
Server API
Plan Server (packages/server/index.ts)
| Endpoint | Method | Purpose |
|---|---|---|
/api/plan |
GET | Returns { plan, origin, previousPlan, versionInfo } (plan mode) or { plan, origin, mode: "archive", archivePlans } (archive mode) |
/api/plan/version |
GET | Fetch specific version (?v=N) |
/api/plan/versions |
GET | List all versions of current plan |
/api/archive/plans |
GET | List archived plan decisions (?customPath=) |
/api/archive/plan |
GET | Fetch archived plan content (?filename=&customPath=) |
/api/done |
POST | Close archive browser (archive mode only) |
/api/approve |
POST | Approve plan (body: planSave, agentSwitch, obsidian, bear, feedback) |
/api/deny |
POST | Deny plan (body: feedback, planSave) |
/api/image |
GET | Serve image by path query param |
/api/upload |
POST | Upload image, returns { path, originalName } |
/api/obsidian/vaults |
GET | Detect available Obsidian vaults |
/api/reference/obsidian/files |
GET | List vault markdown files as nested tree (?vaultPath=<path>) |
/api/reference/obsidian/doc |
GET | Read a vault markdown file (?vaultPath=<path>&path=<file>) |
/api/plan/vscode-diff |
POST | Open diff in VS Code (body: baseVersion) |
/api/doc |
GET | Serve linked .md/.mdx file (?path=<path>) |
/api/draft |
GET/POST/DELETE | Auto-save annotation drafts to survive server crashes |
/api/editor-annotations |
GET | List editor annotations (VS Code only) |
/api/editor-annotation |
POST/DELETE | Add or remove an editor annotation (VS Code only) |
/api/external-annotations/stream |
GET | SSE stream for real-time external annotations |
/api/external-annotations |
GET | Snapshot of external annotations (polling fallback, ?since=N for version gating) |
/api/external-annotations |
POST | Add external annotations (single or batch { annotations: [...] }) |
/api/external-annotations |
PATCH | Update fields on a single annotation (?id=) |
/api/external-annotations |
DELETE | Remove by ?id=, ?source=, or clear all |
Review Server (packages/server/review.ts)
| Endpoint | Method | Purpose |
|---|---|---|
/api/diff |
GET | Returns { rawPatch, gitRef, origin, diffType, base, gitContext } |
/api/diff/switch |
POST | Switch diff type and/or base branch (body: { diffType, base? }) |
/api/file-content |
GET | Returns { oldContent, newContent } for expandable diff context (?path=&oldPath=&base=) |
/api/git-add |
POST | Stage/unstage a file (body: { filePath, undo? }) |
/api/feedback |
POST | Submit review (body: feedback, annotations, agentSwitch) |
/api/image |
GET | Serve image by path query param |
/api/upload |
POST | Upload image, returns { path, originalName } |
/api/draft |
GET/POST/DELETE | Auto-save annotation drafts to survive server crashes |
/api/editor-annotations |
GET | List editor annotations (VS Code only) |
/api/editor-annotation |
POST/DELETE | Add or remove an editor annotation (VS Code only) |
/api/ai/capabilities |
GET | Check if AI features are available |
/api/ai/session |
POST | Create or fork an AI session |
/api/ai/query |
POST | Send a message and stream the response (SSE) |
/api/ai/abort |
POST | Abort the current query |
/api/ai/permission |
POST | Respond to a permission request |
/api/ai/sessions |
GET | List active sessions |
/api/external-annotations/stream |
GET | SSE stream for real-time external annotations |
/api/external-annotations |
GET | Snapshot of external annotations (polling fallback, ?since=N for version gating) |
/api/external-annotations |
POST | Add external annotations (single or batch { annotations: [...] }) |
/api/external-annotations |
PATCH | Update fields on a single annotation (?id=) |
/api/external-annotations |
DELETE | Remove by ?id=, ?source=, or clear all |
/api/agents/capabilities |
GET | Check available agent providers (claude, codex, tour) |
/api/agents/jobs/stream |
GET | SSE stream for real-time agent job status updates |
/api/agents/jobs |
GET | Snapshot of agent jobs (polling fallback, ?since=N for version gating) |
/api/agents/jobs |
POST | Launch an agent job (body: { provider, command, label }) |
/api/agents/jobs |
DELETE | Kill all running agent jobs |
/api/agents/jobs/:id |
DELETE | Kill a specific agent job |
/api/pr-diff-scope |
POST | Switch between layer and full-stack diff scope |
/api/pr-list |
GET | List PRs for the current repo (cached 30s) |
/api/pr-switch |
POST | Switch to a different PR in-place (body: { url }) |
/api/tour/:jobId |
GET | Fetch Code Tour result (greeting, stops, checklist) for a completed tour job |
/api/tour/:jobId/checklist |
PUT | Persist checklist item state for a Code Tour |
Annotate Server (packages/server/annotate.ts)
| Endpoint | Method | Purpose |
|---|---|---|
/api/plan |
GET | Returns { plan, origin, mode: "annotate", filePath, sourceInfo?, gate } |
/api/feedback |
POST | Submit annotations (body: feedback, annotations) |
/api/approve |
POST | Approve without feedback (review-gate UX, --gate) |
/api/exit |
POST | Close session without feedback |
/api/image |
GET | Serve image by path query param |
/api/upload |
POST | Upload image, returns { path, originalName } |
/api/draft |
GET/POST/DELETE | Auto-save annotation drafts to survive server crashes |
/api/external-annotations/stream |
GET | SSE stream for real-time external annotations |
/api/external-annotations |
GET | Snapshot of external annotations (polling fallback, ?since=N for version gating) |
/api/external-annotations |
POST | Add external annotations (single or batch { annotations: [...] }) |
/api/external-annotations |
PATCH | Update fields on a single annotation (?id=) |
/api/external-annotations |
DELETE | Remove by ?id=, ?source=, or clear all |
All servers use random ports locally or fixed port (19432) in remote mode.
Paste Service (apps/paste-service/)
| Endpoint | Method | Purpose |
|---|---|---|
/api/paste |
POST | Store compressed plan data, returns { id } |
/api/paste/:id |
GET | Retrieve stored compressed data |
Runs as a separate service on port 19433 (self-hosted) or as a Cloudflare Worker (hosted).
Plan Version History
Every plan is automatically saved to ~/.plannotator/history/{project}/{slug}/ on arrival, before the user sees the UI. Versions are numbered sequentially (001.md, 002.md, etc.). The slug is derived from the plan's first # Heading + today's date via generateSlug(), scoped by project name (git repo or cwd). Same heading on the same day = same slug = same plan being iterated on. Identical resubmissions are deduplicated (no new file if content matches the latest version).
This powers the version history API (/api/plan/version, /api/plan/versions) and the plan diff system.
History saves independently of the planSave user setting (which controls decision snapshots in ~/.plannotator/plans/). Storage functions live in packages/shared/storage.ts (runtime-agnostic, re-exported by packages/server/storage.ts). Pi copies the shared files at build time. Slug format: {sanitized-heading}-YYYY-MM-DD (heading first for readability).
Plan Diff
When a user denies a plan and Claude resubmits, the UI shows what changed between versions. A +N/-M badge appears below the document card; clicking it toggles between normal view and diff view.
Diff engine (packages/ui/utils/planDiffEngine.ts): Uses the diff npm package (diffLines()) to compute line-level diffs. Groups consecutive remove+add into "modified" blocks. Returns PlanDiffBlock[] and PlanDiffStats.
Two view modes (toggle via PlanDiffModeSwitcher):
- Rendered (
PlanCleanDiffView): Color-coded left borders — green (added), red (removed/strikethrough), yellow (modified) - Raw (
PlanRawDiffView): Monospace+/-lines, git-style
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.
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.
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.
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.
Data Types
Location: packages/ui/types.ts
enum AnnotationType {
DELETION = "DELETION",
COMMENT = "COMMENT",
GLOBAL_COMMENT = "GLOBAL_COMMENT",
}
interface ImageAttachment {
path: string; // temp file path
name: string; // human-readable label (e.g., "login-mockup")
}
interface Annotation {
id: string;
blockId: string;
startOffset: number;
endOffset: number;
type: AnnotationType;
text?: string; // For comment
originalText: string; // The selected text
createdA: number; // Timestamp
author?: string; // Tater identity
images?: ImageAttachment[]; // Attached images with names
source?: string; // External tool identifier (e.g., "eslint") — set when annotation comes from external API
diffContext?: 'added' | 'removed' | 'modified'; // Set when annotation created in plan diff view
startMeta?: { parentTagName; parentIndex; textOffset };
endMeta?: { parentTagName; parentIndex; textOffset };
}
interface Block {
id: string;
type: "paragraph" | "heading" | "blockquote" | "list-item" | "code" | "hr" | "table" | "html" | "directive";
content: string;
level?: number; // For headings (1-6)
language?: string; // For code blocks
alertKind?: "note" | "tip" | "warning" | "caution" | "important"; // GitHub alerts (blockquote subtype)
order: number;
startLine: number;
}
Markdown Parser
Location: packages/ui/utils/parser.ts
parseMarkdownToBlocks(markdown) splits markdown into Block objects. Handles:
- Headings (
#,##, etc.) with slug-derived anchor ids - Code blocks (``` with language extraction)
- List items (
-,*,1.) - Blockquotes (
>) — including GitHub alerts (> [!NOTE|TIP|WARNING|CAUTION|IMPORTANT]) which setalertKind - Horizontal rules (
---) - Tables (pipe-delimited) — rendered via
TableBlockwith aTableToolbar(copy as markdown/CSV) andTablePopoutoverlay - Raw HTML blocks (
<details>,<summary>, etc.) — rendered viaHtmlBlockthroughmarked+ DOMPurify - Directive containers (
:::kind ... :::) — rendered viaCallout - Paragraphs (default) with inline extras: bare URL autolinks,
@mentions/#issue-refs, emoji shortcodes, smart punctuation
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.
Annotation System
Selection mode: User selects text → toolbar appears → choose annotation type Redline mode: User selects text → auto-creates DELETION annotation
Text highlighting uses web-highlighter library. Code blocks use manual <mark> wrapping (web-highlighter can't select inside <pre>).
URL Sharing
Location: packages/ui/utils/sharing.ts, packages/ui/hooks/useSharing.ts
Shares full plan + annotations via URL hash using deflate compression. For large plans, short URLs are created via the paste service (user must explicitly confirm).
Payload format:
// Image in shareable format: plain string (old) or [path, name] tuple (new)
type ShareableImage = string | [string, string];
interface SharePayload {
p: string; // Plan markdown
a: ShareableAnnotation[]; // Compact annotations
g?: ShareableImage[]; // Global attachments
d?: (string | null)[]; // diffContext per annotation, parallel to `a`
}
type ShareableAnnotation =
| ["D", string, string | null, ShareableImage[]?] // [type, original, author, images?]
| ["C", string, string, string | null, ShareableImage[]?] // [type, original, comment, author, images?]
| ["G", string, string | null, ShareableImage[]?]; // [type, comment, author, images?]
Compression pipeline:
JSON.stringify(payload)CompressionStream('deflate-raw')- Base64 encode
- URL-safe: replace
+/=with-_
On load from shared URL:
- Parse hash, decompress, restore annotations
- Find text positions in rendered DOM via text search
- Apply
<mark>highlights - Clear hash from URL (prevents re-parse on refresh)
Settings Persistence
Location: packages/ui/utils/storage.ts, planSave.ts, agentSwitch.ts
Uses cookies (not localStorage) because each hook invocation runs on a random port. Settings include identity, plan saving (enabled/custom path), and agent switching (OpenCode only).
Syntax Highlighting
Code blocks use bundled highlight.js. Language is extracted from fence (```rust) and applied as language-{lang}class. Each block highlighted individually viahljs.highlightElement().
Requirements
- Bun runtime
- Claude Code with plugin/hooks support, or OpenCode
- Cross-platform: macOS (
open), Linux (xdg-open), Windows (start)
Development
bun install
# Run any app
bun run dev:hook # Hook server (plan review)
bun run dev:review # Review editor (code review)
bun run dev:portal # Portal editor
bun run dev:marketing # Marketing site
bun run dev:vscode # VS Code extension (watch mode)
Build
bun run build:hook # Single-file HTML for hook server
bun run build:review # Code review editor
bun run build:opencode # OpenCode plugin (copies HTML from hook + review)
bun run build:portal # Static build for share.plannotator.ai
bun run build:marketing # Static build for plannotator.ai
bun run build:vscode # VS Code extension bundle
bun run package:vscode # Package .vsix for marketplace
bun run build # Build hook + opencode (main targets)
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:
bun run --cwd apps/review build && bun run build:hook # For review UI changes
bun run build:hook # For plan UI changes only
bun run build:hook && bun run build:opencode # For OpenCode plugin
Running only build:hook after review-editor changes will copy stale HTML files. When testing locally with a compiled binary, the full sequence is:
bun run --cwd apps/review build && bun run build:hook && \
bun build apps/hook/server/index.ts --compile --outfile ~/.local/bin/plannotator
Running only build:opencode will copy stale HTML files.
Marketing Site
apps/marketing/ is the plannotator.ai website — landing page, documentation, and blog. Built with Astro 5 (static output, zero client JS except a theme toggle island). Docs are markdown files in src/content/docs/, blog posts in src/content/blog/, both using Astro content collections. Tailwind CSS v4 via @tailwindcss/vite. Deploys to S3/CloudFront via GitHub Actions on push to main.
Test plugin locally
claude --plugin-dir ./apps/hook