Phase 2 of live app annotation: full parity on Pi over one shared
implementation instead of drifting copies.
- Extract every proxy decision into packages/shared/live-proxy-core.ts
(HTML injector state machine, loopback/Host/Origin predicates,
CSP/X-Frame-Options policy, redirect rewrite, WS origin gate, bridge
assembly, liveAppDraftIdentity) and the CLI probe + live-mode messages
into packages/shared/live-probe.ts. packages/server/live-proxy.ts is
now a thin Bun transport over the core; its test suite passes
unmodified.
- Add packages/shared/live-proxy-node.ts, the node:http transport the Pi
extension runs: streaming request/response piping through the shared
injector, and WebSocket (HMR) passthrough that replays the client's
handshake upstream over raw TCP and pipes the sockets byte-for-byte.
Transport tests run the proxy in a real node child process, because
Bun's node:http shim drops writes to an upgrade event's socket.
- Wire Pi: /plannotator-annotate probes loopback URLs live-first with
the shared probe (same 3s timeout, same <500 gate, same messages),
recognizes --app/--static via parseAnnotateArgs's liveFlags opt-in
(OpenCode deliberately does not opt in), and serves mode annotate-app
from serverAnnotate.ts with the shared per-target draft identity,
live sessions excluded from history/submissions, the remote hard-off
throw, and guarded live-proxy shutdown.
- Vendor live-proxy-core/live-probe/live-proxy-node plus the
dependency-free bridge-script constants to generated/.
- Docs: AGENTS.md phase-gate passages, marketing annotate page, Pi
README.
* 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
* feat: message picker for annotate-last (#800)
When running /plannotator-last after /rewind, the newest transcript
entry is no longer the message the user intended to annotate, and there
was no affordance to pick a different one.
Adds a picker UI that surfaces the recent assistant messages so the
user can choose which one to annotate:
- A "Message N of M" button in the Viewer's sticky-top action bar
(alongside Copy / Global comment / Attachments), so it stays
accessible while scrolling.
- A "Messages" tab in the left sidebar with the full list
(newest-first, preview + timestamp, default ★), mirroring the
existing Files / Versions / Archive tab pattern.
Wired for Claude Code, Codex, and Droid (all share apps/hook/server).
OpenCode, Pi, and Copilot still get the original single-message
behavior — they don't emit recentMessages, so the picker affordances
hide cleanly.
Default selection (index 0) matches today's "last message" behavior,
so users who don't interact with the picker see no change.
* feat: extend annotate-last picker to Copilot and OpenCode
The picker UI from #800 was wired for Claude / Codex / Droid only. Pull
Copilot and OpenCode onto the same shape so users on those harnesses
also get the recent-messages picker when annotating the last assistant
message.
- Copilot: replace getLastCopilotMessage with getRecentCopilotMessages,
walking events.jsonl newest-first up to 25 assistant.message events.
- OpenCode: rewrite the session walk to collect up to 25 messages
(newest first) instead of bailing on the first hit; normalize the SDK
time.created (ms epoch) to ISO to match the shared picker contract.
- Both pass recentMessages to startAnnotateServer only when length > 1,
matching the existing Claude/Codex/Droid behavior.
Also trims a leftover narrating comment in MessagesBrowser and refreshes
the stale Copilot session-parser header.
Pi parity follows in the next commit (needs round-trip of the picker
selection through /api/feedback so its post-submit anchoring quotes the
right message).
* feat(pi): wire annotate-last picker with feedback round-trip
Extends the picker UI (#800) to Pi and fixes a Pi-specific anchoring bug
the picker would otherwise introduce.
Picker plumbing
- assistant-message: getRecentAssistantMessages walks the active branch
newest-first, returning { messageId, text, timestamp? } in the same
shape the other harnesses produce.
- Plumbed through plannotator-browser / plannotator-events so the Bun
server's recentMessages option is populated when the branch has more
than one assistant message.
Anchoring fix
- Pi quotes the targeted assistant message back to the agent because its
UX is async — the conversation may have moved on by feedback time.
With the picker, that target is no longer guaranteed to be the
snapshot taken when the UI opened. The editor now sends the user's
selectedMessageId with /api/feedback; Pi looks it up in the current
branch via findAssistantMessageByEntryId and quotes that message
instead. Falls back to the original snapshot if the entry is gone.
- The round-trip field is optional and only meaningful in annotate-last
mode; other harnesses (and other modes) ignore it.
Timestamp safety
- Pi's SDK currently types SessionEntryBase.timestamp as string, but the
picker contract everywhere else is ISO. Treat the value as unknown and
normalize string/number(ms)/Date to ISO; drop anything else, rather
than blind-casting and risking silent drift if the SDK changes.
* chore: strip issue-number references from comments
Comments shouldn't rely on external references — issue numbers age out
of context, link rot is a thing, and a reader shouldn't need to open
GitHub to understand why a line exists. Strip the `(#800)` and `(#570)`
parentheticals from comments and doc strings across the picker and
review-gate code; the surrounding "why" content is preserved.
* fix: prevent removeChild crash when switching annotate-last messages
Switching the picked message remounted nothing, so React reconciled new
content against DOM that web-highlighter had mutated with <mark> nodes,
throwing removeChild. Drive the Viewer key (and StickyHeaderLane's
remount token) off a shared viewerContentKey so a message switch fully
remounts the Viewer and re-anchors the sticky-header observer.
Also cap MessagesBrowser row previews via previewText() and drop the
redundant 'block' class that was overriding line-clamp-2.
* feat: persist annotate-last feedback across messages
---------
Co-authored-by: Michael Ramos <mdramos8@gmail.com>
Add --render-html flag to plannotator annotate that renders HTML files
as-is in an iframe instead of converting to markdown. Includes annotation
support via postMessage bridge, sharing via paste service, and theme
inheritance from Plannotator's 30+ themes.
New skill: plannotator-visual-explainer — wraps nicobailon/visual-explainer
with Plannotator theme tokens, extended patterns (timelines, SVG diagrams,
code blocks, risk tables, Pierre diffs via CDN), and plan/PR-specific guidance.
All three servers (Bun, Pi, OpenCode) support the new flag.
Replaces `--silent-approve` with `--hook`, which emits hook-native JSON
that works directly with Claude Code and Codex PostToolUse/Stop hooks.
- Approve/Close: empty stdout (hook passes)
- Annotate: `{"decision":"block","reason":"<feedback>"}` (hook blocks)
- `--hook` implies `--gate` (three-button UX), applied in the parser
so all harnesses (binary, OpenCode, Pi) get it automatically
- Rewrites hook integration guide to be agent-agnostic
Follow-up to #606. Refs #570.
For provenance purposes, this commit was AI assisted.
Adds an opt-in review gate flow to annotation mode with three composable flags:
- `--gate`: 3-way UX (Approve / Send Annotations / Close)
- `--json`: structured decision output (`{"decision":"approved|annotated|dismissed"}`)
- `--silent-approve`: suppresses plaintext approve marker for naive hooks
Includes shared arg parser, @-reference handling, updated templates across all
harnesses (Claude Code, Copilot, Gemini, OpenCode, Pi), and full documentation.
Closes#570
For provenance purposes, this commit was AI assisted.