mirror of
https://github.com/backnotprop/plannotator.git
synced 2026-09-14 14:17:26 +08:00
main
66 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
35c3e2bc47 |
fix(ui): drop the annotation count pill from the header decision primary (#1516)
* fix(ui): drop the annotation count pill from the header decision primary The primary reads Send Feedback / Post Comments with no inline count; the count stays in the caret menu's discard and note copy where it is needed. DecisionPrimary loses its count field, tests pin the count-free label, and the docs no longer spell the control as 'Send Feedback · n'. * chore(guides-show): sync the viewer manifest |
||
|
|
9cd6cf591b |
fix(test): isolate test-run data from contributor history (#1473)
Closes #1455. Resolves the data directory per call in shared storage and sandboxes PLANNOTATOR_DATA_DIR for every bun test via the preload, so test runs can no longer write into a contributor's real ~/.plannotator. Claude-Session: https://claude.ai/code/session_019GV6EKtzh8Pf9GA2rrBLNf |
||
|
|
36d59d3bbd |
fix(ui): vim/popover Escape ownership, ladder defaultPrevented, doc-flow accuracy (PR7 review)
F1: with vim enabled, Escape on an open ActionMenu/ApproveDropdown/ DecisionControl was consumed by useVimDocumentFocus (registered first on document, preventDefaults while reclaiming focus; the popover hook then skips the defaultPrevented event) and the menu stayed open. Open popovers now stamp data-pn-dismissable-popover and the vim hook's BLOCKING_OVERLAY_SELECTOR treats them as blocking overlays, so vim defers. Pinned in ActionMenu.test.tsx (verified failing without the selector entry). F2: the plan editor's diff-exit Escape listener gains the defaultPrevented early-return so a popover-consumed Escape no longer also exits the diff view. Audit of the remaining document-level Escape listeners found only component-internal ones (Settings theme preview, which already consumes; dialog focus traps handle Tab, not Escape) — left alone. F7: onDismiss callbacks memoized in ActionMenu/ApproveDropdown so useDismissablePopover stops re-registering listeners every render while open. F8: ActionMenu.test.tsx, PlanHeaderMenu.mobile.test.tsx, and ReviewHeaderMenu.mobile.test.tsx were DOM-gated but never registered in the CI DOM lane; added to the test.yml file list so they actually run (the F1 pin lives in a lane-registered file). F3-F5 (docs): test-opencode-review.sh stdout describes the decision control (Approve at zero) instead of the removed always-on Send Feedback button; UI-TESTING.md's platform-mode flow uses the real positional PR URL; the AGENTS.md annotate flow diagram ends on the adaptive primary like the review diagram. |
||
|
|
dd68e339fb |
docs: decision-control manual QA checklist, stack docs coherence, ui 0.38.0 handoff accuracy (PR7)
tests/UI-TESTING.md gains the Decision Control Manual Checklist (annotate file/gate/folder/last, HTML/live-app iframe dismissal, review agent + platform modes, compact/touch, sidebar general comment) and fixes the test-opencode-review.sh flow that asserted a Send Feedback button at zero annotations. AGENTS.md: the code-review flow diagram stops claiming Approve sends the literal LGTM (gone since the approve-with-notes delivery), and the shortcuts-root scope list names decisionControl.shortcuts.ts beside history.shortcuts.ts. packages/ui/HANDOFF.md's 0.38.0 line now covers the whole stack accurately: internal primitives stay off the host-supported surface, decisionControlShortcuts rides the blessed shortcuts barrel, and the ToolbarButtons muted-prop removal is internal. The stack ships as one unpublished 0.38.0 — no further version bump (spec §2.1/§9: batch the bump into PR1 and PR7; nothing was published in between). |
||
|
|
990f3e8905 |
feat(server): durable feedback archive for every submitted review (#1438)
* feat(server): archive every submitted review to a durable local feedback store Submitted feedback was only as durable as the agent session that asked for it. Code review persisted nothing at all: /api/feedback deleted the draft, settled the decision promise, and if the invoking agent had already timed out the review existed nowhere (the failure #678 fixed for annotate). Plan decisions only reached plans/ while the client-side planSave setting was on, and repeat decisions on one plan overwrote each other. Annotate kept the #678 record for single local files only. Every submission now appends one record to ${PLANNOTATOR_DATA_DIR}/feedback/{project}/index.jsonl, plus a records/{stamp}-{surface}-{decision}.md sidecar when it carries content, written at decision settlement time inside the servers so all nine agent frontends are covered by two implementations. Surfaces wired in both runtimes: plan approve and deny, code review /api/feedback (Send Feedback, Approve, LGTM) and /api/exit, annotate submit, approve and exit. Bare approvals, LGTMs and dismissals are decision-only JSONL lines with no sidecar. Records are cheap by design. Code review carries diff identity (vcsType, diffType, base, gitRef, snapshotId, cwd, PR metadata, changed-file count, patch byte count) and never the patch bytes; plan records carry the decision text plus a reference to the history/{project}/{slug}/NNN.md version the decision was made on rather than a second copy of the plan. Annotation provenance (source, author) is preserved, so external, review-agent and WebMCP findings stay tagged and source == null selects the reviewer's own comments. The shared module never throws: an archive failure is logged, degrades silently for the user, and keeps the annotation draft as the recovery copy. The append happens before deleteDraft, generalizing the #678 ordering. Controlled by PLANNOTATOR_FEEDBACK_HISTORY / feedbackHistory (default on). PLANNOTATOR_ANNOTATE_HISTORY=0 additionally suppresses records for every annotate surface, so the documented stateless-annotate promise still holds. "feedback" is added to PURGE_OWNED_TOP_LEVEL so uninstall purge removes it. AI-assisted (Claude) under maintainer direction. * fix(server): stop the feedback archive from writing into the real data dir in tests Review findings on the durable feedback archive. 1. The archive is default-on, and most server tests boot a real plan, review, or annotate server without redirecting PLANNOTATOR_DATA_DIR, so `bun test` deposited records in the contributor's own ~/.plannotator/feedback (24 files across 12 buckets from two test files alone) on CI and every machine. A new bunfig test preload, tests/setup/feedback-archive-off.ts, turns the archive off for the suite; the archive's own tests opt back in inside their test bodies, which is also how they exercise the opt-out. Those tests now use distinctive project names and remove the annotate history they leave in the real data dir, since storage.ts fixes its data directory at import time. 2. PR reviews bucketed under feedback/pr-<n>/. PR mode never sets gitContext and --local points agentCwd at a pool/pr-<n> checkout, so deriving the project from the review cwd was wrong. ReviewServerOptions now takes a `project` option, mirroring the annotate server, preferred over the cwd derivation on both runtimes; the Claude Code, OpenCode, and Pi entry points pass their already-computed detectProjectName() result. 3. changedFiles overcounted renames: extractChangedFiles unions the a/ and b/ sides so a reader can resolve either path. The record now counts b-side paths through countChangedFiles, so a rename is one file. 4. Docs: the feedback archive is added to the privacy page and PLANNOTATOR_FEEDBACK_HISTORY (plus PLANNOTATOR_ANNOTATE_HISTORY) to the environment variables reference. The overclaim that every submitted review is archived is corrected: a review posted straight to GitHub or GitLab through /api/pr-action is not archived locally yet. Three behaviors are now written down: O_APPEND is not atomic on NFS or SMB and a genuine interleave damages both records that raced, folder-session records carry the folder path rather than the open document, and URL-session records store the full URL including its query string. 5. Pi parity: the Node mirror now has the failed-archive-write test (the one invariant its handler copies by hand) and the PR-mode bucketing test. Comments only, no behavior change: the pool checkout recorded in target.review.cwd can be cleaned up before anyone reads the record, and getPlanVersionPath resolves the data directory storage.ts captured at import while the archive resolves it per call. AI-assisted (Claude) under maintainer direction. * docs(server): make the feedback index an explicit multi-client contract plannotator-tui will append to the same feedback/{project}/index.jsonl with client "plannotator-tui", so the module's stance of "a client tool may emit this shape under its own clients/ namespace" is out of date. The index is one shared source of records, labeled by client. 1. The module docstring and the FEEDBACK_RECORD_CLIENT comment now describe the shared index: several tools append to the same file, separated by `client`; plannotator-tui is a known second writer, herdr-annotate is reserved, and `client` is an open set rather than an enum to validate against. 2. Two optional fields are declared so v1 reserves their names across clients: target.agent ({ host, session, transcript }) for surfaces whose subject is an agent session rather than a file or a diff, and top-level clientVersion. Neither is populated here. clientVersion stays unset deliberately: there is no runtime-agnostic version constant in packages/shared, and reading package.json from a vendored module would be a new filesystem dependency for cosmetic data. 3. Sidecar naming is documented at the naming site and in AGENTS.md: other clients suffix their id ({stamp}-{surface}-{decision}-plannotator-tui.md), so recordFile values carrying such suffixes are valid and nothing may parse a sidecar name. Nothing in this repo did: every consumer treats recordFile as an opaque handle and no test pins a filename pattern. A new test appends a foreign line (unknown client, unknown fields, suffixed recordFile) and pins that the reader keeps it. 4. Honesty fix to the atomicity comments, in code and in AGENTS.md: appendFileSync loops internally, so "one write syscall" was wrong even on a local filesystem. The real model is that a line-sized buffer handed to a single append-mode write completes without interleaving in practice locally, with the reader's skip-unparsable tolerance as the backstop and the NFS/SMB caveat unchanged. 5. Exhausting the sidecar collision counter now throws a named error instead of re-throwing a bare EEXIST, so the server log says what actually happened: 100 taken names in one millisecond means a stopped clock or a runaway writer, not a transient disk problem. 6. AGENTS.md and the parseFeedbackIndex doc state the reader contract: lines are gated on a numeric `v` and unparsable ones are skipped, so analyzers that depend on v1 semantics should filter v <= 1 themselves. Fields are added, never repurposed, so a v2 would mean a real shape change. AI-assisted (Claude) under maintainer direction. |
||
|
|
8e0c51f5ff |
fix(ui): 0.33.0 adoption feedback and bump @plannotator/ui to 0.34.0 (#1402)
Follow-ups from the Workspaces adoption of 0.33.0: a math-slot module hosts can redirect Mermaid's own katex import to (importer-scoped resolveId recipe in HANDOFF) so a math document fetches one KaTeX chunk owned by the host; HtmlViewer bridgeErrorDisplay ('banner' default, 'none' lets a host own the failure banner while onBridgeUnavailable still fires); the documented bridge alias narrowed to relative sibling imports; resetMathRenderer keeps a registered loader and discards stale in-flight loads via an epoch, with setMathRendererLoader(null) and getMathRendererLoader added. Plannotator's own bundles unchanged (markers and sizes within noise of main). Bumps @plannotator/ui to 0.34.0; core stays 0.25.0.
AI-assisted (Claude) under maintainer direction.
|
||
|
|
e807e2b89c |
fix(ui): 0.32.0 adoption feedback from hosts (#1399)
Follow-ups from the Workspaces adoption of 0.32.0: the default KaTeX loader moves to its own module so a host that registers a mathRendererLoader can alias it away and drop the unused KaTeX chunk (a registered loader is never backfilled by the default); docs state when onUnanchoredChange first delivers, what projectHostThreads loses on markdown surfaces (export ordering, line labels, repeated-text disambiguation, no-flash restore) while still re-anchoring by text search, and that cap-dropped handling is a backstop once maxAdditionalTargets is enforced upstream; HANDOFF.md now ships in the tarball so the README references resolve. Plannotator's own behavior is unchanged (the eager entry fills the slot before first render; built-bundle markers and sizes match main). AI-assisted (Claude) under maintainer direction. |
||
|
|
7d6dd29c08 |
perf(ui): load the HTML viewer bridge by URL for hosts, with a protocol version and ready timeout (#1398)
Opt-in bridgeScriptUrl on HtmlViewer so multi-chunk hosts can serve the 185 KB bridge as a hashed asset instead of an inlined string; the inline bridge stays the default and Plannotator's own builds, the Pi and OpenCode copies, and the live-app proxy are unchanged apart from a protocolVersion field on the bridge's ready message. The parent checks the version (one warning naming both versions; on the URL path a dismissible banner plus onBridgeUnavailable while the old bridge keeps working), arms a ready timeout on the URL path only, and resolves the URL against the parent document before it reaches the frame so a page's own base href cannot redirect the load. A prepack-generated bridge-script.asset.js (byte-for-byte the inline string) and a bridge-script.lite.ts alias target ship in the tarball. CSP and CORP requirements for hosts are documented. AI-assisted (Claude) under maintainer direction. |
||
|
|
0b167cc478 |
perf(ui): lazy diagram and math renderers with eager entries for Plannotator (#1394)
Bundle-weight optimization of @plannotator/ui for multi-chunk hosts, requested by Workspaces: the Mermaid runtime and Graphviz engine load inside the render effect, the username dictionary sits behind a synchronous identity generator slot, and KaTeX sits behind a math renderer slot with a loader seam on configurePlannotatorUI. Plannotator's own apps import eager entries (math, identity, and Mermaid for the plan editor) so their behavior is unchanged: single-file builds within noise of main, math typeset on first paint, identities from the full dictionary, and the share portal keeps Mermaid in its entry chunk so its failure surface matches main. Built-HTML registration markers guard the eager imports. Hosts that omit the eager entries get the lazy paths, a one-shot automatic re-attempt, and a Retry affordance on the diagram error panel; the module-map limitation of in-page retries is documented. AI-assisted (Claude) under maintainer direction. |
||
|
|
6903d7a3dd |
feat(webmcp): expose plan review and annotate as WebMCP tools for browser agents (#1393)
Phase 1 of WebMCP support: a zero-dependency, feature-detected engine in packages/ui/webmcp plus a read-and-comment tool catalog for plan review and annotate (read_document, add_comments, update_comment, remove_comments, reveal, nudge_user, list_documents). No decision tools; the human approves. Zero footprint in browsers without document.modelContext (DOM, network, console, timers, and cookies identical to main), idle until called where the API exists, and never registered inside the annotate iframes. Adds an optional inReplyTo field on annotations for threaded replies. Client-only; no server changes. AI-assisted (Claude) under maintainer direction. |
||
|
|
e206a1f5e8 |
fix(review): infer the jj line-of-work base from the fork point (#1365)
* fix(review): detect JJ mutable line-of-work base Use JJ's mutable-stack revset to find the line boundary directly instead of inferring a parent from bookmark ordering, which is ambiguous because JJ has no current bookmark. * fix(review): harden the JJ line-of-work base inference Maintainer follow-up on the line-of-work base detection. Skip the bookmarks `jj git push --change` generates. They name one change, not a line of work, and they do reach the fork point: a colleague's pushed change bookmark arrives as an untracked remote bookmark, which makes its commit immutable and therefore a candidate base, so the reviewer was told they were comparing against `push-vmopwunwxopv@origin`. The commit id is used instead. Pass a full commit id through `jjCompareTargetRevset` as a revision. It has no separators, so the commit-id fallback was being wrapped as `bookmarks(exact:"<sha>")`, which resolves to no revisions and made the whole Line of work diff fail. Fall back to `trunk()` instead of throwing. The only live caller is `getJjContext` on the review startup path, which has no handler above it, so a throw aborted `plannotator review` with a stack trace before the server was built rather than reporting anything. That also covers a `jj` too old for `fork_point`/`reachable`. Make the query explicitly single-record with `latest(..., 1)`. The parser reads one record, and bookmark preference (remote before local) is only meaningful within one commit, so the tie-break belongs in the revset rather than in a silent "first row wins" slice. Isolate the real-jj test behind its own JJ_CONFIG. It was reading the developer's real config, where `[signing] behavior = "own"` alone makes it fail with a GPG error. Live fixtures cover the generated-push-bookmark stack, the untracked remote push bookmark, and a stacked local bookmark. --------- Co-authored-by: Michael Ramos <mdramos8@gmail.com> |
||
|
|
e1ce7dabe1 |
feat(ui): Totman/Classic P favicon style switcher (#1325)
Favicon style switcher in Settings > Theme: the Totman mascot or the historical dark-navy P tile (byte-identical to the pre-Totman asset, sha256 pinned). Served server-side from first paint in both runtimes; opt-in for hosts of the published UI package. Contributed by @FNDEVVE |
||
|
|
8e88dcec8c |
fix: v0.27.2 pre-release QA batch (mobile TOC, dialog bounds, seed guard) (#1311)
* fix(plan): make the compact TOC scroll the document again The compact navigator overlay rendered outside App's ScrollViewportProvider, so the TableOfContents it hosts resolved a null viewport and every "jump to heading" tap was a silent no-op on phones. The provider is context-only, so hoisting it above the overlay fixes the lookup without touching desktop DOM structure or order. * fix(plan): scope the permission-mode chooser to plan review and bound its card The one-time chooser fired in every non-goal-setup Claude Code session, so annotate, annotate-last, annotate-folder and archive reviewers got a blocking dialog about what happens after plan approval. Gate it on plan review, which is the absence of a mode field in the /api/plan payload. The card itself was hand-rolled with no height cap and no internal scroll, so on a short landscape phone it overflowed both edges of a modal that has no dismiss control. Give it the same bounded shell the sibling one-time dialogs use: safe-area padding, a visible-viewport max height, and the option list as the only scrolling region. Content and cookie behavior are unchanged. * fix(review): never seed Tree over a persisted panel view The first-run initializer gated only on the setup-seen cookie, but sessions that never reach it (non-git, workspace, PR, no since-base) still let Settings persist a panel view. A reviewer could hold an explicit Git status choice with "seen" unset, and the next plain git session seeded Tree over it. Treat a persisted view as the decision: consume the one-time setup and write nothing. * fix(comments): give the geometry-forced composer a working Escape When the anchor has no room the position tracker forces dialog mode. On a fine-pointer viewport Escape took the collapse branch, the tracker instantly re-forced the dialog, and the keystroke was eaten; the Collapse button bounced the same way. Track forced expansion separately from the preferred kind: in that state Escape closes (draft-preserving) and Collapse is hidden, because collapsing is geometrically impossible. * fix(review): stop the compact Editor tab editing the desktop diff style The dock's Split/Unified control returns null under the compact touch layout, but the Settings copy of it kept rendering while the phone showed the session-only unified diff. It looked dead and silently rewrote the persisted desktop preference. Hide it on compact and state what the session is doing; the prop defaults to false, so the plan editor and desktop are untouched. * fix(portal): give the share portal the mobile app shell The portal mounts the same plan editor App as the hook but kept the pre-mobile entry document: no viewport-fit=cover (so every safe-area token was inert) and a min-h-screen body without the shell's scroll ownership. Mirror the hook's body class, root class, and viewport meta, and extend the entry-asset pin to cover the portal alongside them. * fix(plan): keep compact overlays out of the printed document The compact plan stage and the compact navigator are full-viewport transient surfaces with no print-hide marker, so printing on a touch device with Annotations, Ask AI, Versions or Archive open clipped the document behind them. Mark both with data-print-hide, which print.css already hides. The desktop rail is untouched. * fix(plan): give the selection toolbar real touch targets Copy / Delete / Comment / quick label / looks-good / Cancel measured 28x28 with 2px gaps on a phone because the toolbar never got the touch-target markers the rest of the stack uses. Stamp them on its buttons and add a compact-scoped gap so adjacent destructive and comment actions are not a mis-tap apart. Both are inert outside the compact scope, so desktop geometry is unchanged. * docs: keep the new QA-batch comments free of em dashes |
||
|
|
e181b824cc |
Mobile-safe plan and code comment composition (#1297)
* feat: harden mobile comment composition * fix(ui): keep mobile app inside Safari viewport * docs: record physical mobile triage * fix(ui): extend plan canvas behind Safari controls * fix(ui): let mobile plans drive Safari chrome * fix(ui): release Safari top edge on mobile plans * docs: triage mobile feedback and close phase 1b * fix(ui): harden compact touch behavior |
||
|
|
c08b188812 |
perf(ui): single Shiki highlighter, palette-matched code blocks, drop highlight.js (#1218)
* perf(build): stub out the dead Oniguruma WASM in every bundle
@pierre/diffs picks its Shiki engine with a runtime ternary:
engine: preferredHighlighter === "shiki-wasm"
? createOnigurumaEngine(import("shiki/wasm"))
: createJavaScriptRegexEngine()
Plannotator pins `preferredHighlighter: 'shiki-js'` (and Pierre's own
default is 'shiki-js'), so the Oniguruma branch never executes. Because
the choice is a runtime ternary, bundlers keep the `import("shiki/wasm")`
edge anyway and inline `@shikijs/engine-oniguruma/wasm-inlined`, a
~622 KB base64 blob, into the single-file HTML builds. The review app
paid for it twice: once on the main thread (via
`highlighter/shared_highlighter.js`) and once inside the `?worker&inline`
Pierre worker.
Alias `shiki/wasm` to a stub that throws if it is ever reached. Wired via
`resolve.alias` rather than a plugin because `resolve.alias` is shared
with Vite's worker build and `plugins` are not.
Highlighting output is unchanged: the JS regex engine was already the one
doing the work. Opting back into 'shiki-wasm' now fails loudly instead of
silently costing every user a megabyte of dead bytes.
apps/review/dist/index.html 19,424,646 -> 18,180,545 (-1,244,101 raw / -463,348 gzip)
apps/hook/dist/index.html 23,032,467 -> 22,410,416 (-622,051 raw / -233,485 gzip)
* perf(ui): consolidate code highlighting onto Shiki, drop highlight.js
The app shipped two highlighters. Shiki already tokenised the code-review
diff pane (via @pierre/diffs, JavaScript regex engine); highlight.js
separately coloured markdown fences and review suggestion snippets at
~982 KB minified for a full build of ~190 grammars. That second
highlighter is now gone.
Every call site moves onto `packages/ui/utils/codeHighlight.ts`, a thin
wrapper over Pierre's SHARED Shiki instance:
CodeBlock, Viewer, PlanCleanDiffView markdown fences
InlineMarkdown code-file hover preview
HighlightedCode review suggestion snippets
Reusing Pierre's instance rather than standing up a second fine-grained
one is deliberate. Pierre imports Shiki's full bundle, so every grammar
and theme is ALREADY inlined in the single-file builds: a separate
highlighter with a curated language list would have duplicated a subset
of bytes that are already there. Sharing costs nothing, gives every
language Shiki bundles instead of a shortlist, and — the point of the
change — guarantees fences resolve the exact same theme the diff pane
resolves.
Theming. `SHIKI_THEME_MAP` / `resolveSyntaxTheme` move from
`packages/review-editor/hooks/usePierreTheme.ts` to
`packages/ui/utils/syntaxTheme.ts`; usePierreTheme re-exports them, so
the review editor's imports are unchanged. `useFenceTheme()` feeds the
components and re-highlights on palette or mode change. Code blocks now
follow the active palette across all ~52 themes in both light and dark,
instead of always rendering github-dark and relying on hand-written
`.hljs-*` override stacks to stay legible. Those stacks are deleted:
`packages/editor/index.css`'s light-mode token palette, and
`colorblind.css`'s hand-tuned tokens which existed to APPROXIMATE
@pierre/theme's protanopia-deuteranopia themes that are now simply used.
Behaviour held fixed:
- Language-less fences stay plain text (#1212). No auto-detection
anywhere, including the hover preview, which previously called
`hljs.highlightAuto`. `HighlightedCode` derives its language from
the caller's file path; an unknown extension renders plain.
- `applyHighlight(el, ...)` keeps the imperative `hljs.highlightElement`
DOM contract the annotation layer reaches into, and writes plain text
at final size first so async highlighting causes no layout shift.
Already-attached grammars highlight synchronously — no flicker on
cached highlights.
- It also verifies the rendered text is byte-identical to the source
and falls back to plain otherwise, because annotations address code
blocks by text offset.
- `@plannotator/ui`'s public API is unchanged: the highlighter is a
module-level default like the package's other seams, no new props.
The `hljs` class on fenced `<code>` becomes `pn-code` (it is a
structural hook for blockTargeting, vim navigation and print.css, and it
named a library we no longer ship). `language-*` stays.
apps/review/dist/index.html 18,180,545 -> 17,270,889 (-909,656 raw / -291,921 gzip)
apps/hook/dist/index.html 22,410,416 -> 21,704,434 (-705,982 raw / -238,096 gzip)
Verified the diff pane is untouched: the rendered Pierre shadow-DOM
markup is byte-for-byte identical between an origin/main build and this
one (SHA-256 aa1ee88a…).
* fix(ui): strip stray NUL bytes from the code-highlight source
Two U+0000 bytes slipped into comments in the previous commit, which made
git treat the file as binary. Replaced with spaces; no behaviour change.
* fix(ui): keep code-block annotation marks across highlight swaps
Fenced code is annotated by hand: one `<mark data-bind-id>` inside the
`<code>` element, which `applyHighlight` also owns. Every highlight swap
(palette change, dark/light toggle, or the first async grammar attach
after load) replaces that element's children, so the mark was silently
wiped and nothing put it back. Annotation state, the sidebar panel and
exports were unaffected; the loss was purely visual, and deterministic.
`applyHighlight` now publishes every write through `onCodeHighlightSwap`,
synchronously, immediately after it. `Viewer` subscribes and re-paints the
fence's mark, so a swapped block ends up with BOTH the new theme's tokens
and its annotation. The shared painter (`paintCodeBlockMark`) moves the
token spans into the mark instead of flattening them to text, so creating
an annotation no longer costs a block its colours either.
Being driven by the swap also fixes the cousin race by ordering rather
than timing: share/draft restore runs on a timer after load, and on a slow
machine the first async swap could land after it and wipe the restored
marks per block. A restore that painted before the swap is now
re-established in the same task the swap ran in, and one that runs after
finds the mark already there.
Removal tombstones the id before re-highlighting, because the host drops
the annotation from state a tick later — without it the swap listener
would paint the just-removed annotation back in, and a fence carrying a
second annotation would end up bare.
Also closes the named gap in the WASM coverage: entry-assets only grepped
source, so a future @pierre/diffs bump could reintroduce the inlined blob
through a different import specifier unnoticed. It now greps the built
`apps/{review,hook}/dist/index.html` for the base64 WASM magic, skipping
on an unbuilt checkout and running for real in the CI job that builds the
bundles.
|
||
|
|
7cd023cbc7 |
docs: correct privacy and network claims (#1163)
* docs: correct privacy and network claims * docs: address privacy review findings * docs: clarify GitLab avatar lookup concurrency |
||
|
|
47157e7a55 |
feat(editor): add Vim keyboard annotation controls and live HUD (#1127)
* feat(editor): add Vim keyboard annotation controls * feat(ui): add optional live Vim HUD * feat(ui): finish Vim HUD experience * feat(ui): promote Vim to dedicated settings panel * feat(ui): make Vim document focus automatic * feat(ui): let Vim HUD hide its key panel * fix(ui): harden Vim selection UX |
||
|
|
98fa3b2173 | Restore production Totman favicon (#1081) | ||
|
|
8597ca3563 | Use the tight-fit Totman favicon (#1071) | ||
|
|
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> |
||
|
|
17b862339d |
chore(test): use loadable image URLs in the LaTeX/media fixture
The <img>/<picture> examples pointed at example.com placeholders that render broken. Point them at picsum.photos / placehold.co so a manual run actually shows images loading (and still proves the tags parse as media, not text). |
||
|
|
16db952000 |
fix(ui): stop unclosed math from pairing with a stray delimiter in a later code fence
The unclosed-$$/\[ guard scanned ahead to EOF for the closing delimiter, so an unterminated opener could match a $$ (or \]) that appears far below - e.g. inside a code fence - and swallow every heading/paragraph in between into one broken math block. The scan now stops at a blank line: real display math has no blank line before its close, so a blank both confirms "unclosed" and keeps the search from reaching distant delimiters. Adds a regression test and a manual LaTeX + media test fixture. |
||
|
|
740d6fb2eb |
Add WebTUI agent panel to annotate mode (#941)
* feat(annotate): add WebTUI agent terminal * feat(annotate): wire WebTUI agent into annotate UI * docs: recap annotate agent terminal work * fix(annotate): harden agent terminal runtime * docs: add annotate agent terminal runtime ADRs * fix(annotate): polish agent terminal integration * fix(ui): preserve comment draft on Ask AI failure * fix(annotate): address terminal review findings * fix(annotate): harden agent terminal runtime fallback |
||
|
|
1287bfa5d8 |
Disable bunfig autoload for release binaries (#937)
Add Bun's compile autoload guard to every release binary build so distributed executables do not inherit bunfig.toml from the caller's current directory. Extend the binary smoke test to launch --help from a temporary directory with an invalid preload entry, matching the crash reproduction. Update the Codex sandbox manual compile path so locally rebuilt binaries use the same guard. Co-authored-by: Codex <codex@openai.com> |
||
|
|
f564448650 |
fix(annotate): block symlink escape in HTML asset serving + cleanups (#927)
* fix(annotate): resolve symlinks before HTML asset containment check The /api/html-assets route and the share-payload inliner checked path containment lexically, so an in-directory symlink pointing outside the HTML's folder (e.g. evil.css -> ~/.ssh/id_rsa) passed the check and was served or base64-inlined into the share payload. In remote mode the inliner auto-fires at startup, so this could upload symlinked local files to the paste service with no user action. Resolve symlinks with realpathSync on both the asset and the root before the relative-path check, in all three runtime copies (Bun route handler, shared node inliner, Pi route handler). Non-existent assets fall back to the lexical path and 404 on read. Adds regression tests for both sinks. * test(pi): build rich git state in sandbox-pi.sh for review diff modes Expand the Pi sandbox harness to create multiple commits, a feature branch, and a rename+delete+modify commit so /plannotator-review can exercise every diff mode (uncommitted, staged, branch, merge-base). * docs: fix broken verification link in READMEs The READMEs pointed at a non-existent anchor (installation/#verifying-your-install); the verification guide is a standalone reference page. Point to /docs/reference/verifying-your-install/ (and split the hook README's link so version pinning -> installation, verification -> the reference page). |
||
|
|
3de555f5e5 |
Fix OpenCode plugin runtime compatibility (#849)
* fix(opencode): add host-compatible runtime bridge * fix(opencode): preserve parity in cli bridge * test(opencode): add isolated sandbox launcher * test(opencode): keep reusable sandbox launchers * test(opencode): export local plugin default * test(opencode): install OpenChamber deps when needed * test(opencode): avoid OpenChamber default port collision * fix(opencode): harden cli bridge fallback * test(opencode): clean isolated sandbox helpers |
||
|
|
4272312eb0 |
Add jj evolog diff mode to code review UI (#702)
* review: add jj evolog diff mode to code review UI Adds a new "Evolution diff" diff type for jj repos that shows what changed between two evolutions (amendments) of the current change — i.e. what was amended since a prior state of `@`. Changes: - Add `JjEvoLogEntry` type to `review-core.ts` and export from `shared/types.ts` - Add `getJjEvoLogEntries()` to `jj-core.ts` — runs `jj evolog` with a tab-delimited template to collect commit ID, description, and age for each historical state of `@` - Add `jj-evolog` to the `DiffType` union in `review-core.ts` and to the `JJ_DIFF_TYPES` set in `vcs-core.ts` - Wire evolog mode into `runJjDiff()` and `getJjDiffArgs()` — defaults to the second evolog entry (the most recent prior state) when no explicit base is given; propagates `error` when there's no history - Expose `jjEvologs` on `GitContext` and include the "Evolution diff" option in `diffOptions` only when two or more entries exist - Add `EvoLogPicker.tsx`: a Radix Popover component that lists evolog entries and lets the user pick which prior state to compare against; shows commit ID, description, and age with a "default" badge on the auto-selected entry - Update `FileTree.tsx` to render `EvoLogPicker` instead of `BaseBranchPicker` when `jj-evolog` is the active diff type - Update `App.tsx` to thread `jjEvologs` and `detectedEvoBase` through to the file tree, auto-set the base to the second evolog entry on mode switch, and show an appropriate empty-state message - Update `agent-review-message.ts` with an inspect hint for the evolog diff type - Add unit tests covering: diff args, evolog output parsing, error cases, and the auto-base-selection fallback path * review: fix evolog picker overflow and add evolog test sandbox Fix the EvoLogPicker trigger and popover rows to handle narrow sidebar widths — age strings now truncate instead of bleeding out of the container. Add --with-evolog flag to the JJ manual test sandbox that amends the working-copy change four times to create realistic evolution history. Also writes a create-evolog.sh helper script into the sandbox for on-demand evolog creation without the flag. * fix: restore compare target when switching away from jj-evolog When leaving Evolution diff mode, the evolog commit ID was left in selectedBase and leaked into the next diff request. For jj-line this caused jjLineBaseRevset() to treat the commit ID as a bookmark name, producing a "didn't resolve to any revisions" error. Now explicitly passes the restored default branch as baseOverride to fetchDiffSwitch when leaving evolog, avoiding the React state batching race that made the previous setSelectedBase-only approach insufficient. --------- Co-authored-by: Michael Ramos <mdramos8@gmail.com> |
||
|
|
6e3efe8e7d | Update Codex hooks feature flag (#708) | ||
|
|
fcf2ba4cf5 |
fix: indent loose list continuation content under parent bullet (#705)
Closes #704 |
||
|
|
13c667c044 |
feat(hook): PFM reminder & improvement hook support across all runtimes (#689)
PFM reminder & improvement hook support across Claude Code, OpenCode, and Pi. - Add opt-in PFM reminder (pfmReminder config flag) injected on EnterPlanMode - Wire composeImproveContext() into all three runtimes - Fix OpenCode system.transform array reference bug (pushes were going to dead array) - Fix install scripts silently stripping PreToolUse/EnterPlanMode hook entry - Isolated Pi sandbox testing (--no-extensions -e) |
||
|
|
69ef11bdfb |
feat(review): add jj review workflows (#675)
* feat(review): add jj support for local diffs * feat(review): add jj review workflows * fix(review): tighten jj diff defaults * test(review): add jj manual sandbox * fix(review): share jj agent diff prompts * fix(review): quote jj agent revsets * feat(review): share jj vcs handling with pi * fix(review): tighten jj bookmark and pi pr handling * fix(review): tighten jj defaults and detection * fix(review): harden jj diff and vcs detection --------- Co-authored-by: Michael Ramos <mdramos8@gmail.com> |
||
|
|
84a0b434f9 |
fix(ui): smart resolution + existence-validation for code-file paths (#654)
* fix(ui): smart resolution + existence-validation for code-file paths
The bare-prose / backtick path detector linkifies anything that looks
like a code path. Two failure modes regularly produce dead links: prose
abbreviations like `editor/App.tsx` (real file is
`packages/editor/App.tsx`) and references to files the plan proposes
but hasn't created yet. Both 404 on click with no UX cue.
Resolves abbreviated paths via a case-insensitive suffix-match against
a cached project walk (`resolveCodeFile` in `packages/shared/resolve-file.ts`),
mirroring what `resolveMarkdownFile` already does for markdown. The walk
is pre-warmed when the plan/annotate server boots and on every
`/api/doc` request, with a 30s TTL so newly-created files can resolve
mid-review. Storing the walk as a Promise makes the cache race-safe —
concurrent callers piggyback rather than starting a second walk.
A new `POST /api/doc/exists` endpoint takes a batch of candidate paths
and reports `found` / `ambiguous` / `missing` / `unavailable` per path.
On the frontend, `useValidatedCodePaths` extracts candidates from the
markdown on load and POSTs once. The renderer reads the result via
`CodePathValidationContext`: `found` opens directly with the resolved
absolute path, `ambiguous` opens a `CodeFilePicker` popover listing all
matches (common in monorepos where `App.tsx` exists in several
packages), `missing` demotes the link to plain code, and `unavailable`
falls back to the optimistic linkification we have today. While
validation is in flight, every detected path renders as a link, so
first paint is unchanged.
The detection itself gets a shape filter (`isPlausibleCodeFilePath`)
that hard-rejects shell brace expansion (`{a,b}`), glob wildcards, and
whitespace, while explicitly allowing `[` / `]` so Next.js dynamic
routes (`app/[slug]/page.tsx`) still resolve. The bare-prose regex moves
out of `InlineMarkdown.tsx` into `code-file.ts` so the renderer and the
new server-side extractor use the same source of truth, and the
extractor strips fenced code blocks, HTML comments, and URL ranges
before scanning so it only emits candidates the renderer would actually
paint.
Pi extension mirrors the Bun changes (handler upgrade, pre-warm,
`/api/doc/exists` route). When the popout's `/api/doc` request 404s the
dialog now surfaces "File not found in repo: <path>" instead of
silently swallowing the error.
Tests: `code-file.test.ts` extended with shape-filter and Next.js-route
cases; new `extract-code-paths.test.ts` covers extraction, dedup,
fenced/HTML/URL exclusion, and the URL-with-parens regression; new
`resolve-file.test.ts` covers the suffix-match strategy, leading `./`
handling, ambiguous results, and ignored-dir behavior.
* fix(ui): thread doc-base through code-path validator
Out-of-tree linked docs (and annotate-mode files outside cwd) reference
files relative to themselves. The validator was resolving against cwd
only, so those paths got marked missing and the renderer demoted them
to plain text — even though clicks still resolved correctly via base.
Also tightens the suffix-match's leading-segment strip so `../foo.ts`
no longer silently misresolves to an unrelated `foo.ts` in cwd.
Cleanup: delete unused extract-code-paths import in reference-handlers,
add the export entry to packages/shared so consumers don't rely on
Bun's lenient subpath resolution. Add TODO(security) comments at both
handleDocExists sites flagging that absolute paths bypass project-root
containment.
223 tests pass (3 new resolver cases for baseDir + ../ regression).
* refactor(editor): dedupe activeDocBaseDir; expand security TODO
Self-review fallout:
1. The doc-base expression `linkedDocHook.filepath ? dirname(...) :
imageBaseDir` lived in two places (click-time URL builder and Viewer
prop). If they drift, validator and click resolve against different
bases and we silently re-introduce the demote-correct-link bug.
Extract to a single useMemo.
2. The handleDocExists security TODO mentioned absolute paths in
`paths[]` but I just added `base` acceptance, which has the same
shape of leak (hostile sender supplies base=/secret/dir + relative
path). Both vectors flagged in one TODO, mirrored Bun + Pi.
223 tests pass; both builds clean.
* fix(ui): code-file popout shows real error; misc consistency
Review fallout:
- `CodeFilePopout` hardcoded "File not found in repo" regardless of
cause. The hook already captures the server's error string, so an
ambiguous-path 400 (which can happen if a user clicks an optimistic
link before validation completes) was surfacing as a misleading
not-found message. Render the actual `error` and only show the
planned/future-file caveat when the error matches "file not found".
- `InlineMarkdown` emitted demoted bare-prose paths as raw strings
while every other plain-text branch in `emitPlainTextWithBareUrls`
routes through `transformPlainText`. Cosmetic-only today since
paths rarely contain transformable content, but the divergence
invites copy-paste rot. Routed through the same helper.
- CLAUDE.md missed the new POST /api/doc/exists endpoint in both
Plan Server and Annotate Server tables. Added.
223 tests pass; both builds clean.
* fix(ui): demote paths the extractor excluded from validation
When the validator is ready but a candidate path has no entry in the
validated map, the extractor intentionally excluded it — e.g. inside
an HTML comment or fenced code block. The renderer was optimistically
linking these because gateCodePath returned 'link' for missing entries.
Found during manual testing: `<!-- packages/editor/App.tsx -->` inside
a paragraph (parser doesn't recognize HTML comments as block-level)
was rendered as a clickable link. The extractor correctly stripped the
comment, but the renderer's optimistic fallback overrode that.
Also adds manual test harness: tests/manual/path-detection/ with
sandbox setup + three launcher scripts (plan mode, annotate in-tree,
annotate out-of-tree) covering ~30 test cases.
223 tests pass; both builds clean.
* fix(ui): skip HTML comments in InlineMarkdown scanner
The parser doesn't recognize <!-- --> as block-level HTML, so comments
inside paragraphs fall through to InlineMarkdown. The scanner then
finds paths inside the comment text and linkifies them.
The previous gateCodePath fix (demote when not in validated map) didn't
help here because the same path appeared elsewhere in the document —
the map had an entry from the non-comment occurrence.
Fix: match <!-- ... --> at the top of the scanner loop and skip the
entire comment. HTML comments should be invisible per CommonMark spec.
* fix: handle unavailable variant in markdown resolve narrowing
The shared ResolveResult type gained an `unavailable` variant for code
files. The markdown resolver never returns it, but TS can't narrow
past it without an explicit guard. Both Bun and Pi handlers now guard
`not_found || unavailable` before accessing `result.path`.
|
||
|
|
a22a744749 |
Add Codex Stop-hook plan review (#577)
* feat: add codex stop hook plan review * Install Codex plan review hooks * Remove Codex manual test screenshots * Update Codex plan mode docs * Tighten Codex release readiness * Preserve custom Codex hook wrappers * ci: smoke test release artifacts * ci: reduce release smoke flake risk * fix: keep Codex last-message extraction to output text * ci: poll release smoke servers on loopback * ci: skip macOS release smoke jobs --------- Co-authored-by: Michael Ramos <mdramos8@gmail.com> |
||
|
|
5a41a21861 |
fix(ui): in-page anchor navigation for headings (#605)
* fix(ui): in-page anchor navigation for headings
Intercept `#`-prefixed link clicks and smooth-scroll inside the sticky
scroll viewport instead of letting the browser jump (which broke when the
content lives inside a scrollable container). Headings now expose both a
canonical slug and a legacy slug via `data-anchor-aliases` so older shared
URLs keep resolving. Also listen for `hashchange` / initial hash so direct
links land on the right heading.
Adds `packages/ui/utils/anchors.ts` with `normalizeAnchorText`,
`decodeAnchorHash`, `slugifyHeadingAnchor`, `legacySlugifyHeadingAnchor`,
and `getHeadingAnchorAliases` (+ tests).
Also ignore `.serena/` tooling state.
* refactor(ui): drop unjustified slug aliasing
Anchor navigation didn't work before this branch, so no shared URLs exist
using any slug format — the legacy slug alias was preserving nothing.
The canonical slug was also a redefinition: heading ids are already
written by `utils/slugify.ts` (unicode-aware, `\p{L}\p{N}`). The new
ASCII-only `slugifyHeadingAnchor` would miss unicode headings ("Café"
writes `id="café"` but the nav code looked for `cafe`).
Nav path is now just: decode the hash and `getElementById`. The
sticky-header offset math — the part that actually mattered — stays.
* fix(ui): anchor-nav edge cases — share-hash collision and raw HTML blocks
Two issues flagged by cross-model review of
|
||
|
|
1338802a58 | Scope OpenCode submit_plan to planning agents (#571) | ||
|
|
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. |
||
|
|
14bff361f8 |
fix(opencode): reuse local server for review flows (#567)
* fix(opencode): reuse local server for review flows Try the default local OpenCode server before spawning a new one, and resolve bundled assets and command paths correctly when the plugin is loaded from source during local testing. * Fix typecheck after narrowing the opencode type to the sdk |
||
|
|
062106ff90 |
fix: improve markdown rendering and bullet alignment (#530)
* fix: pin bullet markers to top of multi-line list items - Add `items-start` to list item flex container so markers align to the first line instead of centering vertically across the full item height - Use consistent bullet glyph (•) across all nesting levels - Add test fixture 10 covering all bullet/list types and inline gap cases For provenance purposes, this commit was AI assisted. * fix: improve inline markdown rendering - Add strikethrough support (~~text~~) - Fix ***bold italic*** by checking triple-asterisk before double - Nudge checkbox icons down 3px to align with text baseline For provenance purposes, this commit was AI assisted. * fix: add backslash escaping to inline markdown renderer - Add \* \_ \` \[ \~ \\ escape sequences — consume backslash and emit literal character - Add \ to nextSpecial scanner so the escape handler fires correctly mid-text For provenance purposes, this commit was AI assisted. * fix: add autolink support to inline markdown renderer - Handle <https://url> as clickable external links - Handle <email@domain.com> as mailto links - Add < to nextSpecial scanner so autolinks are detected mid-text For provenance purposes, this commit was AI assisted. * chore: remove HTML entities from test fixture Not a supported feature — matches Obsidian behavior. For provenance purposes, this commit was AI assisted. * fix: prevent empty DocBadges wrapper in sticky header row layout In row layout, check hasPreviousVersion && planDiffStats (what PlanDiffBadge actually needs) instead of onPlanDiffToggle (always truthy). Prevents an empty wrapper div that created phantom width via gap-x-3 in the sticky bar. For provenance purposes, this commit was AI assisted. |
||
|
|
b3fc1f724f |
fix(ui): render numerals for ordered list items (#520)
* feat(parser): detect ordered list markers and compute display indices
The block parser collapsed `*`, `-`, and `\d+.` markers into a single
`list-item` block type, discarding ordered/unordered status. Add
`ordered` + `orderedStart` to Block, capture the numeric marker in the
list regex, and introduce `computeListIndices()` — a pure helper that
walks a list group and assigns each ordered item a CommonMark-correct
display number (sequential renumbering, streak break/restart on
unordered items, deeper-level state truncation, top-level numbering
preserved across nested children).
21 new unit tests cover both the parser changes and the indexing
helper, including the tricky cases: `1./2./99.` renumbers as 1,2,3;
sub-bullets between ordered items keep the top-level streak alive;
nested ordered sublists number independently and reset between
siblings; numeric checkboxes set both `ordered` and `checked`.
For provenance purposes, this commit was AI assisted.
* feat(ui): render numerals for ordered list items
Branch the list-item marker span on `block.ordered`: render
`${index}.` (with `tabular-nums` and a 1.5rem min-width to keep
columns stable across single- and double-digit numerals) when the
source marker was numeric, otherwise fall through to the existing
`•`/`◦`/`▪` bullet symbols. Indices come from `computeListIndices()`
called once per list group; `groupBlocks` is unchanged so mixed
nested lists still share a single `data-pinpoint-group="list"`
hover wrapper and annotation anchoring is unaffected. Checkbox
items still take precedence over numerals.
Adds a real-world manual fixture (06-ordered-list-plan.md) whose
`## Verification` section exercises a 10-item ordered list, the
case that originally surfaced the bug.
For provenance purposes, this commit was AI assisted.
* fix(parser): merge consecutive blockquote lines into one block
Each `>` line was emitted as its own blockquote block, so the
renderer's `my-4` margin produced visible gaps between every line
of a multi-line quote (the parser had a literal TODO comment:
"Check if previous was blockquote, if so, merge? No, separate for
now"). Fix: append to the previous block when it's a blockquote
and the prior line wasn't blank, mirroring the list-continuation
pattern. A blank line still breaks the quote so two `>` runs
separated by a blank line stay distinct.
Adds 5 unit tests (merge, blank-line break, paragraph boundaries,
single-line) and a manual fixture (07-blockquotes.md) covering the
bug case, the blank-line-break case, sandwich-between-paragraphs,
and inline markdown across merged lines.
For provenance purposes, this commit was AI assisted.
* fix(ui): address code review — diff view, task lists, blockquote paragraphs
Three issues surfaced by PR review #520:
1. **Diff view flattened ordered lists to bullets.** PlanCleanDiffView's
SimpleBlockRenderer duplicated Viewer's list-item JSX with hardcoded
bullet symbols, so a denied+resubmitted plan with numbered steps
showed numerals in the main view but `•` in the diff view — exactly
the screen where "which step changed?" matters most. Fixed by
threading computeListIndices through MarkdownChunk and sharing the
marker rendering via a new ListMarker component used by both
renderers, which also removes the root-cause duplication.
2. **Ordered task lists dropped their numbers.** `1. [ ] step` set both
`ordered=true` and `checked=false` in the parser, but the renderer's
checkbox branch took precedence and the numeral was never shown.
GitHub renders `1. [ ]` as numeral + checkbox side by side; we now
match that by rendering both glyphs in ListMarker when an ordered
task list item appears.
3. **Multi-paragraph blockquotes collapsed.** After the blockquote-merge
fix in the previous commit, `> a\n>\n> b` produced content
`"a\n\nb"` but the renderer passed it straight to InlineMarkdown,
which renders `\n\n` as whitespace — so two quoted paragraphs
mashed into one line. Fixed by splitting blockquote content on
`/\n\n+/` in both Viewer and PlanCleanDiffView and emitting one
`<p>` child per paragraph.
Adds one unit test for the multi-paragraph blockquote content shape and
a manual fixture (08-ordered-edge-cases.md) covering ordered task lists,
multi-paragraph quotes, nested-bullet counter preservation, double-digit
alignment, and start-at-N numbering.
The fourth review comment — loose ordered lists with intervening non-list
blocks restarting numbering — is deferred. It requires parser-level
loose-list detection (CommonMark's indented-continuation rule) and the
bug only fires when users rely on lazy `1./1./1.` markers across a
break. Tracked as a follow-up.
For provenance purposes, this commit was AI assisted.
* fix(parser): don't merge blockquote lines containing block-level markers
Round-two review flagged a regression: `> 1. foo\n> 2. bar\n> 3. baz` was
merging into one blockquote whose content was `"1. foo\n2. bar\n3. baz"`.
The renderer split on `\n\n+` (paragraph breaks), found none, and emitted
a single `<p>` — so `\n` collapsed to whitespace in HTML and the user
saw `"1. foo 2. bar 3. baz"` as one run-on line. Worse than the pre-PR
behavior (which at least kept each line in its own box).
Pragmatic fix: don't merge a `>` line whose stripped content starts with
a block-level marker (`*`, `-`, `\d+.`, `#`, `` ``` ``, `>`). Those stay
as separate blockquote blocks so each marker line is visually distinct
(legible, matching pre-PR behavior for quoted lists). Wrapped prose
quotes — the original motivating case — still merge correctly because
prose lines don't start with markers.
Also check the PREVIOUS block's content for markers so a trailing prose
line after a `> 1. item` doesn't glue onto the list-item block.
7 new unit tests cover: quoted ordered list stays separate, quoted
unordered list stays separate, quoted heading stays separate, quoted
code fence stays separate, nested blockquote stays separate, wrapped
prose quote still merges (regression guard), and mixed prose+list where
prose merges and list lines stay separate.
Adds tests/test-fixtures/09-quoted-list-regression.md as a manual repro.
Known follow-ups (tracked separately, not in this PR):
- Consecutive separate blockquote blocks still get individual `my-4`
margins, so a quoted list shows as stacked boxes with gaps between
lines. The proper fix is recursive blockquote parsing (render the
content as its own Block[] tree with an actual nested list inside
the quote). Deferred — requires `children?: Block[]` on Block,
parser rework, and exportAnnotations traversal changes.
- Clean diff view renumbers ordered lists from the start of each diff
chunk when users rely on CommonMark's lazy `1./1./1.` markers. Same
power-user population as the earlier deferred loose-list case.
- Pure code-hygiene items from the second review (non-list-block
handling in computeListIndices, BULLET_BY_LEVEL modulo cycle,
<ListGroup> extraction, CLAUDE.md Block interface drift,
splitBlockquoteParagraphs helper) — batch into a follow-up cleanup.
For provenance purposes, this commit was AI assisted.
|
||
|
|
76221b9b09 |
chore: remove stray icons, move test-fixtures into tests/
Delete import-octi/ octicon SVGs accidentally committed in #491 (no references in source). Relocate root-level test-fixtures/ markdown files under tests/test-fixtures/ for consistency. For provenance purposes, this commit was AI assisted. |
||
|
|
1800c24d80 |
feat(gemini): add Gemini CLI plan review integration
* feat(gemini): add Gemini CLI plan review integration Adds a new `apps/gemini-hook/` adapter that enables Plannotator plan review for Gemini CLI users via the BeforeTool hook system. The adapter reads the plan file from disk (Gemini provides a path, not inline content), delegates to the shared @plannotator/server for the browser-based review UI, and translates the decision back into Gemini's hook output format. Requires an upstream fix (google-gemini/gemini-cli#21802) that makes `decision = "allow"` user policies work for exit_plan_mode, allowing hooks to replace the built-in TUI approval dialog. Includes: - apps/gemini-hook/server/index.ts — stdin/stdout adapter - apps/gemini-hook/hooks/ — policy TOML + settings snippet - scripts/install.sh — Gemini binary download, policy install, settings config For provenance purposes, this commit was AI assisted. * refactor(gemini): use single binary with auto-detection instead of separate app Removes apps/gemini-hook/ — the plannotator binary now auto-detects Gemini CLI from stdin (plan_path = file on disk) vs Claude Code (plan = inline content) and branches input parsing + output formatting. Config fixtures live in apps/gemini/ (policy TOML + settings snippet). Install script gates on ~/.gemini existing so Claude-only users are unaffected. For provenance purposes, this commit was AI assisted. * test(gemini): add manual sandbox script for Gemini CLI integration Three modes: - --simulate: pipes BeforeTool JSON to hook, tests approve/deny output - (default): runs local patched Gemini build - --nightly: installs Gemini nightly and runs it Backs up and restores ~/.gemini config on exit. For provenance purposes, this commit was AI assisted. * feat(gemini): add slash commands, marketing tab, and docs for Gemini CLI - Add /plannotator-review and /plannotator-annotate slash commands (.toml) - Install Gemini slash commands in all three install scripts (sh, ps1, cmd) - Add Gemini tab to marketing landing page with icon - Add Gemini CLI to top-level README install section - Create apps/gemini/README.md with full setup and usage docs - Remove stale dev:gemini script and regenerate bun.lock For provenance purposes, this commit was AI assisted. * fix(gemini): merge hook into existing settings.json instead of printing instructions When ~/.gemini/settings.json already exists, use node to JSON-merge the BeforeTool hook config rather than asking the user to do it manually. Falls back to instructions only if node is unavailable. For provenance purposes, this commit was AI assisted. * fix(gemini): handle plan_filename rename and fix scoping bug Gemini CLI nightly renamed plan_path to plan_filename in exit_plan_mode. Accept both field names for forward/backward compatibility. Reconstruct full plan path from transcript_path + session_id + plans/ + filename. Also hoist planFilename variable out of try block so it's accessible in the deny output path (was causing ReferenceError). For provenance purposes, this commit was AI assisted. * fix(gemini): dim approve button when annotations exist for Gemini CLI Gemini's hook runner ignores systemMessage on the allow path, so approve-with-feedback is silently dropped — same limitation as Claude Code. Extend the existing UI gate to also apply for gemini-cli origin. For provenance purposes, this commit was AI assisted. * fix(gemini): add AGENT_CONFIG entry and fix sandbox simulate mode Register "gemini-cli" in AGENT_CONFIG so the UI shows "Gemini CLI" with proper badge styling instead of generic "Coding Agent" fallback. Update sandbox simulate mode to match production input format: use plan_filename instead of plan_path, include transcript_path, and simulate the Gemini directory structure for path reconstruction. For provenance purposes, this commit was AI assisted. |
||
|
|
4627f75426 |
feat: external annotations API with real-time SSE (#400)
Adds a general-purpose External Annotations API that allows external programs (linters, AI tools, security scanners) to push annotations into a live Plannotator session via HTTP, with real-time delivery over SSE. ## What's included - **Shared core** (`packages/shared/external-annotation.ts`): types, in-memory store, input validation, SSE serialization - **Server handlers**: Bun + Pi implementations with full CRUD (GET/POST/PATCH/DELETE) + SSE streaming - **Client hook** (`useExternalAnnotations`): EventSource with polling fallback, optimistic updates - **Editor integration**: two-array state model (local + external), content-aware dedup, ID-based routing - **Persistence**: source field preserved through share URLs and crash-recovery drafts - **Docs**: new Integrations category with API overview page, updated API reference ## API surface All three servers (plan, review, annotate) expose: - `GET /api/external-annotations/stream` - SSE stream - `GET /api/external-annotations` - JSON snapshot (polling fallback) - `POST /api/external-annotations` - Add annotations (single or batch) - `PATCH /api/external-annotations?id=` - Update fields - `DELETE /api/external-annotations` - Remove by id, source, or clear all For provenance purposes, this commit was AI assisted. |
||
|
|
8280cc0aca |
perf(opencode): lazy-load HTML to fix plugin startup time (#411)
* perf(opencode): lazy-load HTML to cut plugin startup from ~160ms to ~35ms
The two SPA HTML files (~20 MB combined) were inlined as string literals
via Bun's `with { type: "text" }` imports, forcing Bun to parse a 21 MB
bundle at module load time. Replace with lazy readFileSync getters and
background preload during plugin init, reducing the bundle to 0.81 MB.
Closes #410
For provenance purposes, this commit was AI assisted.
* test: add OpenCode plugin startup benchmark script
Measures real-world startup time across three scenarios:
no plugin, published npm, and local optimized. Uses
`opencode run` for non-interactive measurement and parses
log timing.
For provenance purposes, this commit was AI assisted.
* fix(bench): resolve project dir to repo root and auto-build before scenario 3
PROJECT_DIR pointed to tests/ instead of the repo root, so the local
plugin path was invalid and scenario 3 silently measured a no-plugin run.
Also auto-run build:opencode when dist/index.js is missing.
For provenance purposes, this commit was AI assisted.
|
||
|
|
401793e35f |
feat: custom display name + config file foundation (#399)
Adds user-editable display names and persistent config via ~/.plannotator/config.json. - ConfigStore singleton with precedence: server config file > cookie > default - Editable identity input in Settings with "Use git name" and regenerate buttons - POST /api/config endpoint for write-back across all 6 servers - getServerConfig() reads config fresh per request (no stale cache) - Eager constructor hydration so the store is safe to read before init() - vendor.sh as single source of truth for Pi extension vendoring - Vendor parity test to prevent missing generated modules Closes #396 |
||
|
|
f96758da0a |
feat(pi): complete Pi server rewrite — modular architecture, full Bun parity, shared code extraction (#382)
* feat(pi): add missing endpoints to plan, review, and annotate servers Phase 1-3 of Pi endpoint parity: Plan server: image, upload, draft, editor-annotations, agents, favicon, linked documents, Obsidian vaults/files/doc, file browser, VS Code diff Annotate server: image, upload, draft, favicon, linked documents, file browser Review server: extract shared handlers, add favicon Shared utilities extracted from review server inline code into reusable functions (handleImageRequest, handleUploadRequest, handleDraftRequest, handleFavicon). Reference handlers (doc, Obsidian, file browser) implemented using Node.js fs APIs replacing Bun.Glob/Bun.file. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(pi): add PR review endpoints and Node.js PR runtime adapter Phase 4 of Pi endpoint parity: - Node.js PRRuntime using child_process.spawn (matches Bun adapter pattern) - GET /api/pr-context — fetch PR summary, comments, checks - POST /api/pr-action — submit review to GitHub/GitLab - PR mode guards on /api/diff/switch and /api/git-add - /api/diff response includes prMetadata and platformUser in PR mode - /api/file-content fetches from platform API in PR mode - Build script copies pr-provider, pr-github, pr-gitlab from shared Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(pi): wire AI backbone with Node.js Pi SDK provider Phase 5 of Pi endpoint parity: - Create packages/ai/providers/pi-sdk-node.ts — PiProcessNode class using child_process.spawn instead of Bun.spawn, same RPC protocol - Register 4 AI providers in Pi review server (claude-agent-sdk, codex-sdk, pi-sdk-node, opencode-sdk) with graceful degradation - Route /api/ai/* endpoints through createAIEndpoints handlers - Pipe Web Response → node:http response with ReadableStream support for SSE streaming - Dispose AI sessions and registry on server stop Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(pi): address parity audit findings across all three servers Plan server: - /api/plan: add repoInfo and projectRoot to response - /api/approve: pass agentSwitch and permissionMode in decision - Update decision promise type to include agentSwitch, permissionMode Review server: - /api/diff/switch: pass gitContext.cwd to runGitDiff - /api/file-content: pass gitContext.cwd to getFileContentsForDiffCore - /api/git-add: add fallback to gitContext.cwd when worktree parse fails Annotate server: - /api/plan: add repoInfo and projectRoot to response - /api/feedback: capture annotations array (was silently dropped) - Update decision promise type to include annotations Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(pi): complete parity — integrations, planSave, save-notes Ports all remaining missing functionality: - Node.js versions of saveToObsidian, saveToBear, saveToOctarine (Bun.write → writeFileSync, Bun.$ → spawn) - Node.js detectProjectNameSync (Bun.$ → execSync) - extractTags, generateFrontmatter, generateFilename, extractTitle - POST /api/save-notes — decoupled note saving - POST /api/approve — full implementation: note integrations, planSave snapshots, saveAnnotations, saveFinalSnapshot - POST /api/deny — planSave snapshots on denial - Import saveAnnotations, saveFinalSnapshot from storage.js Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * refactor(pi): wire domain module imports and fix type errors - Add all missing imports from ./server/* domain modules to server.ts - Export interfaces from integrations.ts (ObsidianConfig, BearConfig, etc.) - Move toWebRequest to helpers.ts, remove duplicate from handlers.ts - Add git() helper to project.ts (was in server.ts, needed by getRepoInfo) - Fix os default import → named imports in handlers.ts and network.ts - Fix readdirSync Dirent type in reference.ts - Fix Headers.entries() → forEach for Node compat in AI endpoint piping - Fix ReadableStream type cast in AI SSE streaming - Fix matchAll iterator compat in integrations.ts (use while + exec) - Cast pi-sdk provider config to any (PiSDKConfig not in base union) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * refactor(pi): move generated shared files to generated/ directory Moves all build-time copied shared files (feedback-templates, review-core, storage, draft, project, pr-provider, pr-github, pr-gitlab) from the pi-extension root into generated/ subdirectory. Updates build script to output there. Updates all imports in server.ts, index.ts, and server/ domain modules to use ./generated/ paths. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * refactor(pi): replace hand-maintained utils.ts with generated checklist utils.ts was a manual copy of parseChecklist, extractDoneSteps, and markCompletedSteps from packages/shared/checklist.ts. Add checklist to the build-time copy list and import from generated/checklist.js. Delete the redundant utils.ts. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * chore(pi): gitignore generated/ and built HTML files These are build artifacts created by `bun run build:pi`. Untrack them and add .gitignore to prevent re-adding. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * refactor(pi): split server.ts into domain-organized modules - server.ts is now a barrel re-exporting from server/ modules - server/serverPlan.ts — plan review server - server/serverReview.ts — code review server - server/serverAnnotate.ts — annotate server - server/helpers.ts — add requestUrl() to eliminate non-null assertions - server/project.ts — linter fix (sanitizeTag import path) - packages/ai/package.json — add pi-sdk-node export entry - index.ts — fix waitForDone non-null assertion with guard check, update imports for generated/checklist.js Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(pi): parity audit fixes + shared code extraction Systematic side-by-side audit of Pi vs Bun servers (A1-A22, B1-B2 complete). Fixes found during audit: - PlanServerResult.waitForDecision missing savedPath/agentSwitch/permissionMode - Missing permissionMode option and /api/plan response field - editorAnnotations created unnecessarily in archive mode - repoInfo called per-request instead of cached at init - Approve handler missing effectivePermissionMode fallback - Deny handler missing savedPath in decision resolution - Archive /api/plan response had extra pasteApiUrl - Missing GET method guards on archive/plans, archive/plan, doc, obsidian/files, obsidian/doc, reference/files - Review server had stray pasteApiUrl option/response field - AI getCwd missing worktree support Shared code extraction: - packages/shared/favicon.ts — single source for favicon SVG - packages/shared/integrations-common.ts — note app pure functions - packages/shared/reference-common.ts — file tree building - packages/shared/repo.ts — git remote parsing - Updated all consumers to import from shared sources For provenance purposes, this commit was AI assisted. * fix: parity audit B3-C10 — review + annotate server fixes Review server (B3-B17): - diff/switch missing try/catch error handling - git-add parseBody outside try/catch - feedback missing try/catch error handling - Unknown /api/ai/* paths now return 404 (both Bun and Pi) Annotate server (C1-C10): - Bun annotate server missing pasteApiUrl (short URL sharing broken) - Added pasteApiUrl to Bun options, response, and both hook callers - Pi repoInfo called per-request instead of cached at init - Pi feedback missing try/catch error handling - Missing GET method guards on doc and reference/files For provenance purposes, this commit was AI assisted. * fix: parity audit D3-D5 — draft error handling, editor annotations, resolve-file extraction D3: Pi draft save handler missing error handling — added .catch() with 500 + console.error D4: Pi editor annotation POST missing try/catch — added with "Invalid JSON" 400 D5: Extracted resolveMarkdownFile to packages/shared/resolve-file.ts - Replaced Bun.Glob with runtime-agnostic walkMarkdownFiles (readdirSync) - Made function sync (no longer async) - Pi handleDocRequest now uses shared resolveMarkdownFile instead of inline resolution - Gains Windows path normalization, isWithinProjectRoot security check - Deleted packages/server/resolve-file.ts re-export, consumers import from shared - Cleaned up stale await calls in hook entry, reference handler, and tests - All 19 resolve-file tests pass For provenance purposes, this commit was AI assisted. * fix: parity audit D6-D10 — integrations, PR naming, shared modules D6: Fixed broken detectProjectNameSync — was using require() for non-existent exports. Now uses basename + sanitizeTag directly. D7: Renamed checkAuth → checkPRAuth, getUser → getPRUser across Bun server, hook, and OpenCode plugin to match Pi naming. Also fixed stale resolve-file import in OpenCode plugin. D8-D10: Verified clean — ide, project detection, network. For provenance purposes, this commit was AI assisted. * update openpackage.yml * fix: bump Pi git-add test timeout to 15s for parallel suite stability For provenance purposes, this commit was AI assisted. * test: add route parity test — Bun ↔ Pi server route drift detection For provenance purposes, this commit was AI assisted. * fix(ci): update Pi generate step to use generated/ directory with full file list The Pi extension was refactored to use generated/ subdirectory but the CI generate step still used the old flat layout with a subset of files. For provenance purposes, this commit was AI assisted. * fix(ci): update release workflow Pi generate step to match new layout Same stale generate step as test.yml — old flat layout, missing files. For provenance purposes, this commit was AI assisted. * fix(pi): update files array for modular server layout The files array still referenced the old flat layout (server.ts monolith, root-level generated files, deleted utils.ts). npm publish would have produced a broken package missing server/ and generated/ directories. For provenance purposes, this commit was AI assisted. * feat: add TypeScript type-checking to CI pipeline - Fix broken barrel export: buildFileTree/VaultNode re-exported from @plannotator/shared instead of reference-handlers (P1 bug) - Fix server.port type narrowing in all 3 servers - Fix AI provider type errors (claude-agent-sdk, codex-sdk, opencode-sdk, pi-sdk) - Extract mapPiEvent to pi-events.ts to break Bun→Node type chain - Add tsconfig.json to packages/shared, packages/ai, packages/server, apps/pi-extension - Add `typecheck` script to root package.json - Add type-check step to test.yml and release.yml CI workflows For provenance purposes, this commit was AI assisted. * fix(ci): use bun-types instead of @types/node for typecheck CI environment has bun-types (includes Node types) but not @types/node as a standalone package. For provenance purposes, this commit was AI assisted. * fix(ci): add @types/node for Node-runtime type checks Pi extension and packages/shared run on Node, not Bun — they should type-check against @types/node, not bun-types. Added @types/node as a dev dependency so CI resolves it. For provenance purposes, this commit was AI assisted. * fix: cast Uint8Array.buffer to ArrayBuffer for TS 5.9 compat crypto.subtle.importKey expects BufferSource, but TS 5.9 is stricter about Uint8Array.buffer being ArrayBufferLike (includes SharedArrayBuffer) vs ArrayBuffer. Explicit cast resolves the overload mismatch. Astro pulls in TS 5.9 transitively, so CI resolves a different TypeScript version than local dev. This fix works on both 5.8 and 5.9. For provenance purposes, this commit was AI assisted. * fix(ci): add bun-types as explicit devDependency CI's bun install doesn't hoist bun-types to root node_modules when it's only a transitive dep of @types/bun. Adding it as a direct devDependency guarantees tsc can resolve it. For provenance purposes, this commit was AI assisted. * fix(ci): remove Pi extension from typecheck Pi extension depends on @mariozechner/pi-* peer dependencies that aren't installed in CI. Type-checking it requires Pi's runtime environment. The three packages we check (shared, ai, server) are sufficient to catch barrel export bugs and type errors. Pi extension coverage comes from route parity tests and bun test. For provenance purposes, this commit was AI assisted. --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> |
||
|
|
6b775ea1ed |
feat: /plannotator-last — annotate the last agent message (#325)
* feat: add /plannotator-last command to annotate last assistant message
Adds a new slash command that extracts the last rendered assistant message
from Claude Code's session log and opens it in the annotation UI.
Session log parser (apps/hook/server/session-log.ts):
- Parses Claude Code JSONL logs at ~/.claude/projects/{slug}/*.jsonl
- Finds the last assistant message.id with text content blocks
- Skips noise entries (progress, system, file-history-snapshot, queue-operation)
- Filters system-generated user messages by prefix to avoid false turn boundaries
- Walks backward through empty turns when back-to-back user messages exist
- No anchoring — reads from end of log since <command-message> isn't written
until after the binary completes
New files:
- apps/hook/commands/plannotator-last.md — slash command definition
- apps/hook/server/session-log.ts — Claude-Code-specific log parser
- apps/hook/server/session-log.test.ts — 30 tests covering streaming chunks,
tool call turns, sub-agent noise, stop hooks, thinking blocks, and edge cases
Modified:
- apps/hook/server/index.ts — annotate-last subcommand
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* chore: remove 3 redundant real-world scenario tests
These duplicated coverage already provided by focused unit tests:
- "full conversation" → covered by "grabs last message.id in multi-tool turn"
- "stop hook interrupted" → covered by "skips progress and system noise"
- "long tool-only sequence" → covered by "skips tool-only assistant entries"
Kept the thinking block test (unique coverage). 27 tests remain.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat: add /plannotator-last command to Pi extension
Uses Pi's session manager API to find the last assistant message —
walks backward through ctx.sessionManager.getEntries(), finds the
last entry with role "assistant" and text content, opens it in the
annotation UI. Reuses existing isAssistantMessage(), getTextContent(),
startAnnotateServer(), and runBrowserReview() from the extension.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat: add /plannotator-last to OpenCode plugin + extract command handlers
Adds annotate-last command that fetches session messages via
client.session.messages(), finds the last assistant message with text
parts, and opens it in the annotation UI.
Refactors command handling: extracts review, annotate, and annotate-last
handlers from the inline event hook into commands.ts module. Reduces
index.ts by ~120 lines and makes adding future commands cleaner.
New files:
- apps/opencode-plugin/commands.ts — extracted command handlers
- apps/opencode-plugin/commands/plannotator-last.md — command metadata
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat: context-aware UI labels for annotate-last mode
Adds "annotate-last" mode to the annotate server, passed through to the
UI via /api/plan response. The editor uses this to show "Copy message"
instead of "Copy plan", and "annotations on the message" in the
completion overlay.
- packages/server/annotate.ts: new `mode` option on AnnotateServerOptions
- packages/editor/App.tsx: annotateSource state derived from mode
- packages/ui/components/Viewer.tsx: copyLabel prop for button text
- All three harnesses pass mode: "annotate-last" in their callers
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat: add Codex support to annotate-last command
Detects Codex via CODEX_THREAD_ID env var (injected by Codex into every
spawned process). Uses the thread ID to find the rollout file in
~/.codex/sessions/, parses the Codex rollout JSONL format to extract
the last assistant message.
Also adds `plannotator last` alias for shorter usage in Codex bang
commands (!plannotator last).
New files:
- apps/hook/server/codex-session.ts — Codex rollout parser
- apps/hook/server/codex-session.test.ts — 9 tests
Modified:
- apps/hook/server/index.ts — Codex detection + `last` alias
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: context-aware feedback title + top spacing for paragraph-first content
- exportAnnotations now accepts a title param: "Message Feedback" for
annotate-last, "File Feedback" for file annotation, "Plan Feedback"
for plan review (default)
- Adds top spacer when content starts with a paragraph (not a heading)
and has no frontmatter, fixing tight spacing in annotate-last mode
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* chore: add sandbox scripts for Pi and Codex testing
- sandbox-pi.sh: builds extension, creates temp project, installs via
`pi install`, launches Pi with sample files
- sandbox-codex.sh: compiles binary, creates temp project, launches
Codex. Test with `!plannotator last`
Both follow the same pattern as sandbox-opencode.sh.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: add hook build step to opencode sandbox script
The opencode build copies HTML from hook/dist/ — without building hook
first, the sandbox could use stale HTML. Pi and Codex sandboxes already
had this step.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: remove command body from plannotator-last to prevent agent response
The .md body was being sent to the agent as a prompt, causing it to
respond with "Opening annotation UI..." before the event handler could
fetch messages. That response became the "last message" instead of the
actual one. Empty body = agent stays silent, event handler intercepts.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: use command.execute.before hook for OpenCode annotate-last
Moves plannotator-last from the passive event hook to the
command.execute.before hook. This intercepts the command before the
agent sees it, clears output.parts so the agent stays silent, fetches
session messages, opens the annotation UI, then sends feedback via
client.session.prompt() — same pattern as review/annotate.
Previously the agent would respond to the command body before the
event handler could fetch messages, polluting the session history.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: add Codex to origin type and agent name mapping
Origin "codex" was falling through to the default "Coding Agent" label.
Added "codex" to the origin union type across annotate server, editor,
and removed the `as any` cast in the hook.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: remote share link, plan-specific prose, and codex type unions
- Add writeRemoteShareLink to annotate-last onReady callback so remote
sessions get a reachable URL
- Add subject parameter to exportAnnotations so feedback says "message"
or "file" instead of "plan" when appropriate
- Add 'codex' to origin type unions in useAgents, Settings, UpdateBanner,
and App.tsx fetch handler
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: correct JSDoc for projectSlugFromCwd (leading dash is kept, not stripped)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* refactor: use RenderedMessage type instead of inline structural type
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
||
|
|
5437a37da9 |
feat: add mobile compatibility (#260)
* feat: add mobile compatibility - Add responsive hamburger menu with all header actions (MobileMenu) - Annotation panel renders as full-screen overlay on mobile with backdrop and close button - Panel starts closed on mobile (<768px) - Touch support for resize handles, pinpoint annotations, and toolstrip buttons - Mobile text selection creates annotations via highlighter.fromRange() bridge - Card action buttons always visible on touch devices (hover:none media query) - Settings modal uses horizontal tab bar on mobile - CommentPopover width capped to viewport on small screens - Replace mousedown with pointerdown for touch-compatible click-outside handling - Add useIsMobile reactive hook for breakpoint detection - Desktop layout (>=768px) unchanged Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: mobile layout polish — panel gap, button overlap, responsive labels - AnnotationPanel: fix bottom gap on mobile overlay (inset-y-12 → top-12 bottom-0) - Viewer: push in-plan action buttons below badges on mobile (mt-6), add clear-right before frontmatter, show short labels (Comment/Copy) on mobile - App: reduce mobile horizontal padding to 8px (px-2) - test-hook.sh: build review before hook to fix missing dist error Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat: add plan width display setting with compact/default/wide options Adds a configurable plan width preference (compact 832px, default 1040px, wide 1280px) with an abstract layout preview in Settings. Dynamic max-width flows through to Viewer, PlanDiffViewer, and the toolstrip. Default is compact to preserve existing behavior. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Co-authored-by: Michael Ramos <mdramos8@gmail.com> |
||
|
|
933f44737e |
feat: split worktree/diff-type into separate controls (#248)
* feat: split worktree/diff-type into separate controls The single dropdown that mixed diff type selection (uncommitted, last-commit, branch) with worktree switching was confusing — entering a worktree silently replaced all dropdown options with no persistent indicator of which worktree was active. Split into two controls: - Context dropdown: switches between main repo and worktrees (only shown when worktrees exist, highlighted when a worktree is active) - View dropdown: always shows the same diff type options regardless of context Server changes: - GitContext now exposes worktrees as a separate field - Removed getWorktreeDiffOptions() and back-to-main sentinel handling - Simplified /api/diff/switch (no more diffOptions replacement) Client changes: - New activeWorktreePath state + handleWorktreeSwitch callback - handleDiffSwitch composes worktree prefix from context automatically - Derived activeDiffBase strips worktree prefix for dropdown display Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * refactor: derive activeWorktreePath from diffType instead of separate state Eliminates redundant state — activeWorktreePath and activeDiffBase are now both derived in a single useMemo from the composite diffType string. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * refactor: move DiffOption, WorktreeInfo, GitContext to @plannotator/shared Eliminates duplicate interface definitions across server and client. Types now live in packages/shared/types.ts and are imported by both packages/server/git.ts and packages/review-editor/. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> |
||
|
|
472e17be3d |
feat: expandable diff context in code review (#247)
* feat: expandable diff context in code review (closes #243) Switch from PatchDiff to FileDiff component from @pierre/diffs to enable GitHub-style "show more lines" buttons between hunks. The library handles all expansion UI when provided full file contents via oldLines/newLines. - Add /api/file-content endpoint to serve old/new file content per diff type - Add getFileContentsForDiff() helper with ref mapping for all diff types - Parse patch client-side via getSingularPatch(), augment with file contents - Update @pierre/diffs from 1.0.4 to 1.0.11 (scroll sync fix, no breaking changes) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: upgrade @pierre/diffs to beta, re-parse patch with full file contents The previous approach spread file content arrays onto a partial-mode FileDiffMetadata, causing hunk index mismatches and Shiki decoration errors. Now uses processFile() to re-parse the patch with oldFile/newFile so hunk indices are computed against the full file (isPartial: false), which is required for expansion to work correctly. Also fixes a flash error on file switch by tagging fileContents with the filePath they were fetched for, preventing stale contents from being paired with the wrong patch during the render before useEffect fires. - Upgrade @pierre/diffs from ^1.0.x to ^1.1.0-beta.19 in all workspaces - Add test fixtures for disjoint hunks, deleted/renamed/new files Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: abort stale file-content fetches, validate path params - Add AbortController to file-content fetch effect to cancel in-flight requests when switching files - Reject path traversal (.. or absolute paths) on /api/file-content - Document /api/file-content endpoint in CLAUDE.md Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> |
||
|
|
dcb979c08f |
test: add light mode code block fixture to test-hook.sh
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> |