mirror of
https://github.com/backnotprop/plannotator.git
synced 2026-09-14 14:17:26 +08:00
69ca6d546cbb790be5fea6fc5ce47da7cd218e9e
154 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
69ca6d546c |
chore: bump version to 0.23.0
Claude-Session: https://claude.ai/code/session_01SFy9fY27SA8g5BtotWPi1G |
||
|
|
070d9a5f6d |
Make the document UI reusable as published building blocks (#957)
* docs(adr): revert failed document-ui cutover, add ADR 004 with corrected reuse plan The document-ui extraction/cutover (ADRs 002/003) was an AI-driven rewrite that broke the app; the code was reverted. Add ADR 004 as the source of truth: share @plannotator/ui as published building blocks for the Workspaces app, keep Plannotator's app unchanged, gate on human-verified parity. Banner the reverted ADRs and point AGENTS.md/CLAUDE.md at 004 so future agents don't rebuild the mess. * docs(adr): add verified document-ui extraction plan, supersede draft inventory 36-agent verification of the reuse inventory: confirmed the /api coupling but found the draft missed Viewer's transitive backend call, the cookie settings layer, 3 React contexts + identity singleton, SSE transports, and harder packaging blockers. Adds the verified per-subsystem extraction plan with a parity guardrail on every step; flags the draft inventory as superseded. * docs(adr): add document-ui extraction roadmap + parity checklist Phase 0-7 execution roadmap (safety net -> packaging -> foundation seams -> rendering -> navigation -> comments -> extras -> publish) and the reusable 'did it break?' parity checklist run after every step. Both enforce the law: move + decouple, never rewrite; Plannotator's experience cannot change. * build(ui): packaging unblock for external install (Phase 1) — no runtime change Phase 0: captured parity baseline (typecheck/test/build + shipped-bundle hashes). Phase 1 packaging fixes to packages/ui, metadata only: - add phantom dompurify ^3.3.3 dep (imported in sanitizeHtml/aiChatFormat, was undeclared) - align diff ^8.0.3 -> ^8.0.4 with root - add peerDependencies (react, react-dom, tailwindcss, tailwindcss-animate); keep as devDeps - add files allowlist (excludes tests); remove dead tsconfig @plannotator/shared alias Verified byte-identical: typecheck pass, 1620 tests pass/0 fail, all 3 builds OK, shipped plan+review bundle hashes unchanged from baseline. Remaining Phase 1 blocker (@plannotator/ai + @plannotator/shared workspace:* deps) deferred pending a publish-vs-inline decision; logged in worklog. * feat(ui): make image URL resolution host-overridable (Phase 2, seam 1) getImageSrc now delegates to a module-level resolver defaulting to the verbatim Plannotator /api/image logic; add setImageSrcResolver/resetImageSrcResolver so a host (Workspaces) can resolve images via its own backend. All 5 consumers and the signature unchanged. Verified: default URLs byte-identical, typecheck pass, 1620 tests pass/0 fail, builds OK. No Plannotator behavior change. * feat(ui): make settings storage backend host-overridable (Phase 2, seam 2) storage.ts cookie impl is now the default 'cookieBackend'; add setStorageBackend/ resetStorageBackend so a host (Workspaces) can persist settings via its own storage. getItem/setItem/removeItem delegate to the active backend; the ~24 consumers and literal plannotator-* keys are unchanged. Verified: swap works, typecheck pass, 1620 tests pass/0 fail, builds OK, theme persists across reload. * feat(ui): make MarkdownEditor theme mode host-supplyable (Phase 3) Add optional mode? prop; mode now mode ?? resolvedMode. Plannotator passes no mode (App.tsx:4261) so it keeps using ThemeProvider's resolvedMode unchanged. A host without ThemeProvider can supply mode directly. Verified: typecheck pass, 1620 tests/0 fail, builds OK, App.tsx untouched. * feat(ui): allow hosts to opt out of code-path validation (Phase 3) Viewer gains optional disableCodePathValidation? threaded to a new disabled? arg on useValidatedCodePaths; when set, the /api/doc/exists probe is skipped. Default undefined for Plannotator => validation stays on, /api/doc/exists fires exactly as today. Verified: typecheck pass, 1620 tests/0 fail, builds OK, App.tsx untouched. Also logs Phase 3 workflow outcome + remaining scroll/docfetch pieces. * feat(ui): make code-file hover preview fetch host-overridable (Phase 3) Add DocPreviewFetcher seam (default = verbatim /api/doc fetch) + setDocPreviewFetcher/resetDocPreviewFetcher; route handleMouseEnter through it, useCallback deps unchanged. No caller overrides it => Plannotator fetches /api/doc identically. typecheck pass, 1620 tests/0 fail, builds OK. * feat(ui): ship ScrollViewportProvider with the library (Phase 3 scroll) Add render-transparent ScrollViewportProvider (createElement, keeps .ts) so the scroll-viewport context travels with @plannotator/ui instead of living only in App.tsx. Rewire App.tsx provider tags (3-line delta); identical tree/value/ position, sidebar TOC still reads the MAIN viewport. Fix stale OverlayScrollbars doc-comment. typecheck pass, 1620 tests/0 fail, builds OK, eyeball: TOC tracks. * fix(ui): disabled code-path validation should keep links clickable (self-review) The Phase-3 disabled branch set ready=true with an empty map, which makes gateCodePath demote every code link to plain text. Leave ready=false so the no-validation fallback renders links optimistically. No Plannotator impact (never disables). Logs Phase 3 completion + reusability note. typecheck pass, 1620 tests/0 fail, builds OK. * feat(ui): make file-tree backend host-overridable (Phase 4) Lift useFileBrowser's three backend wires (load-dir fetch, obsidian-vault fetch, and the SSE live-watch effect moved VERBATIM) into an injectable FileTreeBackend with default + setFileTreeBackend/resetFileTreeBackend, same pattern as the image /storage seams. useFileBrowser() stays zero-arg; default fetch/SSE URLs identical. Sidebar confirmed noop (zero backend wires, already reused by review-editor). Verified: useFileBrowser.test.tsx passes 6/0 UNMODIFIED (DOM_TESTS=1), typecheck pass, 1620 tests/0 fail, builds OK, App.tsx untouched, manual eyeball (annotate adr/: tree loads, file-switch works, new file appears live via SSE). Plannotator byte-unchanged. Logs two pre-existing bugs found during testing (not regressions). * docs(adr): research + synthesis + spec for Phase 5 (comments/annotations/drafts) Five-probe code research of the comment system. Key finding: most comment UI is already portable (panel/popover/toolbar/highlighter prop-driven; review-editor already reuses the hooks). Phase 5 narrows to 3 seams — draft transport (+ the 3-party generation protocol), external-annotation transport (SSE->polling, move verbatim), and identity/authorship — plus 2 non-extraction items: renderer coupling (document as a contract) and replies/threading (defer as a new feature). * docs(adr): accept ADR 005 — make comments/annotations/drafts host-overridable (Phase 5) Three seams (identity, draft transport, external-annotation transport), each defaulting to today's behavior; renderer coupling documented as a contract; replies/threading deferred as a new feature. Locks in the recommended choices from the Phase 5 spec/synthesis. * feat(ui): make annotation identity host-overridable (Phase 5 seam 1) Add IdentityProvider + setIdentityProvider/resetIdentityProvider in identity.ts; getIdentity/isCurrentUser now delegate to a module-level provider defaulting to today's ConfigStore tater behavior. The ~9 author-stamp sites and 2 (me)-badge sites delegate with zero call-site edits. No caller overrides => Plannotator byte-unchanged. typecheck pass, 1620 tests/0 fail, builds OK. * feat(ui): make draft persistence transport host-overridable (Phase 5 seam 2) Add DraftTransport (load/save/remove) + getDraftTransport/setDraftTransport/ resetDraftTransport in useAnnotationDraft.ts, default = today's /api/draft fetches verbatim. useCodeAnnotationDraft reads getDraftTransport() live. The generation pre-increment, 500ms debounce, keepalive retry-gate, and pagehide/visibilitychange flush stay in the hooks; getDraftGeneration() still escapes to the host. save rejects-on-failure so the gated retry is preserved. No caller overrides => Plannotator byte-unchanged. shared/draft.test.ts 10/0, annotationDraftPersistence 13/0, typecheck pass, 1620 tests/0 fail, builds OK. * feat(ui): make external-annotation transport host-overridable (Phase 5 seam 3) Add ExternalAnnotationTransport<T> (subscribe/getSnapshot/CRUD) + setters in useExternalAnnotations.ts; default = today's SSE->polling wire moved verbatim into createDefaultTransport. The reducer (applyEvent), fallback-once gate, 500ms poll, versionRef scoping, optimistic-before-await, and [enabled] gate stay in the hook. A host (Workspaces) can implement the same event contract over Durable Objects. No override caller => Plannotator byte-unchanged. external-annotations test green, typecheck pass, 1620 tests/0 fail, builds OK. Logs Phase 5 completion. * docs(adr): research + synthesis + spec for Phase 6 (versions, settings, sharing, AI) Five-probe code research. Most of the four subsystems is already portable; the real work is 5 seams (version fetchers + vscode-diff, config write-back, obsidian detect, save-to-notes, AI transport) + 1 CSS move (block/raw diff classes from the app shell into the package's theme.css). Fragile do-not-touch: the AI SSE reader loop + epoch guards, and configStore debounce/deepMerge. Five Plannotator-only pieces (OpenInApp, HooksTab, useUpdateCheck, useAgents/useAgentJobs) stay home. * docs(adr): accept ADR 006 — make extras (versions/settings/sharing/AI) host-overridable (Phase 6) Five seams + one CSS move, each defaulting to today's behavior. AI reader loop + epoch guards and configStore debounce/deepMerge stay verbatim. Five Plannotator- only pieces stay home. Locks the recommended choices from the Phase 6 spec. * feat(ui): make version fetchers + vscode-diff host-overridable; move diff CSS into package (Phase 6 versions) usePlanDiff gains optional fetchers (default /api/plan/version(s), error asymmetry kept: selectBaseVersion alerts, fetchVersions silent). PlanDiffViewer gains optional onOpenVscodeDiff (default /api/plan/vscode-diff). Relocate .annotation-highlight* + .plan-diff-* block/raw CSS from editor/index.css into ui/theme.css (next to .plan-diff-word-*) so the diff/highlights are self-styling from the package. Verified: relocated CSS gone from index.css, present in shipped bundle (33x), diff renders identical; typecheck pass, 1620 tests/0 fail, builds OK, App.tsx untouched. * feat(ui): make config write-back + obsidian-detect host-overridable (Phase 6 settings) configStore.setServerSync(fn) injects only the terminal POST /api/config; the 300ms debounce, deepMerge batching, singleton, and eager cookie reads stay verbatim. Settings gains optional onDetectObsidianVaults (default /api/obsidian/vaults), with the [obsidian.enabled] effect dep + auto-select-first-vault verbatim. No override caller => Plannotator unchanged. typecheck pass, 1620 tests/0 fail, builds OK. * feat(ui): make save-to-notes host-overridable (Phase 6 sharing) ExportModal gains optional onSaveToNotes (default = verbatim POST /api/save-notes); showNotesTab = isApiMode && !!markdown kept byte-for-byte. Sharing utils already parameterized (noop). No override caller => Plannotator unchanged. typecheck pass, 1620 tests/0 fail, builds OK. * feat(ui): make Ask AI transport host-overridable (Phase 6 ai) useAIChat gains a module-level AITransport (session/query/abort/permission) + setAITransport/resetAITransport, default = the five /api/ai/* fetches verbatim. The SSE reader loop, epoch/createRequest guards, and the supersede-abort position inside createSession stay untouched. Capabilities + provider-resolution stay host-owned in App.tsx. No override caller => Plannotator unchanged. ai.test.ts 97/0, typecheck pass, 1620 tests/0 fail, builds OK. * docs(adr): log Phase 6 completion (4 seams + diff CSS move) * docs(adr): research + synthesis + spec for Phase 7 (carve @plannotator/core + publish) Carve a browser-safe @plannotator/core: move the ~15 pure shared modules in, extract types from the 3-4 node-bound ones (config/storage/workspace-status) so nothing duplicates, shim @plannotator/shared so Plannotator's 99 import sites stay unchanged, re-point @plannotator/ui to depend only on core, move wideMode.ts, then publish core+ui (source-only). shared + ai stay private. Open: registry, versions, CI job. Publish is the one outward-facing step — confirm before pushing. * docs(adr): fold configurePlannotatorUI() front door + precompiled CSS into Phase 7 spec Add the single typed configure() facade over the 9 global host-override setters (zero-risk, additive) and an optional precompiled CSS bundle (smooths the Tailwind-in-shared-lib wrinkle) to the Phase 7 publish scope. Both make the published surface nicer to consume; neither touches Plannotator. * docs(adr): lock Phase 7 publish decisions + carry over review fixes Decided: ship JS as source (single internal consumer on controlled stack, no build to maintain, no dist drift); precompiled CSS now REQUIRED (the @source glob is fragile under pnpm symlinks); core CI typecheck node-free; pin ui->core exact. Recorded the interrogation's carried-over Phase-5 code fixes (useExternalAnnotations split-transport + fallbackRef reset, per-seam override tests, configStore loadFromBackend) to do before publish. * docs(adr): ADR 007 — carve @plannotator/core, complete settings provider, publish Locks Phase 7 decisions: public npm; lockstep version at repo 0.21.0 (ui->core pinned exact); JS ships as source + required precompiled CSS; core CI node-free; ai stays unpublished-to-npm. Settings provider completed (loadFromBackend, prefetch +sync) is now IN SCOPE — Workspaces uses the same UI settings stored in its own backend. CI publish job wired but artifacts validated on-branch (pack + dry-run) before merge; first publish gated. Carries the 2 override-path bug fixes + per-seam override tests as pre-publish work. * fix(ui): make external-annotation transport reads consistent + reset fallback on re-enable Two override-path bugs found by the interrogation pass (both unreachable on Plannotator's path; harden the host-override path for a real consumer): 1. Split-transport: the effect captured the transport at mount for subscribe/poll while the CRUD callbacks read the module global live, so a host swapping the transport after mount would split reads and writes across two backends. Capture once in a ref and use it in all four spots. 2. fallbackRef/receivedSnapshotRef were not reset on effect re-run, so an enabled false->true toggle inherited a stale 'already fell back' flag and silently stopped updating. Reset both at the top of the effect. Plannotator unchanged: it never overrides the transport (same default singleton captured) and enabled never toggles (reset is a no-op). typecheck clean; full test suite shows zero delta (1605 pass / 45 pre-existing env failures, identical with and without this change). * docs(adr): align Phase 7 spec with ADR 007 (version 0.21.0 lockstep, CSS required, scope completeness) * feat(core): carve @plannotator/core — move pure modules, extract node-bound types, shim shared (Phase 7 step 1) * feat(ui): depend only on @plannotator/core — re-point all shared/ai imports (Phase 7 step 2) * refactor(ui): relocate wideMode helper to @plannotator/ui/utils (Phase 7 step 3) * feat(ui): add loadFromBackend settings rehydration + configurePlannotatorUI front door (Phase 7 step 4) * build(ui): precompiled styles.css CSS build + madge circular-dep check (Phase 7 step 5) * test(ui): per-seam override tests + configure routing test (Phase 7 step 6) Add one override test per seam (setX(fake)→drive→assert→resetX()) for all 9 seams + loadFromBackend, modeled after the existing seam test pattern. Fix configure.test.ts to defer mock.module() into beforeAll and restore with captured real function references in afterAll so sibling seam test files are not poisoned by spy replacements in the shared Bun worker module registry. * fix(ui): apply Phase 7 review findings — version lockstep + seam consistency - Bump @plannotator/ui to 0.21.0 (lockstep with @plannotator/core + repo, per ADR 007) [was the 1 critical review finding] - useAnnotationDraft: route persistNow/dismissDraft save+remove through getDraftTransport() so all paths read the transport consistently (matches the load path; makes the single-global invariant explicit) - configStore.loadFromBackend: document it must be called BEFORE init() or server values get overwritten - packages/core/tsconfig: add explicit types:[] so the node-free invariant is first-class (verified: planted node:fs still fails TS2882) * docs(adr): Phase 7 implementation plan (workflow-generated, durable artifact) * fix(ui): reconcile #948 with the draft-transport seam + lockstep 0.21.1 Rebased onto origin/main (picks up #948 draft-deletion fix, the 0.21.1 bump, and the #949/#950 editor fix). The rebase auto-merged #948's code-draft logic (hasHadAnnotationsRef, empty-state tombstone, clearTimeout in restore/dismiss) with the Phase-5 transport refactor cleanly — except the empty-state tombstone delete was left as a raw fetch('/api/draft', DELETE). Route it through getDraftTransport().remove() so a host backend tombstones its own stored draft on clear (the #948 guarantee, for hosts). Plannotator unchanged (default transport hits the same endpoint). Bump @plannotator/core + @plannotator/ui 0.21.0 -> 0.21.1 to match main's version (lockstep per ADR 007). Verified: typecheck clean, madge no-cycles, plain suite 1637 pass / 0 fail, #948 draft-clear test 3/0. (The 45 DOM_TESTS failures are the known server/network integration tests that need a real OS env — same set on main, not regressions.) * fix(ui): address review nits — host-path robustness + cleanups - PlanDiffViewer: wrap onOpenVscodeDiff in try/finally so a host opener that throws can't wedge the VS Code button in a permanent loading state (default unaffected) - useExternalAnnotations: (re-)capture the transport inside the effect on enable so a host that installs a transport before enabling annotations is honored, not the stale default — keeps the split-transport fix (effect + CRUD share one ref) - configure.ts: import ServerSyncFn from configStore instead of duplicating the type - repoint the 2 remaining @plannotator/shared test imports to @plannotator/core - AGENTS.md/CLAUDE.md: document the new packages/core package All host-path only — Plannotator behavior unchanged. typecheck clean, no cycles, full suite green. Skipped (not simple/over-engineering): usePlanDiff prop->module-level (design change), Obsidian late-bind, getSnapshot guard (inert), transport <any> (variance). * docs: collapse 29 ADR process docs into one packages/ui/README.md The branch had accumulated ~6,200 lines of ADR scaffolding (6 decisions, 7 specs, 10 research spikes/synthesis, 6 worklogs/roadmaps/plans) for this one effort. Replace all of it with a single concise README that ships with the published package: what @plannotator/ui + @plannotator/core are, why they exist (commercial reuse), how the host-override seams work (configurePlannotatorUI), how a consumer installs/builds, and the one rule (don't reimplement from scratch — add a seam). Repoint the CLAUDE.md banner at the README. No code references the deleted docs; main's pre-existing adr/ docs untouched. * docs(ui): add packages/ui/AGENTS.md guardrail + CLAUDE.md symlink Directory-scoped agent guidance for anyone editing @plannotator/ui: don't rewrite from scratch, add a seam (default = today's behavior, Plannotator byte-for-byte unchanged), core stays node-free, never delete working code until human parity. Points to README.md for the architecture. CLAUDE.md -> AGENTS.md symlink mirrors the repo root convention. * build: remove madge circular-dep check (unmaintained) madge is unmaintained (~3 years stale) and the check was never wired into CI, so it was a dormant script + devDependency on a load-bearing path. Drop it: remove the check:cycles script, the madge devDependency, and .madgerc. The no-cycle invariant still holds by construction — @plannotator/core imports nothing (zero @plannotator deps in its package.json), so any accidental core->shared/ui import fails at publish-time bun pm pack (and review). No automated tripwire, but no stale unmaintained tooling either. * fix(ui): address review — TDZ guard, html-viewer export, doc corrections - useExternalAnnotations: declare unsubscribe as let (not const) + guard calls, so a host transport that fires onError synchronously during subscribe falls back to polling instead of throwing a TDZ ReferenceError (Plannotator's EventSource fires async, never hit) - package.json: add explicit ./components/html-viewer export (dir has index.ts; the ./components/* -> *.tsx wildcard can't resolve it, so external installers would fail) - README: fix configurePlannotatorUI sample keys to the real option names (storageBackend/identityProvider/imageSrcResolver/externalAnnotationTransport) - AGENTS.md: point the Ask-AI mapping at packages/core/agents.ts (shared/agents.ts is a shim now) All publish/host-path/doc only — Plannotator unchanged. (#1 CSS-build font collision deferred to publish-prep — it needs the asset pipeline + files allowlist, not a one-liner.) * build(ui): don't bundle fonts in published styles.css — app loads fonts (review #1) Industry standard for a shared UI package: ship theme + component CSS, let the consuming app load fonts. Drop the @fontsource imports from styles-entry.css (the publish CSS entry); the theme still defines --font-sans/--font-mono, and the app provides those families. Fixes the asset-name collision (every emitted .woff2 was renamed styles.css) and shrinks the published stylesheet 555kB -> 185kB. README documents the two-line @fontsource install. Plannotator unaffected: its apps (editor/review-editor index.css) load fonts via their own entry CSS — styles-entry.css is consumed ONLY by the publish CSS build. * fix(ui): build styles.css on prepack, not prepublishOnly (review #4) prepublishOnly doesn't run for npm pack / bun pm pack / git / file: installs, so the package exported ./styles.css without shipping it. prepack runs on any pack, so the stylesheet is always present. Verified: bun pm pack now emits styles.css. * chore(ui): post-rebase reconciliation — version lockstep 0.21.3, awaitable AI abort seam Rebased onto main (0.21.3). Bump @plannotator/core + @plannotator/ui to 0.21.3 to stay in lockstep with the repo version. Resolve the useAIChat conflict: main added postServerAbort (an awaitable abort that prevents session-busy races) using a raw fetch. Route it through the AITransport seam by making AITransport.abort return Promise<unknown> instead of void, so the host override is honored AND main's await-the-abort behavior is preserved. Update the abort mocks in the seam/configure tests accordingly. * fix(ui): make postServerAbort never reject regardless of AI transport The await site in ask() relies on postServerAbort resolving so a superseding query can proceed. main's original guaranteed this with its own .catch on the fetch; routing through the AITransport seam delegated that guarantee to the transport. Restore it at the call site (Promise.resolve(...).catch) so a host override that rejects — or returns void at runtime — can't throw out of ask(). * fix(ui): address review — core import, abort sync-throw, snapshot guards - useAIProviderConfig: import Origin from @plannotator/core/agents (was the only ui file still importing @plannotator/shared); drop the masking shared/* path alias from ui/tsconfig.json so a stray shared import now fails typecheck. The hook is part of the published surface — a standalone install had no @plannotator/shared to resolve. - useAIChat.postServerAbort: defer the transport call into .then so a host abort that throws *synchronously* also can't reject (the .catch only caught async). - useExternalAnnotations: default getSnapshot returns null (skip) on a malformed 200 instead of coercing to []/0, so it can't clear annotations or reset the version cursor — restoring the pre-seam behavior. * feat(ui): add upload + identity-editable seams for host backends Two override points the Workspaces app needs that had no seam: - UploadTransport (utils/upload.ts): image attachments hardcoded POST /api/upload with no override. Add a setX/resetX/getX seam (default = today's /api/upload, verbatim) and route AttachmentsButton through it. Workspaces sends bytes to its R2 asset API and returns the content-addressed URL. - IdentityProvider.isEditable() (utils/identity.ts): the Settings rename/regenerate controls wrote to the cookie store, bypassing a host identity provider — so a host with server-owned identity could split one user across two author names. Add an optional isEditable() (default true) and hide the rename controls when a host returns false. Plannotator's cookie identity stays editable — unchanged. Both wired into configurePlannotatorUI(); seam tests added; configure routing test covers uploadTransport. HANDOFF.md updated with the Workspaces seam mapping from the repo research (asset layer, identity, realtime, no-AI-infra, the Me display-name backend follow-up). README publish command corrected to bun pm pack + npm publish. * refactor(ui): capture sessionId synchronously in postServerAbort Self-review: the deferred .then read sessionIdRef.current a microtask after the guard checked it. Capture the id synchronously so the abort always targets the session current at call time and there's no double-read. * fix(ui): address review — seed host store, browser-safe timer type, harden abort - configStore.loadFromBackend: seed the host StorageBackend with resolved defaults for keys it lacks. The constructor runs at module load (before a host installs its backend), so its default-seeding writes went to the cookie backend; without this a fresh host store was never populated and generated defaults (e.g. displayName) regenerated every reload. [P1, host path] - Viewer.tsx: replace NodeJS.Timeout with ReturnType<typeof setTimeout> (2 refs) so a browser-only consumer compiling the published source doesn't need @types/node. Matches the pattern already used in configStore. [P1, published path] - useAIChat: harden the create-session supersede abort the same way as postServerAbort, so a host transport that throws can't surface an unhandled rejection. No impact on Plannotator (default self-catches). [nit] - .gitignore: correct stale 'prepublishOnly' comment to 'prepack'. [nit] Plannotator behavior unchanged (it never calls loadFromBackend; the timer/abort changes are behavior-preserving). Strengthened configStore seam test to assert first-run seeding. typecheck clean, 1773 pass / 0 fail. * refactor(ui): single-source the never-reject abort via safeAbort helper Self-review: the hardened abort pattern (defer into .then + .catch so a host transport that throws can't reject) was duplicated across postServerAbort and the create-session supersede site — the exact drift the review flagged. Extract a module-level safeAbort(sessionId) so both call sites share one hardened implementation and can't diverge again. Behavior unchanged; reads aiTransport at call time so a late override is honored. * chore(ui): post-rebase version lockstep to 0.21.4 Rebased onto main (0.21.4, adds markdown math #878 + parser hardening). Bump @plannotator/core + @plannotator/ui to 0.21.4 to stay in lockstep with the repo. katex (main's math dep) merged into ui; typecheck clean, 1810 pass / 0 fail. * docs(ui): consumer-lens handoff hardening + ADR 005 - HANDOFF.md: add supported-imports allowlist vs unsupported (hardcoded /api/*) list; document the annotation anchor schema, reattachment order, and untested stale-anchor degradation; state that the markdown editor cannot take CM6/Yjs extensions yet and the plan of record; note AI avoidability re-verified post-rebase; fix stale 0.21.3 ref. - adr/decisions/005: record the publish-as-packages decision (packages over copy/vendor, core/ui split, seam-singleton pattern + SSR revisit condition, the law, lockstep publish model). * fix(ui): make shipped source strict-TS clean for consumers + seam type barrel Consumers compile the published TS source with their own compiler options, and strict mode failed with 35 errors inside the package: - settings.ts: satisfies SettingDef<unknown> is contravariantly illegal under strictFunctionTypes (33 errors) — use SettingDef<any> - useDismissOnOutsideAndEscape: RefObject<HTMLElement> rejects React 19's useRef<T>(null) refs — widen to HTMLElement | null - globals.d.ts: declare *.png / *.webp modules, referenced from each asset-importing component so any consumer program that includes one gets the ambient declarations Also unscatter the seam contract types: configure.ts re-exports every seam type next to configurePlannotatorUI, and ServerSyncFn is now exported from config/index.ts (it was unreachable through the exports map). Verified: standalone Vite consumer importing the full supported surface passes tsc --noEmit under full strict (was 35 errors). * fix(ui): keep KaTeX fonts out of published styles.css (back to ~187KB, was 1.6MB) Main's math PR imports katex/dist/katex.min.css in theme.css; the publish build (Vite lib mode) force-inlines all 60 KaTeX math fonts as data URIs, ballooning styles.css to 1.6MB (977KB gzip) and breaking the package's consumer-owns-fonts policy. Alias the katex stylesheet to an empty stub in vite.css.config.ts only — theme.css stays untouched (no rebase surface) and Plannotator's own apps, which import theme.css directly, still bundle KaTeX as before. Hosts that render math load katex.min.css themselves (bundler import, CDN tag, or self-hosted copy per HANDOFF.md), which also gets them lazy font loading. Verified: fresh build is 186.9KB / 30.8KB gzip with zero @font-face data URIs; consumer vite build CSS drops 1.66MB -> 200KB. * docs(ui): HANDOFF corrections from adversarial consumer review - Math rendering section: KaTeX css/fonts excluded from styles.css by design; three one-time host setup options (self-hosted recommended, CDN tag, bundler import) - styles.css size claim corrected (~187KB / ~31KB gzip) + strict-TS guarantee documented (verified against a standalone consumer) - AI-avoidability claim made precise: configure.ts statically imports useAIChat for its setter; unused AI code tree-shakes to zero (bundle- verified) — the runtime claim holds, the static wording was wrong - Loud warning on the loadSettingsFromBackend ordering footgun: configuring before hydration seeds generated defaults into the host backend and nothing re-runs hydration - DraftTransport.load() tombstone-generation contract spelled out - Seam-type barrel documented on the configure row; 'everything is importable' softened (some components/*.ts don't resolve via the *.tsx wildcard); stale diff stats refreshed * docs(ui): math setup pointer in README + pnpm caveat on the katex bundler-import option * fix(ui): lazy settings resolution — zero cookies on a configured host The configStore resolved all settings eagerly in its constructor, at module import — before a host's configurePlannotatorUI() could install its StorageBackend — writing 17 plannotator-* cookies (including a generated identity) onto the host origin. Resolution now runs lazily on first settings access (get/set/init/loadFromBackend): by then the host backend is live, so the initial reads AND default-seeding writes route through it. A configured host gets zero cookies, ever. Plannotator unchanged: same resolution, same cookie seeding, same values — on first settings read (same page load) instead of at import. New configStore.lazyInit.seam.test.ts proves the contract from a fresh module graph; full suite + consumer strict tsc green. * chore(ui): post-rebase version lockstep to 0.22.0 * fix(ui): round-2 review batch — dedupe asset declarations, CI seam tests, strict consumer gate, doc corrections - components/types.d.ts: drop the *.png/*.webp declarations that globals.d.ts now owns — both shipping was a duplicate-identifier error for any consumer with skipLibCheck: false - untrack packages/ui/styles.css (generated by prepack, gitignored; got scooped into the carve commit during the rebase by git add -A before the ignore entry existed in the replay) - CI: the DOM test step now runs ALL packages/ui tests, so the seam contract tests (AI/draft/external-annotations/file-tree/inline- markdown) actually execute in CI instead of skipping - new packages/ui/tsconfig.strict-consumer.json wired into root typecheck: type-checks the supported-import surface under full strict, so the consumer strict-TS guarantee can't silently rot - HANDOFF: rot-proofed the diff stat, strict guarantee now cites the CI gate, CDN katex pinned-version wording, theme-vs-styles.css caveats (theme still imports KaTeX + needs Tailwind), Viewer required props, Yjs plan-of-record updated to the atomic-editor fork - README: @source fallback wording (build entry isn't shipped) * test(ui): make the lazy-resolution seam test deterministic The test asserted lazy resolution on the module singleton and relied on its test file getting a fresh module graph — an isolation assumption that doesn't hold under all bun test orderings (CI failed with zero observed reads because another file had already resolved the store). Test the contract on a fresh instance instead: ConfigStore is exported as @internal ConfigStoreForTest, the spy backend is installed before construction, and the test asserts construction reads nothing while the first get() resolves and seeds through the live backend. Deterministic by construction. * test(ui): poll for the debounced reconnect refetch instead of a fixed sleep The reconnect-refresh assertion waited a fixed 150ms against the SSE watcher's 120ms debounce — a 30ms margin that slower CI runners lose, flaking 'refreshes after an SSE ready event from reconnect'. The watched logic is unchanged (verified byte-identical to main's inline version — the seam only relocated it into the default watchTrees and added the onChange indirection). Poll for calls.length===2 up to 1s so the pass/fail is hardware-independent. * test(ui): poll the committed tree state, not the fetch call count Prior fix polled calls.length===2, but the fetch call is counted one tick before its result commits to React state — so the poll exited early and the next assertion (dirs[0].tree === reconnectedTree) lost the race on slow CI (toEqual failure). Poll on the committed tree itself, which is exactly what the assertion checks: now the only way to fail is a genuine no-refresh, not a timing margin. * test(ui): give the reconnect-refetch poll a 10s ceiling + 20s test timeout A CI runner was measured at 6x normal speed (1676ms for a ~275ms test), blowing through the 1.5s poll ceiling before the 120ms debounce fired — same commit passed on a faster runner. Raise the poll to ~10s and set an explicit 20s test timeout (bun's 5s default would otherwise kill the poll). Root cause is load, not logic: this timing-sensitive test only started flaking when the CI DOM step was broadened to run the whole ui suite in one process. * ci: run the file-browser DOM test isolated; scope the DOM step to DOM files Root-causes the intermittent 'refreshes after an SSE ready event from reconnect' failure. The round-2 change ran the ENTIRE ui suite under DOM_TESTS=1 to catch the seam contracts; that load intermittently starved the test's 120ms real-timer debounce so the reconnect refetch never fired (observed failing after a full 10s poll — not a margin issue). The hook logic is byte-identical to main, and main runs this test in its own process (green for months). Fix at the CI layer, not the test: run useFileBrowser.test.tsx isolated (matching main), and run the seam contracts + remaining DOM-gated tests as an explicitly-scoped light batch. The test file is reverted to main verbatim (today's timing-poll experiments dropped). Follow-up issue to file: the underlying re-subscription race the load exposed. |
||
|
|
82f5648ccb | chore: bump version to 0.22.0 | ||
|
|
9bded34324 | chore: bump version to 0.21.4 | ||
|
|
ec8342c34a | chore: bump version to 0.21.3 | ||
|
|
cb6667e991 |
fix(ai): drive Codex Ask AI via codex app-server (#971)
* fix(ai): drive Codex Ask AI via app-server + answer-first review prompts (#971) Codex Ask AI previously ran via @openai/codex-sdk (codex exec), which forces approval_policy=never and breaks in enterprise-managed Codex environments that ban it (#971). Replace the transport with a long-lived 'codex app-server' process over JSON-RPC. - New provider packages/ai/providers/codex-app-server.ts (registered as 'codex-sdk' to preserve cookie/agents.ts/UI-gate); omits approvalPolicy so Codex resolves the user's + managed policy, pins read-only sandbox, and surfaces interactive approvals through the existing PermissionCard. - Delete codex-sdk.ts and drop the @openai/codex-sdk dependency (and its 6 prebuilt platform binaries); gate registration on 'which codex'. - SessionManager: additive, optional dispose?() hook to kill the spawned process on evict/remove — a no-op for Claude/OpenCode/Pi (they don't implement it). Also rework the Ask AI prompts (all providers, separate from the transport): - Every mode now instructs the agent to answer the user's message directly and not launch an unprompted review of the context. - Code review stops pasting the whole diff for git-reproducible diff types and instead tells the agent how to inspect it (git diff <base>..HEAD, three-dot for merge-base); non-git/PR/workspace types still paste. - Claude gains the Bash tool so it can run git (still gated by approvals). - The UI passes diffType/base (session) and what the user is viewing (per question) into the context. Verified: full typecheck, full test suite (101 ai tests), and a live end-to-end smoke against codex app-server. * fix(review): pin AI approval card above the input/model bar Render pending approval cards just above the input + provider/model bar in both the document chat (DocumentAIChatPanel) and code-review AI tab, instead of at the top of the scroll, so the user sees them where they act. * fix(ai): harden Codex abort/cancel + add Ask AI Stop button Addresses code-review findings on the codex app-server provider: - turn/interrupt was sent as a notification (no id) so Codex ignored it and abort never took effect. It's now a proper JSON-RPC request. - Filter turn events/approvals by turnId and reject an aborted turn's stragglers, so a stopped turn can no longer leak output into — or prematurely finish — the next turn (ask-stop-ask race). - Guard listeners by query generation and end the drain loop on the abort signal, so a superseded/stopped turn can't touch the live one and abort returns promptly instead of waiting for turn/completed. - Handle abort during startup: once the turn id is known, interrupt it instead of running it in the background. - Add a sendAndWait timeout so a stalled (alive-but-unresponsive) process errors instead of hanging forever. - Drain stderr (stdio 'ignore') to avoid a pipe-buffer deadlock. Also add a Stop button to both Ask AI surfaces (plan/annotate DocumentAIChatPanel and code-review AITab via ReviewSidebar). It replaces Send while streaming and calls the hook's abort -> /api/ai/abort -> session.abort(); the hook already exposed abort but nothing surfaced it. * feat(ai): drive Codex models + reasoning levels from model/list - Codex provider fetches model/list at startup (throwaway app-server, like Pi/OpenCode) and populates the real models plus each model's actual supportedReasoningEfforts + defaultReasoningEffort. Replaces the hardcoded model list and the static AI_REASONING_EFFORTS (which mislabeled xhigh as 'Max' and omitted minimal). - AIProviderBar + AIConfigBar now show the selected model's real efforts and hide the control when a model reports none. xhigh is shown verbatim. - Fix: the Stop button now also appears in the populated code-review chat state (a prior edit missed the second GeneralInput due to indentation). * fix(ai): scope Claude Ask AI Bash to read-only git; clear stale approval cards on Stop - Claude Ask AI no longer auto-allows bare Bash (which ran arbitrary shell with no Allow/Deny prompt). Replace it with scoped read-only git rules (Bash(git diff:*), show, log, status, rev-parse, merge-base, ls-files). git reads auto-run so the agent can inspect large diffs itself; any other command (write git, arbitrary, or injected compound) falls through to the permission card. Keeps the git-inspect approach (large diffs don't fit in the prompt) while closing the auto-exec hole. - useAIChat.abort() now drops still-undecided permission cards: abort cancels them server-side, so leaving them visible was a dead Allow/Deny. * feat(ai): code-review Ask AI shares the agent-review prompt machine, delivered as user messages Code-review Ask AI built its own diff description (gitInspectInstruction) in the system prompt from just diffType+base, which was wrong for full-stack, hide-whitespace, untracked files, and PR worktrees. Replace it with the same machine the review jobs use, delivered on the user's messages. - Server: buildCurrentAiReviewContext() reuses buildAgentReviewUserMessageForTarget (contextOnly) for the current view and ships it as aiReviewContext in every diff payload (/api/diff + switch/PR/scope). Mirrored in the Pi server. - Client: review-editor latches aiReviewContext onto each question via the pure buildReviewContextPreamble (packages/ui/utils/aiPrompt.ts) and buildDefaultPrompt — full block on the first message / when the view changes (incl. after a provider switch via the !sessionId fresh-session check), a short reminder otherwise (never re-pastes a large diff). - context.ts: delete the duplicate gitInspectInstruction; code-review system prompt is now role-only. Providers untouched (provider-agnostic user message). - Tests: machine scenario gaps (plain PR, full-stack default, PR-worktree origin/<base> + stale-main warning, untracked mention, jj-evolog, workspace lines); composition (first/reminder, command/pasted, preamble ordering). * fix(ai): agentic remote-PR context, UTF-8 stream decode, real Stop on supersede/disconnect Addresses review findings on the Ask AI prompt work: - Remote PR without a confirmed local checkout no longer gets URL-only. The agent is told it's in a PR worktree that's being prepared, to verify the PR files exist before relying on them, and to diff with git diff origin/<base>... HEAD (URL fallback). Inform + trust the agent rather than pasting. Shared machine, so review jobs get the same framing. - Decode Codex stdout with a streaming TextDecoder instead of per-chunk toString(), so multi-byte UTF-8 split across chunks no longer corrupts into U+FFFD (matches the Pi provider). - Stop now actually stops the server turn: ask() awaits /api/ai/abort when a new question supersedes a streaming one (awaiting avoids racing the new query into session_busy), and the /api/ai/query SSE stream gains a cancel handler so tab-close/navigation aborts the turn too. Both reuse the existing per-provider session.abort(); factored a shared postServerAbort helper. * fix(ai): per-model reasoning effort, shared PR-checkout readiness, Stop-then-ask race, type hole - Reasoning effort is now tracked per model (a map keyed by model) instead of one global value, so switching to a model that doesn't support the prior level (e.g. xhigh) no longer posts a stale/unsupported effort that Codex rejects. Each model keeps its own level; nothing leaks across. - Extract resolvePoolCwd into packages/shared/worktree-pool.ts (ready/pending/ absent) and use it from both servers' resolvePRLocalCwd so the readiness rule can't drift. Fix the Pi Ask AI helper to ready-check like Bun, so a warming PR checkout no longer claims 'checked out at PR head' and misdirects the diff. - Stop-then-ask no longer races into session_busy: the abort promise (from Stop or a superseding question) is stashed and the next ask() awaits it before sending. Stop still kills the turn instantly; a follow-up just waits the one abort round-trip. - Declare aiReviewContext on the initial /api/diff response type. * fix(ai): surface real Codex error messages, skip transient retries, auto-deny permission escalations Validated against codex-rs: - Codex's ErrorNotification nests the text at params.error.message (TurnError.message); we read params.message → always 'Unknown error'. Read the nested field (top-level fallback) so auth/usage-limit/stream failures show their real, actionable text. - Skip transient error notifications (willRetry=true): Codex retries on its own and the turn continues, so surfacing them flashed a spurious failure before the real answer. - Handle item/permissions/requestApproval: respond {permissions:{}, scope:'turn'} — byte-for-byte Codex's own cancel response (codex_delegate.rs) — instead of 'Unsupported request'. Ask AI is read-only, so denying the escalation lets the turn continue sandboxed rather than failing. Interactive grant deferred. * refactor(ai): share AI provider/model config in one hook; aggregate paginated model/list - Extract useAIProviderConfig (packages/ui/hooks): one home for provider/model/ reasoning-effort selection — initial state, auto-resolve on capabilities load, per-model effort (no leak across models), and persistence. Both the plan and code-review apps now call it and only compose the session reset (the hook can't own reset without a cycle through useAIChat). Plan editor adapted to the effect-based resolve (adds aiDefaultProvider state). This fixes the plan editor's stale-effort-on-model-switch bug by construction and stops the two apps' copies from drifting again. - fetchModels now follows model/list's nextCursor and aggregates every page into one list, so larger model catalogs aren't silently truncated (with a page guard against a misbehaving cursor). Skipped per discussion: legacy v1 approval methods (we're a v2 client). * feat(agents): per-agent review default; bound Codex model discovery so it can't stall the AI panel - The selected review profile is now tracked per review engine (claude/codex/ cursor/opencode) instead of one flat global value, so each agent keeps its own review default. Public hook API (reviewProfileId/setReviewProfileId) is unchanged — getter derives the current engine's value, setter writes it — so AgentsTab needs no changes. One-shot migration seeds every engine with any existing flat pick. Adds parseReviewProfileByEngine + tests. - fetchModels (Codex model discovery) now uses a short 6s timeout for its initialize + model/list RPCs instead of the 30s default. /api/ai/capabilities awaits model discovery, so an installed-but-unauthenticated codex could otherwise block the AI panel for ~30s; it now falls back to the static model list fast. Authed codex is unaffected (discovery completes in tens of ms). |
||
|
|
3efd9962cd | chore: bump version to 0.21.2 | ||
|
|
caf8bb54bf | chore: bump version to 0.21.1 | ||
|
|
90bc8f8db5 | chore: bump version to 0.21.0 | ||
|
|
5e31b79a01 | chore: bump version to 0.20.3 | ||
|
|
aaa0cab402 | chore: bump version to 0.20.2 | ||
|
|
afac44b940 |
feat(review): migrate all-files code review to Pierre CodeView (#885)
Replaces the all-files renderer with Pierre's virtualized CodeView (one scroller, identical look, scales to huge diffs); deletes the legacy AllFilesDiffView/LazyFileDiff and their flag; single-file panels stay on FileDiff. @pierre/diffs pinned exact 1.2.8 (1.2.9's tree is broken on npm — see #880). Also ships: diff staleness detection ('Diff out of date · Refresh' toolbar notice backed by shared per-VCS fingerprints + GET /api/diff/fresh on both Bun and Pi servers), a stale-content guard so files edited mid-review degrade to raw-patch view instead of breaking virtualization, the Ask AI wrong-file fix (pre-existing), and toolbar/header UI polish (global settings cog, aligned sem badges, no all-files split dragger). Hardened through five review waves; tested with 1384 passing tests incl. real-git fingerprint and patch-consistency suites. Known minor gaps documented in the PR. |
||
|
|
97b5e62a4d | chore: bump version to 0.20.1 | ||
|
|
4e841e2124 |
fix(deps): pin @pierre/diffs to exact 1.1.20
Upstream @pierre/diffs@1.2.x depends on @pierre/theming@0.0.1, which is not published on npm, so any fresh install resolving our ^1.1.12 range floated to 1.2.9 and failed with a 404. Our lockfile masked this for repo builds, but npm consumers of @plannotator/pi-extension resolve fresh and hit it (#880). Pin every declaration to exact 1.1.20, whose dependency tree is fully resolvable (@pierre/theme@0.0.28). |
||
|
|
ad2bb25147 | chore: bump version to 0.20.0 | ||
|
|
b2b8ce8090 |
chore: bump version to 0.19.27
For provenance purposes, this commit was AI assisted. |
||
|
|
d8e0b86f96 |
chore: bump version to 0.19.26
For provenance purposes, this commit was AI assisted. |
||
|
|
3638190844 |
chore: bump version to 0.19.25
For provenance purposes, this commit was AI assisted. |
||
|
|
d479106038 |
chore: bump version to 0.19.24
For provenance purposes, this commit was AI assisted. |
||
|
|
17c808cdca |
chore: bump version to 0.19.23
For provenance purposes, this commit was AI assisted. |
||
|
|
6254a3c181 |
chore: bump version to 0.19.22
For provenance purposes, this commit was AI assisted. |
||
|
|
4db7fcc5da |
chore: bump version to 0.19.21
For provenance purposes, this commit was AI assisted. |
||
|
|
fa4482d7d5 |
chore: bump version to 0.19.20
For provenance purposes, this commit was AI assisted. |
||
|
|
f471ebf99e |
chore: bump version to 0.19.18
For provenance purposes, this commit was AI assisted. |
||
|
|
1ec5243448 |
chore(deps): drop stub @types/dompurify
dompurify ships its own types since v3.x — the @types package is an empty stub. Removes it from both root and packages/ui devDependencies. Credit to @av1155 who identified this in #726. For provenance purposes, this commit was AI assisted. |
||
|
|
9fed785141 |
chore: bump version to 0.19.17
For provenance purposes, this commit was AI assisted. |
||
|
|
12e3e2d35b |
chore: bump version to 0.19.16
For provenance purposes, this commit was AI assisted. |
||
|
|
32e871415f |
chore: bump version to 0.19.15
For provenance purposes, this commit was AI assisted. |
||
|
|
c5b8535173 |
chore: bump version to 0.19.14
For provenance purposes, this commit was AI assisted. |
||
|
|
071083e97d |
chore: bump version to 0.19.13
For provenance purposes, this commit was AI assisted. |
||
|
|
d0bb3badda |
chore: bump version to 0.19.12
For provenance purposes, this commit was AI assisted. |
||
|
|
f4493fc6ff |
Optimize plan editor rendering (#696)
- Replace blocks/frontmatter useState + useEffect with useMemo (eliminates mount cascade) - Migrate toast notifications from React state to Sonner (App no longer re-renders for toasts) - Extract AppHeader component with React.memo (header no longer re-renders on unrelated state changes) - Fix color transition flash on initial load (suppress global * transition until mount settles) - Add Tailwind @source path for new editor components directory |
||
|
|
36948e8572 |
chore: bump version to 0.19.11
For provenance purposes, this commit was AI assisted. |
||
|
|
71492ec880 |
chore: bump version to 0.19.10
For provenance purposes, this commit was AI assisted. |
||
|
|
cc57710df2 |
chore: bump version to 0.19.9
For provenance purposes, this commit was AI assisted. |
||
|
|
ff2759b553 |
chore: bump version to 0.19.8
For provenance purposes, this commit was AI assisted. |
||
|
|
87e92b9d86 |
Add bun link support for local CLI development (#656)
* chore: support bun link for local CLI development * move Local Development section to end of README as Development For provenance purposes, this commit was AI assisted. --------- Co-authored-by: Michael Ramos <mdramos8@gmail.com> |
||
|
|
6fc60a6a84 |
chore: bump version to 0.19.7
For provenance purposes, this commit was AI assisted. |
||
|
|
6324a0c859 |
chore: bump version to 0.19.6
Co-authored-by: Eric Clemmons <15182+ericclemmons@users.noreply.github.com> For provenance purposes, this commit was AI assisted. |
||
|
|
5aaac6bc88 |
chore: bump version to 0.19.5
For provenance purposes, this commit was AI assisted. |
||
|
|
3636e9ed76 |
chore: bump version to 0.19.4
For provenance purposes, this commit was AI assisted. |
||
|
|
b017b2992e |
fix(review): gutter hover button not rendering + update @pierre/diffs to 1.1.20 (#630)
The renderHoverUtility callback was gating on getHoveredLine() during React render, but Pierre updates hover state imperatively — so the button was permanently null. Always render the button; call getHoveredLine() only in the click handler. Also aligned button sizing/colors with Pierre's own [data-utility-button] spec. For provenance purposes, this commit was AI assisted. |
||
|
|
117c7c8ad1 |
chore: bump version to 0.19.3
For provenance purposes, this commit was AI assisted. |
||
|
|
8a5082d3ed |
chore: bump version to 0.19.2
For provenance purposes, this commit was AI assisted. |
||
|
|
f13807bd54 | chore: bump version to 0.19.1 | ||
|
|
289a2aa494 |
chore: bump version to 0.19.0
Bump version across all 7 package/plugin manifests. Also refresh AGENTS.md (new Block types from #597, Code Tour endpoints from #569), prune removed flags from the Pi README (--plan-file, /plannotator-set-file from #595), and pin Renovate off bun-version bumps to keep the macOS codesign hotfix from v0.17.9 in place. For provenance purposes, this commit was AI assisted. |
||
|
|
ba2e4d2a1d |
feat(ui): markdown reader parity — HTML blocks, GitHub alerts, GFM inline extras (#597)
* feat(ui): markdown reader parity — HTML blocks, GitHub alerts, GFM inline extras Brings the in-app markdown reader to parity with GitHub's flavored rendering. Additive across the parser + renderer; no behavior change for existing blocks. Refactor: - Extract InlineMarkdown (262 lines) out of Viewer.tsx into its own file - Extract BlockRenderer + block-type components (CodeBlock, HtmlBlock, AlertBlock, Callout) into components/blocks/ — Viewer drops from 1279 to ~770 lines - Each new block-level feature lands in BlockRenderer or a new blocks/*.tsx, not Viewer Block-level features: - Raw HTML blocks (<details>, <summary>, etc.) via balanced-tag parser branch, rendered through marked + DOMPurify for nested-markdown support; inner innerHTML set imperatively so React reconciliation doesn't collapse open <details> - GitHub alerts (> [!NOTE] / [!TIP] / [!WARNING] / [!CAUTION] / [!IMPORTANT]) with inline Octicons, title-case labels, GitHub's Primer colors (light + dark) - Directive containers (:::kind ... :::) with arbitrary kinds for project-specific callouts (note, tip, warning, danger, info, success, question, etc.) - Heading anchor ids — slugifyHeading() strips inline markdown, preserves unicode Inline features (all in InlineMarkdown, all code-span-safe): - Bare URL autolinks (https://...) with trailing-punctuation trimming - @mentions and #issue-refs — render as clickable links when repo is GitHub, styled spans otherwise; threaded via repoInfo.display through BlockRenderer - Emoji shortcodes (👋, 🚀, 29 curated codes) via transformPlainText() - Smart punctuation (curly quotes, em/en dashes, ellipsis) applied only to plain-text fragments after code spans have been consumed Safety: - Render-time transforms live inside InlineMarkdown's plain-text push, which is only reached after code-span regex consumes code content. Backticks stay literal for shell/regex snippets. - DOMPurify allowlist (no on* handlers, no style attrs, no scripts) gates every raw HTML block. Unsafe link protocols (javascript:/data:/vbscript:/file:) stripped by sanitizeLinkUrl. Tests: +40 (149 total). New files: - utils/slugify.test.ts (10) — unicode, markdown stripping, edge cases - utils/inlineTransforms.test.ts (9) — emoji + smartypants - utils/parser.test.ts — alert detection (5 cases), directives (5 cases), HTML block balancing (5 cases) Fixtures for manual verification: - tests/test-fixtures/11-html-blocks.md - tests/test-fixtures/12-gfm-and-inline-extras.md (release-plan-shaped demo) Known limitations (not blockers): - Bare URL regex doesn't balance parens (https://en.wikipedia.org/wiki/Foo_(bar) drops the trailing ")") - Duplicate heading text → duplicate anchor ids (browser picks first on hash nav) - Directive body is inline-only (no nested headings/lists) For provenance purposes, this commit was AI assisted. * fix(ui): wire typecheck for packages/ui, address PR review findings Root-cause fix for the missing-import bug caught in review: the UI package had no tsconfig.json and no typecheck script, so missing references like `getImageSrc` in the extracted InlineMarkdown slipped past vite/esbuild (which only type-strip, they don't resolve imports). Infrastructure: - Added packages/ui/tsconfig.json with proper module resolution, JSX config, and bundler-style paths. - Added globals.d.ts to accept side-effect CSS imports. - Added @types/react, @types/react-dom, @types/bun, @types/dompurify as devDeps on packages/ui so React / Bun / DOMPurify types actually resolve. - Wired `tsc --noEmit -p packages/ui/tsconfig.json` into the top-level `bun run typecheck` script. With the typecheck running, 0 errors remain in this PR's scope. Four pre-existing errors on main (plan-diff SVG type narrowing, sharing.ts SharePayload shape) are unrelated and tracked separately. Review fixes: - InlineMarkdown: import getImageSrc from ImageThumbnail. Was calling the helper without importing it — markdown images with relative paths (``) would throw ReferenceError at render. Regression caused by the InlineMarkdown extraction. - useAnnotationHighlighter: findTextInDOM now retries with the rendered form (transformPlainText) when the raw originalText doesn't match. Annotations made before smart-punctuation / emoji shortcodes shipped (straight quotes, `👋` text) still re-bind after reload. - sanitizeHtml: allow the `open` attribute so `<details open>` preserves its default-expanded state instead of always rendering collapsed. - parser.test.ts: narrow a string->AlertKind assertion to satisfy strict typechecking. Deferred (tracked as known limitation in PR description): - HtmlBlock relative URL rewriting for nested <img src="./logo.png"> / <a href="note.md">. New-feature gap, not a regression. For provenance purposes, this commit was AI assisted. * fix(ui): HtmlBlock rewrites relative <img>/<a> refs to match markdown paths Raw HTML blocks inject sanitized HTML verbatim, so nested <img src="./logo.png"> and <a href="notes.md"> resolved against the plannotator server URL instead of the plan's directory — images 404'd, .md links navigated away instead of opening in the linked-doc overlay. This is the path README.md content hits (hero <img>, YouTube thumbnails, <details> sections with anchors). Fix: after setting innerHTML, walk <img> and <a> elements and apply the same rewriting markdown content uses: - <img> relative src → getImageSrc(src, imageBaseDir), routing through /api/image?path=... with the plan's base directory. - <a> relative href matching .md / .mdx / .html → click handler that calls onOpenLinkedDoc, same pattern as [label](./foo.md) markdown links. - http(s):, data:, blob:, mailto:, tel:, and #anchor hrefs pass through untouched. BlockRenderer now threads imageBaseDir + onOpenLinkedDoc into HtmlBlock. React.memo equality extended to compare those props too, so legitimate changes still re-run the rewrite pass without forcing re-renders on every parent update. Verified against the repo's own README.md — hero image, YouTube thumbnails, and <details> sections all render correctly in annotate mode. For provenance purposes, this commit was AI assisted. * feat(ui): table conveniences — hover toolbar, popout dialog with sort/filter Extracts table rendering into blocks/TableBlock and adds two companion surfaces: a hover toolbar for quick copy, and a full-screen popout dialog with TanStack-powered sort/filter/copy for power use. No pagination — plan tables don't get that big. Hover toolbar (blocks/TableToolbar.tsx): - Floats above the table on mouse enter via React portal, positioned with getBoundingClientRect + scroll/resize listeners, entry/exit animations. Same positional pattern as AnnotationToolbar's top-right mode. - Debounced hover state in Viewer (100ms leave → 150ms exit animation), mirroring hoveredCodeBlock's state machine. - Three actions: Copy markdown (icon), CSV (short uppercase button, RFC 4180 escaping), Expand (opens popout). Popout dialog (blocks/TablePopout.tsx): - Radix Dialog, fullscreen-ish card with ~2rem backdrop visible for click-to-close. max-w-[min(calc(100vw-4rem),1500px)]. - Portaled into Viewer's containerRef so the annotation hook can walk into the popout's text nodes — selection-based annotations, text-search restoration, and shared blockId all work across the collapsed and popped-out views. - TanStack Table for the grid: click column headers to sort (asc → desc → clear), global filter input, no pagination. Row count indicator shows "15 of 27" when filter reduces the set. - Copy / CSV buttons in the header row: filter- and sort-aware. When visible rows < total, tooltips read "Copy 15 rows as markdown" / "Copy 15 rows as CSV". When no filter, copies whole table (normalized whitespace). Read is one-shot on click — no derived state to sync. - Floating X close button (absolute top-right), no header bar. Chrome stacking while popout is open (CSS-only, via :has()): - body:has([data-popout="true"]) drops four element types behind the dialog: annotation sidebar, sticky header lane, app nav header, overlay scrollbars. :has() observes the dialog's presence directly — when the dialog unmounts, the selector stops matching and everything returns to natural stacking. No JS state, no useEffect cleanup. Shared helpers in TableBlock.tsx (exported): - parseTableContent — pipe-delimited markdown → { headers, rows } - buildCsvFromRows / buildMarkdownTable — inverse, from parsed data - buildCsv — thin wrapper for the hover toolbar's raw-block path Dependencies added: - @radix-ui/react-dialog ^1.1.15 (~6 KB gzipped) - @tanstack/react-table ^8.21.3 (~14 KB gzipped) Fixture: - tests/test-fixtures/12-gfm-and-inline-extras.md — added a 27×11 "Detailed feature backlog" table to exercise wide + deep tables, horizontal scroll in the popout, and the sort/filter flows. For provenance purposes, this commit was AI assisted. * fix(ui): table popout — annotation flow, chrome stacking, sidebar tabs Tightens the popout so annotations work inside it and chrome doesn't overlap the dialog. Annotation flow inside popout: - Radix Dialog modal={false} so the focus trap doesn't yank focus back from CommentPopover's textarea (CommentPopover portals to document.body, outside the dialog's DOM subtree). - Dialog.Content onInteractOutside handler whitelists the annotation toolbar, CommentPopover, and FloatingQuickLabelPicker so clicking them doesn't dismiss the dialog. Backdrop click + Escape still close. - aria-describedby={undefined} on Dialog.Content (Radix opt-out; the popout doesn't need a description). - React.memo on TablePopout with a custom comparator (block id/content, open, container, imageBaseDir, githubRepo). Prevents upstream Viewer re-renders from re-running TanStack's flexRender on every cell, which conflicted with web-highlighter's live DOM mutations and caused a NotFoundError in React's reconciler. Widget markers for :has()-based chrome stacking: - [data-comment-popover="true"] on CommentPopover (both popover + dialog variants). - [data-floating-picker="true"] on FloatingQuickLabelPicker. - [data-sidebar-tabs="true"] on SidebarTabs (left-side TOC/Files/Versions flags that sit on top of the dialog otherwise). - theme.css extended: sidebar tabs join the annotation sidebar, sticky header lane, app header, and overlay scrollbars in dropping to z-index -1 while body:has([data-popout="true"]) matches. Known limitation (not addressed): annotations created inside the popout show their <mark> only while the popout is open; when it closes, the <mark> unmounts with the popout's DOM and does not reappear on the collapsed table. The annotation itself persists in state (sidebar, shared URLs, exports). Round-tripping visual marks between popout and collapsed view requires either a second web-highlighter instance or a switch to the CSS Custom Highlight API — out of scope here. For provenance purposes, this commit was AI assisted. * fix(ui): review findings — flags, alerts, forges, tabs, anchors, URL brackets Six targeted fixes from the v0.19 PR review. Each is small and scoped; the riskier items from the review (plan-diff block variants, HTML relative non-doc links) are tracked as follow-ups. Smart punctuation — CLI flags preserved: - Narrowed the `--` → en-dash rule to only fire between digits (`pages 3--5` still converts; `bun --watch` stays literal). GitHub alerts — list/code/heading bodies absorb correctly: - Blockquote merge now always merges into a previous alert blockquote, regardless of whether the new line starts with a block marker. Without this, `> [!NOTE]\n> - item` split the list off into a plain italic quote and emptied the alert. - AlertBlock got a mini block-level renderer for the body so `- item` / `* item` / `1. item` render as real <ul>/<ol>, not flattened prose. Forge-aware mentions/issue refs: - packages/shared/repo: new parseRemoteHost() extracts the host from the git remote URL; RepoInfo gains an optional `host` field. - packages/server/repo: getRepoInfo populates host alongside display. - Viewer only passes githubRepo to InlineMarkdown when the host is exactly "github.com". Non-GitHub repos render mentions/issue refs as styled text, no wrong github.com links. HTML block external links: - rewriteRelativeRefs now forces `target="_blank"` and `rel="noopener noreferrer"` on every external http(s) link inside raw HTML. Fixes two problems in one pass: external links no longer hijack the review tab, and pasted-HTML links can't tab-nab the plannotator tab via window.opener. Heading anchor dedup: - New buildHeadingSlugMap() walks all heading blocks and assigns `foo`, `foo-1`, `foo-2`, ... for repeats (GitHub convention). BlockRenderer receives the anchor id as a prop from Viewer via a memoized map rather than computing per-block; first occurrence keeps the bare slug so existing links stay stable. URL autolink bracket balance: - Trailing `)`/`]`/`}` in bare URLs are kept when they balance an earlier opener inside the URL. Wikipedia-style `https://en.wikipedia.org/wiki/Function_(mathematics)` now keeps its paren; `(see https://x.com)` still trims the orphan. Tests: +8 (157 total). - utils/slugify.test: buildHeadingSlugMap dedup behavior, non-heading skipping, empty-slug skipping. - utils/inlineTransforms.test: CLI flags stay literal, `3--5` still converts. - utils/parser.test: alerts with list body / code fence body, blank line ending an alert. Fixture: - tests/test-fixtures/13-known-issues.md — reproduces each of the review findings end-to-end; useful as a regression check going forward. Deferred (tracked for follow-up): - Plan diff view doesn't render html / directive / alertKind semantics (SimpleBlockRenderer has no cases for the new block variants). - Relative non-doc links inside raw HTML (.pdf, .csv) don't get rewritten — only .md/.mdx/.html are routed through the linked-doc overlay today. Not a regression; narrow audience. For provenance purposes, this commit was AI assisted. * fix(ui): round-3 review — drop host gate, link paren balance, data/blob images - Viewer: remove repoInfo.host === 'github.com' gate so @user/#123 links render for GitHub Enterprise and runtimes (Pi) that don't populate host. - HtmlBlock: treat protocol-relative //host links as external and harden with target=_blank rel=noopener noreferrer. - InlineMarkdown: data:/blob: image sources bypass /api/image rewrite. - InlineMarkdown: replace [text](url) regex with a depth-tracking scanner so URLs with balanced parens (Wikipedia /Function_(mathematics)) and backslash-escapes no longer truncate. Empty text/url guard preserves prior fall-through behavior. - InlineMarkdown: isLocalDoc accepts .md/.mdx/.html/.htm with optional #fragment; fragment stripped before onOpenLinkedDoc so guide.md#setup opens the linked doc instead of a broken anchor. For provenance purposes, this commit was AI assisted. * fix(ui): round-4 review — table pipe escape, callout lists, emoji h-splitter - TableBlock: buildMarkdownTable now re-escapes literal | as \| in each cell. parseTableContent already unescapes on parse; without the mirror on serialize, the popout's copy-as-markdown produces extra columns for tables with pipes in regex, shell, or boolean content. - AlertBlock + Callout: extract the shared paragraph-and-list body renderer into blocks/proseBody.tsx. Fixes directive callouts (:::note with a bulleted list) rendering as literal hyphens instead of a list. Paragraph lines join with '\n' so InlineMarkdown's hard-break handler still fires. Callout passes an empty text-color class so directive color tokens inherited from the container are preserved. - InlineMarkdown: drop `h` from the plaintext chunk-break class; it was splitting emoji shortcodes like ❤️, 👍, 🤔 at the h, so the :word: pattern never reassembled and transformPlainText couldn't replace the shortcode. Bare URL detection moves inline via emitPlainTextWithBareUrls, which scans chunks for https?:// at word boundaries and emits anchors, passing surrounding text through transformPlainText so emoji + smart punctuation still apply to non-URL slices. - InlineMarkdown: extract trimUrlTail (shared between the top-of-loop URL branch and the new inline scanner) — one balanced-paren trim implementation instead of two. +8 unit tests covering the trim cases (Wikipedia parens, unbalanced brackets, stacked punctuation). - Fixture: section 9 in 13-known-issues.md demonstrates the table copy corruption for manual verification. For provenance purposes, this commit was AI assisted. * fix(ui): resolve pre-existing typecheck errors surfacing in CI - PlanCleanDiffView: narrow heading Tag to 'h1'..'h6' so hover props resolve to HTMLHeadingElement instead of the SVGSymbolElement branch of keyof IntrinsicElements. - VSCodeIcon: spread mask-type as a kebab-case attribute; React 19's typings no longer expose the camelCase maskType prop on SVG masks. - useSharing / sharing: cast decompress() result to SharePayload — the shared compress module returns unknown by design; callers were implicitly any and TS 5.x now flags the assignment. For provenance purposes, this commit was AI assisted. |
||
|
|
3a7f1156cd |
chore: bump version to 0.18.0
Also fix stale annotate description in README and add missing PLANNOTATOR_ORIGIN entry to the env vars table. For provenance purposes, this commit was AI assisted. |
||
|
|
54c206c77d |
Add ~ support for user-entered file paths (#572)
* refactor(path): centralize user path resolution * fix(annotate): resolve user paths in file entrypoints * fix(pi-extension): restore resolve import and add typecheck to CI The refactor removed `resolve` from `node:path` imports, but `resolvePlanPath()` and the planning-mode write/edit guards still call `resolve(...)`. That breaks plan submission and plan-file restriction at runtime for Pi users. Also wires pi-extension's tsconfig into the root `typecheck` script so CI catches this class of missing-symbol regression in the future. Required adding @mariozechner/pi-* packages as explicit devDependencies so tsc can resolve them (they were previously only reachable transitively via the peer dep, which Bun keeps in its `.bun/` store unhoisted). For provenance purposes, this commit was AI assisted. * fix(path): reject whitespace-only user paths and run vendor before typecheck resolveUserPath() trims input, so whitespace-only customPath/vaultPath resolved to process.cwd(). Plans silently wrote into the repo root and Obsidian notes landed in <cwd>/plannotator/ instead of erroring. Guard at both call sites (getPlanDir, saveToObsidian — Bun + Pi copies). Also prepend vendor.sh to the root typecheck script so fresh-clone `bun run typecheck` works without a separate vendoring step. For provenance purposes, this commit was AI assisted. * fix(path): short-circuit resolveUserPath on empty input Trimming in normalizeUserPathInput meant whitespace-only input resolved to cwd/baseDir. Callers like the annotate CLI and reference API endpoints would then list the project root instead of erroring. Return "" early so downstream existsSync/resolveMarkdownFile checks fail naturally. For provenance purposes, this commit was AI assisted. --------- Co-authored-by: Michael Ramos <mdramos8@gmail.com> |
||
|
|
e22c911c2c |
chore: bump version to 0.17.10
For provenance purposes, this commit was AI assisted. |