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.
Pass `-c core.quotePath=false` to all git invocations so paths with
non-ASCII characters (Korean, Chinese, accented, etc.) and whitespace
are output as raw UTF-8 instead of octal escapes. Fixes the 0/0 files
issue when repos contain such paths.
Closes#628
For provenance purposes, this commit was AI assisted.
* feat(ui): extract reusable PopoutDialog, fix backdrop blur
The table popout lost its backdrop blur when we switched to modal={false}
to keep annotation toolbars interactive. Radix ignores Dialog.Overlay in
non-modal mode, so replace it with a plain div backdrop that works
regardless. Extract the dialog shell (backdrop, close button, portal,
annotation-aware dismiss) into a reusable PopoutDialog component for
upcoming use cases. Add a demo table to the dev plan content.
* Add read-only code file popout
* Add code file annotation support
* Fix code selection popover position
* fix(editor): restore global-attachment-only drafts
The save condition was broadened to persist drafts with only global
attachments, but the restore handler still skipped applying when both
annotation arrays were empty — silently dropping the attachments.
* fix(ui): import SelectedLineRange from @pierre/diffs base package
SelectedLineRange is not re-exported from @pierre/diffs/react —
import it from the base @pierre/diffs entry point instead.
* chore: add TODO for bot callback + code annotation limitation
* feat(review): add hide whitespace setting for diffs
Adds a "Hide Whitespace" toggle that suppresses whitespace-only changes
in diffs, matching GitHub's ?w=1 behavior. Uses parseDiffFromFile with
ignoreWhitespace when full file contents are available. Includes demo
data with a whitespace-heavy file (settings.ts) and a vite dev server
plugin to serve file contents in demo mode.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat(review): add quick-settings popover on file header
Adds a gear icon to the file header that opens a compact Radix popover
with all diff display options (style, overflow, indicators, inline diff,
line numbers, background, hide whitespace). Provides quick access to
settings without opening the full settings dialog. Exports option
constants from Settings.tsx for reuse.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
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.
* feat(shared): add isSameProject, PR stack types, and PR list provider
Extends PRRef/PRMetadata with defaultBranch, PRStackInfo, PRStackTree,
PRStackNode, PRDiffScope, and PRListItem types. Adds isSameProject()
for owner/repo validation on PR switching. Adds fetchPRStack() and
fetchPRList() dispatch functions (GitHub-only for now, GitLab stubs).
Includes 9 new tests for isSameProject covering GitHub, GitLab, and
cross-platform scenarios.
For provenance purposes, this commit was AI assisted.
* feat(shared): add GitHub PR stack tree walking and PR list fetching
Implements fetchGhPRStack() which walks up/down the PR stack via
GraphQL, resolving numbers and titles for each node in the chain.
Collapses queryPRsByHead/queryPRsByBase into a single queryPRsByRef
helper. Adds fetchGhPRList() using gh pr list. Fixes GHE support
by removing hostnameArgs from fetchGhPRList (--repo already handles
GHE). Filters jq "null" string from defaultBranch detection.
For provenance purposes, this commit was AI assisted.
* feat(shared): fetch defaultBranch for GitLab MRs
Queries the project's default_branch via glab API so getPRStackInfo
can detect stacked MRs on GitLab. Best-effort — caught errors fall
back to undefined.
For provenance purposes, this commit was AI assisted.
* feat(shared): add PR stack detection and full-stack diff module
New pr-stack module with:
- getPRStackInfo(): detects stacked PRs from baseBranch vs defaultBranch
- getPRDiffScopeOptions(): generates layer/full-stack scope options
- runPRFullStackDiff(): computes diff from default branch to HEAD
- resolvePRFullStackBaseRef(): resolves origin/main or local main
- checkoutPRHead(): fetches and checks out a PR head in a worktree
- buildMinimalStackTree(): builds UI tree from stack info
Includes 13 tests covering ref resolution, branch fallbacks, and
GitLab ref formats.
For provenance purposes, this commit was AI assisted.
* feat(shared): add worktree pool for per-PR agent isolation
Creates a session-scoped pool of git worktrees — each PR visited
during a stacked review gets its own isolated checkout. Agents run
in their PR's worktree undisturbed by PR switches. Handles
deduplication of concurrent ensure() calls for the same PR.
Includes 11 tests covering caching, cross-repo restrictions, GitLab
ref formats, and cleanup.
For provenance purposes, this commit was AI assisted.
* feat(shared): add diffScope/prUrl to agent jobs, branch diff type
Adds prUrl and diffScope optional fields to AgentJobInfo so agent
findings carry the PR and scope context they were launched under.
Exports new pr-stack and worktree-pool modules from package.json.
Adds 'branch' to DefaultDiffType union for branch diff as default.
For provenance purposes, this commit was AI assisted.
* feat(ui): add PR annotation fields, Popover, and SearchableSelect
Extends CodeAnnotation with prUrl, prNumber, prTitle, prRepo, and
diffScope fields for stacked PR attribution. Adds shared Popover
wrapper around radix-ui. Adds SearchableSelect for filterable
dropdown lists (used by PR selector).
For provenance purposes, this commit was AI assisted.
* feat(ui): add branch diff as default option, new Git settings tab
Adds 'Branch' as a fourth default diff type option in both the
first-run dialog and settings panel. Moves the default diff type
setting from the Display tab to a new Git tab in review mode.
Updates config store validators to accept 'branch'.
For provenance purposes, this commit was AI assisted.
* feat(server): add prUrl/diffScope plumbing to agent jobs and prompts
Threads prUrl and diffScope through the agent job lifecycle so
findings carry the PR and scope they were generated under. Adds
full-stack prompt branch to codex-review and tour-review — when
in full-stack mode, the diff is inlined in the prompt instead of
telling the agent to run git diff. Re-exports isSameProject and
new PR provider functions from server/pr.ts.
For provenance purposes, this commit was AI assisted.
* feat(server): add stacked PR support to Bun review server
Adds PR switching, layer/full-stack scope toggling, PR list caching,
worktree pool integration, and multi-PR platform posting to the Bun
review server. Key additions:
- /api/pr-diff-scope: switch between layer and full-stack diffs
- /api/pr-list: cached PR list for the current repo
- /api/pr-switch: in-place navigation between PRs in a stack
- /api/pr-action: targetPrUrl support for multi-PR posting
- /api/file-content: full-stack branch for hunk expansion
- prSwitchCache/prStackTreeCache for session-scoped caching
- diffScope tagging on agent job completion
- Scope guard: returns 400 on full-stack diff failure instead of
overwriting the working diff with empty content
For provenance purposes, this commit was AI assisted.
* feat(ai): pass cwd to Claude agent SDK for worktree support
Forwards the working directory to the Claude agent provider so
agents run in the correct worktree when reviewing stacked PRs.
For provenance purposes, this commit was AI assisted.
* feat(pi): add stacked PR support to Pi server (Bun parity)
Mirrors all stacked PR features from the Bun server:
- PR switching, scope toggling, PR list, multi-PR posting
- prSwitchCache/prStackTreeCache with initial PR seeding
- diffScope/prUrl plumbing in agent jobs
- Worktree pool creation and lifecycle
- Full-stack file-content resolution matching Bun's guard structure
- targetPrUrl support on /api/pr-action
Hoists worktreePool declaration to outer scope in plannotator-browser
to fix TS18004 scoping error. Updates vendor.sh for new shared modules.
For provenance purposes, this commit was AI assisted.
* feat(hook): create worktree pool for PR review sessions
Creates a worktree pool when opening a PR review with --local,
seeding it with the initial PR's checkout. Integrates pool cleanup
into server shutdown. Passes the pool to startReviewServer for
agent isolation during PR switching.
For provenance purposes, this commit was AI assisted.
* feat(review-editor): add hooks for PR stack, context, and annotations
- useAnnotationFactory: stamps prUrl/prNumber/prTitle/prRepo/diffScope
onto annotations, only when viewing a stacked PR
- usePRStack: handles scope selection and PR switching with loading state
- usePRContext: adds URL-change detection to prevent stale-fetch race
when switching PRs (discards in-flight responses for previous PR)
For provenance purposes, this commit was AI assisted.
* feat(review-editor): add stacked PR UI components
- PRSelector: searchable dropdown for switching between PRs in a repo
- PRSwitchOverlay: loading animation during PR switch
- StackedPRLabel: stack tree popover with scope selector and PR navigation
- ReviewSubmissionDialog: multi-PR submission dialog with per-target
status, orphaned findings section with copy-as-markdown, and
partial failure retry
For provenance purposes, this commit was AI assisted.
* feat(review-editor): multi-PR export with heading hierarchy
Updates exportReviewFeedback for multi-PR sessions:
- Groups annotations by prUrl, then by file within each PR
- Uses proper heading hierarchy (## for files, ### for annotations
in multi-PR mode)
- Detects single-PR mismatch (annotations from a different PR than
the current view) and uses annotation-level PR context
- Adds diffScope labels per PR group when present
Includes 5 new tests: multi-PR headings, single-PR mismatch,
diffScope labels, and non-stacked annotation handling.
For provenance purposes, this commit was AI assisted.
* feat(review-editor): integrate stacked PR into sidebar, diff panel, and agents
- ReviewSidebar: groups annotations by PR in multi-PR sessions,
shows PR headers with annotation counts
- ReviewDiffPanel: filters annotations by prUrl and diffScope so
only matching annotations appear in the diff gutter
- ReviewStateContext: adds prDiffScope to shared review state
- ReviewAgentJobDetailPanel: shows diffScope in job detail
- PRSummaryTab: shows stack info in PR summary
- index.css: PR switch shimmer and overlay animations
For provenance purposes, this commit was AI assisted.
* feat(review-editor): wire stacked PR into main review app
Integrates all stacked PR features into the review editor:
- PR stack state management (prStackInfo, prStackTree, prDiffScope)
- applyPRResponse: shared handler for PR switch and scope toggle,
preserves active file index on scope changes
- Multi-PR platform posting via Promise.allSettled with parallel
requests, partial failure retry, and per-target status tracking
- ReviewSubmissionDialog replaces inline dialog JSX
- useAnnotationFactory stamps PR context onto annotations
- keepalive on /api/feedback to survive tab closure
- Proper try/catch/finally on handlePlatformAction
For provenance purposes, this commit was AI assisted.
* docs: add stacked PR review documentation
Updates AGENTS.md, code-review command docs, and AI code review
guide with stacked PR review capabilities.
For provenance purposes, this commit was AI assisted.
* fix(server): stamp prNumber/prTitle/prRepo on agent findings
Agent annotations only had prUrl and diffScope, missing prNumber,
prTitle, and prRepo. When agent findings were the only annotations
for a PR target in the submission dialog, the target rendered as
#0 with no title. Now resolves full PR context from prSwitchCache
at job completion and stamps all five fields. Both Bun and Pi.
For provenance purposes, this commit was AI assisted.
* feat(ui): rename diff options — "Committed" replaces "Branch" / "Current PR Diff"
Consolidates two confusing committed-diff options into one:
- Settings/first-run: "Committed" — "Everything you've committed on this branch"
- Mid-session switcher: "Committed changes" (replaces both "vs main" and "Current PR Diff")
Uses merge-base under the hood (matches GitHub PR behavior). Removes
the two-dot branch diff from the UI — it stays in the runtime DiffType
union for backwards compat. Old "branch" values in config/cookies are
silently upgraded to merge-base.
Git settings tab now uses radio cards with descriptions instead of
a cramped segmented control. First-run dialog descriptions rewritten
in plain language — no git commands.
For provenance purposes, this commit was AI assisted.
* fix(review-editor): rename client-side "PR Diff" labels to "Committed changes"
DiffTypePicker.tsx had a hardcoded "PR Diff" override for merge-base
when the base picker is present. exportFeedback.ts also used "PR Diff"
in export labels. Both now say "Committed changes" to match the
server-side label and settings UI.
For provenance purposes, this commit was AI assisted.
* fix(server): discover stack UI for root PRs targeting the default branch
Root PRs (baseBranch === defaultBranch) were excluded from stack
detection because getPRStackInfo returned null. Now the server
always fetches the stack tree in PR mode. If the tree reveals
descendant PRs, prStackInfo is retroactively set with source
"tree-discovered", enabling the stack UI, scope selector, and
PR navigation from the root of a stack.
Both Bun and Pi servers updated. Adds "tree-discovered" to the
PRStackInfo source union.
For provenance purposes, this commit was AI assisted.
* fix(review-editor): derive diff scope from annotations, not UI state
The export function now reads diffScope from annotations instead of
the prReviewScope parameter. Fixes two issues:
1. Agent job "Copy All" showed the wrong scope when the user switched
between layer/full-stack after launching the agent
2. Mixed-scope annotations produced a confusing "layer, full-stack"
comma-joined label instead of grouping by scope
Extracts renderScopedGroups helper for scope-aware grouping — used
by both single-PR and multi-PR export paths. When annotations share
one scope, it appears in the header. When mixed, annotations are
grouped under ## Layer / ## Full-stack headings.
Includes 4 new tests: uniform scope derivation, mixed scope grouping,
single scope header, and prReviewScope override prevention.
For provenance purposes, this commit was AI assisted.
* fix(server): add tree-discovered stack fallback to pr-switch handler
The initial-load path upgrades prStackInfo for root PRs when the
stack tree reveals descendants, but the pr-switch handler was missing
this logic. The server now sends correct prStackInfo after switching
to a root-of-stack PR. Both Bun and Pi.
Also removes stale prReviewScope dependency from agent job panel's
copyAllText useMemo.
For provenance purposes, this commit was AI assisted.
* fix: extract resolveStackInfo helper, fix stack UI on non-stacked PRs
Extracts the tree-discovered stack fallback into resolveStackInfo()
in pr-stack.ts — eliminates 4 copies of the same logic across Bun
startup, Bun pr-switch, Pi startup, and Pi pr-switch.
Fixes StackedPRLabel showing on every PR: the check now counts
non-default-branch nodes (> 1) instead of all nodes (> 1). Without
this, every PR showed a "Stack (1 PR)" popover because the tree
always has at least [defaultBranch, currentPR].
For provenance purposes, this commit was AI assisted.
* fix(review-editor): don't re-open already-succeeded PR tabs on retry
On partial failure retry, openUrls was pre-seeded with URLs from
previously succeeded targets, causing those PR pages to re-open
in the browser alongside newly succeeded ones. Now starts empty —
only URLs from the current attempt are opened.
For provenance purposes, this commit was AI assisted.
* refactor(review-editor): extract PR session state into usePRSession hook
Consolidates 5 independent useState calls (prMetadata, prStackInfo,
prStackTree, prDiffScope, prDiffScopeOptions) into a single
usePRSession hook with atomic updatePRSession callback.
Replaces two identical 5-line setter blocks (initial load and
applyPRResponse) with single updatePRSession calls. All ~60 consumer
sites unchanged — same variable names via destructuring.
For provenance purposes, this commit was AI assisted.
* docs: scrub Insert/Replace from docs to match shipped UI
The annotation toolbar ships with Delete, Comment, Quick label (⚡),
Looks good (👍), and Copy. AnnotationType only has DELETION, COMMENT,
and GLOBAL_COMMENT. Insertion/Replacement were removed in #400 but
several docs still describe them as toolbar options.
Updates 9 files (the 7 listed in #617 plus two additional pages with
the same drift):
- apps/marketing/src/content/docs/getting-started/quickstart.md
- apps/marketing/src/content/docs/getting-started/ui-settings.md
- apps/marketing/src/content/docs/commands/plan-review.md
- apps/marketing/src/content/docs/commands/annotate.md
- apps/marketing/src/content/docs/commands/code-review.md
- apps/marketing/src/content/blog/welcome.md
- apps/marketing/src/content/blog/plannotator-meets-pi.md
- apps/marketing/src/content/blog/sharing-plans-with-your-team.md
- apps/portal/ANNOTATE.md
Where it made sense, added a note that users can ask for replacements
or insertions inside a Comment — the agent handles it during revision.
Closes#617
* docs: include Looks good (👍) in toolbar enumerations
Pi's reviewRuntime.runGit used spawnSync, which blocked the Node.js event
loop during git operations. The ls-remote call from #609 could freeze the
HTTP server for up to 5 seconds on slow networks. Now mirrors the Bun
server's async pattern: spawn with piped stdio, kill timer for timeouts,
stream-based stdout/stderr collection.
For provenance purposes, this commit was AI assisted.
Replaces `--silent-approve` with `--hook`, which emits hook-native JSON
that works directly with Claude Code and Codex PostToolUse/Stop hooks.
- Approve/Close: empty stdout (hook passes)
- Annotate: `{"decision":"block","reason":"<feedback>"}` (hook blocks)
- `--hook` implies `--gate` (three-button UX), applied in the parser
so all harnesses (binary, OpenCode, Pi) get it automatically
- Rewrites hook integration guide to be agent-agnostic
Follow-up to #606. Refs #570.
For provenance purposes, this commit was AI assisted.
* feat(ui): atomize balanced emphasis pairs in plan diff
Before word-diffing, replace each balanced `**…**`, `__…__`, `~~…~~`,
`*…*`, `_…_` (and triples `***…***` / `___…___`) with a unique
word-char sentinel — same pattern as the existing code-span / link
atomization passes. Identical phrases pair as unchanged; different
phrases produce a single remove+add.
Fixes the "preliminary analysis" → "final analysis" demo case (⑯),
which previously orphaned the closing `**` into the unchanged tail and
rendered as literal asterisks. Now renders as one clean bold-struck →
bold-green swap.
Pair matching uses CommonMark-ish flanking rules so stray `2**3` or
intraword `my__var` / `snake_case` stay literal. Longest-first ordering
prevents single delimiters from eating the inside of a double-delim pair.
* feat(ui): coalesce adjacent diff sites separated by thin tokens
After `diffWordsWithSpace` and sentinel restoration, merge dirty runs
of ≥2 change sites separated only by thin unchanged tokens (whitespace,
commas, periods, semicolons, colons, dashes, quotes) into a single
phrase-level swap. Parens and brackets are excluded so inline links
and bracketed content stay as hard boundaries.
Turns alternating red/green word-noise (e.g. paragraph reworks with
multiple adjacent word swaps) into a readable before/after. Also
rescues the atomization edge case where wrapping a previously-plain
phrase in emphasis (`foo bar baz` → `foo **bar baz**`) would otherwise
surface as fragmented literal delimiters inside colored tags.
Single-site dirty runs pass through unchanged so isolated word swaps
keep word-level highlighting.
* feat(ui): atomize hyphenated compounds in plan diff
Hyphens between word chars (`ninety-five`, `64-byte`, `state-of-the-art`)
are semantic compound words, not two tokens. `diffWordsWithSpace` splits
on word boundaries, so without this pass `ninety-five` → `ninety-nine`
fragments into an unchanged `ninety-` prefix and a swapped `five`/`nine`
suffix — a visually noisy partial-word diff.
Added a sentinel pass that replaces infix hyphens with a word-char
marker before diffing and restores them afterwards. Runs after the
code/link/emphasis passes so hyphens inside those constructs stay
hidden. Unlike the other sentinels this one uses a fixed marker — all
hyphens restore to the same character, so uniqueness isn't needed.
Leading/trailing dashes and em-dash-like separators (dash with space on
one side) are not substituted; only true compound infixes.
getDefaultBranch relied on `symbolic-ref refs/remotes/origin/HEAD`
which is commonly unset in worktrees, manual remote setups, and
long-lived repos. When absent, the code fell through to local
`main`/`master`, silently losing the upstream-preference behavior.
Adds `detectRemoteDefaultBranch()` — queries the remote via
`git ls-remote --symref origin HEAD` to find the actual default
branch name (works for any name: develop, trunk, etc.).
The call is fire-and-forget at server startup: non-blocking, runs
concurrently while the browser loads. By the time the user opens
Branch diff or PR Diff (usually 1-2 seconds later), the result has
arrived and `currentBase` has been silently upgraded to the upstream
ref. If offline or slow, the 5-second timeout fires and the local
fallback sticks. If the user has already switched bases manually,
their choice is never overwritten.
`ReviewGitRuntime.runGit` gains an optional `timeoutMs` param. Bun
kills the process via setTimeout + proc.kill(). Pi uses spawnSync's
native `timeout` option. Both treat timeout as a non-zero exit.
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.
Adds a searchable base-branch picker to the code review UI so reviewers
can compare against any branch, not just the auto-detected default.
Key changes:
- Base-branch picker (Radix Popover + search, local/remote groups)
- Diff-type picker (Radix DropdownMenu + info tooltips)
- Worktree picker (Radix Popover + search)
- getDefaultBranch prefers upstream ref (origin/main) for fresher diffs
- listBranches populates GitContext.availableBranches
- resolveBaseBranch trusts caller verbatim (no silent rewrites)
- --end-of-options hardens git argv against flag-injection via base refs
- Branch diff / PR Diff always available (removed hide-on-default guard)
- merge-base agent prompt runs git itself instead of inlining the patch
- Agent jobs snapshot diff context at launch for honest "Copy All"
- Feedback export includes diff mode + base header for agent context
- Round-trip base through /api/diff so refresh preserves the selection
- selectedBase / committedBase split prevents file-content race
- Optimistic picker reverts on switch failure
- DiffViewer remounts on mode switch to invalidate stale file content
- Worktree switch preserves active diff mode
- Pi parity: initialBase, currentBase, agent diffContext, vendor.sh
- Tooltip wide mode, TooltipProvider mounted, keyboard/touch fixes
- GPT-5.5 added to Codex model picker
- CLAUDE.md API table updated
Closes#599
For provenance purposes, this PR 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.
* fix(pi): let agent submit any markdown plan file by path
Removes all configured plan-file state (module var, --plan-file flag,
/plannotator-set-file command, interactive path prompt). The agent now
writes its plan as a markdown file anywhere inside the working directory
and calls plannotator_submit_plan with the file path.
During planning, the write gate allows any .md / .mdx file inside cwd
(with traversal/absolute-escape rejection) instead of a single configured
path. plannotator_submit_plan gains a required filePath parameter, which
is validated, stat-checked, and read. Version history in ~/.plannotator
continues to key off plan content so naming is free-form without losing
version linking.
For provenance purposes, this commit was AI assisted.
* refactor(pi): drop isSubmitPathAllowed alias
isSubmitPathAllowed was a pure pass-through to isPlanWritePathAllowed
with no plausible reason to diverge. Use the write-gate helper directly
from submit_plan and drop the alias.
For provenance purposes, this commit was AI assisted.
* fix(pi): update bundled planning prompt for filePath-based submit
The shipped plannotator.json planning prompt still referenced
${planFilePath} and instructed the agent to call plannotator_submit_plan
with no arguments. After the submit tool gained a required filePath
parameter, default sessions hit the stale prompt and submits failed.
Rewritten to match the new flow: agent picks its own plan filename and
passes it to plannotator_submit_plan.
For provenance purposes, this commit was AI assisted.
* fix(pi): persist state on plan denial
Mirror the approve branches by calling persistState() before returning
on the deny path. Without this, a Pi restart between denial and the
agent's next submit would lose the lastSubmittedPath in memory, forcing
the agent to re-pick a plan file instead of resuming on the same one.
For provenance purposes, this commit was AI assisted.
* fix(session-log): walk ancestor PIDs to find Claude Code session metadata
When plannotator is invoked from a slash command's `!` bang, the direct
parent process is an intermediate bash shell spawned by the Bash tool —
not Claude Code itself. The old resolveSessionLogByPpid() only checked
process.ppid, so it always missed the session metadata file and fell back
to mtime-based selection, which picks the wrong log when multiple sessions
exist for the same project.
New resolution ladder (four tiers):
1. Ancestor-PID walk: call `ps -o ppid=` repeatedly from process.ppid up
to 8 hops, checking ~/.claude/sessions/<pid>.json at each hop.
Deterministic — no guessing, matches exact session every time.
2. Cwd-scan: read every ~/.claude/sessions/*.json, filter by cwd, pick
most recent startedAt. Handles cases where ps is unavailable.
3. CWD slug mtime (legacy): existing behavior, fragile with multiple
sessions.
4. Ancestor directory walk: handles cd-deeper-into-subdirectory cases.
Adds getAncestorPids (injectable getParent for testing), resolveSessionLogByAncestorPids,
and resolveSessionLogByCwdScan. Exports SessionMetadata and accepts
projectsDirOverride on findSessionLogsForCwd for test isolation.
17 new tests cover: edge cases for getAncestorPids (cycles, maxHops,
self-loops), resolveSessionLogByAncestorPids (finds correct session among
multiple, skips missing logs, falls back when no metadata matches), and
resolveSessionLogByCwdScan (picks newest startedAt, ignores mismatched
cwd, handles missing sessions dir).
Closes#458
* refactor(session-log): simplify resolver and findSessionLogs for clarity
* fix(session-log): add Windows support to ancestor-PID resolver
Tier 1 used `ps`, which doesn't exist on Windows. Multiple Claude Code
sessions in the same repo remained broken on Windows because tier 2
(cwd scan) can't disambiguate when both sessions share the same cwd.
- Replace per-hop `ps` calls with a single process-table snapshot (`ps`
on Unix, PowerShell Get-CimInstance on Windows), cached across the
walk. One spawn instead of up to eight; faster on both platforms.
- Export pure-function parsers (parseProcessTablePs,
parseProcessTableCsv) so the Windows path is unit-testable without
a Windows runner.
- Normalize cwd comparison in resolveSessionLogByCwdScan: Windows is
case-insensitive and processes may report drive letters in either
case, so fold slashes and lowercase before comparing.
Windows viability confirmed empirically: ~/.claude/sessions/<pid>.json
exists with the expected schema on Windows 11 + Claude Code 2.1.116.
For provenance purposes, this commit was AI assisted.
---------
Co-authored-by: Michael Ramos <mdramos8@gmail.com>
* feat(server): add Code Tour agent as third review provider
Adds a "tour" provider alongside "claude" and "codex" that generates a
guided walkthrough of a changeset from a product-minded colleague's
perspective. Reuses the entire agent-jobs infrastructure (process
lifecycle, SSE broadcasting, live logs, kill support, capability
detection) so the only new server plumbing is the prompt/schema module,
the GET endpoint for the result, and checklist persistence.
- tour-review.ts: prompt, JSON schema, Claude (stream-json) and Codex
(file output) command builders, and parsers for both. Prompt frames
the agent as a colleague giving a casual tour, orders stops by reading
flow (not impact), chunks by logical change (not per-file), and writes
QA questions a human can answer by reading code or using the product.
- review.ts: tour buildCommand with per-engine model defaults (fixes a
bug where Codex was defaulting to "sonnet", a Claude model), an
onJobComplete handler that parses and stores the tour, plus GET
/api/tour/:jobId and PUT /api/tour/:jobId/checklist endpoints.
- agent-jobs.ts: tour capability detection and config threading from the
POST body through to the provider's buildCommand.
- shared/agent-jobs.ts: engine/model fields on AgentJobInfo so the UI
can render tour jobs with their chosen provider + model.
For provenance purposes, this commit was AI assisted.
* feat(review-editor): Code Tour dialog with animated walkthrough
Adds the tour overlay surface: a full-screen dialog with three animated
pages (Overview, Walkthrough, Checklist) that renders the CodeTourOutput
from the server-side agent. Opens automatically when a tour job completes
and can be dismissed with Escape or backdrop click.
- components/tour/TourDialog.tsx: three-page animated dialog. Intro page
uses a composition cascade (greeting, Intent/Before/After cards with
one-shot color-dot pulse, key takeaways table) with a pinned Start
Tour button and a bottom fade so content scrolls cleanly under it.
Page slides between Overview/Walkthrough/Checklist are ease-out
springs with direction auto-derived from tab index.
- components/tour/TourStopCard.tsx: per-stop accordion using motion's
AnimatePresence for height + opacity springs with staggered children
for detail text and anchor blocks. Anchors lazy-mount DiffHunkPreview
on first open to keep mount costs low. Callout labels (Important,
Warning, Note) are deterministic typography, no emoji.
- components/tour/QAChecklist.tsx: always-open list of verification
questions with per-item spring entrance and persistent checkbox state
that PUTs to the server.
- hooks/useTourData.ts: fetch + checklist persistence with a dev-mode
short-circuit when jobId === DEMO_TOUR_ID so UI iteration doesn't
require a live agent run.
- demoTour.ts: realistic demo data (multi-stop, multiple takeaways,
real diff hunks) for dev iteration.
- App.tsx: tour dialog state, auto-open on job completion, and a
dev-only "Demo tour" floating button + Cmd/Ctrl+Shift+T shortcut
guarded by import.meta.env.DEV.
- index.css: all tour animations (dialog enter/exit, page slides, stop
reveal cascade, intro exit) with reduced-motion fallbacks. Dark-mode
contrast tuning across structural surfaces (borders, surface tints,
callout backgrounds) so the design works in both themes.
- package.json: adds motion@12.38.0 for spring-driven accordion physics
and the intro composition cascade.
For provenance purposes, this commit was AI assisted.
* feat(review-editor): wire Code Tour into agent jobs UI + polish DiffHunkPreview
Connects the tour provider to the existing agent jobs sidebar and detail
panel so the user can launch a tour the same way they launch Claude or
Codex reviews. Also tightens DiffHunkPreview (used inside tour anchors)
to render correctly on first mount and handle every diff hunk shape the
agent might produce.
- ui/components/AgentsTab.tsx: tour engine (Claude/Codex) + model select
when launching a tour job. Resets model to blank when falling back to
Codex-only so Codex uses its own default instead of inheriting
"sonnet" (a Claude model).
- ui/hooks/useAgentJobs.ts: forward engine/model config from the UI
through to the POST /api/agents/jobs body so the server's tour
provider can pick the right command shape.
- dock/panels/ReviewAgentJobDetailPanel.tsx: tour-aware detail panel.
Replaces the "Findings" tab with a "Status" card for tour jobs and
surfaces an "Open Tour" button that opens the dialog overlay when
the tab is already in the dock.
- components/DiffHunkPreview.tsx: synchronous pierre theme init via a
useState lazy initializer so the first render (inside a tooltip) is
already themed; robust hunk parser that handles bare @@ hunks, file-
level --- hunks, and full git diffs; "Diff not available" fallback
for broken hunks.
- components/ReviewSidebar.tsx, dock/ReviewStateContext.tsx: small
wiring changes to expose openTourPanel from the review state context.
Also removes two dead components (TourHeader, DiffAnchorChip) that were
superseded by the inline title bar in TourDialog and the inline
AnchorBlock in TourStopCard.
For provenance purposes, this commit was AI assisted.
* refactor(server): extract Code Tour lifecycle into shared createTourSession factory + Pi parity
The route-parity test suite requires the Pi server (apps/pi-extension/)
to expose the same routes as the Bun server (packages/server/). After
Code Tour shipped in the prior 3 commits, Pi was missing /api/tour/:jobId
(GET) and /api/tour/:jobId/checklist (PUT).
A naive mirror would duplicate ~100 lines of provider-branch logic
(buildCommand, onJobComplete, in-memory maps) into Pi's serverReview.ts,
perpetuating the existing claude/codex duplication problem. Instead,
extract the pure runtime-agnostic tour lifecycle into a
createTourSession() factory that both servers consume. Route handlers
stay per-server (different HTTP primitives) but are ~5 lines each.
Net effect: Pi port is ~25 lines instead of ~100. Future providers that
adopt the same pattern cost ~15 lines per server.
- tour-review.ts: new createTourSession() at the bottom of the module.
Encapsulates tourResults + tourChecklists maps, buildCommand (with
the Claude-vs-Codex model-default fix baked in), onJobComplete
(parse/store/summarize), plus getTour/saveChecklist lookup helpers
for route handlers.
- review.ts (Bun): tour branch in buildCommand, tour branch in
onJobComplete, and both route handlers collapse to one-line calls
into the factory. Drops ~70 lines.
- vendor.sh: add tour-review to the review-agent loop so Pi regenerates
generated/tour-review.ts on every build:pi.
- serverReview.ts (Pi): import createTourSession from
../generated/tour-review.js; add tour branch to buildCommand (one
line), tour branch to onJobComplete (three lines), and GET/PUT route
handlers using Pi's json() helper. ~25 lines added.
- agent-jobs.ts (Pi): extend buildCommand interface to accept config
and return engine/model; thread config from POST body; extend
spawnJob to persist engine/model on AgentJobInfo; add tour to
capability list.
Claude and Codex branches are intentionally left in the old pattern;
they can migrate to the factory approach when next touched to keep
this change's blast radius contained.
Tests: 518/518 passing (previous 3 route-parity failures resolved,
plus 2 extra assertions passing since tour is now in both servers'
route tables).
For provenance purposes, this commit was AI assisted.
* refactor(tour): self-review cleanup — Pi route match parity + remove setOpen shim
Two small cleanups surfaced by a self-review pass:
- Pi's GET /api/tour/:jobId route used `endsWith("/checklist")` to block
the checklist sub-route, while Bun uses `includes("/", ...)`. The two
are not equivalent: a URL like /api/tour/abc/extra would be accepted
by Pi (jobId becomes "abc/extra") but correctly rejected by Bun.
Align Pi to Bun's pattern.
- TourStopCard had a leftover `setOpen` shim from when open state was
local to the card. State is now lifted to TourDialog, so the shim
just aliases onToggle and ignores its argument. Replace with a
direct `onClick={onToggle}` on the trigger button.
520/0 tests still pass.
For provenance purposes, this commit was AI assisted.
* feat(review): per-provider model + effort controls across all review agents
Exposes model, effort/reasoning, and fast-mode (Codex) as per-job knobs
for Claude, Codex, and Code Tour via the Agents tab. Threads the config
from the UI through the POST body into each provider's buildCommand,
which emits the corresponding CLI flags (--model/--effort for Claude;
-m / -c model_reasoning_effort / -c service_tier for Codex). Bun and Pi
servers stay in parity.
UI: option catalogs (CLAUDE_MODELS, CLAUDE_EFFORT, CODEX_MODELS,
CODEX_REASONING, TOUR_CLAUDE_MODELS) are defined once and mapped into
every dropdown so there's a single source of truth for the choices.
Tour's Claude/Codex settings are kept separate from the standalone
provider's state so toggling the tour engine no longer overwrites the
provider's last choice.
Job badge now surfaces the model/reasoning/fast selection for both Tour
and the standalone Codex provider.
For provenance purposes, this commit was AI assisted.
* docs: add Prompts reference page
Documents the three-layer structure every review call travels through
(CLI system prompt, our user message of review-prompt + user-prompt,
output schema flag), names each constant + its file, and calls out
that the Claude/Codex review prompts are the upstream ones from those
projects (only Code Tour's prompt is original to Plannotator).
Linked from the Code Review command page.
For provenance purposes, this commit was AI assisted.
* feat(review): persist per-agent, per-model settings in a single cookie
Drops the hidden "Default" options from the agent dropdowns, locks in explicit
sensible defaults (Opus 4.7/High for Claude review, gpt-5.3-codex/High for Codex,
Sonnet/Medium for Tour Claude, gpt-5.3-codex/Medium for Tour Codex), and
remembers the last-used effort/reasoning/fast-mode per (agent job × model) so
switching models reveals the choices you made last time.
Backed by a single `plannotator.agents` cookie holding the whole settings tree —
one read on mount, one mirror write per change, all mutations funnel through a
single React state owner to avoid stale-read or lost-write races across rapid
successive updates.
For provenance purposes, this commit was AI assisted.
* fix(tour): unblock reduced-motion nav + flush pending checklist save on close
Two P2 issues surfaced in review:
- Under prefers-reduced-motion, tour page animations are suppressed, so
onAnimationEnd never fires and exitingPage stuck on 'intro' kept the
walkthrough/checklist gated out. navigate() now swaps pages directly when
reduced motion is on, mirroring the pattern already used in the wrapper.
- Checklist toggles are debounced 500ms before the PUT, but unmount only
cleared the timer — checking an item and closing within the window dropped
the save. Cleanup now flushes the pending payload with keepalive: true.
For provenance purposes, this commit was AI assisted.
* feat(review): surface Claude model + effort in job badge, trim redundant labels
Claude effort was never persisted on AgentJobInfo, so the job badge had nothing
to render beyond "Claude". Plumbs `effort` through the shared type, both
server build-command pipelines (Bun + Pi), and spawnJob, then teaches the
ProviderBadge to display Claude and Tour Claude model + effort with the same
shape Codex already had. Labels resolve via the dropdown catalogs so the
badge shows "Opus 4.7" / "High" instead of raw ids.
Server labels for both Claude and Codex reviews collapse to plain "Code Review"
(matching tour's "Code Tour"), since the badge now carries the provider +
model + settings — the title only needs to name the action.
For provenance purposes, this commit was AI assisted.
* polish(review): prefix agent dropdown options with the action name
The Run launcher listed "Claude Code", "Codex CLI", and "Code Tour" side by
side — the CLI's detection name was doing double duty as the action label.
Prefixes the review entries with "Code Review · " so scanning three options
surfaces two reviews + one tour instead of three raw provider names.
For provenance purposes, this commit was AI assisted.
* fix(review): drop invalid codex reasoning option + fail tour on empty output
Two P2 issues from PR review:
- Tour jobs exited 0 but returned null output would still be marked "done",
and the auto-open watcher would greet the user with a 404 "Tour not found"
dialog. onJobComplete now flips the job to failed with an error message
when nothing was stored, so the card reflects the real state.
- The codex reasoning dropdown offered "None", but codex-rs only accepts
minimal/low/medium/high/xhigh. Picking None sent `-c model_reasoning_effort=none`
and launched a broken job. Removed the option; added a one-shot cookie
migration so users who already saved "none" don't keep shipping it.
For provenance purposes, this commit was AI assisted.
* fix(tour): format Claude-engine logs + allow reading linked issues
Two P2/P3 issues from PR review:
- The stdout formatter keyed only on provider === "claude", so Code Tour
jobs running on the Claude engine streamed raw JSONL to the log panel
while Claude review jobs got formatted text. Widened the check to also
catch spawnOptions.engine === "claude" and mirrored the fix into Pi.
- The tour Claude allowlist permitted PR/MR commands but not issue reads,
yet the prompt explicitly asks the agent to open `Fixes #123` / `Closes
owner/repo#456` targets for deeper context. Claude was being denied
those commands mid-tour. Added gh issue view + gh api issues + glab
issue view to the allowlist.
For provenance purposes, this commit was AI assisted.
* cleanup(review): e2e punchlist — Pi parity + dedup + tests
- Pi now logs Claude parse failures with the same diagnostic as Bun
- Tour route match uses a regex instead of an includes-offset trick
- Drop `any` cast in Pi's Codex blocking-findings predicate
- Extract `patchClaude` twin of `patchCodex` in useAgentSettings
- Factor `resolveAgentCwd` helper in Bun review to match Pi
- AgentsTab launch payload becomes a per-provider dispatch table
- Wrap TourDialog in MotionConfig reducedMotion="user" so motion.*
children honor prefers-reduced-motion alongside the CSS keyframes
- Tests for parseTourStreamOutput/parseTourFileOutput and the Codex
perModel sanitizer
For provenance purposes, this commit was AI assisted.
* test(tour-review): use real TourStop shape in fixtures
The parsers don't validate stop fields so the original made-up shape
passed fine, but future readers would be misled. Use the real
CodeTourOutput / TourStop / TourDiffAnchor shapes from tour-review.ts.
For provenance purposes, this commit was AI assisted.
* cleanup(tour): hoist shared types + subfolder server module + dedup
- Hoist tour output types to packages/shared/tour.ts so server and UI
share one source of truth (prevents silent drift when schema changes).
- Move tour-review into packages/server/tour/ subfolder alongside the
existing packages/review-editor/components/tour/ convention.
- Extend tour.buildCommand to accept tour context directly (patch,
diffType, options, prMetadata), so Bun and Pi no longer duplicate the
buildTourUserMessage call site. Export TOUR_EMPTY_OUTPUT_ERROR from
the tour module to prevent string drift between servers.
- Early-return the tour branch in buildCommand so the review userMessage
is built once on the shared non-tour path instead of unconditionally
(dead work on tour launches) or twice (codex+claude dup).
- Regex capture group for tour checklist jobId in both servers —
replaces a second url.pathname.split("/")[3] fragile fallback.
- Drop existsSync guard in parseTourFileOutput (TOCTOU + duplicate
syscall); the outer try/catch already handles ENOENT.
- Dedupe two matchMedia reads in TourDialog behind a prefersReducedMotion
helper; memoize intro-page card array so unrelated state churn doesn't
recompute it.
- Comment hygiene on tour files — drop WHAT/change-referencing comments
added by this PR; keep non-obvious WHY.
For provenance purposes, this commit was AI assisted.
* fix(tour): hoist introCards useMemo above early returns + subfolder hook
The memoized intro-card array was placed after the loading/error early
returns in TourDialogContent, so the first successful render called one
more hook than the initial loading render — React threw "Rendered more
hooks than during the previous render" (error #310) the moment a tour
finished loading. Move the useMemo above the early returns and use
optional chaining on `tour` so it's safe to run during loading.
Also move useTourData.ts into a hooks/tour/ subfolder to start a
standard for feature-scoped hooks (was flat among siblings).
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>
* feat: detect OpenCode origin + activate PLANNOTATOR_ORIGIN override
Extend the origin detection chain in apps/hook/server/index.ts to recognize
OpenCode via `process.env.OPENCODE`, the stable runtime flag set by the
OpenCode binary (also sets OPENCODE_RUN_ID, OPENCODE_PID, OPENCODE_PROCESS_ROLE,
but OPENCODE=1 is the canonical detection flag).
Additionally, activate `PLANNOTATOR_ORIGIN` as an explicit override. The
comment in packages/server/index.ts already documented this variable, but
it was never actually read. It is now honored at the top of the detection
chain and validated against AGENT_CONFIG so only known origins take effect
— invalid values fall through to env-based detection.
Priority order after this change:
PLANNOTATOR_ORIGIN > Codex > Copilot CLI > OpenCode > Claude Code (default)
Single-point fix: all five existing `detectedOrigin` usages (lines 405,
568, 694, 741, 999) pick up the new chain automatically. The `'opencode'`
origin key is already defined in packages/shared/agents.ts (AGENT_CONFIG),
complete with a UI badge — the detection chain was the last missing piece.
Closes the gap noted in packages/shared/agents.ts header comment:
"If detection is via environment variable, add it to the detection chain
in apps/hook/server/index.ts (detectedOrigin constant)".
* fix: remove stray project argument from getServerConfig calls
getServerConfig only accepts gitUser — the project argument was silently
ignored at runtime but fails the typecheck. Reverts the two call sites to
their correct single-argument form.
For provenance purposes, this commit was AI assisted.
---------
Co-authored-by: Michael Ramos <mdramos8@gmail.com>
* Add annotate wide mode
* Extend wide mode: add Focus variant, enable in all modes, reposition controls
- Drop annotate-only gate in canUseAnnotateWideMode; available in plan review, annotate, and linked-doc flows (archive/diff still excluded)
- Add Focus mode alongside Wide: both collapse sidebars and suppress annotation panel auto-open; only Wide removes the reader width cap
- Move controls out of AnnotationToolstrip/StickyHeaderLane; render as absolute-positioned "Wide | Focus" text buttons floating above the plan card's top-right edge (no vertical space cost)
- Add Radix-based Tooltip primitive (packages/ui/components/Tooltip.tsx) and TooltipProvider; replaces the ad-hoc CSS tooltip with portaled, accessible, animated content
- Polish: tracking-wide for 11px labels, focus-visible ring, active press state, aria-pressed, weight-stable toggle (color-only), 60px right offset under tater mode
For provenance purposes, this commit was AI assisted.
* Update wideMode test to cover both annotate and plan review modes
The annotateMode gate was removed — update the test matrix to assert
wide mode is enabled in annotate AND plan review, and disabled for
archive/diff across both mode values.
For provenance purposes, this commit was AI assisted.
* Drop unused annotateMode parameter from canUseAnnotateWideMode
The parameter was ignored after the annotate-only gate was removed.
Prune it from the signature, the App.tsx call site and deps, and the
test matrix to match the actual behavior (disabled only for archive
and diff).
For provenance purposes, this commit was AI assisted.
---------
Co-authored-by: Michael Ramos <mdramos8@gmail.com>
Add `disable-model-invocation: true` frontmatter so these user-driven
commands (which open browser UIs and require human interaction) can
only be triggered explicitly by the user, not picked up automatically
by the model.
* 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>
* fix(opencode): reuse local server for review flows
Try the default local OpenCode server before spawning a new one, and resolve bundled assets and command paths correctly when the plugin is loaded from source during local testing.
* Fix typecheck after narrowing the opencode type to the sdk
* feat(plan-diff): word-level inline diff rendering
Two-pass hierarchical diff (diffLines outer + diffWordsWithSpace inner)
so modified plan blocks render with inline insertions/deletions in
context instead of showing the whole old block struck-through above the
whole new block. Resolves#560.
Engine (packages/ui/utils/planDiffEngine.ts):
- computeInlineDiff runs a second-pass word diff on modified blocks
that pass a whitelist gate (paragraph/heading/list-item with matching
structural fields).
- Sentinel substitution atomizes inline-code spans, markdown links, and
fenced code blocks before diffWordsWithSpace runs, so diff markers
never land inside backticks, link hrefs, or across fence boundaries.
Fence regex uses a backreference so variable-length (e.g., 4-backtick
wrapping 3-backtick) fences are matched atomically.
- Annotation context for an inline-diffed modified block now captures
both old and new content so comments on struck-through words preserve
that text in the exported feedback.
Renderer (packages/ui/components/plan-diff/PlanCleanDiffView.tsx):
- New InlineModifiedBlock component renders a modified block as one
structural wrapper with <ins>/<del> wrappers inside, parsed through
the local InlineMarkdown in a single pass so markdown delimiter pairs
survive across token boundaries.
- InlineMarkdown extended to recognize <ins>/<del> tag passthrough
(with recursive parsing of the wrapped content) and to recursively
parse link anchor text so diff markers inside links render correctly.
- Plain-text stop-char scanner includes '<' so <ins>/<del> dispatch
re-enters the loop instead of swallowing tag text.
- Click-to-annotate works in every editor mode (not just comment), with
the block-level onClick opening the popover directly.
Mode switcher (packages/ui/components/plan-diff/PlanDiffModeSwitcher.tsx):
- Adds a third "Classic" tab between Rendered and Raw. Rendered is the
new word-level default (labeled "exp"); Classic forces the legacy
block-level stacked fallback for every modified block.
Styling (packages/ui/theme.css, packages/editor/index.css):
- plan-diff-word-added / plan-diff-word-removed utility classes for
inline highlights with box-decoration-break: clone across line wraps.
- Inline <code> inside the diff wrappers picks up a tinted background
so code-pill changes read unambiguously green/red.
- New plan-diff-modified class (amber border) for inline-diff modified
blocks, matching the GitHub/VSCode convention of green=add,
red=remove, yellow=both.
Tests (packages/ui/utils/planDiffEngine.test.ts):
- 18 tests covering the engine's qualification gate, structural-field
matching, sentinel round-trip (inline code / links / fences), token
content for common edit patterns.
For provenance purposes, this commit was AI assisted.
* chore(demo): restructure default demo, add VITE_DIFF_DEMO stress test
Demo content changes that support the word-level diff work but do not
alter shipped app behavior — only what other devs see running dev:hook.
packages/editor/demoPlan.ts (default V3 editor content):
- Added a "Context" section at the top of the plan with prose that
showcases the word-level engine in V2→V3 diff: bold phrase swap,
inline-code pill swaps, a link URL change, and a single-line code
edit inside a config block.
- Moved the mermaid architecture diagram and graphviz service map to
an "Appendix: Diagrams" section at the end of the plan; they were
rendering ugly mid-document.
apps/hook/dev-mock-api.ts (Vite mock for the diff API):
- PLAN_V1 / PLAN_V2 split into *_DEFAULT (original Real-time
Collaboration plan — preserved identically from pre-branch state) and
*_DIFF_TEST (the 20-case Auth Service Refactor diff-engine stress
test, kept as an opt-in tool).
- Resolves which pair to serve based on VITE_DIFF_DEMO env var. Matches
the V2 Context section to the new V3 Context, with differences that
produce rich word-level inline diffs on first load.
- Diagrams moved to Appendix in V2_DEFAULT to match V3.
packages/editor/App.tsx:
- Both demo imports are active. VITE_DIFF_DEMO=1 swaps
DIFF_DEMO_PLAN_CONTENT into the editor's default; unset renders the
original Real-time Collaboration plan as before.
packages/editor/demoPlanDiffDemo.ts (new):
- 20-case stress test (paragraphs, headings, lists, tables, fences,
blockquotes, known limitations). Each case has an identical
"What to watch for" blockquote label in both V2 and V3 so the diff
view cleanly isolates each case. Opt-in only.
.gitignore:
- Ignore .claude/ runtime lock/state files. Machine-specific content
that should not be tracked.
For provenance purposes, this commit was AI assisted.
* style(plan-diff): refine modified-block visual — amber gutter, no fill
Drop the yellow background fill from .plan-diff-modified and keep only a
softened amber left border. Added/removed blocks remain loud (full fill +
strong border) because add/remove are block-scope events — the whole
block matters. Modify is a word-scope event — the individual changed
words carry loud inline red/green highlights, and a block-level fill
would compete with that inline work. The amber gutter at 75% opacity now
reads as a quiet "look inside, the change is in the text" marker that
sits coherently with the rest of the palette.
For provenance purposes, this commit was AI assisted.
* fix(plan-diff): sanitize link hrefs against javascript: / data: schemes
PlanCleanDiffView has its own local copy of InlineMarkdown (separate
from the one in Viewer.tsx). The link-rendering branch was passing the
captured URL directly to href with no validation, so a plan containing
[click me](javascript:alert(document.cookie))
would render as a live clickable anchor in the diff view. Plan content
is attacker-influenced — Claude pulls from source comments, READMEs,
fetched URLs — so this is a real exploit path in the diff flow.
Port the same guard Viewer.tsx already has: sanitizeLinkUrl() rejects
javascript:, data:, vbscript:, and file: schemes (case-insensitive, with
optional leading whitespace). Rejected links render their anchor text as
plain text instead of a clickable <a>, so the content is still visible
to the reader but no longer dangerous.
For provenance purposes, this commit was AI assisted.