Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
61 commits
Select commit Hold shift + click to select a range
ea1cfec
docs(adr): revert failed document-ui cutover, add ADR 004 with correc…
backnotprop Jun 23, 2026
795e900
docs(adr): add verified document-ui extraction plan, supersede draft …
backnotprop Jun 23, 2026
79a1de9
docs(adr): add document-ui extraction roadmap + parity checklist
backnotprop Jun 23, 2026
6ef3c8a
build(ui): packaging unblock for external install (Phase 1) — no runt…
backnotprop Jun 23, 2026
d70620a
feat(ui): make image URL resolution host-overridable (Phase 2, seam 1)
backnotprop Jun 23, 2026
8a04894
feat(ui): make settings storage backend host-overridable (Phase 2, se…
backnotprop Jun 23, 2026
3108efa
feat(ui): make MarkdownEditor theme mode host-supplyable (Phase 3)
backnotprop Jun 23, 2026
8c4d925
feat(ui): allow hosts to opt out of code-path validation (Phase 3)
backnotprop Jun 23, 2026
5f355bd
feat(ui): make code-file hover preview fetch host-overridable (Phase 3)
backnotprop Jun 23, 2026
fab84ac
feat(ui): ship ScrollViewportProvider with the library (Phase 3 scroll)
backnotprop Jun 23, 2026
7061771
fix(ui): disabled code-path validation should keep links clickable (s…
backnotprop Jun 23, 2026
f336450
feat(ui): make file-tree backend host-overridable (Phase 4)
backnotprop Jun 23, 2026
44a67bf
docs(adr): research + synthesis + spec for Phase 5 (comments/annotati…
backnotprop Jun 23, 2026
a0c3177
docs(adr): accept ADR 005 — make comments/annotations/drafts host-ove…
backnotprop Jun 23, 2026
4409465
feat(ui): make annotation identity host-overridable (Phase 5 seam 1)
backnotprop Jun 23, 2026
2db9997
feat(ui): make draft persistence transport host-overridable (Phase 5 …
backnotprop Jun 23, 2026
2c416b1
feat(ui): make external-annotation transport host-overridable (Phase …
backnotprop Jun 23, 2026
e612dc6
docs(adr): research + synthesis + spec for Phase 6 (versions, setting…
backnotprop Jun 23, 2026
903d9cb
docs(adr): accept ADR 006 — make extras (versions/settings/sharing/AI…
backnotprop Jun 23, 2026
e0c5b39
feat(ui): make version fetchers + vscode-diff host-overridable; move …
backnotprop Jun 23, 2026
b0d1ca1
feat(ui): make config write-back + obsidian-detect host-overridable (…
backnotprop Jun 23, 2026
a7927ac
feat(ui): make save-to-notes host-overridable (Phase 6 sharing)
backnotprop Jun 23, 2026
7ccd029
feat(ui): make Ask AI transport host-overridable (Phase 6 ai)
backnotprop Jun 23, 2026
e116eae
docs(adr): log Phase 6 completion (4 seams + diff CSS move)
backnotprop Jun 23, 2026
c2a43cb
docs(adr): research + synthesis + spec for Phase 7 (carve @plannotato…
backnotprop Jun 23, 2026
6719eb8
docs(adr): fold configurePlannotatorUI() front door + precompiled CSS…
backnotprop Jun 23, 2026
5fba2c6
docs(adr): lock Phase 7 publish decisions + carry over review fixes
backnotprop Jun 23, 2026
13378a5
docs(adr): ADR 007 — carve @plannotator/core, complete settings provi…
backnotprop Jun 23, 2026
efce0e6
fix(ui): make external-annotation transport reads consistent + reset …
backnotprop Jun 23, 2026
1c696fd
docs(adr): align Phase 7 spec with ADR 007 (version 0.21.0 lockstep, …
backnotprop Jun 23, 2026
ff94d22
feat(core): carve @plannotator/core — move pure modules, extract node…
backnotprop Jun 23, 2026
23ece9d
feat(ui): depend only on @plannotator/core — re-point all shared/ai i…
backnotprop Jun 23, 2026
2377948
refactor(ui): relocate wideMode helper to @plannotator/ui/utils (Phas…
backnotprop Jun 23, 2026
cc834e7
feat(ui): add loadFromBackend settings rehydration + configurePlannot…
backnotprop Jun 23, 2026
539c12c
build(ui): precompiled styles.css CSS build + madge circular-dep chec…
backnotprop Jun 23, 2026
aecd5b9
test(ui): per-seam override tests + configure routing test (Phase 7 s…
backnotprop Jun 23, 2026
86d0b66
fix(ui): apply Phase 7 review findings — version lockstep + seam cons…
backnotprop Jun 23, 2026
55ceaf9
docs(adr): Phase 7 implementation plan (workflow-generated, durable a…
backnotprop Jun 23, 2026
11b830a
fix(ui): reconcile #948 with the draft-transport seam + lockstep 0.21.1
backnotprop Jun 24, 2026
79ed90b
fix(ui): address review nits — host-path robustness + cleanups
backnotprop Jun 24, 2026
199ea3d
docs: collapse 29 ADR process docs into one packages/ui/README.md
backnotprop Jun 24, 2026
5a63827
docs(ui): add packages/ui/AGENTS.md guardrail + CLAUDE.md symlink
backnotprop Jun 24, 2026
6dd843f
build: remove madge circular-dep check (unmaintained)
backnotprop Jun 24, 2026
d78010d
fix(ui): address review — TDZ guard, html-viewer export, doc corrections
backnotprop Jun 24, 2026
77ec389
build(ui): don't bundle fonts in published styles.css — app loads fon…
backnotprop Jun 24, 2026
dbc83d4
fix(ui): build styles.css on prepack, not prepublishOnly (review #4)
backnotprop Jun 24, 2026
f224bc3
chore(ui): post-rebase reconciliation — version lockstep 0.21.3, awai…
backnotprop Jun 29, 2026
8f1e848
fix(ui): make postServerAbort never reject regardless of AI transport
backnotprop Jun 29, 2026
ae179fa
fix(ui): address review — core import, abort sync-throw, snapshot guards
backnotprop Jun 29, 2026
8f9670f
feat(ui): add upload + identity-editable seams for host backends
backnotprop Jun 29, 2026
851fca7
refactor(ui): capture sessionId synchronously in postServerAbort
backnotprop Jun 29, 2026
a4d8f88
fix(ui): address review — seed host store, browser-safe timer type, h…
backnotprop Jun 29, 2026
b9f6b03
refactor(ui): single-source the never-reject abort via safeAbort helper
backnotprop Jun 29, 2026
7d75969
chore(ui): post-rebase version lockstep to 0.21.4
backnotprop Jul 1, 2026
44b1c7d
docs(ui): consumer-lens handoff hardening + ADR 005
backnotprop Jul 1, 2026
f9a647a
fix(ui): make shipped source strict-TS clean for consumers + seam typ…
backnotprop Jul 1, 2026
d49d6dc
fix(ui): keep KaTeX fonts out of published styles.css (back to ~187KB…
backnotprop Jul 1, 2026
3d07a3c
docs(ui): HANDOFF corrections from adversarial consumer review
backnotprop Jul 1, 2026
465427b
docs(ui): math setup pointer in README + pnpm caveat on the katex bun…
backnotprop Jul 1, 2026
de507e7
fix(ui): lazy settings resolution — zero cookies on a configured host
backnotprop Jul 2, 2026
87acd2d
chore(ui): post-rebase version lockstep to 0.22.0
backnotprop Jul 5, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -62,3 +62,7 @@ plannotator-local

# Local Pi state/memory (not upstream)
/.pi/

# @plannotator/ui CSS build artifacts (generated by prepack — not committed)
packages/ui/styles.css
packages/ui/styles.js
7 changes: 5 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

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.

> **Reusing the document UI (theme / markdown / editor / settings / comments / layout) in the commercial Workspaces app? Read `packages/ui/README.md` FIRST.** It explains the published `@plannotator/ui` + `@plannotator/core` packages and the host-override seams a host plugs its own backend into via `configurePlannotatorUI()`. A prior from-scratch reimplementation of this UI broke the app and was reverted — do **not** rebuild it or recreate `packages/document-ui`. Add a seam to `@plannotator/ui` instead, keep Plannotator's app unchanged, and never delete working code until a human confirms parity in the browser.

## Project Structure

```
Expand Down Expand Up @@ -81,7 +83,8 @@ plannotator/
│ │ ├── 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
│ ├── core/ # @plannotator/core — browser-safe, zero-dep universal slice (pure utils + types) shared by ui + shared; published so @plannotator/ui can be installed standalone. `shared` re-exports the moved modules via one-line shims so Plannotator is unchanged.
│ ├── shared/ # Node/git/server logic + cross-runtime types (re-exports browser-safe modules from @plannotator/core)
│ │ ├── 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)
Expand Down Expand Up @@ -206,7 +209,7 @@ Ask AI's "changes under review" context for **code review** is generated by the

## Ask AI Provider Defaults

Ask AI providers are detected independently from installed/authenticated local CLIs, then the UI picks a default from the detected Plannotator origin. The mapping lives in `packages/shared/agents.ts` and is applied by `packages/ui/utils/aiProvider.ts`:
Ask AI providers are detected independently from installed/authenticated local CLIs, then the UI picks a default from the detected Plannotator origin. The mapping lives in `packages/core/agents.ts` (re-exported via the `packages/shared/agents.ts` shim) and is applied by `packages/ui/utils/aiProvider.ts`:

| Origin | Preferred Ask AI provider |
|--------|---------------------------|
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# 005. Publish the document UI as `@plannotator/ui` + `@plannotator/core`

Date: 2026-07-01

## Status

Accepted

## Context

The commercial Workspaces app (Cloudflare Workers + D1 + R2 + Durable Objects, WorkOS identity, Yjs realtime) needs Plannotator's document UI: the markdown rendering engine, the annotation/comment machinery, the markdown editor, and the theme. Plannotator's open-source experience must not change while making that possible.

Two approaches were on the table:

1. **Copy/vendor** — fork the UI source into the Workspaces repo (an earlier from-scratch reimplementation of this UI was attempted, broke the app, and was reverted).
2. **Publish** — make the UI an installable package with host-override seams.

Copy/vendor loses on two facts we've since measured:

- **The reusable pieces sit deep in the package graph.** `packages/ui` imported freely from `@plannotator/shared`, which is Node-bound (fs/git/server). A copy would have to sever those imports by hand and re-sever them on every sync.
- **Main moves too fast for a fork.** During the packaging PR (#957) alone, the branch was rebased onto main **four times** (46 commits, then #979/#980, #981/#983, #878 + the 0.21.4 release). A vendored copy would already be four syncs behind; every UI feature landing on main (math rendering, parser hardening, PR-context work) would need manual porting.

Seams beat surgery for the same reason: an override point whose default *is* today's code keeps one source of truth, whereas editing call sites per-consumer creates two.

## Decision

Publish two packages, keep one private, and decouple via seams — never rewrite:

- **`@plannotator/core`** — browser-safe, zero-dependency pure utils + types, carved out of `@plannotator/shared` with `git mv`. CI typechecks it without `@types/node` so a `node:` import cannot sneak in. Published.
- **`@plannotator/ui`** — the React components, hooks, theme, and `configurePlannotatorUI()`. Depends on `core`. Published.
- **`@plannotator/shared`** — stays private; re-exports the moved core modules via one-line shims (`export * from '@plannotator/core/x'`), so none of Plannotator's ~99 internal import sites changed.

**The seam pattern:** every place the UI touches a backend is a module-level singleton with a setter — `let impl = default; setX(); resetX(); getX()` — where the default reproduces Plannotator's literal behavior (`/api/*` over fetch). Plannotator passes nothing and stays byte-for-byte unchanged; a host calls `configurePlannotatorUI({...})` once at startup. Seams shipped: storage, identity (incl. `isEditable`), image resolving, uploads, doc previews, file tree, drafts, external annotations, AI transport, server sync.

**Singleton, not a React Provider — with a named revisit condition:** module-level state is safe for client-side apps (one user per browser). It is wrong under SSR, where one server process renders for many users. Neither Plannotator nor Workspaces does SSR. **If SSR is ever added for this UI, that is the trigger to introduce a `<PlannotatorUIServices>` provider** and turn `configurePlannotatorUI` into a compatibility shim over it. Until then the complexity is not paid.

**The law** (guardrails, enforced socially and by CI where possible):

1. Don't reimplement the document UI from scratch — add a seam.
2. Every seam's default must reproduce today's Plannotator behavior.
3. `@plannotator/core` stays browser-safe and zero-dep — no `node:` imports.
4. Never delete working Plannotator code until a human confirms parity in the browser.

**Publish model:** both packages version **in lockstep with the repo** (currently 0.21.4). They ship TypeScript source (consumers bundle it); `styles.css` is precompiled by `prepack`. `workspace:*` inter-deps must resolve to exact versions at publish time, so the flow is `bun pm pack` then `npm publish *.tgz --provenance --access public`, **`core` first, then `ui`**. First publish is manual from `main` after merge; a CI publish job is a follow-up.

## Consequences

- Workspaces installs the packages and plugs in its Cloudflare backend (localStorage settings, WorkOS identity with `isEditable() => false`, R2 uploads, D1 doc previews, DO-backed annotation transport) without forking. Its repo writes the matching consumer-side ADR.
- The published exports map is broad; **importable ≠ supported**. The supported-import allowlist and the unsupported "calls Plannotator's local server" list live in `packages/ui/HANDOFF.md`, alongside the annotation anchor schema and the editor/Yjs status. Restructuring exports was deliberately deferred.
- Known rough edges accepted for v1 (documented in HANDOFF.md): `AITransport`/`FileTreeBackend` leak `Response` objects; the markdown editor doesn't yet accept CM6 extensions (live collab lands via a later atomic PR threading `extensions?` through both wrapper layers after `@plannotator/markdown-editor` is imported into the monorepo).
- Every future "the host needs different behavior" request has one sanctioned shape: add an optional seam in a Plannotator PR whose default is today's behavior.
- Version lockstep means UI releases are repo releases; publishing stays owner-authorized.

## References

- PR #957 (`feat/pkg-document-ui`)
- `packages/ui/HANDOFF.md` — consumer handoff (seam catalog, supported imports, anchor schema)
- `packages/ui/README.md`, `packages/ui/CLAUDE.md` — the short version next to the code
32 changes: 30 additions & 2 deletions apps/pi-extension/vendor.sh
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,29 @@ cd "$(dirname "$0")"
rm -rf generated
mkdir -p generated generated/ai/providers

for f in feedback-templates prompts review-core diff-paths cli-pagination jj-core vcs-core review-args storage draft project pr-types pr-context-live pr-provider pr-stack pr-github pr-gitlab checklist integrations-common repo reference-common favicon code-file resolve-file annotate-reference-roots-node config external-annotation agent-jobs agent-terminal worktree worktree-pool html-to-markdown html-assets html-assets-node url-to-markdown tour guide annotate-args at-reference review-workspace-node review-workspace pfm-reminder improvement-hooks code-nav data-dir semantic-diff-types semantic-diff source-save source-save-node workspace-status open-in-apps review-profiles commit-avatars commit-history; do
# Modules that MOVED to @plannotator/core — vendor the real impl from core.
for f in feedback-templates project favicon code-file external-annotation agent-jobs agent-terminal source-save open-in-apps; do
src="../../packages/core/$f.ts"
printf '// @generated — DO NOT EDIT. Source: packages/core/%s.ts\n' "$f" | cat - "$src" > "generated/$f.ts"
done

# Node-bound shared modules that now import types from @plannotator/core/*-types —
# vendor from shared, rewrite the bare core specifier to the flat relative path.
for f in config storage workspace-status; do
src="../../packages/shared/$f.ts"
printf '// @generated — DO NOT EDIT. Source: packages/shared/%s.ts\n' "$f" | cat - "$src" \
| sed "s|from ['\"]@plannotator/core/\\([^'\"]*\\)-types['\"]|from './\\1-types.js'|g" \
> "generated/$f.ts"
done

# Extracted type files those node-bound modules now depend on — vendor from core.
for f in config-types storage-types workspace-status-types; do
src="../../packages/core/$f.ts"
printf '// @generated — DO NOT EDIT. Source: packages/core/%s.ts\n' "$f" | cat - "$src" > "generated/$f.ts"
done

# Everything else in the original flat list stays sourced from packages/shared.
for f in prompts review-core diff-paths cli-pagination jj-core vcs-core review-args draft pr-types pr-context-live pr-provider pr-stack pr-github pr-gitlab checklist integrations-common repo reference-common resolve-file annotate-reference-roots-node worktree worktree-pool html-to-markdown html-assets html-assets-node url-to-markdown tour annotate-args at-reference review-workspace-node review-workspace pfm-reminder improvement-hooks code-nav data-dir semantic-diff-types semantic-diff source-save-node review-profiles guide commit-avatars commit-history; do
src="../../packages/shared/$f.ts"
printf '// @generated — DO NOT EDIT. Source: packages/shared/%s.ts\n' "$f" | cat - "$src" > "generated/$f.ts"
done
Expand Down Expand Up @@ -56,9 +78,15 @@ for f in guide-review; do
> "generated/$f.ts"
done

# Vendor the moved AI context types from core into generated/ai/.
printf '// @generated — DO NOT EDIT. Source: packages/core/ai-context.ts\n' \
| cat - "../../packages/core/ai-context.ts" > "generated/ai/ai-context.ts"

for f in index types provider session-manager endpoints context base-session; do
src="../../packages/ai/$f.ts"
printf '// @generated — DO NOT EDIT. Source: packages/ai/%s.ts\n' "$f" | cat - "$src" > "generated/ai/$f.ts"
printf '// @generated — DO NOT EDIT. Source: packages/ai/%s.ts\n' "$f" | cat - "$src" \
| sed "s|from ['\"]@plannotator/core/ai-context['\"]|from './ai-context.js'|g" \
> "generated/ai/$f.ts"
done

for f in claude-agent-sdk codex-app-server opencode-sdk command-path pi-sdk pi-sdk-node pi-events; do
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,8 @@
"build:vscode": "bun run --cwd apps/vscode-extension build",
"package:vscode": "bun run --cwd apps/vscode-extension package",
"test": "bun test",
"typecheck": "bash apps/pi-extension/vendor.sh && tsc --noEmit -p packages/shared/tsconfig.json && tsc --noEmit -p packages/ai/tsconfig.json && tsc --noEmit -p packages/server/tsconfig.json && tsc --noEmit -p packages/ui/tsconfig.json && tsc --noEmit -p apps/pi-extension/tsconfig.json"
"typecheck": "bash apps/pi-extension/vendor.sh && tsc --noEmit -p packages/core/tsconfig.json && tsc --noEmit -p packages/shared/tsconfig.json && tsc --noEmit -p packages/ai/tsconfig.json && tsc --noEmit -p packages/server/tsconfig.json && tsc --noEmit -p packages/ui/tsconfig.json && tsc --noEmit -p apps/pi-extension/tsconfig.json",
"build:ui-css": "bun run --cwd packages/ui build:css"
},
"dependencies": {
"@anthropic-ai/claude-agent-sdk": "^0.2.92",
Expand Down
4 changes: 3 additions & 1 deletion packages/ai/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,7 @@
"./providers/opencode-sdk": "./providers/opencode-sdk.ts",
"./providers/pi-sdk-node": "./providers/pi-sdk-node.ts"
},
"dependencies": {}
"dependencies": {
"@plannotator/core": "workspace:*"
}
}
85 changes: 2 additions & 83 deletions packages/ai/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,89 +10,8 @@
// Context — what the AI session knows about
// ---------------------------------------------------------------------------

/** The surface the user is interacting with when they invoke AI. */
export type AIContextMode = "plan-review" | "code-review" | "annotate";

/**
* Describes the parent agent session that originally produced the plan or diff.
* Used to fork conversations with full history.
*/
export interface ParentSession {
/** Session ID from the host agent (e.g. Claude Code session UUID). */
sessionId: string;
/** Working directory the parent session was running in. */
cwd: string;
}

/**
* Snapshot of plan-review-specific context.
* Passed when AIContextMode is "plan-review".
*/
export interface PlanContext {
/** The full plan markdown as submitted by the agent. */
plan: string;
/** Previous plan version (if this is a resubmission). */
previousPlan?: string;
/** The version number in the plan's history. */
version?: number;
/** Total number of versions in the plan's history. */
totalVersions?: number;
/** Project/repository label used for plan history. */
project?: string;
/** Annotations the user has made so far (serialised for the prompt). */
annotations?: string;
}

/**
* Snapshot of code-review-specific context.
* Passed when AIContextMode is "code-review".
*/
export interface CodeReviewContext {
/** The unified diff patch. Used as a fallback when the changeset can't be
* reproduced locally with a single VCS command. */
patch: string;
/** The VCS diff type (e.g. "uncommitted", "branch", "merge-base"). When set
* to a git-reproducible type, the prompt tells the agent how to inspect the
* changes with git instead of pasting the whole diff. */
diffType?: string;
/** The base branch/ref the diff is computed against (for branch/merge-base). */
base?: string;
/** The specific file being discussed (if scoped). */
filePath?: string;
/** The line range being discussed (if scoped). */
lineRange?: { start: number; end: number; side: "old" | "new" };
/** The code snippet being discussed (if scoped). */
selectedCode?: string;
/** Summary of annotations the user has made. */
annotations?: string;
}

/**
* Snapshot of annotate-mode context.
* Passed when AIContextMode is "annotate".
*/
export interface AnnotateContext {
/** The markdown file content being annotated. */
content: string;
/** Path to the file on disk. */
filePath: string;
/** Source attribution shown in the UI, such as an original URL or filename. */
sourceInfo?: string;
/** True when the document was converted from HTML or a remote reader result. */
sourceConverted?: boolean;
/** Render mode for the annotated content. */
renderAs?: "markdown" | "html";
/** Summary of annotations the user has made. */
annotations?: string;
}

/**
* Union of mode-specific contexts, discriminated by `mode`.
*/
export type AIContext =
| { mode: "plan-review"; plan: PlanContext; parent?: ParentSession }
| { mode: "code-review"; review: CodeReviewContext; parent?: ParentSession }
| { mode: "annotate"; annotate: AnnotateContext; parent?: ParentSession };
import type { AIContext, AIContextMode, PlanContext, CodeReviewContext, AnnotateContext, ParentSession } from '@plannotator/core/ai-context';
export type { AIContext, AIContextMode, PlanContext, CodeReviewContext, AnnotateContext, ParentSession };

// ---------------------------------------------------------------------------
// Messages — what streams back from the AI
Expand Down
Loading
Loading