Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 2 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,8 @@ jobs:
packages/ui/components/AnnotationPanel.inReplyTo.test.tsx
packages/editor/webmcp/useDocumentWebMcp.toggle.test.tsx
packages/editor/components/AppHeader.webmcpIndicator.test.tsx
packages/ui/components/MathBlock.firstPaint.test.tsx
packages/ui/components/DiagramBlock.lazyRetry.test.tsx

opencode-v2:
name: OpenCode 2 installed package
Expand Down
10 changes: 10 additions & 0 deletions apps/guides-show/viewer/stubs/generateIdentity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,13 @@
export function generateIdentity(): string {
return 'reader';
}

export type IdentityGenerator = () => string;

/**
* No-op registration, so the viewer build keeps working should
* `packages/ui/configure.ts` (which imports `setIdentityGenerator` from the
* real module) ever enter its import graph. The viewer never mints a name, so
* there is nothing to register into.
*/
export function setIdentityGenerator(_next: IdentityGenerator): void {}
11 changes: 11 additions & 0 deletions packages/editor/App.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,14 @@
// Eager renderer registration (side-effect imports, evaluated before every
// other module below). These keep Plannotator's first paint, identity minting
// and failure surface byte-identical now that @plannotator/ui loads KaTeX, the
// username dictionary and the Mermaid runtime lazily for hosts: math is typeset
// on the first commit, names come from the full dictionary, and Mermaid stays
// in this app's entry chunk (the review editor never renders Mermaid and does
// not import that entry). Guarded by tests/entry-assets.test.ts; do not drop
// or reorder any of these lines.
import '@plannotator/ui/utils/math-eager';
import '@plannotator/ui/utils/identity-tater';
import '@plannotator/ui/utils/mermaid-eager';
import React, { useState, useEffect, useLayoutEffect, useMemo, useRef, useCallback } from 'react';
import { toast, Toaster } from 'sonner';
import { type Origin, getAgentName } from '@plannotator/shared/agents';
Expand Down
8 changes: 8 additions & 0 deletions packages/review-editor/App.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,11 @@
// Eager renderer registration (side-effect imports, evaluated before every
// other module below). These keep Plannotator's first paint and identity
// minting byte-identical now that @plannotator/ui loads KaTeX and the username
// dictionary lazily for hosts: math is typeset on the first commit and names
// come from the full dictionary. Guarded by tests/entry-assets.test.ts; do not
// drop or reorder either line.
import '@plannotator/ui/utils/math-eager';
import '@plannotator/ui/utils/identity-tater';
import React, { useState, useEffect, useCallback, useMemo, useRef } from 'react';
import { type Origin, getAgentName } from '@plannotator/shared/agents';
import { ThemeProvider, useTheme } from '@plannotator/ui/components/ThemeProvider';
Expand Down
35 changes: 35 additions & 0 deletions packages/ui/HANDOFF.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,8 @@ Pass any subset of these to `configurePlannotatorUI({ ... })`. Anything omitted
| `aiTransport` | `AITransport` | The "Ask AI" chat session/query/abort/permission | `POST /api/ai/{session,query,abort,permission}` |
| `serverSync` | `ServerSyncFn` | Push a settings change back to the server | No-op-ish (Plannotator's local sync) |
| `loadSettingsFromBackend` | `boolean` | After install, re-hydrate settings from your `storageBackend` | off |
| `mathRendererLoader` | `() => Promise<MathRenderer>` | How KaTeX is loaded when no renderer is registered before the first math node renders (see "Lazy renderers and eager entries") | `import('katex')`, JS only; CSS stays yours |
| `identityGenerator` | `() => string` | The synchronous generator behind the default "tater" display name when no `identityProvider` is installed | A built-in 16 x 16 word pool of the same `adjective-noun-tater` shape; Plannotator registers the full dictionary via `utils/identity-tater` |

### Interface details worth knowing

Expand Down Expand Up @@ -206,6 +208,9 @@ We deliberately did **not** restructure the exports map in this PR (move-don't-r
| `utils/annotationHelpers` | Pure annotation utilities (`getAnnotationCountBySection`, `buildTocHierarchy` + `TocItem`). *(Blessed in 0.24.0.)* |
| `components/html-viewer` (`HtmlViewer`) | The raw-HTML annotation viewer: overlay-projected placed markers, pinpoint anchors, multi-target comments. Props + validated bridge protocol; no backend of its own. See "Raw-HTML annotation viewer + syntax-highlighting migration (0.29.0)". *(Blessed in 0.29.0.)* |
| `utils/codeHighlight` / `utils/codeBlockMark` / `utils/syntaxTheme` | The Shiki-based fence highlighter, swap-surviving annotation marks, and palette→Shiki theme mapping. Replaces all `.hljs` styling. *(Blessed in 0.29.0.)* |
| `utils/math` (`loadMathRenderer`, `getMathRenderer`, `getMathRendererSource`, `setMathRenderer`) and `utils/math-eager` | The math renderer slot and its eager KaTeX registration. Import `utils/math-eager` for synchronous typesetting on the first commit; call `loadMathRenderer()` to pre-warm the lazy path. See "Lazy renderers and eager entries". |
| `utils/identity-tater` | Side-effect entry that registers the full username dictionary into the identity generator slot. Import it only if you rely on the default tater names and want the full dictionary; a host with `identityProvider` should not. |
| `utils/mermaid` (`loadMermaidRuntime`, `getMermaidRuntime`, `getMermaidRuntimeSource`, `setMermaidRuntime`, `MERMAID_CONFIG`) and `utils/mermaid-eager` | The Mermaid runtime slot and its eager registration. Import `utils/mermaid-eager` to keep Mermaid in your entry chunk as Plannotator does; omit it for the lazy path with retry. See "Lazy renderers and eager entries". |

**AI is fully avoidable** — with one precision worth knowing. No AI *UI* is reachable from the supported components: `useAIChat` is imported only by `components/ai/DocumentAIChatPanel` and `useAIProviderConfig`, neither of which any supported component imports, and `CommentPopover`'s Ask-AI affordance exists only behind the optional `onAskAI` prop. `configure.ts` does statically import the `useAIChat` module (it needs `setAITransport`), but if you never use AI the hook is dead code and bundlers eliminate it — verified empirically: a standalone consumer's production bundle importing the full supported surface contains zero `/api/ai` strings. Don't import `components/ai/*` and don't pass `aiTransport`, and you ship no AI code.

Expand Down Expand Up @@ -238,6 +243,8 @@ The renderer's `MathBlock` (and inline math) uses KaTeX. **KaTeX's stylesheet an

If you skip all three and render math, equations appear as broken-looking raw HTML — that's the symptom to recognize. If you never render math, do nothing.

The JS side is separate and lazy by default: KaTeX's runtime is no longer on the static import graph of `MathBlock` / `InlineMarkdown`. A host that renders `Viewer` without importing `@plannotator/ui/utils/math-eager` gets the TeX source in the same wrapper for one frame, then the typeset markup once `import('katex')` resolves. See "Lazy renderers and eager entries" for the opt-back and the loader seam.

---

## The annotation anchor schema (what you're storing)
Expand Down Expand Up @@ -438,6 +445,34 @@ Behavior is pinned by `components/MarkdownEditor.embedPicker.test.ts`, the suppo

---

## Lazy renderers and eager entries (unreleased)

Four modules that used to ride every document read for a host that bundles by route now load on demand: the Mermaid runtime, the Graphviz engine, KaTeX, and the username dictionary. Plannotator's own apps register KaTeX and the dictionary eagerly in both the plan editor and the review editor, and the plan editor also registers the Mermaid runtime eagerly (the review editor never renders a Mermaid block and deliberately does not), so every surface renders exactly as before; the single-file builds are unchanged in size and first paint and the portal entry chunk keeps Mermaid as on main (the built-HTML markers and the A/B proof live in `tests/entry-assets.test.ts` and the PR that shipped this).

1. **Graphviz: no seam, nothing to do.** `GraphvizBlock` imports `@viz-js/viz` inside its render effect. It already showed the source fence until the SVG landed, so the only change for a chunking host is that the first dot fence on a page fetches the engine. A failed import is dropped from the memo and re-attempted once with a fresh `import()` after a short delay; a persistently failing chunk surfaces as the existing error panel with the source, plus a Retry button that issues another fresh attempt (a diagram syntax error shows the panel exactly as before, without Retry). Hosts that aliased the specifier to a lazy shim can delete the shim.

**Mermaid: a runtime slot, filled eagerly by Plannotator.** `utils/mermaid` holds the slot (`getMermaidRuntime`, `setMermaidRuntime`, `getMermaidRuntimeSource`) and the one code path `MermaidBlock` uses, `loadMermaidRuntime()`: it resolves at once from a filled slot and otherwise imports `mermaid` lazily, initialized once with `MERMAID_CONFIG` (`securityLevel: 'strict'` pinned by test), with the same drop-on-rejection, one automatic re-attempt and Retry button as Graphviz. `utils/mermaid-eager` imports the runtime statically, initializes it at module evaluation (where the old module-scope `initialize` ran) and fills the slot; `packages/editor/App.tsx` imports it by policy, so Plannotator's plan surfaces keep Mermaid in their entry chunk and it can never fail separately from the app (on the share portal `mermaid.core` stays in the entry, as on main). The review editor does not import it because it never renders a Mermaid block. A host that wants the same adds `import '@plannotator/ui/utils/mermaid-eager'`; a host that omits it gets the lazy path.

**Retry, honestly.** An in-page retry cannot recover a chunk whose first fetch failed: browsers record a failed module fetch in the module map for the page lifetime, so a fresh `import()` of the same URL rejects without a request, and package code cannot re-import under a new URL because Rollup minifies the chunk's export names. The retry therefore recovers failures after the fetch (engine instantiation, `initialize`) and hosts that version chunk URLs; a host that needs recovery from a failed first fetch uses versioned chunk URLs or a `vite:preloadError` reload at app level. The panel with the source is always shown, never a blank.

2. **KaTeX: a renderer slot, filled eagerly by Plannotator.** `utils/math` holds a synchronous slot (`getMathRenderer`, `setMathRenderer`, `subscribeMathRenderer`), an idempotent `loadMathRenderer()` whose default loader is `import('katex')` (JS only; stylesheet policy is unchanged, see "Math rendering"), and `setMathRendererLoader`. `MathBlock` and inline math read the slot during render: filled, they typeset synchronously in the same render exactly as before; empty, they render the same wrapper (`math-block` / `math-inline`, `math-annotatable`, `data-math-tex`, `data-math-display`, `aria-label`, `data-block-id`) with the trimmed TeX as a text child, load the renderer from an effect, and re-render typeset when it lands. Annotation restore and block targeting key on those attributes, so a placeholder is addressable exactly like the typeset node. `throwOnError: false` and `trust: false` are applied to every renderer, including one you register.

**This is the one place the pass-nothing law bends.** A host that renders `Viewer` and never imports the eager entry now gets lazy math: one frame of TeX text, then typeset. The one-line opt-back for the old behavior:

```ts
import '@plannotator/ui/utils/math-eager';
```

The seam for the lazy path: `configurePlannotatorUI({ mathRendererLoader: () => Promise.all([import('katex'), import('katex/dist/katex.min.css')]).then(([m]) => m.default) })` puts KaTeX and its CSS on one chunk; `loadMathRenderer()` can be awaited before mounting a body that carries math if you would rather gate first paint yourself.

3. **Identity: a generator slot, filled eagerly by Plannotator.** `utils/generateIdentity` no longer imports `unique-username-generator`. It holds a synchronous generator slot (`setIdentityGenerator`, `getIdentityGenerator`) with a built-in fallback that produces the same `adjective-noun-tater` shape from a 16 x 16 pool. `utils/identity-tater` registers the full dictionary as a side effect and is what Plannotator's entries import. A host with `identityProvider` never calls the generator and, with the static import gone, no longer ships the word lists; delete any dictionary shim. A host that wants the full dictionary without its own provider imports `@plannotator/ui/utils/identity-tater`, or passes its own `identityGenerator` to `configurePlannotatorUI`. The slot is synchronous on purpose: `configStore` persists the first generated name to the identity cookie during the first render-time settings read, so a name that arrived later would be a visible identity change.

4. **What did not ship (deliberately).** The raw-HTML bridge script as a separately served asset and a lazy table popout are not in this release; both are tracked in the design record for a follow-up.

Pinned by `utils/math.test.ts`, `components/MathBlock.firstPaint.test.tsx`, `utils/generateIdentity.test.ts`, `components/MermaidBlock.test.ts`, and the eager-entry and built-HTML marker guards in `tests/entry-assets.test.ts`.

---

## Frozen markdown diff (0.28.0)

One additive component for the Workspaces versions/approvals surface: `components/MarkdownDiff`, a theme-bridging shim over `@plannotator/markdown-editor@0.4.0`'s `MarkdownDiff` — a **frozen two-revision markdown comparison**. The newer revision renders as the real document (uncollapsed, full length); deletions are projected struck-through at their original positions; changed spans get character/word emphasis; a toolbar shows the change count with prev/next navigation; a clickable, keyboard-accessible overview rail and a changed-line gutter complete the review chrome. Every 0.27.0 surface is unchanged.
Expand Down
12 changes: 12 additions & 0 deletions packages/ui/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ configurePlannotatorUI({
skillContentTransport, // human-only skill contents for feedback injection
serverSync,
webmcp, // browser-agent (WebMCP) provider policy: { enabled, namePrefix }
mathRendererLoader, // how KaTeX loads when no renderer is registered before first math render
identityGenerator, // sync generator behind the default "tater" name (no identityProvider)
});
```

Expand All @@ -48,6 +50,16 @@ The sidebar/panel resize handle exposes seams for hosts that want different edge

Building your own tooltip and removing the built-in double-click reset are host-side concerns (override `onDoubleClick` where you render the handle).

### Lazy renderers and the eager entries (`utils/math`, `utils/generateIdentity`, `utils/mermaid`)

The Mermaid runtime, the Graphviz engine, KaTeX and the username dictionary are off the static import graph of `Viewer`, so a host that bundles by route does not download them for a plain markdown read. Graphviz needs nothing from you (the block imports the engine inside its render effect and shows the source fence until the SVG lands, as it always did). Mermaid, KaTeX and the dictionary sit behind synchronous slots:

- **Math.** Without registration, a math node renders its TeX as text in the same wrapper (same `data-math-tex` / `data-math-display` / `aria-label` / class names), loads KaTeX via `import('katex')`, and re-renders typeset. To keep math typeset on the very first commit, as Plannotator does, add one line to your entry: `import "@plannotator/ui/utils/math-eager";`. To put KaTeX and its stylesheet on one lazy chunk instead, pass `mathRendererLoader`. The stylesheet remains your job either way (see "Consuming it", step 3).
- **Mermaid.** Without registration, the first diagram on a page fetches the runtime through `import('mermaid')`; a failed import is dropped from the memo, re-attempted once after a short delay, and the error panel (with the source) offers Retry, which issues another fresh attempt. Plannotator keeps Mermaid eager by policy so it can never fail separately from the app: `import "@plannotator/ui/utils/mermaid-eager";` in your entry does the same for your bundle. Honest limit of any in-page retry: a browser records a failed module fetch in its module map for the page lifetime, so a fresh `import()` of the same chunk URL rejects without a request; the retry recovers failures after the fetch (engine instantiation, initialize) and hosts that version chunk URLs. A host that needs recovery from a failed first fetch uses versioned chunk URLs or a `vite:preloadError` reload at app level.
- **Identity.** With an `identityProvider` the generator is never called and the word lists stay out of your bundle. Without one, default names come from a small built-in pool of the same `adjective-noun-tater` shape; `import "@plannotator/ui/utils/identity-tater";` registers the full dictionary, or pass your own `identityGenerator`.

Plannotator's own entries import the eager modules (`math-eager` and `identity-tater` in both `packages/editor/App.tsx` and `packages/review-editor/App.tsx`; `mermaid-eager` in the plan editor only, since the review editor never renders a Mermaid block), which is what keeps its single-file builds byte-identical and its portal entry chunk shaped as before; `tests/entry-assets.test.ts` fails if any of them is dropped. See HANDOFF.md "Lazy renderers and eager entries".

### Markdown editor extensions + wiki links (`MarkdownEditor` / `InlineMarkdown`)

- **`MarkdownEditor` takes CM6 extensions.** `extensions?: readonly Extension[]` (from `@codemirror/state`) is forwarded verbatim into the underlying editor — the seam for `wikiLinks(config)`, `y-codemirror.next` collab bindings, custom keymaps.
Expand Down
Loading