Adds a server-side "open the current file in an external app" control to the code-review and annotate surfaces (split-button with host-detected apps; last-used becomes the default; cross-platform launch mirrored across the Bun and Pi runtimes), plus a code-review UX pass: file-header change letters and line counts, the diff-settings cog grouped with Split/Unified, an all-files collapse/expand-all toggle, and the semantic diff moved to a resizable sidebar accordion. Also fixes Cmd+click code navigation (worker token transformer).
Hardened over three adversarial review rounds: shared, tested path-containment for open-in (Bun + Pi), annotate scoping aligned to /api/doc reference roots, strict PR-checkout rooting (never the launch repo), deduped app-catalog and semantic-diff fetches, macOS bundle-only availability, Windows launch fixes (reveal + terminal off cmd's parser), and PR-checkout tracking across switch and pool warmup.
* fix(server): always show remote URL + close share-html symlink gap
Two pre-release fixes found by the QA sweep.
1. Remote URL stranding: handleServerReady only opened a browser (silent on
a headless box) and wrote a ready-file; the user-visible URL came solely
from writeRemoteShareLink, gated on sharingEnabled. So a remote/SSH user
with sharing disabled (PLANNOTATOR_SHARE=disabled or config.json
{share:"disabled"}, widened by #921) saw no URL and the agent hung on
waitForDecision() forever. Now handleServerReady prints the reachable
localhost URL whenever the session is remote, independent of sharing; the
share link stays an extra. Pi/OpenCode already printed the URL
unconditionally and are unaffected.
2. share-html symlink gap: the #927 symlink-containment fix hardened the
/api/html-assets asset sinks but missed packages/server/annotate.ts's
/api/share-html containment, which stayed lexical. A symlinked *.html
inside the doc directory pointing outside it leaked the target's contents
into the share payload. Now realpath-resolves both root and target like
the other sinks. Bun-only — Pi's copy was already hardened.
Adds regression tests for both (handleServerReady remote stderr; share-html
returns 403 on symlink escape).
* refactor(server): single shared isWithinDirectory for all asset/share sinks
The symlink-containment check existed as four byte-identical copies (Bun
html-assets route, share inliner, annotate /api/share-html, Pi server). That
duplication is exactly why the escape was missed in one sink before — #927
hardened three and missed the fourth, and the #929 fix added a fifth-in-waiting.
Export the single canonical isWithinDirectory from the shared (Pi-vendored)
html-assets-node module and have every sink import it; delete the three
duplicate bodies and their now-dead realpathSync/relative/isAbsolute imports.
A new sink can no longer silently diverge. Regression tests + 690 server/shared
tests pass; build:pi clean.
* fix(pi): surface the session URL in the in-turn 'opened' notice for remote
Remote Pi users never saw a review/annotate/last URL: it was emitted from
openBrowserForServer AFTER the command's turn ended (fire-and-forget), and
Pi only renders a notify during an active turn — so it silently dropped.
The 'X opened. You can keep chatting' notice fires in-turn and DOES show,
so fold the URL into it for remote sessions (single-line, matching the
notify convention). Local sessions are unchanged (browser auto-opens).
* 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).
* feat(annotate): render html files by default
* fix(annotate): support raw html assets and sharing
* fix(annotate): address html first review followups
* fix(editor): avoid raw html sidebar init crash
* fix(annotate): support portable html shares
* fix(annotate): harden html share support
* fix(share): clear attachments when loading shared payloads
* fix(share): warn on remote share link failures
* perf(annotate): lazy-build html share payloads
* test(annotate): guard lazy html share generation
* test(annotate): drop flaky html share server test