mirror of
https://github.com/backnotprop/plannotator.git
synced 2026-09-14 14:17:26 +08:00
feat/opencode2-native-commands
5 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
89f0b6628e |
feat(pi): live local app annotation through a shared proxy core and Node transport (#1366)
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. |
||
|
|
67f47dbac1 |
fix(annotate): armed-mode interaction fixes from the v0.27.5 QA gate (#1363)
* fix(annotate): pre-release QA fixes for the armed-mode interaction seams
Six confirmed QA findings on the HTML/live annotate surface plus missing
pi-extension resync coverage:
1. Armed pinpoint drifted click (>4px, no selection) was swallowed AND
leaked to the page: the always-on drag work armed the trailing-click
suppression on drift alone. The mouseup arming site now requires the
drag to have actually produced a text selection; drifted clicks pin
normally and never reach the page. Bridge tests for armed drift,
armed real drag, and Interact drift.
2. Esc ladder: hover-clear is no longer its own rung; clearing the
pinpoint outline and posting annotate-exit happen on the same press
when no draft is open. Draft-close keeps its own press.
3. Compact touch layouts no longer apply a restored toolsHidden:true
chrome cookie (both header toggles are desktop-only, so applying it
stranded the user); the cookie value is preserved for desktop.
4. The live-app probe now announces the static-conversion downgrade on
stderr when a loopback probe fails, naming --app to force live mode.
5. Live-app export: page group headers are now '## Page:' with '### N.'
entries nested below them; exports without pageUrl stay byte-identical.
6. Shift+1-4 mode shortcuts no longer fire while the annotation
toolbar's type-to-comment listener owns printable keys, so typing
! @ # $ into a starting comment cannot silently switch modes.
Also adds the missing tests for the two resyncPhaseFromSession
executing->idle fallbacks that arm idleNoticePending (verified by
mutation: flipping either arm fails its test).
* fix(annotate): compact arm/disarm affordance, guarded shutdown, restored chrome guards
Follow-up scope from the forensics sweep, same surface:
- Compact touch layouts get Options-menu actions for the HTML/live
surface: 'Annotate page'/'Interact with page' (the desktop pen and
Mod+Shift+A were unreachable on touch, so every tap annotated with no
way out) and 'Show tools'/'Hide tools' (the desktop eye). With the
menu as the way back, the toolsHidden cookie now applies on compact
again (desktop parity) instead of being ignored.
- The annotate servers' stop() now guards every disposal step
individually (Bun: runGuardedShutdown, mirrored inline in Pi): a
throwing agent-terminal teardown (#1314-class) no longer skips
liveProxy.stop() and the other disposals after it. Unit-tested with a
throwing disposer.
- Re-added the two regression guards dropped in the htmlHideTools ->
htmlChrome test rename: the restore commit never writes stale
pre-restore chrome values to the cookie, and the sidebar stays
reachable via Mod+B while tools are hidden.
* fix(annotate): scope the Agent TUI display reset to display settings only
The Display popover's 'Reset terminal display settings' button also called
onSideChange('left'), durably overwriting a user's chosen right/hidden
placement in config.json with no disclosure — the label scopes the reset
to font/appearance. Position is a layout preference with its own explicit
segmented control right below, so the reset no longer touches it: the
button now resets exactly the display settings through the panel's one
sanitized update path, and the popover no longer has any code path from
reset to the side.
AgentTerminalDisplayPopover is now exported with a defaultOpen test seam
(the surrounding panel needs a live WebTUI session to render it); tests
assert reset restores the display defaults without firing onSideChange,
and that the Position control remains the explicit way to change
placement.
|
||
|
|
2ca55c8332 |
feat(annotate): live local app annotation through a loopback reverse proxy (#1352)
* feat(bridge): additive live-mode gate + LIVE_BRIDGE_BOOTSTRAP Adds the config-gated live branch to BRIDGE_SCRIPT: frame gate, pinned parent origin, token-stamped postToParent, origin+token checks on both inbound handlers, pinpoint-only clamp, vim and resize off, pageUrl on ready, and coalesced page-change reporting for SPA history navigation. With no config present (srcdoc) every branch is inert and behavior is unchanged; the existing html-viewer suites pass unmodified as the regression proof. LIVE_BRIDGE_BOOTSTRAP installs the annotation CSS from the JSON config prelude before the IIFE runs. New package export exposes the string constants without the React barrel. * feat(ui): live-session parent side for proxied app annotation useHtmlAnnotation gains a live option (origin + token validated before parseBridgeMessage; token + concrete targetOrigin on every outbound post) and a validated page-change message with onPageChange. HtmlViewer gains src/liveSession/currentPageUrl/onPageChange: src-mode iframe with no sandbox and no srcdoc, ready pageUrl handling, per-page restore filtering with explicit clear-marks + re-sync on navigation, and one postToBridge choke point for its direct posts. Annotation.pageUrl is additive; exportAnnotations groups by page (with global numbering kept) only when a pageUrl is present, byte-identical otherwise. AnnotationPanel shows the page label; AnnotationToolstrip can hide the input switch. The editor app wires mode annotate-app: full-viewport live surface, forced pinpoint, vim off, diff/share hidden, pageUrl stamping. * feat(server): loopback reverse proxy for live app annotation Whole-origin mirror of a local dev server on a dedicated 127.0.0.1 port: streaming bridge injection (after the head open tag, before a bare </head>, or appended; exactly one per document; 8-byte holdback plus a state machine for tags split across chunks), header hygiene (upstream Host rewrite, X-Forwarded-*, identity Accept-Encoding on document intent only, hop-by-hop strip), CSP drop-and-replace with frame-ancestors listing the editor origins, X-Frame-Options removal, target-origin Location rewrite, byte-identical passthrough for assets and encoded HTML (no injection, once-per-session diagnostic), SSE streaming, and WebSocket passthrough with a bounded pending queue for HMR. Host header validation runs before any upstream contact; the bind is the literal loopback constant and the advertised-URL override is never applied. Tests boot a fake dev server and cover injection, hygiene, fidelity, WS echo, and the security posture. * feat(annotate): annotate-app server mode + CLI live probe with remote hard-off startAnnotateServer gains mode annotate-app and a liveApp option: it throws under PLANNOTATOR_REMOTE, generates the per-session token, composes the proxy-served bridge body (JSON config prelude with both editor origin forms, localhost first, plus bootstrap and bridge supplied by the caller so packages/server never imports @plannotator/ui), starts the loopback proxy after the annotate port is known, serves the live /api/plan payload (no rawHtml, no version fields, sharing off), and stops the proxy with the server. Version history and durable submission records stay excluded via the explicit mode gate. The CLI resolution probes loopback http URLs (3s, accept text/html) and defaults them to live mode when the probe returns HTML; --static forces conversion, --app forces live and fails loudly on non-loopback, https, unreachable, or non-HTML targets; both flags are mutually exclusive transport-shape flags never echoed in the tolerant handoff. A live resolution under PLANNOTATOR_REMOTE is a startup failure suggesting --static. OpenCode and Pi parsers are untouched this phase. * test(live-annotate): protocol, server, and probe suites + smoke script + docs htmlLiveProtocol.test.tsx covers the parent trust boundary (origin and token rejection before parseBridgeMessage, token + targetOrigin on every outbound post, validated page-change and ready pageUrl, per-page restore filtering with full-list numbering) and the bridge live gate, executed as the composed config + bootstrap + bridge body inside a dedicated harness iframe so the srcdoc suites keep running the same script uncontaminated in this process. annotate.test.ts gains annotate-app cases (live payload shape, composed bridge served by the proxy, no-history version endpoints, proxy stopped with the server, remote rejection); annotate-live-resolution.test.ts covers the probe matrix. The two post helpers now drop unmatched-targetOrigin posts silently, matching browser semantics where some DOM environments throw. Adds the manual Vite/Next smoke script and the AGENTS.md live app annotation section (phase gate, security posture, limitations). * test(annotate-cli): cover the CLI layer of the live app remote hard-off Spawns the real CLI entry (async, so the in-process fake app can answer the live probe) with PLANNOTATOR_REMOTE=1 against a loopback HTML server and asserts the startup-failure exit with the --static hint. Completes per-layer coverage of the three-layer hard-off (CLI exit, server throw, unconditional loopback proxy bind). * fix(live-annotate): harden the loopback trust boundary end to end - isLoopbackHostname (now canonical in live-proxy.ts, re-exported by the CLI resolution) requires localhost, ::1, or a LITERAL 127/8 IPv4 address: DNS names like 127.0.0.1.evil.example no longer classify as loopback, so neither the default probe nor --app can start a live proxy against an off-box origin. - The live-eligibility probe judges the FINAL response URL: a target that redirects off its loopback origin falls back to the static pipeline (or fails loudly under --app) instead of opening a live session whose iframe immediately leaves the proxy. - WS upgrades with a browser Origin not naming the proxy itself are refused, so a hostile page's cross-site connect is never laundered into the origin-less shape dev servers trust as a non-browser client (Vite CVE-2025-24010 class). - /__plannotator__/bridge.js refuses cross-site/same-site Sec-Fetch-Site fetches: the per-session token is no longer readable via an off-origin script include on modern browsers. - X-Frame-Options is stripped only on HTML responses (where frame-ancestors replaces it); non-HTML responses keep the app's own framing protection. - Redirect Locations are re-anchored by loopback-host + port equivalence instead of a string prefix: alternate loopback spellings are now caught and lookalike ports (5173 vs 51730) pass through untouched. - --app on a non-URL target fails loudly instead of being silently swallowed. * fix(live-annotate): session correctness for SPA restores, origins, and pathful targets - A live find-and-mark that resolves nothing keeps its record, seeded with unresolved placeholder targets from the durable anchor/text params, so the mutation-driven reconcile re-acquires the pin once a lazy route or data-dependent tree renders (SPA navigation no longer permanently drops pins). Srcdoc restores keep the fail-closed drop. - The bridge posts every outbound message once per listed editor origin; the browser delivers only the one matching the parent document, so an editor opened at 127.0.0.1 instead of localhost no longer silently loses ready and every subsequent message. - The advertised appUrl is the proxy under its localhost spelling with the target URL's own path and query: the framed app stays same-site with the editor, shares the dev app's host-only localhost cookies and storage, and a pathful target opens its page instead of the app root. The proxy still binds the 127.0.0.1 literal. * ci(live-annotate): run the live protocol DOM suite; document the hardened posture htmlLiveProtocol.test.tsx is DOM-gated and was absent from the workflow's DOM_TESTS file list, so none of its trust-boundary assertions ran in CI. Add it, and update the live-app section of the project docs: literal-loopback gate, probe redirect rule, WS Origin check, bridge.js delivery gate, localhost appUrl advertisement, live restore resilience, and the remote-mode behavior change (loopback URL annotate under PLANNOTATOR_REMOTE now exits asking for --static instead of silently converting). * fix(live-annotate): absorb the v0.27 mainline into the live session surface Post-rebase seam work after replaying the branch onto main (v0.27.4 era): - Route the bridge's unanchored-transparency report through postToParent so live sessions deliver it token-stamped to the listed editor origins; the raw '*' post main introduced for srcdoc would be dropped by the live parent's message authentication exactly where restores fail most. New live-harness test pins the contract. - Extend the live remote hard-off to --tailscale sessions (flag postdates the branch): CLI startup failure + startAnnotateServer throw keyed on tailnetPublished, matching how the annotate agent terminal treats tailnet publication. Covered in annotate.test.ts and documented in AGENTS.md. - Keep main's compact-touch input controls and effective mode/input values on the HTML surface while preserving the live pinpoint-only clamps. - Regenerate the pinned guide-viewer manifest (CSS hash moved with the new UI classes; JS unchanged). * feat(live-annotate): Interact/Annotate mode toggle for live app and raw HTML sessions A live app session used to be unusable: the pinpoint capture-phase click handler owned every click, so buttons, checkboxes, inputs, and links never fired. One boolean mode now governs the HTML/live viewer surface: - Interact: the bridge is fully passive. Pinpoint capture, hover outline, drag-selection toolbar, [data-annotate] clicks, and committed-highlight click interception are all gated behind annotateModeActive, so clicks, forms, text selection, and SPA navigation reach the page natively. Committed markers and highlights stay VISIBLE, and marker buttons keep their clicks (a marker click still opens its comment). - Annotate: classic behavior, unchanged. Live sessions annotate exclusively via pinpoint while armed. Control: a single bubble icon button in the editor header (icon never changes; armed = accent + visible border, idle = transparent border of the same width, so the box is pixel-identical in both states), plus a subtle inset accent ring floated over the viewer while armed (pointer-transparent, no layout shift). Keyboard: Mod+Shift+A through the shortcut registry (html-annotate scope; the bridge mirrors the chord inside the iframe and forwards it over the authenticated postToParent path). Esc gains a final ladder rung: draft closes first, then the hover outline clears, then Esc exits Annotate back to Interact (bridge posts annotate-exit; a parent-side listener covers Esc with editor focus). The parent owns the mode and pushes it with the same re-post-on-ready pattern as set-input-method, so it survives live page changes, HMR reloads, and bridge re-injection without ever reloading the iframe. Defaults: live app sessions START in Interact; static/raw HTML sessions START in Annotate (today's behavior preserved, and the srcdoc bridge default keeps behavior byte-identical when no set-annotate-mode ever arrives). Session-only state, no persistence. Vim navigation is available only while Annotate is armed. Covered by new bridge-harness and parent-side DOM tests in htmlLiveProtocol.test.tsx and htmlPinpointProtocol.test.tsx: Interact pass-through, armed capture, the Esc ladder order, mode survival across re-injection, marker clicks in Interact, and both defaults. * feat(live-annotate): pinpoint-armed default, always-on drag comments, comment-only HTML surfaces Simplifies the Interact/Annotate design after live review. The new contract replaces the previous one where they conflict: - BOTH surfaces (raw HTML and live app) now START ARMED with pinpoint; the live-session Interact default is gone. Esc keeps the ladder (close draft, clear hover, then exit to Interact) and the header toggle re-arms. The bridge also paints the pinpoint cursor at init instead of waiting for the parent's first round trip. - The header toggle is a PEN icon: the old bubble sat next to the annotations-panel bubble and the two were indistinguishable. Same box geometry (armed = accent + visible border, idle = transparent border of identical width), aria-pressed, Mod+Shift+A, and the armed ring over the viewer are all unchanged. - Text drag-selection commenting is ALWAYS live on HTML/live surfaces, in BOTH states: the selection pass is ungated from annotateModeActive and from the pinpoint input method. In armed pinpoint, click = pin an element and drag = select text, simultaneously; the >4px drag arming decides which one a gesture was, a completed drag's trailing click never re-pins (one-shot dragEndedClick), and a plain click is never swallowed (the pass only acts on a real selection and never preventDefaults). Esc in Interact still closes an open drag draft before yielding to the page. - HTML/live surfaces are COMMENT-ONLY: useHtmlAnnotation clamps redline/quickLabel (both the host mode and a bridge-posted modeOverride, so a hostile page cannot force a DELETION), the selection toolbar drops Delete and quick labels behind a new commentOnly seam on AnnotationToolbar, and the quick-label picker portal is gone from HtmlViewer. Markdown surfaces keep the full toolbar, and persisted DELETION annotations still restore. - The "Show tools"/"Hide tools" header button is removed. It hid the floating toolstrip (now gone from HTML surfaces entirely: with comment-only plus both input paths live there is nothing left to switch), the collapsed sidebar tab flags, and the viewer's floating action cluster (attachments + global comment + version-diff toggle), all of which are now always visible. htmlChrome persistence keeps only the sidebar/panel state; an old cookie's toolsHidden flag is read tolerantly and ignored, so a stale record cannot strand a user with hidden chrome and no way back. - HTML surfaces pin the viewer input method to pinpoint (the drag/ pinpoint switch is meaningless when both are live); the Alt input switch no-ops there. Vim stays armed-only, as built. No server, proxy, or protocol-security changes; the armed flag stays session-only. Tests: the live-bridge harness is reworked around the armed default (forged-DISARM posture, drag-selection passes in armed and Interact, the trailing-click guard), the pinpoint suite covers the comment-only toolbar and the redline/quickLabel clamp at the trust boundary, a new AnnotationToolbar.commentOnly seam test guards both surfaces' toolbars, App.htmlChrome.test.tsx replaces App.htmlHideTools.test.tsx (no tools button, stale-cookie tolerance, pen armed default), and the htmlChrome tests cover the narrowed persisted shape. * feat(live-annotate): collapsible floating controls cluster The simplification removed the Hide tools toggle, which left the floating comment/attachments cluster permanently over the page. Restore a hide affordance on the cluster itself: a collapse chevron shrinks it to a small expand pill in the same corner, so the page is never obstructed without a way back. Collapsed state persists with the rest of the HTML chrome cookie (sidebar/panel), tolerantly read. Hosts that do not wire the toggle (readOnly viewers, review-editor panels) are unchanged. * feat(live-annotate): header Show/Hide tools replaces the collapse pill The collapse pill was a half measure: it left its own artifact over the page and the sidebar tongue tabs stayed. Revert it and restore the real thing as a header control: an eye toggle immediately left of the pen that removes ALL floating chrome over the page from the DOM (sidebar tongue tabs + the comment/attachments cluster), leaving nothing behind. The toggle lives in the header, so a hidden state always has a way back, which also makes honoring a persisted (or pre-existing) toolsHidden cookie safe again. |
||
|
|
d2d2dba7fa |
feat(annotate): configurable extra markdown extensions (#1309)
* feat(annotate): configurable extra markdown extensions (#1307) Adds a config-only `markdownExtensions` key to ~/.plannotator/config.json, e.g. { "markdownExtensions": [".livemd"] } for Livebook notebooks. A listed extension is accepted everywhere .md is on the annotate path: CLI target resolution, folder discovery and the file browser, /api/doc plus relative and wiki-link navigation between sibling docs, the 2MB size cap, and per-file version history. Listed extensions render as markdown with frontmatter stripped, never as raw HTML, and they only widen the accepted set. Design: - packages/core/annotatable.ts stays browser-safe and zero-dep. Its regexes and predicates now take an optional, defaulted-empty list of extra extensions, plus a normalizer and regex builders. - packages/shared/markdown-extensions.ts is the node-side seam: it reads config.json once per process through the existing loadConfig() and threads the normalized list into those pure functions. resolve-file re-exports the config-aware predicates so both runtimes pick them up; the Bun server, the Pi mirror, the OpenCode plugin and the CLI all go through them. - The annotate /api/plan payload ships the resolved list so the renderer can linkify links to sibling documents (module-level UI registry, empty by default, so nothing changes without config). Validation: entries must be dot-led, lowercase-normalized, and free of path separators, globs and whitespace. Invalid entries are dropped silently, built-ins are deduplicated, and `.env` is denylisted so config can never register it (annotate copies file contents into the data dir). Deliberately unchanged: the Pi plan-write allowlist (ALLOWED_PLAN_EXTENSIONS in tool-scope.ts) and Edit Mode source save (SOURCE_SAVE_FILE_REGEX), which keep their own narrower allowlists. * fix(annotate): deny the dotenv family and sandbox config-aware tests Review follow-ups on #1309: - deny the whole dotenv family (.prod.env, .env.local, ...) in normalizeMarkdownExtensions, not just the exact .env name - resolve config.json path per call instead of at module scope so PLANNOTATOR_DATA_DIR sandboxing works in single-process test runs - stop resolve-file.test.ts reading the real user config: pure predicate imports plus pinned empty extras on every resolve call - add the config.json -> memo -> predicate integration test using resetMarkdownExtensionsCache under a temp data dir * test(call-flow): make the stale-read advert test self-sufficient The read-only GET only probes the node runtime while Call flow is enabled. The stale-read test relied on earlier tests' settings POSTs leaking callFlow=true through the process-frozen config path; with lazy config resolution each sandbox is genuinely isolated, so the test now enables Call flow in its own data dir. Locally the dependency was masked by an fnm-shimmed sem sidecar spawning node coincidentally. |
||
|
|
747b5ea7e6 |
fix(annotate): resolve natural-language arguments or hand off to the agent (#1183)
* fix(annotate): resolve natural-language arguments or hand off to the agent Claude Code skills run the CLI through a bash-substitution prefix that executes before the model sees anything, so any trailing natural language in /plannotator-annotate died with 'File not found: the'. Worse, a non-zero exit from that prefix aborts the whole prompt before the model runs (verified empirically), so the error was never even visible to the agent. Three-tier resolution in the binary's annotate argument handling, shared by every host via packages/shared/annotate-target.ts: 1. Fast path: probe each whitespace-delimited token; exactly one naming an existing file, URL, or folder proceeds with it directly. 2. Ambiguity: two or more tokens resolve; error naming every candidate, never guess. 3. Handoff: nothing resolves; emit an agent-addressed message echoing the words tried and asking the reading agent to interpret the request and re-run with a concrete target, preserving flags. In plain mode it lands on stdout with exit 0, the only combination that reaches the model through the bang prefix; in --json/--hook mode it goes to stderr with exit 1 so machine stdout stays clean. Single-token invocations run the unchanged pipeline first, so bare correct invocations are byte-identical. Strict gates (--require-approval or --result-file) bypass the tolerance entirely: a typo'd path stays a startup failure with exit 2 and no agent-facing prose. The CLI resolution pipeline moves to apps/hook/server/annotate-resolution.ts (returns typed outcomes instead of exiting) so the token fallback can run it once with a selected candidate; OpenCode and Pi wire the same shared selection into their own not-found paths. Skill bodies gain one line telling the agent to re-run with a concrete target when the command reports unresolvable arguments. Closes #1182 Reported-by: @technicalpickles * fix(annotate): harden tolerant resolution per review Review fixes for the three-tier annotate argument handling: - A single unresolvable token now falls through to the legacy pipeline verbatim: 'annotate nope.md' is exit 1 with 'File not found: nope.md' again in every non-strict mode, instead of an exit-0 handoff that fail-opened scripts gating on the exit code. The handoff fires only when two or more words resolve to nothing. - Unrecognized dash-prefixed tokens disable tolerance instead of being skipped, so a typo'd flag ('--no-jna') errors the way it did on base rather than silently fetching via Jina. Known flags are stripped before selection as before. - Token selection now receives the original argv tokens, so a quoted missing path ('my notes.md') is probed as one token and can never be re-split into a silently resolving 'notes.md'. - Bare directory names only count as fast-path candidates when they are the sole argument; a stray word matching a directory (or '.') hands off instead of opening folder mode. Explicit paths like 'src/' keep resolving, and the bare-existence probe fallback is file-only. - The handoff re-run suggestion echoes content flags only (--markdown, --no-jina, --render-html), never transport flags (--gate, --json, --hook). - New subprocess suite (annotate-cli.test.ts) spawns the real CLI entry and pins the contract: single-token typo exit 1, strict invocations (--require-approval and --result-file) exit 2 with empty stdout and no handoff prose, unknown-flag error, quoted-token preservation, and the directory-hijack case. Placeholder dist files are created when a build is absent so the suite runs in CI. - The copilot and gemini annotate command bodies gain the same handoff instruction as the Claude, core, and kiro skills. - AGENTS.md documents the three tiers under Annotate Flow and corrects the strict-section sentences that claimed non-strict behavior was fully unchanged; the marketing annotate doc mentions the tolerant arguments. Refs #1182 |