* 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>
* 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>
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
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
* 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>
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.
Add --render-html flag to plannotator annotate that renders HTML files
as-is in an iframe instead of converting to markdown. Includes annotation
support via postMessage bridge, sharing via paste service, and theme
inheritance from Plannotator's 30+ themes.
New skill: plannotator-visual-explainer — wraps nicobailon/visual-explainer
with Plannotator theme tokens, extended patterns (timelines, SVG diagrams,
code blocks, risk tables, Pierre diffs via CDN), and plan/PR-specific guidance.
All three servers (Bun, Pi, OpenCode) support the new flag.
PFM reminder & improvement hook support across Claude Code, OpenCode, and Pi.
- Add opt-in PFM reminder (pfmReminder config flag) injected on EnterPlanMode
- Wire composeImproveContext() into all three runtimes
- Fix OpenCode system.transform array reference bug (pushes were going to dead array)
- Fix install scripts silently stripping PreToolUse/EnterPlanMode hook entry
- Isolated Pi sandbox testing (--no-extensions -e)
Registers submit_plan tool and slash commands without modifying prompts
or agent permissions. Fills the gap between manual (commands only) and
plan-agent (full automation) for users who want to manage prompts and
permissions themselves.
* fix(remote): notify URL in remote mode for OpenCode and Pi (#551, #574)
PR #440 removed writeRemoteShareLink from OpenCode because the base64
share URL flooded the TUI — but no replacement was added. Remote users
got zero feedback about where the server was listening.
OpenCode: log the short localhost URL via client.app.log() in all onReady
callbacks (submit_plan, review, annotate, annotate-last, archive).
Pi: check isRemoteSession() directly instead of relying on the openBrowser
return value, which only sets isRemote when BROWSER env is unset. Users
running via Cursor (which sets BROWSER) now get the notification.
* fix(pi): use neutral URL notification for remote sessions
The previous wording ("Remote session. Open manually:") implied the
browser failed even when BROWSER env successfully opened it via port
forwarding. Use a neutral informational message instead.
Surface agent switching directly on the Approve button as a split
dropdown for OpenCode users, so they can pick which agent to switch
to (or disable switching) without hunting through Settings.
Also fix the "approved with notes" prompt to not say "Proceed with
implementation" when agent switching is disabled.
Closes#575, closes#114, closes#106, closes#159
The client-side approach from PR #635 normalized file contents before
diffing, which destroyed all indentation. Move whitespace handling to
the server by threading a `-w` flag through the git diff pipeline.
- Add `GitDiffOptions` to review-core.ts, inject `-w` in all 7 diff
type paths + untracked file diffs
- Thread options through git.ts → vcs.ts → review.ts / Pi server
- Read `hideWhitespace` from ~/.plannotator/config.json on startup so
the initial diff already respects the persisted preference
- Accept `hideWhitespace` in `/api/diff/switch`, echo in responses
- Client toggles trigger a lightweight server refetch that preserves
the active file (no panel reset)
- Handle edge case where current file disappears when `-w` removes
whitespace-only diffs
- Remove broken client-side parseDiffFromFile/normalize hack
- Update API docs in AGENTS.md
Closes the indentation bug reported by @zeroZshadow on PR #631.
For provenance purposes, this commit was AI assisted.
Unified feedback pipeline for all plan approvals, plan denials, annotation
feedback, and review suffixes. Users customize messages via config.json with
{{variable}} template interpolation and per-runtime overrides.
Closes#624
Co-authored-by: Aviad Shiber <aviadshiber@users.noreply.github.com>
For provenance purposes, this commit was AI assisted.
* feat(review): add configurable approval prompts
Let users override the agent message Plannotator sends after approving a code review. Keep the existing behavior by default while supporting runtime-specific overrides in ~/.plannotator/config.json.
* fix(shared): export prompts helper
Expose the new shared prompts module through @plannotator/shared so Bun can resolve it during hook builds and CI.
* fix: add Gemini CLI to agent origin detection chain
Gemini CLI sets GEMINI_CLI=1 in the environment. Add it to the
detectedOrigin chain so runtime-specific prompt overrides work
on all paths (review, annotate, plan), not just plan review.
For provenance purposes, this commit was AI assisted.
---------
Co-authored-by: Michael Ramos <mdramos8@gmail.com>
Each annotation in exported plan/annotate feedback now carries source
line numbers — single-line blocks show `(line N)`, multi-line blocks
show `(lines N–M)`. Diff-context and global comments stay lineless.
When the document was produced by Turndown/Jina (HTML file or URL),
the export carries a caveat that line numbers refer to the converted
markdown rather than the original source.
Key implementation details:
- extractFrontmatter() returns contentStartLine so block line numbers
account for stripped YAML headers
- blockEndLine() computes end lines per block type, with code blocks,
directives, and alerts accounting for stripped wrapper lines
- isConvertedSource() helper in url-to-markdown.ts centralizes the
source-type check across all entry points
- sourceConverted threaded from all CLIs through annotate servers
to the /api/plan payload; isConverted added to /api/doc responses
- Per-document conversion tracking in useLinkedDoc ensures the correct
flag is used when viewing linked HTML docs
Supersedes #621.
For provenance purposes, this commit was AI assisted.
Adds an opt-in review gate flow to annotation mode with three composable flags:
- `--gate`: 3-way UX (Approve / Send Annotations / Close)
- `--json`: structured decision output (`{"decision":"approved|annotated|dismissed"}`)
- `--silent-approve`: suppresses plaintext approve marker for naive hooks
Includes shared arg parser, @-reference handling, updated templates across all
harnesses (Claude Code, Copilot, Gemini, OpenCode, Pi), and full documentation.
Closes#570
For provenance purposes, this commit was AI assisted.
Bump version across all 7 package/plugin manifests. Also refresh
AGENTS.md (new Block types from #597, Code Tour endpoints from #569),
prune removed flags from the Pi README (--plan-file, /plannotator-set-file
from #595), and pin Renovate off bun-version bumps to keep the macOS
codesign hotfix from v0.17.9 in place.
For provenance purposes, this commit was AI assisted.
Also fix stale annotate description in README and add missing PLANNOTATOR_ORIGIN entry to the env vars table.
For provenance purposes, this commit was AI assisted.
* refactor(path): centralize user path resolution
* fix(annotate): resolve user paths in file entrypoints
* fix(pi-extension): restore resolve import and add typecheck to CI
The refactor removed `resolve` from `node:path` imports, but `resolvePlanPath()`
and the planning-mode write/edit guards still call `resolve(...)`. That breaks
plan submission and plan-file restriction at runtime for Pi users.
Also wires pi-extension's tsconfig into the root `typecheck` script so CI
catches this class of missing-symbol regression in the future. Required
adding @mariozechner/pi-* packages as explicit devDependencies so tsc can
resolve them (they were previously only reachable transitively via the
peer dep, which Bun keeps in its `.bun/` store unhoisted).
For provenance purposes, this commit was AI assisted.
* fix(path): reject whitespace-only user paths and run vendor before typecheck
resolveUserPath() trims input, so whitespace-only customPath/vaultPath
resolved to process.cwd(). Plans silently wrote into the repo root and
Obsidian notes landed in <cwd>/plannotator/ instead of erroring.
Guard at both call sites (getPlanDir, saveToObsidian — Bun + Pi copies).
Also prepend vendor.sh to the root typecheck script so fresh-clone
`bun run typecheck` works without a separate vendoring step.
For provenance purposes, this commit was AI assisted.
* fix(path): short-circuit resolveUserPath on empty input
Trimming in normalizeUserPathInput meant whitespace-only input resolved
to cwd/baseDir. Callers like the annotate CLI and reference API endpoints
would then list the project root instead of erroring. Return "" early so
downstream existsSync/resolveMarkdownFile checks fail naturally.
For provenance purposes, this commit was AI assisted.
---------
Co-authored-by: Michael Ramos <mdramos8@gmail.com>
* Wire PLANNOTATOR_PASTE_URL through opencode/pi servers and Landing demo link
OpenCode plugin only read PLANNOTATOR_SHARE_URL; add a getPasteApiUrl helper
and thread it into plan/annotate/archive server starts. Pi extension's
serverReview gains the same shareBaseUrl/pasteApiUrl env-var pair already
used by serverPlan/serverAnnotate. Landing.tsx now accepts a shareBaseUrl
prop for self-hosters' demo link. Paste-service CORS defaults grow a
comment clarifying that self-hosters must override ALLOWED_ORIGINS.
* Embed custom paste origin in short URL fragment
When PLANNOTATOR_PASTE_URL is set to a non-default paste service, the
generated short link now includes a base64url-encoded paste param in the
fragment (#key=...&paste=...). The share portal and importFromShareUrl
extract it on load so they can fetch from the right paste backend without
needing a server — fixing broken short links for self-hosters who use a
custom paste service but keep the hosted share portal.
Backward compatible: links without a paste param continue to use the
default or server-provided paste API URL as before.
For provenance purposes, this commit was AI assisted.
---------
Co-authored-by: Claude <noreply@anthropic.com>