* fix(opencode): consolidate V2 system parts into one composed prompt (#1114)
The OpenCode 2 adapter still shipped the pre-#1114 multi-part system
injection: replacePlanningSystemParts kept one part per source and the
generic reminder pushed a separate part, so Qwen3.x Jinja template
corruption persisted for OpenCode 2 users. Mirror the V1 entry exactly:
compose the stripped existing text plus additions into a single system
part via composeSystemPrompt, and compose the generic reminder into the
existing text instead of appending a second part.
Also adds the regression tests for the bug class flagged in #1114's
review: both helpers must read/compose the existing system text BEFORE
truncating the array (a reorder to 'system.length = 0' first drops the
host prompt and goes red here).
* perf(annotate): harden the raw-HTML overlay reconcile (dead-target backoff, cull, batching)
Bridge-script hardening for mutation-heavy pages and large annotation
sets, plus the lost click-to-select hover affordance:
- A: dead-target re-search now carries a wall-clock backoff (300ms
doubling to a 5s cap, reset on success) ON TOP of the generation gate,
plus a 2-searches-per-reconcile-pass budget with a scheduled follow-up
pass for budget-skipped eligible targets. A page that mutates every
frame advances domGeneration every frame, so the generation gate alone
re-ran the whole-document TreeWalker sweep (and anchor re-resolution)
per frame forever for permanently unresolvable targets.
- B1: early viewport cull (64px margin) for element and range targets:
wholly offscreen targets skip targetStyleHidden / getComputedStyle /
clipBoundsFor / client-rect collection entirely and just omit their
markers, which is what the visible pipeline produced anyway.
- B2: read/write batching in renderAnnotationOverlay: highlight rects are
queued during the read phase and flushed as one write phase, so the
pass no longer forces a synchronous layout per record.
- B3: restoreAnnotation defers its render through the existing
rAF-coalesced reconcile scheduler; restoring N annotations now renders
once instead of N full passes (searches stay synchronous for the
mark-applied reply). DOM tests flush the frame via the suite's
standard macrotask flush.
- B4: zero-work observer gate: page mutations with no records, no
pending draft, and pinpoint inactive still bump domGeneration but no
longer schedule a reconcile frame.
- D: hover affordance for click-to-select: the rAF-throttled mousemove
hit-tests the pointer against the CACHED rendered committed rects and
toggles a brightness class on that annotation's rect divs inside the
shadow root. No page-DOM writes, rects stay pointer-transparent, and
shadow-root writes are unobserved so there is no reconcile loop.
- G: while a text drag is in progress in drag mode, placed markers yield
pointer input (data-pn-hittest) so the 25px bubble cannot capture a
selection drag; armed only by a >4px primary-button move from a
non-overlay mousedown, so marker clicks and click-to-select paths are
untouched. withMarkersYielded now restores (not clears) the attribute.
New regression tests for A, B1, B3, B4, D; A/B1/B3 mutation-verified
(fix reverted, test observed failing, fix restored).
* fix(annotate): make on-page marker numbers match exportAnnotations numbering
The HtmlViewer sync excluded GLOBAL_COMMENT annotations before numbering
while exportAnnotations numbers '## N.' sections across the FULL list
including globals — so an on-page 'Comment 2' could be '## 3.' in the
feedback the agent reads. The sync now derives each marker's number from
its position in the full createdA-sorted list (globals occupy a number
but ship no entry, leaving the correct gaps on-page). Export format is
unchanged.
New buildSyncNumbering helper + tests asserting a mixed list yields
identical numbers between the sync payload and exportAnnotations output
(mutation-verified against the pre-fix ordering).
* chore: sync stale workspace versions in bun.lock (0.26.1 -> 0.26.7)
* docs: document raw-HTML overlay model, multi-target types, and known limitations
- Data Types: add htmlAdditionalTargets to the Annotation listing plus
the HtmlElementAnchor (including the optional normalized point used by
placed markers) and HtmlAnnotationTarget shapes.
- Annotation System: describe the post-#1257 raw-HTML surface (placed
comment markers + overlay-projected highlights, no inline mark
mutation; durable anchors persisted, disposable markers projected) and
the print-parity limitation.
- URL Sharing: note that share links intentionally drop HTML element
anchors and additional targets (restore is text-search based, per
sharing.multiTarget.test.ts).
* test: fix Range.getClientRects stub typing in the B1 cull test
* fix(annotate): hover-race teardown and unbounded one-shot dead-search passes
Polish round on the overlay hardening:
- Hover race (1): switching into pinpoint mode (or opening a draft) now
tears hover down fully via clearHoverHighlight() — cancels the pending
rAF hit test and clears the tracked position and id — and the rAF
callback itself refuses to paint outside drag mode / with an open
draft. Previously the pending callback re-applied the class after the
mode switch and every flushQueuedHighlights re-painted it from the
stale hoverHighlightId, leaving a permanent phantom hover.
- One-shot budgets (3): beginDeadSearchPass takes a per-pass budget.
Reconcile passes keep 2 (they repeat, skipped targets get follow-up
frames); print and scroll-to are user-initiated one-shots with no
follow-up and now run unbounded (backoff and generation gates still
apply), so printing with 3+ dead-but-recoverable targets no longer
silently prints fewer highlights.
Both changes carry new regression tests, mutation-verified (fix
reverted, test observed failing, fix restored).
* fix(annotate): number markers by array position and cap entries after dropping globals
The createdA sort made the export-match invariant false with external
annotations: exportAnnotations' sort keys tie for every raw-HTML
annotation (blockId '', startOffset 0), so its stable sort numbers the
combined [...local, ...external] list in ARRAY order — and external
annotations arrive appended with server-stamped createdA values that can
interleave with local timestamps. buildSyncNumbering now numbers by
array position of the input (verified to be the same combined list both
consumers receive from packages/editor/App.tsx allAnnotations; the
viewerAnnotations diffContext filter is order-preserving and vacuous on
the raw-HTML surface).
Also reorders the cap: number the full list, drop globals, THEN slice
512 entries — globals no longer waste sync capacity and a non-global the
export numbers past position 512 still syncs while slots remain. Numbers
may now exceed 512 (array positions); the bridge's own bound (100000)
accepts them and its 512-entry cap still agrees with the sender.
Tests updated: interleaved-external agreement with exportAnnotations
(mutation-verified against the createdA sort) and slice-after-filter
capacity.
* docs(opencode): note the accepted cache-hint flattening trade-off in V2 consolidation
* fix(plugin): consolidate system prompt injections into single array element
The plugin previously pushes planning prompts and improvement contexts as
separate elements in the output.system array. This change appends them to
output.system[0] with newline separators instead. This keeps all system
instructions within a single message block to prevent potential parsing or
formatting issues when the agent processes the context.
* refactor(opencode-plugin): extract composeSystemPrompt helper to centralize system prompt assembly and add unit tests
* style(opencode-plugin): remove extra newline before plan submission reminder heading
* fix(opencode-plugin): store composed prompt result before clearing system array to prevent data loss
Previously, `output.system` was cleared with `length = 0` before being passed into `composeSystemPrompt`, causing the function to compose from an empty array instead of the original system content. The fix stores the composition result in a variable first, then pushes it after clearing. Additionally, add `.trim()` in `stripConflictingPlanModeRules` to normalize whitespace before filtering empty entries, and include a test case for empty string collapse behavior.
* refactor(plan-mode.ts): move string trimming from stripConflictingPlanModeRules to composeSystemPrompt for centralized whitespace handling
* test(plan-mode): add test case for trimming trailing newlines in composeSystemPrompt
npm >= 7 auto-installs peer dependencies, and the npm registry package
named bun ships the full Bun binary, so every install of
@plannotator/opencode pulled a useless ~50MB second copy of Bun. Express
the runtime requirement as an informational engines field instead, which
npm never installs. Also update the stale fixture comment that referenced
the peer dependency's install weight.
The smoke's wait budgets were sized on a warm macOS dev box (5s to a healthy
server, 20s to plugin activation). On a cold Linux runner OpenCode 2 needs
longer to boot and has to install the packed plugin plus its whole dependency
closure through the fixture's throwaway registry first, so the job has failed
on every run since it was introduced.
Measured, same opencode2 build and the same packed tarball:
macOS, warm caches: healthy 0.8s, plugin activated 6.3s
linux/amd64 container: healthy 8.2s, plugin activated 45.3s
Raise the budgets to 120s and 300s (overridable via
PLANNOTATOR_SMOKE_HEALTH_TIMEOUT_MS / PLANNOTATOR_SMOKE_PLUGIN_TIMEOUT_MS) and
give the job a 25 minute backstop. The assertion is untouched: the smoke still
requires the plugin registry to report the plannotator plugin.
Also make a failure legible and prompt. Each poll gets a per-request timeout so
one wedged request cannot swallow the budget, waits report progress, failures
carry the elapsed time and the last HTTP status/body, and teardown escalates to
SIGKILL and force-closes the registry. The CI failure previously burned five
minutes in teardown before printing anything.
* fix(opencode): drop runtime dependency on prerelease plugin nightly
`@opencode-ai/plugin` was a runtime dependency pinned to the exact nightly
0.0.0-next-16775, so every `npm install @plannotator/opencode` resolved a
prerelease snapshot sitting inside npm's 72h unpublish window and pulled
95MB across 101 packages (effect@4.0.0-beta.101 alone is 47MB). None of it
is executed by OpenCode 1 users.
The only runtime use was `Plugin.define`, which is an identity function
(`export function define(plugin) { return plugin; }`, verified identical
across 0.0.0-next-16775, next-16600, next-16797 and stable 1.18.13). The
import is now type-only and the plugin is a plain object literal checked
with `satisfies Plugin.Plugin`. OpenCode 2's loader validation is purely
structural (`Schema.Struct({ id: String, setup: function })` in its
supervisor), so an object literal satisfies it.
The package moves to devDependencies. Built `dist/index.js` and
`dist/embedded.js` are byte-identical to the pre-change build;
`dist/server.js` differs only by the dropped import and the two
`Plugin.define(...)` wrapper lines.
* docs(opencode): explain why the V2 logReady callback is empty
The old one-liner read as an unfinished TODO. The empty function is
correct: OpenCode 2's server-plugin Context exposes no `log` or `tui`
domain, `@opencode-ai/client` has no `tui` namespace and zero `/tui/*`
routes (checked on 0.0.0-next-16775 and next-16797), and `createV2Client`'s
`app.log` bottoms out in `console.error`, the same stderr stream
`handleServerReady` already writes to. Wiring it would print the session
URL twice in remote mode and add a stray line locally. V1 targets
`client.app.log` and `client.tui.showToast`, which are HTTP surfaces
distinct from stderr, so V1 never repeats itself. `tui.toast.show` exists
in V2 only as a subscribe-only event and on the separate
`@opencode-ai/plugin/tui` context, a different plugin kind in a different
process, so a real toast needs an upstream OpenCode API.
Comment only, no behavior change.
* 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
* fix(opencode): default agent switching to disabled
* fix(opencode): keep plan-approval build handoff; default no-switch for review feedback only
The agent switch cookie is shared by plan approval and code review, so
flipping the stored default to `disabled` also removed OpenCode's
plan-approval hand-off for every user who never configured the setting.
Make the unset default surface-aware instead: `getAgentSwitchSettings('plan')`
keeps the historical build hand-off, `getAgentSwitchSettings('review')`
stays on the current agent. An explicit user choice still applies to both
surfaces. Settings and the agent warning resolve the default from the mode
they render in, and the OpenCode "agent not available" warning now names
plan approval on the plan path.
Claude-Session: https://claude.ai/code/session_01H5KQWqXqjrPxyxUNso1QHS
---------
Co-authored-by: Michael Ramos <mdramos8@gmail.com>
* feat(annotate): add strict atomic result output
* feat(annotate): exit 2 for strict-gate usage and publication errors
Adopt the grep convention for the strict annotate gate's exit codes:
0 = approved, 1 = negative human outcome (annotated/dismissed under
--require-approval), 2 = the gate itself was misconfigured or could not
start/deliver a decision. Previously all usage/startup/validation
failures shared exit 1 with "reviewer did not approve", so callers could
not tell a denied review from a broken gate.
- parseStrictAnnotateOptions failures (bad flag combos, strict flags
outside annotate --gate --json) now exit 2
- --result-file preflight failures (missing parent, pre-existing or
dangling-symlink destination) now exit 2
- post-decision publication failures (destination raced into existence,
hard links unavailable, stdout write failure) now exit 2: they deliver
no decision record at all, so the code's own fail-closed handling
presents them as environment errors, never as a reviewer outcome --
and never approval, since only 0 means approved
- decision outcomes keep 0/1 exactly as before; signal deaths keep 128+n
- document the contract in AGENTS.md and the annotate-gates guide
Claude-Session: https://claude.ai/code/session_01YXkgsNucxDwAL4GdR4XYRk
* feat(annotate): preserve notes on structured approval
* test(pi): use exact annotate outcome import
* fix(annotate): exit 2 for strict-gate startup failures
The six startup-failure sites in the annotate path (missing path, unreachable
URL, empty folder, ambiguous name, missing/unsupported file, oversized file)
run after flag parsing and exited 1. Under --require-approval / --result-file,
1 is the "reviewer requested changes" signal, so a typo'd path made automation
misclassify a configuration error as a legitimate rejection.
Route those sites through exitAnnotateStartupFailure(), which picks its code
from the already-parsed strict options via the new pure helper
annotateStartupFailureExitCode(). Non-strict invocations still exit 1 with
byte-identical stderr; strict invocations exit STRICT_GATE_ERROR_EXIT_CODE (2).
Claude-Session: https://claude.ai/code/session_01H5KQWqXqjrPxyxUNso1QHS
* fix(annotate): emit the strict decision on stdout before publishing it
writeResultFile ran before the decision JSON reached stdout. On a filesystem
without hard links (exFAT, FAT32, most SMB/NFS, some container bind mounts)
publication fails deterministically, the catch exited 2 with nothing written
anywhere — and the reviewer's autosaved draft had already been deleted by the
feedback flow, so their completed decision was lost.
Emit the stdout record first, then publish the result file. Exit semantics are
unchanged: a publication failure still exits 2, but the decision has reached
stdout by then. Only a stdout write failure now leaves no record at all.
Correct the docs and comments that claimed exit 2 delivers no decision record:
it means the result *file* was not published. Also document the two publication
caveats: the 0600 mode is a no-op on Windows, and the atomic link/rename is not
followed by a parent-directory fsync, so publication is atomic but not
crash-durable.
Claude-Session: https://claude.ai/code/session_01H5KQWqXqjrPxyxUNso1QHS
* fix(annotate): parse linked docs with the render-side frontmatter rule on export
buildCompleteAnnotateFeedback re-parsed each linked document with
parseMarkdownToBlocks(entry.markdown) — no options, so frontmatter
stripping defaulted on. The render side parses with
{ frontmatter: shouldStripFrontmatter(path) }.
For plain-text linked docs (.yaml/.json/.toml/…) a leading `---` is real
content, not frontmatter: a multi-document YAML opens with it. Stripping
it on the export side shifted every block id, so ordinary Send Feedback
and deny emitted wrong `(line N)` labels — or dropped them entirely when
the annotation's block no longer existed.
Pass the same shouldStripFrontmatter(filepath) option at the export call
site so both sides agree.
Claude-Session: https://claude.ai/code/session_01H5KQWqXqjrPxyxUNso1QHS
* fix(annotate): carry the message scope through approve-with-notes
/api/feedback forwards selectedMessageId and feedbackScope; /api/approve
dropped them. Pi resolves the anchor message from those fields, so notes
delivered on the approve path anchored to the last message instead of the
one the reviewer picked in a multi-message annotate-last session — while
Send Feedback in the same session anchored correctly.
Forward both fields on the approve path in the Bun and Pi servers, and
have the client build the approval body with the same scope resolution
Send Feedback uses (extracted as getFeedbackMessageScope so the two can
no longer drift).
Claude-Session: https://claude.ai/code/session_01H5KQWqXqjrPxyxUNso1QHS
* docs(annotate): tell agents an approval may carry notes
The skill and slash-command files still described `"decision": "approved"`
as "acknowledge and stop", with no mention of the feedback field the gate
can now attach — so an agent reading them would silently drop the
reviewer's approval notes.
Update the Claude core/claude skills, the Copilot commands, the Gemini
annotate command, and the annotate command reference so the approved
branch names the optional feedback field and says what to do with it:
carry it into subsequent work, do not treat it as a change request.
Claude-Session: https://claude.ai/code/session_01H5KQWqXqjrPxyxUNso1QHS
* docs(annotate): document the real approvedWithNotes default
The default annotate.approvedWithNotes template is
`{{contextBlock}}{{feedback}}`, not `{{context}}` on its own line, and
{{contextBlock}} was missing from the variable table entirely.
Show the actual default, add {{contextBlock}} to the variable table, and
explain why the default prefers it: it collapses to nothing for message
annotations instead of leaving a stray blank line.
Claude-Session: https://claude.ai/code/session_01H5KQWqXqjrPxyxUNso1QHS
---------
Co-authored-by: Michael Ramos <mdramos8@gmail.com>
* feat(annotate): accept common plain-text config formats (.yaml, .json, .toml, …)
Annotate previously rejected every file that wasn't .md/.mdx/.txt (or
.html/.htm), even though the pipeline reads files as UTF-8 text and
renders anything. Widen the accepted set to unambiguously plain-text
config/data formats: .yaml .yml .json .jsonc .json5 .toml .ini .cfg
.conf .properties .csv .tsv .log .xml .env.example. They render exactly
the way .txt renders today.
- New single source of truth: packages/core/annotatable.ts
(ANNOTATABLE_TEXT_REGEX / ANNOTATABLE_DOC_REGEX + predicates),
re-exported through @plannotator/shared/resolve-file and vendored into
the Pi extension.
- .env stays excluded (commonly holds secrets; annotate history copies
file contents into the data dir). Source-code extensions stay with
code review.
- Single-file accept + bare-filename fuzzy search widen in
resolveMarkdownFile; folder discovery and the file-browser listing
widen in all three runtimes (hook CLI, OpenCode, Pi).
- /api/doc gains a `doc=1` param set by the file browser so extensions
that overlap CODE_FILE_REGEX (.yaml/.json/.toml/.ini/.xml) render as
annotatable documents there while code-file links inside documents
keep the syntax-highlighted popout.
- Error messages now list the wider set; docs updated (AGENTS.md,
marketing annotate page).
Closes#1029
Claude-Session: https://claude.ai/code/session_01YXkgsNucxDwAL4GdR4XYRk
* fix(annotate): frontmatter, size caps, edit-guard, and skill docs from review
Review fixes for #1099:
- Frontmatter: `--- … ---` stripping is a markdown convention; for
non-markdown annotatable sources (multi-document YAML, .txt starting
with ---) the delimiters are real content. parseMarkdownToBlocks gains
a { frontmatter } option and the editor keys it off the active
document's path via shouldStripFrontmatter() (strip for .md/.mdx and
pathless/converted sources; keep raw for other annotatable text).
- Size caps: new shared MAX_ANNOTATABLE_FILE_BYTES (2MB — same limit the
code-file popout always had) now guards the annotate CLI single-file
read in all three runtimes and the /api/doc document branches in both
servers. Also applies to .md/.txt (behavior change for pathological
inputs; previously unbounded).
- Editing guard: mid-edit file opens gate on isSourceSaveFilePath
(.md/.mdx/.txt) instead of the wider annotatable set — config files
are view-only, so switching to one mid-edit no longer silently
downgrades "Done editing" to feedback-only edits.
- Skill docs: plannotator-annotate SKILL.md (core + Kiro) now mention
the plain-text config formats.
Claude-Session: https://claude.ai/code/session_01YXkgsNucxDwAL4GdR4XYRk
Adds current-architecture GitButler workspace, stack, and branch review support across Bun and Pi while preserving the existing Git, JJ, and P4 paths.
Co-authored-by: Dan Susman <56033661+dansusman@users.noreply.github.com>
Fixes for the three-agent adversarial sweep findings:
- reference-handlers.ts: seed the user's modified/untracked files BEFORE
the bulk walk — the 5000-file cap (d3c9de1e) filled in raw readdir order
and could silently drop the exact files the user just edited from the
annotate browser (and the truncated latch broke the merge loop on its
first iteration).
- opencode commands.ts: pass `project` to startAnnotateServer at both
call sites (annotate + annotate-last) via detectProjectName, matching
the hook and Pi runtimes — OpenCode annotate history no longer lands in
the shared "_unknown" bucket.
- App.tsx: the Alt-Alt destination double-tap now dismisses the
DestinationSpotlight — the coachmark advertised that exact gesture but
its own keydown handler deliberately ignores modifiers, so performing
the tip left the dim overlay stranded.
- annotate.ts + pi mirror: degradation notice now reads "warning: annotate
history unavailable" so OpenCode's logCliWarnings forwarder (which
filters on \bwarn(ing)?\b) actually surfaces it.
- cli-bridge.ts: a rejected showToast un-marks the URL (the other delivery
path can retry) and logs the failure instead of being fully silent.
- test.yml: register packages/editor/editableDocumentsHook.test.tsx (7
draft/conflict tests, DOM-gated since #936, never ran in CI). Repo-wide
sweep confirms all 19 DOM-gated test files are now registered.
Gates: DOM batch 102/0 across 19 files, review+server 507/0, opencode 70/0,
install harness 85/0, full typecheck incl. strict-consumer, review build OK.
Claude-Session: https://claude.ai/code/session_01SFy9fY27SA8g5BtotWPi1G
Self-review hardening of e5fcc415: the hey-api SDK returns {error} for HTTP
failures (404 on pre-toast hosts is safe), but a fetch-level failure (host
server restarting) REJECTS the promise, and `void promise` doesn't catch
that. Both toast call sites now .catch(() => {}) when the result is
thenable, so the cosmetic toast path is strictly quieter than the
surrounding app.log pattern.
Claude-Session: https://claude.ai/code/session_01SFy9fY27SA8g5BtotWPi1G
QA traced the repeatedly-regressed remote-URL invariant to its root cause on
OpenCode: every URL path (logPlannotatorReady, the cli-bridge stderr
forwarder, and the ready-file poller) funneled exclusively through
client.app.log, which OpenCode documents as "write a log entry to the server
logs" — it is never rendered in the TUI. Remote users therefore never saw
the session URL. All three paths now ALSO call tui.showToast (the SDK's
visible surface), best-effort with optional chaining so older hosts without
/tui/show-toast no-op. A shared per-run toastedUrls set dedupes the stderr
and ready-file deliveries so one session never stacks two toasts.
Also: recognize the current binary's "Plannotator session ready" stderr
phrasing in formatUserFacingCliStderrLine (the old "Open this link" match no
longer fires; only the bare-URL line was being forwarded), and add
data-print-hide to the resize-handle cursor tooltip portal so print.css hides
it (QA print-clipping finding).
Claude-Session: https://claude.ai/code/session_01SFy9fY27SA8g5BtotWPi1G
Combine the PR Summary/Comments/Checks tabs into one PR Overview panel, then
make the description and comments annotatable and render their media.
- PR Overview panel (one sidebar entry) + comment UI (avatars, filters, hide
bots, live context, responsive stacking).
- Annotate the PR description (select → comment) and PR comments (Annotate
button), with Ask AI; notes show in the Annotations sidebar and ship to the
agent.
- Split/Unified diff toggle relocated into the dock tab strip.
- Render images + video in descriptions and comments (raw HTML + markdown),
capped to the card so nothing bleeds.
- Review-flow fixes: copy-all feedback, prose-only feedback preamble, no image
control on prose notes, GitHub review-body seeding; stronger review trailer.
- Add Claude Sonnet 5 as the default Ask AI model.
No server, endpoint, or Pi-runtime changes.
* fix(annotate): add /api/save-notes POST endpoint to both servers
Copies the save-notes route from the plan review server into the
annotate server (Bun source and Pi extension copy), enabling Save to
Obsidian in annotation mode.
Fixes#844
* test(annotate): add saveToObsidian unit tests and HTTP endpoint tests
Verifies saveToObsidian writes files correctly and handles missing
vaults. HTTP endpoint tests cover success, empty integrations, and
integration-level error (not 500).
Imports consolidation from ./integrations into a single statement.
* fix(annotate): normalize server port fallback
* refactor(server): extract shared handleSaveNotes handler, fix catch-block bug
Move the /api/save-notes logic into shared handler modules
(shared-handlers.ts for Bun, handlers.ts for Pi) following the existing
pattern for handleImage, handleUpload, handleDraftSave. Replaces four
inline copies with two canonical implementations.
Fixes:
- Bun annotate catch block now correctly returns 500 (was logging only)
- Misindented brace in Pi serverAnnotate.ts resolved by extraction
- Revert unrelated port fallback change (keep server.port! for consistency)
- Static imports in integrations.test.ts
- Add /api/save-notes to CLAUDE.md Annotate Server API table
* fix(opencode): inject annotate server starter instead of global mock.module
commands.test.ts mocked the annotate server with
`mock.module("@plannotator/server/annotate", ...)`. Bun module mocks are
process-global and cannot be unset (oven-sh/bun#7823, #12823), so the stub
leaked into every suite that runs after it — in particular any test that boots
the real annotate server received a stub with no `.url`.
Make `startAnnotateServer` injectable through the existing CommandDeps
(defaulting to the real import, so production is unchanged) and have the test
pass its stub that way. This keeps the fake local to the opencode suite and
unblocks real annotate-server integration tests.
* test(server): cover save-notes — handler unit tests + annotate e2e wiring
- shared-handlers.test.ts: unit-test handleSaveNotes directly (Obsidian write,
empty integrations, integration-error reported not thrown, 500 on bad body).
- annotate.test.ts: boot the real annotate server and POST /api/save-notes,
asserting it is served as JSON (not the SPA HTML catch-all) — the regression
guard for #844. Now possible because the opencode suite no longer installs a
global annotate module mock.
---------
Co-authored-by: Michael Ramos <mdramos8@gmail.com>
Allow disabling URL sharing through ~/.plannotator/config.json
({ "share": "disabled" }) in addition to the PLANNOTATOR_SHARE
env var. Adds a resolveSharingEnabled() helper (env var > config >
default enabled) and routes all sharing checks through it across the
hook server, OpenCode plugin, and Pi extension. Docs updated.
* 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