mirror of
https://github.com/backnotprop/plannotator.git
synced 2026-09-14 14:17:26 +08:00
codex-mobile-touch-selection
694 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
d0665571c7 | Fix OpenCode plan review cancellation cleanup (#1064) | ||
|
|
8e9a359a8f | fix(review): make remote discovery noninteractive (#1062) | ||
|
|
60b5e8d31a | Narrow review feedback validation to submitted findings (#1065) | ||
|
|
13309e322b |
feat(server): support bounded port ranges (#1042)
* feat(server): support bounded port ranges * fix(server): harden bounded port retries * fix(server): preserve non-range port behavior --------- Co-authored-by: Michael Ramos <mdramos8@gmail.com> |
||
|
|
5b2756b0c7 |
fix(server): return JSON 404 for unknown /api/* routes instead of HTML (#748)
* fix(server): return JSON 404 for unknown /api/* routes instead of HTML All three servers (plan, review, annotate) serve the SPA HTML for any unmatched route. This means hitting /api/nonexistent returns the full HTML page with 200 OK — confusing for API clients and wasteful. Now unknown /api/* paths get a proper JSON 404 response before the SPA fallback kicks in. Non-API paths (like /some/random/path) still serve HTML for SPA routing as before. Tested on all three server types. * fix(server): guard unknown API routes across runtimes --------- Co-authored-by: buihongduc132 <buihongduc132@gmail.com> Co-authored-by: Michael Ramos <mdramos8@gmail.com> |
||
|
|
92c311ab1d |
fix(review): show System theme coherently across UI (#1015)
Use one shared Light/Dark/System registry across review, plan, annotate, Settings, and published UI consumers. Preserve the public Mode contract, scope wider menu geometry to theme pickers, and keep constrained palettes coherent in rendering and previews. Co-authored-by: Vladyslav Hrabovyi <vladyslav.hrabovyii@gmail.com> Co-authored-by: sasha <kredich05@gmail.com> |
||
|
|
29513e1984 |
chore: bump version to 0.23.1
Claude-Session: https://claude.ai/code/session_01K9G9vurTg1v5uD37GbDMpP |
||
|
|
1f70937d51 |
fix(ui): keep Ask AI input visible after responses (#1035)
* fix(ui): keep Ask AI input visible after responses Long completed responses caused the h-full chat flex item to retain its automatic minimum height, pushing the follow-up composer below the sidebar viewport. Let the panel consume the remaining flex space with min-h-0 so only the message area scrolls. * chore: remove Ask AI browser harness |
||
|
|
7f0c36028b |
fix(server): bound startup file discovery (#1036)
* fix(server): bound startup file discovery (#978) * test: observe the server's own warm cache key so the ordering tests actually pin the fix Review finding: both bind-before-warm ordering tests raced against observeWarmState(projectRoot), but on macOS mkdtempSync returns /var/... while the chdir'd server warms under the realpath /private/var/... — a different warmFileListCache key. The tests therefore raced a FRESH warm (always pending at observation time) and passed on the OLD broken code too. Observing process.cwd() inside onReady uses the server's real key. Verified: with old resolve-file/server code checked out, all four ordering tests now fail; on the fix they pass (17/17 across both files). Claude-Session: https://claude.ai/code/session_01SFy9fY27SA8g5BtotWPi1G |
||
|
|
69ca6d546c |
chore: bump version to 0.23.0
Claude-Session: https://claude.ai/code/session_01SFy9fY27SA8g5BtotWPi1G |
||
|
|
977f4ce582 |
fix: final QA sweep fixes — file-browser cap priority, OpenCode project scoping, spotlight Alt-Alt dismiss
Fixes for the three-agent adversarial sweep findings:
- reference-handlers.ts: seed the user's modified/untracked files BEFORE
the bulk walk — the 5000-file cap (
|
||
|
|
abceed3f0b |
chore(server): align annotate-history degradation log with the [plannotator] prefix convention
Claude-Session: https://claude.ai/code/session_01SFy9fY27SA8g5BtotWPi1G |
||
|
|
f022cf2302 |
fix(review): spotlight re-measures on header reflow, yields to keyboard use, fixes GitLab article
Adversarial self-review findings on
|
||
|
|
51710b1804 |
fix(review): remove automatic platform attribution (#1033)
Co-authored-by: Leonardo R. Dias <47978193+leoreisdias@users.noreply.github.com> |
||
|
|
7d583df980 |
fix(server): unwritable data dir degrades annotate/sessions instead of failing startup
QA probe: with PLANNOTATOR_DATA_DIR on a read-only mount, `plannotator annotate file.md` crashed before the UI opened — twice. (1) #961's saveToHistory call on annotate open was unguarded, so the new version-diff enhancement gated startup that was stateless at v0.22.0; now wrapped (both the Bun server and the Pi mirror) and degrades to no-version-diff with a stderr notice. (2) registerSession's mkdir/writeFileSync (pre-existing since #795) threw after the server was already listening; the session registry only powers `plannotator sessions` discovery, so it is now best-effort, and unregisterSession/listSessions tolerate an unwritable sessions dir too. Verified: read-only data dir now starts, serves /api/plan with no versionInfo, and prints the session URL. Claude-Session: https://claude.ai/code/session_01SFy9fY27SA8g5BtotWPi1G |
||
|
|
1f7d08f59a |
feat(review): one-time spotlight on the PR feedback-destination switcher
First-time PR reviewers get a driver.js-style coachmark (zero new dependencies: rounded cutout + oversized box-shadow dim, portaled to body) pointing at the header's Agent/GitHub destination dropdown, explaining that feedback can post to the PR or go to the agent session and can be switched at any time (Alt Alt). Cookie-gated one-time flag (plannotator-review-dest-spotlight-seen, versioned) mirroring the guide-intro/review-setup gates. Renders strictly after the first-run dialog chain (guide intro → look-and-feel → review setup) has cleared and only in PR mode; opening the destination menu organically also marks it seen. Escape/click-anywhere/Got-it dismiss. GitLab-aware copy via platformLabel/mrLabel. Claude-Session: https://claude.ai/code/session_01SFy9fY27SA8g5BtotWPi1G |
||
|
|
e5fcc4153c |
fix(opencode): surface session URLs via tui.showToast — app.log never reaches the TUI
QA traced the repeatedly-regressed remote-URL invariant to its root cause on OpenCode: every URL path (logPlannotatorReady, the cli-bridge stderr forwarder, and the ready-file poller) funneled exclusively through client.app.log, which OpenCode documents as "write a log entry to the server logs" — it is never rendered in the TUI. Remote users therefore never saw the session URL. All three paths now ALSO call tui.showToast (the SDK's visible surface), best-effort with optional chaining so older hosts without /tui/show-toast no-op. A shared per-run toastedUrls set dedupes the stderr and ready-file deliveries so one session never stacks two toasts. Also: recognize the current binary's "Plannotator session ready" stderr phrasing in formatUserFacingCliStderrLine (the old "Open this link" match no longer fires; only the bare-URL line was being forwarded), and add data-print-hide to the resize-handle cursor tooltip portal so print.css hides it (QA print-clipping finding). Claude-Session: https://claude.ai/code/session_01SFy9fY27SA8g5BtotWPi1G |
||
|
|
ee28f1a428 | fix: handle Windows install and review edge cases (#1021) | ||
|
|
4934ab6218 |
feat(ui): add GPT-5.6 family (sol alias, terra, luna) to the Codex job model catalog
The launchable-jobs Codex catalog (review jobs, tour, guide, GuideEmptyState) is a hardcoded list, so yesterday's GPT-5.6 release was missing from the selectors. Ask AI needs no change — the codex-app-server provider discovers models dynamically via model/list. The server passes -m through verbatim (codex-review.ts), so the catalog addition is the complete fix. Defaults stay on gpt-5.5. Also relocks bun.lock's workspace entry for ui 0.28.0 (missed in the bump commit). Claude-Session: https://claude.ai/code/session_01SFy9fY27SA8g5BtotWPi1G |
||
|
|
f5390956e5 |
fix(ui): focus comment textarea reliably via ref callback (#1031)
The CommentPopover focus effect scheduled a setTimeout(0) keyed on [mode], but in popover mode the textarea only renders after `position` is measured by a later effect. Whether the 0ms timer fires before or after that commit is engine-dependent task ordering: Chromium commits the re-render first, so focus lands; WebKit fires the timer first, so textareaRef.current is still null and the comment editor opens unfocused (reproduced in a WKWebView host — the glimpseui integration — where users had to tab or click into the field for every annotation). Focus from a ref callback on the textarea instead: it runs exactly when the element mounts, so there is nothing to race. Mode switches (popover/dialog) still refocus because the textarea remounts. The deferred focus and caret-to-end behavior are preserved; isConnected guards the popover closing before the timer fires. |
||
|
|
6cf36bd852 |
fix: disambiguate OpenCode model labels by provider (#1024)
fetchModels() built the dropdown label from the model name alone, so models with identical names from different providers (e.g. deepseek-v4-pro via DeepSeek vs. via OpenRouter) were indistinguishable in the UI even though their ids were already unique. Fixes #988 |
||
|
|
f9d58c1e3e |
chore(ui): 0.28.0 — frozen markdown diff HANDOFF/README docs (version bumped with the feature)
Documents the 0.28.0 round for the Workspaces consumer: the components/MarkdownDiff shim (frozen two-revision diff — newer revision as the real document, deletions projected in place, toolbar/overview/gutter), the byte contract on MarkdownDiffHandle (exact input strings incl. CRLF), the frozen state/view edit rejection, and the extensions composition with its capture-once caveat. Adds the supported-imports row and the 0.28.0 dependency note (wrapper ^0.4.0, engine ^0.8.0, new @codemirror/merge peer). Publish is manual and owner-gated per HANDOFF (bun pm pack, then npm publish --provenance, ui only — core is unchanged at 0.22.0). |
||
|
|
4bbb0f3aff |
feat(ui): re-export MarkdownDiff through the ui surface (engine 0.8.0 / wrapper 0.4.0)
Adds components/MarkdownDiff.tsx, a theme-bridging shim over @plannotator/markdown-editor's frozen two-revision diff, mirroring the MarkdownEditor shim exactly: ThemeProvider mode bridge, plannotator.css theme import, extensions passthrough (wikiLinks composes), gridEnabled -> the same design-system card chrome. Hosts import it from @plannotator/ui/components/MarkdownDiff; AtomicDiffEditor and @plannotator/atomic-editor stay off the allowlist. Deps: @plannotator/markdown-editor ^0.4.0, @plannotator/atomic-editor ^0.8.0 (single lockfile copy each), plus @codemirror/merge — the engine's new required peer — declared directly per the CM6 single-copy discipline. Tests pin the seam end-to-end: public export surface + types, byte-exact getMarkdown()/getOriginalMarkdown() incl. CRLF/trailing-space fixtures, contenteditable="false" frozen contract, change navigation, wiki-link composition through the shim, and theme/host-class forwarding. The strict-consumer tsconfig now typechecks the new module. |
||
|
|
2f1d6a94a9 | feat(ui): re-export slashCommands and selectionToolbar through the MarkdownEditor surface | ||
|
|
7d846879a1 |
chore(ui): retarget deps to engine 0.7.0 / wrapper 0.3.2 (release train moved)
0.6.0 and 0.3.1 will never be published; the engine is stamped 0.7.0 (adds preferResolvedLabel on WikiLinksConfig — labeled links opting into resolved titles, the flag the consumer needs) and the wrapper 0.3.2 (peer widened to ^0.5 || ^0.6 || ^0.7). The previous ^0.6.0 range would have excluded 0.7.0 under 0.x caret semantics and stripped preferResolvedLabel from the re-exported type. - @plannotator/atomic-editor ^0.7.0, @plannotator/markdown-editor ^0.3.2 - HANDOFF/README version references updated - wikiLinks re-export test now sets preferResolvedLabel: true — a type-level seam assertion (excess-property checking rejects unknown keys, verified by negative control) Re-verified against fresh npm-pack tarballs of both packages: full repo typecheck, ui tests (354+66 skip plain / 420 DOM, 0 fail), build:css — all green. Install still resolves only after the user publishes 0.7.0 + 0.3.2. |
||
|
|
b297410b5d |
chore(ui): 0.27.0 — wiki-link seams HANDOFF/README docs + version bump
Documents the 0.27.0 round for the Workspaces consumer: the MarkdownEditor 'extensions' seam with its capture-once-per-documentId caveat, the wikiLinks re-export through the ui surface, InlineMarkdown's sync resolveLinkedDoc semantics, and the retirement of the H-ask-1 html-viewer patch. Marks HANDOFF rough edge #4 (no live-collab extensions) resolved. Publish is manual and user-gated per HANDOFF (bun pm pack, then npm publish --provenance, core then ui); requires @plannotator/markdown-editor 0.3.1 and @plannotator/atomic-editor 0.6.0 on npm first. |
||
|
|
e0519f84c2 |
fix(ui): remove the two TS6133 offenders in html-viewer (retires Workspaces' H-ask-1 patch)
Workspaces carried two byte-identical one-line fixes against 0.25/0.26 for strict tsc (--noUnusedLocals) on the shipped source. Verified with tsc under the strict-consumer flags scoped to components/html-viewer: exactly two TS6133s, no other strict errors. - HtmlViewer.tsx: drop the unused React default import (jsx: react-jsx; zero React.* uses) - useHtmlAnnotation.ts: drop the unused 'annotations' destructured binding; the UseHtmlAnnotationOptions property stays (callers pass it — removing it from the options type would be a breaking API change) |
||
|
|
95c9a111cf |
feat(ui): resolveLinkedDoc — sync host resolution of wiki-links in InlineMarkdown
New optional prop:
resolveLinkedDoc?: (target: string) =>
{ label?: string; status?: 'active' | 'deleted' } | null
Semantics (consumer-contracted, sync-only by design — the host backs it with
an in-memory cache; no async variant, no loading states, no phantom docs):
- absent or null → exactly today's rendering (stored label, live link)
- label → displayed instead of the stored label; stored label is the
fallback, the target the last resort
- status 'deleted' → muted non-link span (no anchor, no pointer, no link
icon, title "Document deleted"), even when onOpenLinkedDoc is present
- the callback receives the RAW stored target (doc_01XYZ), before the
.md-appending path normalization; onOpenLinkedDoc keeps receiving the
normalized path for non-deleted links, exactly as before
Threaded through all six recursive InlineMarkdown call sites so wiki-links
inside emphasis/strikethrough resolve too. Tests pin every branch, including
null → byte-identical innerHTML with and without onOpenLinkedDoc.
|
||
|
|
36441ab398 |
feat(ui): re-export wikiLinks + config types through the ui surface
Hosts' import allowlist forbids @plannotator/atomic-editor; @plannotator/ui is the single contract. wikiLinks and its types (WikiLinksConfig, WikiLinkSuggestion, WikiLinkResolvedTarget, WikiLinkStatus) now re-export from components/MarkdownEditor — colocated with the 'extensions' prop they feed, already on the supported-import allowlist and under the strict-consumer gate. Usage: build wikiLinks(config) and pass it via the shim's extensions prop. Test pins identity with the engine export and exercises the type surface. |
||
|
|
502b19dc69 |
feat(ui): 'extensions' passthrough on the MarkdownEditor shim
Declares extensions?: readonly Extension[] (@codemirror/state) on the shim's props; the existing prop spread forwards it to @plannotator/markdown-editor, which threads it into the atomic-editor engine. This is the seam Workspaces needs to install wikiLinks(...) and, later, y-codemirror.next collab bindings without importing the engine directly. The doc comment carries the consumer-required rider prominently: the engine captures the array ONCE per documentId — swapping the array later is silently ignored until remount; config callbacks may close over live state. Test mounts through the SHIM with a facet-based probe extension (EditorView.editorAttributes) and asserts the attribute lands on the engine's .cm-editor DOM, proving the seam end-to-end, plus byte-fidelity is untouched. |
||
|
|
62c0809350 |
chore(ui): require @plannotator/markdown-editor ^0.3.1 and @plannotator/atomic-editor ^0.6.0
0.3.1 adds the editor 'extensions' passthrough; 0.6.0 adds the wikiLinks extension. Neither is published to npm yet (publishes are imminent and user-gated): install/CI resolves only after they land. Local verification for this branch ran against tarballs built from the source repos via npm pack. |
||
|
|
858b7ff5dd |
chore(ui): 0.26.0 — resize-handle seams + file-browser filtering HANDOFF notes + version bump
The published 0.25.0 tarball predates the file-browser filtering ( |
||
|
|
f4f6bdb0e9 |
feat(ui): handle-wide click-to-collapse + suppressible hover on resize handles (#1028)
* feat(ui): handle-wide click-to-collapse + suppressible hover on resize handles
Rework the sidebar/panel resize-handle interaction:
- useResizablePanel: new `onClick` option that fires on pointer-up only when
the pointer never traveled past `clickThreshold` (4px default), so a genuine
click on the handle can be told apart from a drag-start. Plannotator is
unchanged unless `onClick` is passed.
- ResizeHandle: `hideHoverTrack` to suppress the hover color-change, a
cursor-following `tooltip`, a `trackClassName` prop, and a
`[data-resize-track]` attribute (mirrors `[data-collapse]`) so a host can
restyle/suppress the hover reveal from plain CSS.
- Wire all plan-editor and review-editor handles to the new UX: no track
highlight, cursor tooltip ("Click to close · Drag to resize"), single-click
collapse (double-click reset becomes unreachable), chevron retained.
- README: document the new resize-handle host seams.
* refactor(ui): self-review polish on resize-handle UX
- Skip tooltip-position state updates while dragging (they were hidden anyway).
- Extract the repeated resize-handle tooltip string into a per-app constant
(RESIZE_HANDLE_TOOLTIP) instead of 7 inline literals.
* fix(ui): don't treat pointercancel as a click-to-collapse
pointercancel fires when the browser aborts a gesture (palm rejection, system
gesture, focus loss). It was routed through the same onUp path as pointerup, so
a no-move cancel with onClick set would collapse the panel. Skip the onClick
branch on cancel — only clean up drag state (a real in-progress drag still
commits its width, matching prior behavior).
|
||
|
|
1267cd50db | Add file browser filtering | ||
|
|
381a876dac |
chore(ui): 0.25.0 — HANDOFF rendering-neutrality notes + version bump
HtmlViewer no longer injects bare theme tokens, root classes, or color-scheme into rendered documents (#1023); host theming is opt-in via <meta name="plannotator-theme" content="host">. Supersedes Workspaces' H-ask-1 patch. Claude-Session: https://claude.ai/code/session_01MDqD8jdgTbdjiXcVVigzV2 |
||
|
|
1047539801 |
Render arbitrary HTML in HtmlViewer without altering it (#1023)
HtmlViewer injected host-app state into rendered documents: ~26 bare theme tokens (--muted, --background, ...) written into :root and re-applied inline on the author's documentElement on every host theme flip, a `light` class toggled on the author's root, an asymmetric color-scheme:light injection, and unconditional ins/del diff styles. Documents defining the same token names rendered with wrong colors in both Plannotator and @plannotator/ui consumers. Arbitrary documents now render exactly as in a standalone tab: - Host tokens are pushed only under the viewer-owned --pn-* prefix; annotation CSS and the bridge read only var(--pn-*, fallback). The bridge refuses non---pn- writes and never touches the root class list unless the document opts in. - Host theme-following is opt-in per document via <meta name="plannotator-theme" content="host">, which restores the bare-token push, the light class, and a symmetric color-scheme sync. The visual-explainer skill now emits the tag in generated artifacts. - Diff CSS is injected only while the version-diff view is active and scoped to ins/del.plannotator-diff, which htmlDiff now emits on its generated wrappers; author <ins>/<del> markup is never restyled. The srcdoc injection logic moves to a pure module (srcdoc.ts) with tests pinning the neutrality contract, including a DOM test that runs the actual bridge script and asserts a theme flip lands nothing on the author's root except --pn-* properties. Claude-Session: https://claude.ai/code/session_01MDqD8jdgTbdjiXcVVigzV2 |
||
|
|
d3c9de1ef0 |
Fix annotate terminal startup in large folders
Avoid starving folder annotate sessions by excluding agent work directories and capping folder file-browser scans. Improve the annotate WebTUI panel startup/stop behavior. |
||
|
|
e20fd97be7 |
Consumer enablement for @plannotator/ui (0.24.0) (#1017)
* feat(ui): AnnotationPanel renderCardFooter + readOnly host props Per-card footer slot for host reply/resolve UI (clicks inside don't select the card); readOnly hides delete/edit on all card kinds. Both optional, both no-op by default — Plannotator unchanged. * feat(ui): bless 6 exports into the strict-consumer surface TableOfContents, ResizeHandle, useResizablePanel, useActiveSection, useScrollViewport, utils/annotationHelpers — verified strict-clean and backend-free (useResizablePanel persists via the storageBackend seam); added to the gate and the HANDOFF supported-imports table. * feat(ui): Viewer allowImages + readOnly host props allowImages threads to both CommentPopover sites (popover already gated its attach affordance; Viewer never exposed the knob). readOnly suppresses every composer entry point — selection toolbar (highlighter 'enabled'), pinpoint, global comment, attachments, checkbox toggles — while existing annotations still render and select. Both default to today's behavior. * fix(ui): lazy-import Viewer/CommentPopover in the consumer test (DOM-less bun test crashed on web-highlighter) * feat(ui): strict-consumer gate gains verbatimModuleSyntax + noUnusedLocals/Parameters Fixed the 24 violations the flags surfaced across the supported-import graph: type-only imports (verbatimModuleSyntax) and dead imports/locals. Consumers with stricter tsconfigs no longer have to relax them. * feat(ui): opt-in content-verifying annotation restore verifyRestoredContent on useAnnotationHighlighter: after a meta-based fromStore restore, the painted text is checked against originalText (whitespace-normalized). Mismatch -> highlight removed, text-search fallback re-anchors; if that fails too, onRestoreMismatch(annotation, restoredText) fires and nothing is painted. Default off — today's trust-the-positions behavior. Workspaces hit this live after document drift; correctness upgrade for every consumer. * chore(ui): 0.24.0 — HANDOFF consumer-enablement notes + version bump |
||
|
|
58f87500f1 |
Migrate packages/review-editor to Base UI (#1014)
* refactor(review-editor): migrate tour QA checkbox to Base UI First component of the Radix→Base UI migration (internal package). @base-ui/react added alongside Radix; Radix deps removed in the final commit. Root typecheck, bun test (1955 pass), build:review all green. Note: bun.lock also absorbs pre-existing manifest drift (the committed lock was stale relative to several package.jsons at HEAD — any install regenerates it identically). * chore(review-editor): give the package its own typecheck gate The package had no tsconfig and is absent from the root typecheck chain (it is typechecked only through apps/review, whose own tsc run is broken for unrelated reasons: it never declares @types/react). Adds a package tsconfig modeled on apps/review's compilerOptions plus @types/react(-dom) devDeps so migration transforms are type-verified. 53 pre-existing errors surfaced (import.meta.env, response-type mismatches) — recorded, untouched; gate for the migration is zero errors in migrated files. * refactor(review-editor): migrate all 7 popover files to Base UI Portal>Content becomes Portal>Positioner>Popup (positioning props and z-index on Positioner), asChild triggers become render props, state classes move to Base UI presence attributes (data-popup-open on triggers), --radix-* vars become --transform-origin, and keyframe enter animations restate as data-starting/ending-style transitions. Focus interception (onOpenAutoFocus) maps to Popup initialFocus, including WorktreePicker's conditional search-input focus. Flagged, not patched: collision defaults differ slightly at viewport edges; exit fade now animates where Radix unmounted instantly. See .migration/popover.md. Tests (1955) and build:review green; zero tsc errors in migrated files. * refactor(review-editor): migrate DiffTypePicker to Base UI Menu Radix DropdownMenu is renamed Menu in Base UI; same restructure as the popovers (Positioner/Popup split, render triggers, --anchor-width for trigger-width matching). Item onSelect becomes onClick. Flagged: focus loop now wraps by default (Base UI default). See .migration/dropdown-menu.md. * refactor(review-editor): migrate FileTreeNode context menu to Base UI Same menu anatomy via @base-ui/react/context-menu (which re-exports Menu's Positioner/Popup/Item parts). Row button keeps left-click select / double-click open; items switch onSelect→onClick. See .migration/context-menu.md for the hand-verify checklist — this is the file tree's core interaction. * refactor(review-editor): drop all Radix deps, finish Base UI migration Removes the 7 @radix-ui/* dependencies — three of them (collapsible, dialog, tooltip) were never imported anywhere and go without a code change. Also fixes two keyboard-nav guards in FileTree/SectionsPanel that matched Radix's [data-radix-popper-content-wrapper] portal attribute (Base UI popups carry ARIA roles directly, so the role selectors already cover every floating surface — verified against the shipped popup implementations). Zero radix references remain in the package (deps, imports, class strings, CSS vars, DOM selectors). Root typecheck, 1955 tests, and build:review all green. packages/ui untouched. Full summary in .migration/project.md. * fix(review-editor): initialFocus fallback semantics in pickers Self-review catch: Base UI's initialFocus function form treats a returned undefined as 'do nothing' (focus stays on the trigger) while null falls back to the default focus. The '?? undefined' I introduced would strand focus on the trigger for short worktree lists — the exact arrow-keys-leak-to-file-tree bug the original Radix code's comment warned about. Returning searchRef.current directly (element or null) matches the original semantics. Also corrects the popover report and adds a Tooltip-prop coordination note for the parallel ui migration. * docs(review-editor): record automated QA results in migration reports Playwright pass against the demo review session: context menu (full flow incl. real clipboard), tour checkboxes (all three toggle paths, incl. the span+hidden-input label association), diff-options popover, and DOM invariants (roles, data-popup-open, no data-state on migrated primitives) all pass. Demo mode never populates gitContext, so the diff-type dropdown and worktree/base pickers don't mount there — their checks remain on the manual list against a real session. * docs(review-editor): annotate QA checklist items untestable from the standalone entrypoint * chore: regenerate bun.lock after rebase onto Base UI ui migration |
||
|
|
ca34a8bbf7 |
Migrate @plannotator/ui to Base UI (0.23.0) (#1013)
* chore: restore packages/core workspace entry missing from bun.lock #957 merged without its bun.lock update; fresh installs couldn't resolve @plannotator/core workspace links. * chore(ui): install @base-ui/react alongside radix; migration assessment * feat(ui): migrate badge to Base UI (Slot -> useRender, asChild -> render) * feat(ui): migrate button to Base UI Button primitive (asChild -> render) * feat(ui): migrate tabs to Base UI (Trigger->Tab, Content->Panel, data-active) * feat(ui): migrate dialog to Base UI (Overlay->Backdrop, Content->Popup) * feat(ui): migrate dropdown-menu to Base UI Menu + sweep both consumers * feat(ui): migrate Popover wrapper to Base UI (Positioner model; drop PopoverAnchor) * feat(ui): migrate Tooltip wrapper to Base UI, public API preserved * feat(ui): migrate PopoutDialog to Base UI (non-modal + reason-based dismissal guard) * feat(ui): remove all @radix-ui dependencies — package is fully Base UI * chore(ui): 0.23.0 release notes — Base UI engine, drop tailwindcss-animate peer HANDOFF.md documents the engine swap for the Workspaces consumer: dependency changes, 11 breaking/behavior items, what did not change. Version bumped to 0.23.0; publish stays owner-gated. * docs(ui): record browser smoke results in migration report * fix(ui): self-review findings — caret Base UI range, Button type default documented, PopoutDialog focus-out guard hardened - @base-ui/react 1.6.0 -> ^1.6.0: a consumer's own Base UI install must dedupe with ours or portals lose context across copies - HANDOFF item 12 + button report: Button now defaults type="button" (implicit form submit is a consumer-only breaking change) - PopoutDialog: guard both event.target and relatedTarget on focus-out — blur-shaped events carry the annotation toolbar in relatedTarget * docs(ui): record human hand-verification pass in migration report * feat(ui): 150ms exit fade on popovers + prefers-reduced-motion for all Base UI popups Popover enter/exit unified on starting/ending-style transitions (the popover-enter keyframe only existed in review-editor CSS — plan-app popovers had no enter animation at all). Reduced-motion rule in theme.css covers dialogs, menus, popovers, tooltips in both apps. |
||
|
|
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. |
||
|
|
73cd4b121d |
feat(annotate): add sidebar shortcuts (#986)
* feat(annotate): add sidebar shortcuts * fix(shortcuts): double-tap requires solo presses The double-tap engine counted ANY two keyups of the tracked key within the window, so the new Shift Shift agent-terminal toggle fired during ordinary Shift usage: typing two capitalized words, extending a selection with two Shift+Clicks, or pressing this PR's own Mod+Shift+B twice. A tap now only counts when the key went down and came up alone — any other keydown, a pointer interaction mid-press, or another modifier held at release breaks the press and resets the sequence. Also honors the binding's preventDefault flag in the double-tap path (it was a silent no-op). --------- Co-authored-by: Michael Ramos <mdramos8@gmail.com> |
||
|
|
73efacfa0c |
feat(annotate): per-file version diff for .md and .html (rendered HTML highlights) (#961)
* feat(annotate): version diff for annotated files Annotate mode never tracked version history, so the existing Plan Diff (highlighted diff vs a previous version) only worked in plan mode. Wire per-file version history into the annotate server so the same diff UI — badge, Version Browser, block-level comments — works when annotating a standalone .md/.txt/.html file. - key history by file path (stable across edits) rather than the plan flow's heading+date slug - save the markdown (or raw HTML source) to history on each open, expose previousPlan + versionInfo + diffCurrent on /api/plan - add /api/plan/version and /api/plan/versions to the annotate server Markdown lights up end to end; HTML needs frontend follow-ups (feed the HTML source as the diff content, surface the badge on the html surface, default to source diff mode). * feat(annotate): rendered HTML version diff with inline highlights For --render-html files, render the version diff as the real page with inline <ins>/<del> highlights instead of a markdown/source diff: - add packages/shared/html-diff.ts: a tag-aware htmlDiff() that wraps changed text in <ins>/<del> while keeping tags balanced (script/style opaque). 9 unit tests. - annotate server computes diffHtml = rewriteHtml(htmlDiff(prev, current)) and exposes it on /api/plan - HtmlViewer: inject ins/del highlight CSS, add a 'Show/Hide changes' toggle in its action bar - App: store diffHtml, swap the iframe to the diff page when toggled, and suppress the markdown block-diff path on the HTML surface Commenting still works because the diff page renders through the same HtmlViewer iframe bridge. * docs(annotate): document the annotate version diff + endpoints * feat(annotate): mirror version diff into the Pi server Parity for the Pi (node:http) runtime: per-file version history, previousPlan/versionInfo/diffCurrent + diffHtml on /api/plan, the /api/plan/version[s] endpoints, and project wiring from the Pi CLI. Vendors @plannotator/shared/html-diff into pi-extension/generated. * review fixes: pi diff dependency, attr-aware tokenizer, history opt-out, hide dead version picker on HTML - apps/pi-extension/package.json: declare the 'diff' dependency — generated/html-diff.js imports it at module load, so a standalone Pi install failed to resolve it and broke every annotate session (the monorepo masked this via root hoisting) - packages/shared/html-diff.ts: tag tokenizer now consumes quoted attribute values whole, so a '>' inside title="a > b" no longer splits the tag and corrupts the diff output; 3 regression tests - annotate history is now gated by PLANNOTATOR_ANNOTATE_HISTORY / config.annotateHistory (default on) and disclosed in AGENTS.md — it writes copies of annotated files into the data dir, which users should be able to see coming and turn off - packages/editor/App.tsx: hide the sidebar Versions tab on the HTML surface — the base-version picker has nothing to drive there (the HTML diff is fixed to current-vs-previous); the viewer's Show changes toggle is unaffected * fix(ui): document content clears the badge cluster dynamically The repo/diff badge cluster is absolutely positioned in the card's top padding, sized by guesswork (py-5..py-12). One chip row fit; the diff badge's second row overflowed into the H1, and mobile wrapping made the badge sit on top of the heading. Measure the cluster (ResizeObserver) and insert exactly the clearance it needs — zero when it fits, so existing single-row layouts don't shift. Pre-existing plan-mode bug surfaced by the annotate version diff. --------- Co-authored-by: Edouard Gouilliard <edouard.gouilliard13@gmail.com> Co-authored-by: Michael Ramos <mdramos8@gmail.com> |
||
|
|
8fcecce495 |
fix(review): render tables and inline video in PR comments / descriptions / commit messages (#1007)
The lightweight MarkdownBody renderer (PR comments, PR descriptions, commit messages) had no 'table' case — pipe tables fell through to the paragraph branch and collapsed into one line of raw pipes. Add a compact table renderer reusing parseTableContent from TableBlock (one pipe parser), with inline markdown in cells. Also render a paragraph consisting solely of a link (or bare URL) to a video file (.webm/.mp4/.mov/.m4v/.ogg) as an inline <video controls> player with a fallback link — GitHub can't inline-play API-posted video links, but our review UI now can. MarkdownBody moves to its own module (it was living inside PRSummaryTab while serving three components); importers re-pointed. New markdownBody.test.tsx covers tables, escaped pipes, video links, bare video URLs, and non-video paragraphs via renderToString (runs in plain CI, no DOM needed). |
||
|
|
ca3f7607d7 |
chore(ui): move to the plannotator/atomic-editor fork (editor 0.2.0) (#1002)
Swap @atomic-editor/editor@^0.4.3 for @plannotator/atomic-editor@^0.5.0 and bump @plannotator/markdown-editor to ^0.2.0 (its peer dependency was renamed to the fork). The fork adds proper frontmatter parsing (a leading --- block no longer misrenders as an HR + giant heading) and the Obsidian-style Properties widget, with byte-fidelity preserved. Also exempt @plannotator/atomic-editor from bun's minimum-release-age gate (same first-party trust as @plannotator/markdown-editor). Verified: typecheck clean, DOM_TESTS=1 markdownEditorFidelity 4/4, full suite 1892 pass / 0 fail, review + hook builds succeed. |
||
|
|
82f5648ccb | chore: bump version to 0.22.0 | ||
|
|
3ebb28a835 |
fix(review): avatar lookups can no longer delay /api/commits
The gh/glab runner has no subprocess timeout, and the commits endpoint awaited avatar resolution before responding — a hanging network (proxy black-hole, dead DNS) held the already-computed commit list hostage to decoration. resolve() now races a 4s ceiling: on expiry the endpoint returns with the initials fallback, the in-flight fetch finishes in the background, and later calls pick its results out of the cache. Timed-out attempts memoize no misses. Shared module, so both runtimes get it. |
||
|
|
b37d10a8e0 |
fix(review): anchor commit-diff annotations to their commit in feedback
Annotations made on a commit:<sha> diff carry line numbers from that commit's diff-vs-parent, but the feedback export derived its one header from the send-time diff — annotate a historical commit, switch back to since-base, hit Send Feedback, and the agent reads those line numbers against the wrong diff. Stamp annotations with the active commit (sha + subject) at creation, mirroring the prUrl pattern, and label any anchor/header mismatch in the export (both directions). The commit-diff header itself now reads "Commit `abc1234` — subject" instead of the raw mode string. |
||
|
|
9fc2c6e9bd |
fix(review): keep the filename visible in git-status rows
Long paths in the Sections panel end-truncated, hiding the one part that matters. TruncatedPath lets the directory absorb all truncation (ellipsis lands right before the final slash); only a filename that alone exceeds the row truncates at its own end. Full path stays on the row tooltip. |
||
|
|
ed4f2e82fe |
feat(review): Guided Reviews intro dialog + Guide button discovery hint (#1000)
* feat(review): one-time Guided Reviews intro dialog + Guide button hint New GuideIntroDialog slots into the announcement chain (look-and-feel -> guide intro -> review setup, never stacking): a hero screenshot of a guided review with a brief plain-language description and a mock header strip pointing at the real Guide button. Shows once per user (cookie), and only when a Guide button actually exists (hasSearchableFiles) -- skipping the render does not consume the one-shot cookie. The hero is CDN-hosted at https://plannotator.ai/assets/guided-review.webp (committed to apps/marketing/public/assets so every marketing deploy re-ships it past the --delete sync; uploaded to S3 now so it is already live) instead of growing the single-file bundle; on load failure the image container hides entirely for a clean text-only dialog. Independently of the dialog, the header Guide button shimmers (TextShimmer) with a pulsing dot until the guide is first opened -- acked on ANY open path (button, shortcut, job auto-open), not just the button click. * fix(review): guide intro first in the dialog chain, full-width panel Chain is now guide intro -> look-and-feel -> review setup (was second); panel widened to max-w-5xl to match the look-and-feel dialog. Intro seen flag switched to the versioned-cookie pattern so future revisions can re-show it. * fix(review): latch guide-intro eligibility at first post-load render hasSearchableFiles changes on mid-session diff switches; an empty-to-non-empty switch would pop the intro over work in progress, or yank an open look-and-feel dialog (its gate reads guideIntroVisible). Eligibility is now decided once when loading clears -- dialogs only mount after that, so the latch is always set before they render. A skipped session keeps the cookie unconsumed. Also corrects AGENTS.md's chain order (guide intro first). |