269 Commits

Author SHA1 Message Date
Michael Ramos 2594b374d2 chore: bump version to 0.24.1
Claude-Session: https://claude.ai/code/session_01YXkgsNucxDwAL4GdR4XYRk
2026-07-20 01:56:34 -07:00
Michael Ramos a9e608d7b6 chore: bump version to 0.24.0
Claude-Session: https://claude.ai/code/session_01YXkgsNucxDwAL4GdR4XYRk
2026-07-19 22:50:01 -07:00
Michael Ramos 56df64c751 Add modern GitButler review support (#1067)
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>
2026-07-17 07:37:50 -07:00
Michael Ramos d0665571c7 Fix OpenCode plan review cancellation cleanup (#1064) 2026-07-16 14:15:23 -07:00
Michael Ramos 60b5e8d31a Narrow review feedback validation to submitted findings (#1065) 2026-07-16 14:15:07 -07:00
Franktronics 4f1ae8eec4 fix(opencode): preserve planning handoff (#1034) 2026-07-15 21:21:50 -07:00
Michael Ramos 29513e1984 chore: bump version to 0.23.1
Claude-Session: https://claude.ai/code/session_01K9G9vurTg1v5uD37GbDMpP
2026-07-12 14:37:49 -07:00
Michael Ramos 69ca6d546c chore: bump version to 0.23.0
Claude-Session: https://claude.ai/code/session_01SFy9fY27SA8g5BtotWPi1G
2026-07-10 08:35:21 -07:00
Michael Ramos 977f4ce582 fix: final QA sweep fixes — file-browser cap priority, OpenCode project scoping, spotlight Alt-Alt dismiss
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
2026-07-10 06:50:40 -07:00
Michael Ramos 888d890b26 fix(opencode): swallow showToast promise rejections — toast can never surface an unhandled rejection
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
2026-07-10 06:14:34 -07:00
Michael Ramos e5fcc4153c fix(opencode): surface session URLs via tui.showToast — app.log never reaches the TUI
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
2026-07-10 05:59:45 -07:00
Michael Ramos 82f5648ccb chore: bump version to 0.22.0 2026-07-05 11:57:30 -07:00
Michael Ramos 9bded34324 chore: bump version to 0.21.4 2026-07-01 11:32:47 -07:00
Michael Ramos e3de938914 feat(review): PR Overview panel + description/comment annotations + media (#981)
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.
2026-06-30 23:05:43 -07:00
Michael Ramos ec8342c34a chore: bump version to 0.21.3 2026-06-28 11:37:37 -07:00
Michael Ramos 3efd9962cd chore: bump version to 0.21.2 2026-06-25 06:10:46 -07:00
Michael Ramos caf8bb54bf chore: bump version to 0.21.1 2026-06-22 05:26:21 -07:00
Michael Ramos 90bc8f8db5 chore: bump version to 0.21.0 2026-06-19 11:10:38 -07:00
Michael Ramos 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
2026-06-19 09:04:15 -07:00
Josemi Liébana b7ef0756d9 fix(annotate): add /api/save-notes POST endpoint to annotate server (#884)
* 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>
2026-06-16 21:38:39 -07:00
Yoni Hornstein 201ca11ec4 feat(config): support share toggle via config.json (#921)
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.
2026-06-16 18:47:24 -07:00
Michael Ramos 9ed3ba8937 feat(editor): markdown edit mode — direct document editing with diff-to-agent feedback
Adds direct markdown editing, source-backed annotate saves, folder edit buffers, and review-hardening fixes.
2026-06-16 16:35:51 -07:00
Michael Ramos be2d06a7c2 Make HTML annotations render HTML by default
* 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
2026-06-16 16:16:05 -07:00
Michael Ramos 5e31b79a01 chore: bump version to 0.20.3 2026-06-16 00:33:34 -07:00
Michael Ramos aaa0cab402 chore: bump version to 0.20.2 2026-06-14 00:52:47 -07:00
Michael Ramos 6c64b96dd7 fix(review): signal-safe cleanup + triage suffix for PR feedback (#914)
Two QA follow-ups found while validating the release:

- Route SIGINT/SIGTERM through process.exit() so the existing "exit"
  handlers actually run on Ctrl-C / termination. A signal death
  previously skipped them, leaking background PR-checkout warmup
  children and stale `git worktree` registrations. A second signal
  still force-quits if cleanup hangs.

- Append the review-denied triage suffix for PR-mode feedback, not just
  local diffs. The old `!isPRMode` gate suppressed it for every PR
  review. Gate on whether the reviewer actually sent annotations
  instead: genuine feedback always carries annotations, while platform
  PR actions (approve/comment posted to the host) return an empty
  annotation set + status message and correctly get no suffix. Applied
  consistently across hook, OpenCode, and Pi.
2026-06-13 16:29:07 -07:00
Michael Ramos 6ec1a66c9b feat(review): large-PR pipeline, instant-open checkout, scroll perf, and worker-pool highlighting (#893)
* feat(review): large GitHub PR fallback + non-blocking PR checkout

Two PR-mode improvements:

1. Large GitHub PRs no longer fail to load. When `gh pr diff` is refused
   (HTTP 406 for oversized diffs), fetchGhPR pages through the pulls files
   API and stitches the per-file patches into a unified diff — mirroring
   the existing GitLab raw_diffs fallback. Path quoting matches git's
   exact rules (bare spaces unquoted) so downstream parsers round-trip;
   truncation at the API's 3000-file cap is surfaced, never silent.

2. The --local worktree/clone no longer blocks startup. The review server
   opens as soon as the platform diff arrives; the checkout warms in the
   background as a seeded not-ready pool entry. Consumers that need real
   files (agent jobs, full-stack diff, code-nav, semantic diff, AI
   sessions) await pool.ensure(), with creations serialized so concurrent
   fetches can't clobber the shared FETCH_HEAD. Cross-repo clone steps
   converted from spawnSync to async spawns; warmup children are killed
   on exit (plus `git worktree prune`) so aborted sessions can't leak
   stale registrations; failed checkouts degrade honestly (no agent runs
   in the wrong directory claiming local access) with a 30s retry
   cooldown.

* fix(review): survive long PR checkout warmups + classify reconstructed renames

Stress-testing against oven-sh/bun#30412 (2,188 files) surfaced three bugs:

- Bun.serve's default 10s idleTimeout killed /api/semantic-diff while it
  parked on the background checkout warmup (a clone that can take minutes).
  Disable the idle timeout on all servers — AI SSE streams can also stall
  >10s between bytes while a permission prompt waits.
- The file-badge hook memoized that failed fetch in a module-level cache
  keyed by patch, pinning every badge to empty until a hard refresh. Never
  cache failures; retry with backoff (5s/15s/30s).
- reconstructGhPatch/reconstructPatch omitted the `similarity index` line,
  which Pierre's parser keys rename classification off — pure renames
  rendered as blank plain changes with no old path. Emit 100% for
  patch-less renames/copies (exactly accurate) and a synthetic 99% for
  patched ones (consumers only branch on 100% vs not).

* feat(review): local full-diff upgrade for PRs whose API diff is truncated

On oversized PRs the platform APIs withhold per-file patch content entirely
(bun#30412: 1,066 of 2,188 files came back with status added/modified, zeroed
counts, and no patch). Those files rendered as empty stubs with no diff.

- fetchGhPR/fetchGlMR flag the result `patchIncomplete` when patch-less
  non-rename entries exist or the 3000-file cap truncates the listing.
- New runPRLayerLocalDiff (pr-stack.ts) recomputes the exact layer diff in
  the local checkout: platform merge-base + head SHA two-dot diff (three-dot
  vs baseSha fallback), fetch-by-SHA for objects missing from shallow clones,
  -l0 so rename detection doesn't silently degrade on huge PRs.
- The review UI shows a "Partial diff · Load full diff" notice in layer
  scope; clicking re-requests the layer scope and the server swaps in the
  recomputed full diff (waiting out the background clone if needed).
- PR scope/switch state writes are epoch-guarded: a request parked on the
  checkout warmup can no longer overwrite a newer scope select or pr-switch.
- draftKey follows the upgraded patch so annotation drafts survive pr-switch
  round-trips; recompute failures surface in the response error field.
- Pi server mirrors all of it, including an agentCwd fallback so the upgrade
  works for PRs switched-to under a cross-repo clone pool.

* fix(review): use GitLab's too_large/collapsed flags for withheld-diff detection

External review caught a false negative: a too-large ADDED file comes back
new_file:true with an empty diff — indistinguishable from a legitimately
empty new file under the old heuristic, so the partial-diff upgrade was
never offered for exactly the files that matter most on big MRs.

The REST /diffs endpoint marks withheld content explicitly per entry
(verified against gitlab.com): too_large/collapsed are now authoritative in
both directions — withheld adds/deletes are flagged, binaries and empty
files are never misflagged. Older GitLab without the fields keeps the
empty-diff-on-modification heuristic.

* feat(prompts): unify review-denied suffix — triage first, no coding off raw feedback

The per-runtime defaults map (#627) gave OpenCode and Pi a different
review-denied suffix than every other runtime; updating one meant the
others silently kept "you must address all of them" — an instruction to
start coding immediately. Claude Code, Amp, Droid, Codex, Copilot, Gemini,
and Kiro were all still on it.

One default for every runtime now: triage the feedback, verify it against
the code, discuss before changing anything. Per-runtime customization
remains available via config (prompts.review.runtimes.<rt>.denied), which
resolves above the built-in default as before.

* fix(prompts): generalize review-denied suffix — 'from review', not 'external AI reviewers'

Review feedback isn't always from AI reviewers or agent jobs; often it's
the human reviewer's own annotations. Neutral wording covers both.

* fix(review): non-blocking 'Load full diff' + flag-handling hardenings

Self-review findings:

- The partial-diff upgrade reused the scope-switch handler, so clicking
  "Load full diff" raised the full-screen PRSwitchOverlay — blocking the
  entire UI, potentially for minutes behind a cold clone, with no text and
  no cancel. The upgrade now has its own loading state: the notice shows a
  spinner ("Loading full diff…") and the reviewer keeps working with the
  partial diff while the request parks. Server-side epoch guards already
  handle scope/PR changes made during the wait.
- GitLab too_large/collapsed: treat explicit null like absent (flags
  inconclusive → legacy heuristic decides) instead of silently exonerating.
- Rename-limit lift uses -l100000 instead of -l0 ("0 = unlimited" only
  holds on git >= 2.29; on older git it could disable detection outright).

* fix(review): stop scroll-driven sem stampede when semantic diff is failing

The badge retry change (a2d19a4e) cleared the client-side sem cache on
failure so transient errors could recover. But file-header badges mount and
unmount on every scroll in the virtualized all-files view, and each mount
re-requests /api/semantic-diff — and the server only cached SUCCESSFUL runs.
With sem erroring, scrolling spawned a continuous stream of sem processes,
pegging the CPU and making scrolling severely choppy.

Bound retry rate by time, not by mount events:
- client: keep the failed result memoized and expire it after a 60s
  cooldown instead of clearing immediately
- server (Bun + Pi): memoize failed sem runs for 30s in
  SemanticDiffResponseCache — request rate can no longer drive execution
  rate

* fix(review): eliminate all-files scroll jank (pre-existing on main, from #885)

The CodeView migration introduced severe scroll chop; scrolling UP could
freeze the viewport entirely ("scrolling but nothing changes"). Three
compounding causes, diagnosed against Pierre 1.2.8 source:

1. Lazy full-content augmentation landed updateItem() mid-scroll-gesture:
   the full-content parse counts collapsed-context regions the raw-patch
   parse doesn't, so the item GROWS — re-render + re-tokenize hitches both
   directions, and when the grown item sat above CodeView's scroll anchor,
   its corrective scrollTo() killed wheel momentum (the up-scroll freeze).
   Fetches still start as items enter the window; the item mutation now
   waits for 150ms of scroll quiet (staleness re-checked at apply time).

2. reportVisibleFile read container.scrollTop/clientHeight/scrollHeight on
   EVERY scroll event — a forced synchronous layout right after each
   frame's DOM writes. Replaced with CodeView's cached accessors and
   coalesced the handler to once per animation frame.

3. Missing containment CSS: Pierre's own production wrapper uses
   contain:strict + will-change:scroll-position so forced layouts stay
   scoped to the scroller instead of the whole document. Adopted.

Also: __devOnlyValidateItemHeights now requires explicit opt-in
(VITE_PIERRE_VALIDATE_HEIGHTS=1) — it runs getBoundingClientRect() per
rendered item per frame and made dev-server scrolling choppy by itself.

* feat(review): change-type status in headers + tree, diffshub CSS parity

Adopts two diffshub practices identified in the architecture comparison:

- DiffFile now carries a derived status (added/deleted/renamed/modified)
  from the chunk's git metadata lines. FileHeader shows a status icon and
  renders renames as "old/path → new/path" (dimmed old, arrow — diffshub's
  treatment, including its rename blue); the file tree shows A/D/R markers.
  'modified' is deliberately undecorated so the others pop. Works in both
  the all-files surface and the single-file panel, including header-only
  pure renames from the large-PR reconstruction.

- CodeView container gains diffshub's remaining perf CSS: overflow-anchor:
  none (native scroll anchoring fights CodeView's own anchor resolution
  whenever item heights change — exactly our augmentation applies),
  overflow-x-clip, and overflow-clip containment on item elements.

* feat(review): worker-pool syntax highlighting (diffshub parity)

A performance trace of scrolling a small local diff attributed 2.2s of
2.6s main-thread CPU to findNextMatchSync — shiki's TextMate regex
scanner tokenizing on the main thread. diffshub avoids this entirely by
running tokenization in Pierre's worker pool; we never opted in.

Wires WorkerPoolContextProvider around the review app (pool size
min(cores-1, 3), 100-entry AST LRU, common languages preloaded), gates
the all-files surface on pool readiness with a 5s escape hatch (a dead
pool degrades to plaintext-then-highlight, never a blank view), and
syncs the UI theme pair into the long-lived pool.

Single-file build constraint solved with Vite's ?worker&inline (base64
blob worker) + worker.format 'es' with inlineDynamicImports — the
worker's lazy import("shiki/wasm") branch collapses into the bundle and
is never taken (shiki-js engine: the win is moving work off the main
thread, with no .wasm asset to smuggle into one HTML file). Bundle
+850KB.

* fix(review): un-poison worker-pool theme dedup on failed setRenderOptions

A failed round-trip recorded the theme as synced and never retried,
pinning the pool to the wrong palette for the session.

* fix(review): report partial diffs without a checkout; fail fast on missing checkout

Dogfood review of this PR (via plannotator itself) caught two valid issues:

- prPatchIncomplete was gated on the worktree pool, so a --no-local session
  showed a truncated diff with no indication at all. Partiality is
  information; upgradability is a capability. The flag is now always
  reported, with a separate prPatchUpgradeAvailable — the UI shows the
  amber notice either way, with the "Load full diff" button only when a
  checkout can exist (otherwise a "re-run with --local" hint).

- After a FAILED checkout warmup, Ask AI sessions and agent jobs fell back
  to process.cwd() (or a wrong revision on Pi) — running in the wrong tree
  instead of failing. Both launch points now refuse with a clear "Local
  PR checkout unavailable — retry shortly" error (503); the job handlers
  surface buildCommand refusals instead of mislabeling them "Invalid
  JSON". Bun and Pi mirrored.

A third finding (sem availability stuck after warmup) was triaged invalid:
the availability probe detects the sem binary, which is cwd-independent.

* fix(review): runtime-neutral copy for the no-checkout partial-diff hint

--local is a CLI remedy; OpenCode sessions have no such flag. Visible
text states the fact, the tooltip carries the CLI guidance.
2026-06-12 13:50:09 -07:00
Michael Ramos 97b5e62a4d chore: bump version to 0.20.1 2026-06-10 09:26:34 -07:00
Michael Ramos ad2bb25147 chore: bump version to 0.20.0 2026-06-09 12:07:48 -07:00
Michael Ramos b19505efd3 chore: remove the redundant /plannotator-status and /plannotator-archive commands (#873)
Two agent command-surface cleanups. Both remove only the command entry points; all underlying infrastructure stays.

1. /plannotator-status (Pi): removed — it echoed phase/plan-file/progress on
   demand, but that state is already shown ambiently (status bar + live
   checklist widget). The phase/checklist state machine is untouched.

2. /plannotator-archive (all agents): removed the command/skill entry points
   across every surface — Claude/Codex/Kiro skills, Pi, OpenCode (handler +
   dispatch + cli-bridge + embedded + stub), Droid, the Kiro agent prompt, all
   three installers, docs, marketing, and the CI deprecated-command guard. The
   installers also gained a stale-skill cleanup so upgraders drop a previously
   installed plannotator-archive skill.

Kept (infrastructure) — archive browsing stays available in-review via the
sidebar: the `plannotator archive` CLI subcommand (apps/hook/server), the
mode:"archive" server path + /api/archive endpoints, ArchiveBrowser/useArchive,
the sidebar Archive tab, sessions.ts "archive" mode, and ~/.plannotator/plans
storage.

Verified: bun test scripts/install.test.ts → 72 pass; pi-extension typecheck +
build:opencode pass; repo-wide residual scan clean; KEEP-set integrity
confirmed; one orphaned import (opencode commands.ts) caught in self-review and
removed.
2026-06-08 11:08:11 -07:00
Oscar Silva f08764063d feat(review): support multi-repo workspace reviews (#543)
* feat(review): support multi-repo workspace reviews (#527)

* fix(workspace): address critical issues from deep review

- Fix race condition in label generation by pre-computing labels sequentially
- Fix rewritePatchLine to support quoted paths and rename/copy headers
- Add separator between aggregated patches to avoid invalid diffs
- Normalize input paths in resolveWorkspaceFilePath
- Add timeout to PR discovery (15s) to prevent server hangs
- Fix PATCH /api/workspace/repo to rollback state on failure via applyRepoMutation
- Validate body.source runtime (must be 'local' or 'pr')
- Snapshot active repo in agent jobs at launch to prevent race in onJobComplete
- Prevent double-prefixing of agent findings when paths are already prefixed
- Fix frontend findWorkspaceRepoForPath to use longest-prefix matching
- Fix shared types: diffType uses DiffType, platformUser is string | null

* fix: remove duplicate gitRuntime export in vcs.ts

* fix: resolve remaining merge conflict in local review mode, remove stale detectManagedVcs import

* Add local multi-repo workspace review support

* Fix workspace review edge cases

* Remove session query from browser launch

* Add switchable workspace review modes

* fix(review): cover opencode workspace bridge

* fix(review): clean workspace review plumbing

* fix(review): clarify workspace agent finding paths

* fix(review): preserve diff paths with spaces

---------

Co-authored-by: Michael Ramos <mdramos8@gmail.com>
2026-06-05 14:29:38 -07:00
Michael Ramos 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
2026-06-04 18:14:05 -07:00
Michael Ramos b2b8ce8090 chore: bump version to 0.19.27
For provenance purposes, this commit was AI assisted.
2026-06-03 14:32:40 -07:00
Graham Lipsman be2c81fa3b annotate-last: pick which message to annotate (fixes #800) (#809)
* 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>
2026-06-03 13:01:36 -07:00
Michael Ramos d8e0b86f96 chore: bump version to 0.19.26
For provenance purposes, this commit was AI assisted.
2026-05-31 20:12:19 -07:00
Michael Ramos 3638190844 chore: bump version to 0.19.25
For provenance purposes, this commit was AI assisted.
2026-05-31 19:19:56 -07:00
Michael Ramos d479106038 chore: bump version to 0.19.24
For provenance purposes, this commit was AI assisted.
2026-05-27 22:22:49 -07:00
Hrand Liu e0aee7451b feat: add PLANNOTATOR_DATA_DIR env var to customize data directory (#795)
* feat: add PLANNOTATOR_DATA_DIR env var to customize data directory

* fix: update missed hardcoded paths to use PLANNOTATOR_DATA_DIR

OpenCode plugin and VS Code extension still used hardcoded
~/.plannotator paths, causing the IPC registry and plan backing
file to diverge from the server when PLANNOTATOR_DATA_DIR is set.

Also exports data-dir from @plannotator/shared and documents the
new env var in AGENTS.md.

Co-authored-by: Chris Werner Rau <14326070+cwrau@users.noreply.github.com>
Co-authored-by: João O. Santos <34689526+Joao-O-Santos@users.noreply.github.com>

* fix: vendor data-dir.ts into Pi extension and rewrite imports

The Pi extension copies shared/server modules into generated/ at
build time. Without vendoring data-dir.ts and rewriting the
parent-relative imports, typecheck fails on all generated files
that import getPlannotatorDataDir.

* refactor: eliminate duplicated data-dir logic and clean up call sites

- VS Code extension: replace inlined getPlannotatorDataDir() copy with
  import from the canonical packages/shared/data-dir.ts (esbuild bundles
  it, so no runtime dependency needed)
- storage.ts: hoist repeated getPlannotatorDataDir() calls to a
  module-level DATA_DIR constant, matching the pattern config.ts uses
- data-dir.ts: remove inaccurate docstring claim about relative path
  resolution (the code does not call resolve())
- improvement-hooks.ts: hoist to DATA_DIR constant, clarify comments
  on the two-level hook lookup (hooks/ subdir vs root fallback)

* fix: resolve relative PLANNOTATOR_DATA_DIR to absolute path

A relative value like ./data would break readArchivedPlan's path
traversal guard, which compares a resolve()'d absolute path against
the still-relative planDir prefix. Always return an absolute path
so all callers get consistent path shapes.

* fix: use @plannotator/shared/data-dir imports in server package

Switch from relative ../shared/data-dir imports to the package
export, matching the convention every other server file follows.
Update Pi vendor script sed rules to match the new import style.

* fix: use package imports in server and respect data dir in compound skill

Server modules: switch from relative ../shared/data-dir imports to
@plannotator/shared/data-dir, matching the convention every other
server file follows. Update Pi vendor script sed rules to match.

Compound skill: update hardcoded ~/.plannotator paths to check
PLANNOTATOR_DATA_DIR first, so the skill reads plans and writes
the improvement hook to the correct location when users set a
custom data directory.

Co-authored-by: Chris Werner Rau <14326070+cwrau@users.noreply.github.com>
Co-authored-by: João O. Santos <34689526+Joao-O-Santos@users.noreply.github.com>

* fix: remove remaining hardcoded ~/.plannotator assumptions

- Settings UI: replace hardcoded path in label and placeholder with
  generic text that doesn't assume a specific data directory
- quickLabels: update agent tip to reference PLANNOTATOR_DATA_DIR
  so the agent checks the correct plans directory
- codex-review: hoist getPlannotatorDataDir() to module-level DATA_DIR
  constant, eliminating redundant per-call resolution in debugLog()
- Tests: make submit-plan and storage tests resilient to
  PLANNOTATOR_DATA_DIR being set in the environment
- Install scripts (sh, ps1, cmd): check PLANNOTATOR_DATA_DIR before
  falling back to ~/.plannotator for config.json attestation lookup

* fix: expand tilde in install script and update test assertions

install.sh: PLANNOTATOR_DATA_DIR set to ~/... stays literal inside
double quotes, so the config file check silently failed. Add case
statement to expand ~ the same way the runtime data-dir.ts does.

install.test.ts: update three assertions that checked for hardcoded
~/.plannotator paths — now verify PLANNOTATOR_DATA_DIR awareness
instead.

* docs: add PLANNOTATOR_DATA_DIR to env var reference with VS Code note

Document the new env var on the marketing site's environment
variables reference page. Include a footnote about ensuring
VS Code inherits the variable when launched from the Dock.

---------

Co-authored-by: Michael Ramos <mdramos8@gmail.com>
Co-authored-by: Chris Werner Rau <14326070+cwrau@users.noreply.github.com>
Co-authored-by: João O. Santos <34689526+Joao-O-Santos@users.noreply.github.com>
2026-05-26 14:56:07 -07:00
Michael Ramos 17c808cdca chore: bump version to 0.19.23
For provenance purposes, this commit was AI assisted.
2026-05-25 12:29:50 -07:00
Michael Ramos 6254a3c181 chore: bump version to 0.19.22
For provenance purposes, this commit was AI assisted.
2026-05-23 23:30:57 -07:00
Michael Ramos 4db7fcc5da chore: bump version to 0.19.21
For provenance purposes, this commit was AI assisted.
2026-05-21 01:56:04 -07:00
Michael Ramos fa4482d7d5 chore: bump version to 0.19.20
For provenance purposes, this commit was AI assisted.
2026-05-19 08:30:48 -07:00
Robert Dailey 29390c9e71 fix(opencode): store active plan backing file outside workspace (#743)
The backing file used for edit-based plan submission was stored at
.opencode/plans/_active-plan.md inside the workspace, causing it to
appear in git status and editor file trees. It is now stored at
~/.plannotator/active/{project}/_active-plan.md alongside the version
history.

- Move getPlanBackingPath to derive path from project name under
  ~/.plannotator/active/
- Derive project name from ctx.directory basename via sanitizeTag at
  call site
- Delete backing file on approval since it is no longer needed after the
  session ends
- Update tests to reflect new path contract
2026-05-18 17:17:48 -07:00
Robert Dailey b3f1deb8d3 fix(opencode): skip end bounds check on empty file in validateEdits (#752)
When a file has no content (lineCount === 0), any edit is a pure insert
and the end field is semantically irrelevant. The previous check
rejected payloads where end was present on an empty file because end >
lineCount always evaluated true, breaking first-call submit_plan
invocations from agents or frameworks that include end unconditionally.

- Skip end > lineCount validation when lineCount === 0; applyEdits
  handles it via splice clamping
- Add applyEdits test: edit on empty file with start=1, end=1 produces
  correct output
- Add validateEdits test: passes for empty file with start=1 and end=1

Fixes #742
2026-05-18 17:17:09 -07:00
Michael Ramos f471ebf99e chore: bump version to 0.19.18
For provenance purposes, this commit was AI assisted.
2026-05-17 23:53:11 -07:00
Robert Dailey 807cc5f09b feat(submit-plan): replace dual-mode with edit-based interface (#730)
* feat(submit-plan): replace text/file-path mode with edit-based interface

Switches the OpenCode submit_plan tool from a dual-mode interface
(inline text or file path) to an edit-based one. The plugin now owns a
backing file at .opencode/plans/_active-plan.md; the agent never reads
or writes it directly. On denial, the response includes the current plan
with line numbers so the agent can apply surgical edits instead of
resubmitting the entire document, reducing token waste on iterative
revisions.

- Add applyEdits, validateEdits, formatWithLineNumbers, and
  getPlanBackingPath helpers to the plugin
- Validate edit ranges (bounds, overlap, size limit) before mutating the
  backing file
- Return line-numbered plan in denial responses to anchor targeted edits
- Remove getPlanDirectory, validatePlanPath, and file-path auto-
  detection from plan-mode.ts
- Replace plan-mode.test.ts path-validation coverage with submit-
  plan.test.ts for the edit engine
- Update custom-feedback.md and opencode.md docs for edit-based
  semantics

Refs #365

* chore(opencode): drop unused buildPlanFileRule import

Removed in PR #730 deny path along with the only call site, but the
import was left behind.

---------

Co-authored-by: Michael Ramos <mdramos8@gmail.com>
2026-05-15 09:54:04 -07:00
Michael Ramos 9fed785141 chore: bump version to 0.19.17
For provenance purposes, this commit was AI assisted.
2026-05-14 17:36:52 -07:00
Michael Ramos 12e3e2d35b chore: bump version to 0.19.16
For provenance purposes, this commit was AI assisted.
2026-05-13 12:42:12 -07:00
Michael Ramos 32e871415f chore: bump version to 0.19.15
For provenance purposes, this commit was AI assisted.
2026-05-13 08:18:34 -07:00
Michael Ramos 1c40655e17 fix(opencode): intercept annotate/review/archive commands before LLM (#713) (#718)
OpenCode's command dispatcher appends `arguments` to the `.md` body and
runs `resolvePromptParts()` over the combined string, which auto-attaches
any file path it finds as a `FilePart`. With `/plannotator-annotate
/path/to/huge.md`, that meant the agent received the file's content as a
user message before the annotation UI even opened — blowing the context
on large files (GLM-5 auto-compact reported in #713).

Move `plannotator-annotate`, `plannotator-review`, and `plannotator-archive`
from the post-hoc `event` handler to `command.execute.before`, matching
the pattern `plannotator-last` already used. The hook clears `output.parts`
in place so the agent never receives the command turn; handlers then run
the UI and inject feedback via `client.session.prompt` as a separate turn.

Empty the bodies of the three `.md` files for defense in depth — only the
frontmatter is needed for OpenCode to register the slash command.

Also fixes a latent bug in the `plannotator-last` path: `output.parts = []`
reassigns the throwaway wrapper object's property but doesn't touch the
`parts` array the caller in `prompt.ts:1944` uses directly. Switched to
`output.parts.length = 0` to mutate in place. `plannotator-last` only
escaped notice because its parts array was always a single benign text
part.
2026-05-13 05:27:02 -07:00