* Collapse viewed files in all-files review
* Style diff hunk separators to be slimmer and subtler
Reduce separator height from 32px to 24px, use theme-integrated
semi-transparent backgrounds via --diffs-bg-separator-override,
and fade text/buttons with opacity for a less visually heavy bar.
* Refine review header layout
Move sidebar toggles to far right, options menu to their left.
Add divider between file tree button and repo label.
Remove worktree pill from header (still accessible via menu).
* Fix gutter drag selecting only one line
Switch from deprecated enableHoverUtility + renderHoverUtility to
enableGutterUtility + onGutterUtilityClick. The old API never entered
Pierre's gutterSelecting mode, so dragging the gutter button always
reported a single line. The new API receives the full drag range.
Closes#679
* Preserve truthful approval semantics for Claude plan bypass
Thread a clear-context reminder flag through approval decisions, expose a Claude Code-only approval entry that requests bypass mode, and keep the hook response honest by emitting a reminder instead of claiming context was cleared.
Constraint: Claude Code PermissionRequest hooks have no documented clearContext response field and bypassPermissions is only a request when the mode is available.
Rejected: adding a permission-mode enum or undocumented clearContext field | those would misrepresent hook capabilities and broaden the contract.
Confidence: high
Scope-risk: moderate
Directive: Do not claim Plannotator clears context until Claude Code documents a hook field for that behavior; keep reminder copy truthful.
Tested: bun test packages/server apps/hook; bun test packages/editor/wideMode.test.ts packages/ui/hooks/useAgentSettings.test.ts; bun test; bun run build:review; bun run build:hook; bun run typecheck; git diff --check --cached.
Not-tested: Browser warning-dialog replay and interactive Claude Code smoke test.
Co-authored-by: OmX <omx@oh-my-codex.dev>
* Keep approval payloads truthful across Claude Code and Pi
Constraint: Claude Code hooks cannot clear context directly; the new action remains a truthful reminder plus bypass-mode approval.
Rejected: Sending OpenCode agent-switch state for Claude Code | it leaked build (?) UI state and misleading payload fields.
Confidence: high
Scope-risk: narrow
Directive: Keep OpenCode agent switching gated to opencode-origin approval payloads.
Tested: bun run typecheck; bun test; bun run build:review; bun run build:hook
Not-tested: Manual browser click-through of the Claude Code dropdown.
* Expose the clear-context reminder permission default
Constraint: Claude Code hooks can only emit a systemMessage nudge, not clear context directly.
Rejected: Server protocol changes | existing permissionMode plus clearContextNudge wire fields already support the behavior.
Confidence: high
Scope-risk: narrow
Directive: Keep bypassPermissionsClearReminder as a UI/storage-only synthetic mode that decomposes before /api/approve.
Tested: bun test packages/editor/approvalBody.test.ts; bun run --cwd apps/review build && bun run build:hook; bun run --cwd packages/ui typecheck; git diff --check
Not-tested: Root bun run typecheck could not run because tsc was not on PATH for the root script.
* Reject invalid persisted permission modes
Constraint: Stored browser values can be stale, corrupt, or from a future Plannotator build.
Rejected: Trusting the storage read with a type assertion | invalid values could flow into UI state and approval request construction.
Confidence: high
Scope-risk: narrow
Directive: Keep PermissionMode storage reads validated against PERMISSION_MODE_OPTIONS when adding or renaming modes.
Tested: bun test packages/editor/approvalBody.test.ts; bun run --cwd packages/ui typecheck; git diff --check
Not-tested: Full repo typecheck/build, outside this review-fix scope.
* Ensure approvals use the live permission setting
Settings persisted permission mode changes, but App kept the original mode in React state and used that stale value when building approval payloads. Push the Settings change back into App so the selected clear-reminder mode reaches the hook decision.
Constraint: Permission mode storage is cookie-backed while approval payload construction reads App state.\nRejected: Read permission cookies during approval | would couple approval construction to browser storage and duplicate Settings state.\nConfidence: high\nScope-risk: narrow\nDirective: Keep permission-mode writes synchronized with approval state when adding or changing modes.\nTested: bun test packages/editor/approvalBody.test.ts packages/ui/components/ApproveDropdown.test.tsx; bun x tsc --noEmit -p packages/ui/tsconfig.json; bun run build:hook; direct Playwright hook smoke verified bypassPermissionsClearReminder UI and hook payload.\nNot-tested: Live interactive Claude CLI session; direct hook/server simulation covered the wire output.
---------
Co-authored-by: OmX <omx@oh-my-codex.dev>
* feat(ui): expand theme system to 49 themes with matched syntax highlighting
Add 25 new themes sourced from shiki's bundled TextMate themes (Everforest,
Nord, Solarized, GitHub, One Dark Pro, Night Owl, Material, Vitesse, Vesper,
Poimandres, Ayu Dark, Houston, Laserwave, Andromeeda, Aurora X, Dark+/Light+,
Min, One Light, Plastic, Red, Slack, Snazzy Light, Vitesse Black). Each theme
was audited against the shiki source JSON to ensure correct color mapping.
Wire pierre/diffs syntax highlighting to match the active UI theme via a
SHIKI_THEME_MAP (35 of 49 themes get matched syntax token colors in diffs).
Add theme preview mode — a compact bottom-docked picker that lets users
browse themes while seeing the page update live behind it.
Settings panel enhancements:
- Syntax highlighting badge on themes with diff color matching
- Mode unavailability dimming (dark-only themes dim in light mode)
- Plannotator pinned first, rest alphabetical
* fix(themes): address review feedback — contrast, stale CSS, syntax mode safety
1. Material light: foreground #90A4AE → #546E7A (Blue Grey 600) for readable
UI text (~7:1 contrast on white). Primary-foreground #FAFAFA → #263238
for contrast on teal buttons.
2. DiffHunkPreview: add colorTheme to useEffect deps so unsafeCSS (bg/gutter
injection) recomputes when themes change, preventing stale mixed-theme
state in mounted tooltips during preview mode.
3. SHIKI_THEME_MAP: make mode-aware with null entries for modes lacking a
shiki counterpart. resolveSyntaxTheme() returns undefined when the current
mode has no match, so pierre falls back to its defaults rather than
applying dark token colors on a light background (fixes Tokyo Night light).
* fix(review): self-review cleanup — memo, double-call, escape key
- DiffHunkPreview: extract resolveSyntaxTheme to a variable instead of
calling it twice in the spread expression
- PierreDiffContent memo: compare syntaxTheme by dark/light string values
instead of reference equality (resolver returns new objects each call)
- Preview mode: add Escape key handler to return to full settings
* fix(ui): consume Escape event in theme preview to prevent leaking to page
* fix(ui): smart resolution + existence-validation for code-file paths
The bare-prose / backtick path detector linkifies anything that looks
like a code path. Two failure modes regularly produce dead links: prose
abbreviations like `editor/App.tsx` (real file is
`packages/editor/App.tsx`) and references to files the plan proposes
but hasn't created yet. Both 404 on click with no UX cue.
Resolves abbreviated paths via a case-insensitive suffix-match against
a cached project walk (`resolveCodeFile` in `packages/shared/resolve-file.ts`),
mirroring what `resolveMarkdownFile` already does for markdown. The walk
is pre-warmed when the plan/annotate server boots and on every
`/api/doc` request, with a 30s TTL so newly-created files can resolve
mid-review. Storing the walk as a Promise makes the cache race-safe —
concurrent callers piggyback rather than starting a second walk.
A new `POST /api/doc/exists` endpoint takes a batch of candidate paths
and reports `found` / `ambiguous` / `missing` / `unavailable` per path.
On the frontend, `useValidatedCodePaths` extracts candidates from the
markdown on load and POSTs once. The renderer reads the result via
`CodePathValidationContext`: `found` opens directly with the resolved
absolute path, `ambiguous` opens a `CodeFilePicker` popover listing all
matches (common in monorepos where `App.tsx` exists in several
packages), `missing` demotes the link to plain code, and `unavailable`
falls back to the optimistic linkification we have today. While
validation is in flight, every detected path renders as a link, so
first paint is unchanged.
The detection itself gets a shape filter (`isPlausibleCodeFilePath`)
that hard-rejects shell brace expansion (`{a,b}`), glob wildcards, and
whitespace, while explicitly allowing `[` / `]` so Next.js dynamic
routes (`app/[slug]/page.tsx`) still resolve. The bare-prose regex moves
out of `InlineMarkdown.tsx` into `code-file.ts` so the renderer and the
new server-side extractor use the same source of truth, and the
extractor strips fenced code blocks, HTML comments, and URL ranges
before scanning so it only emits candidates the renderer would actually
paint.
Pi extension mirrors the Bun changes (handler upgrade, pre-warm,
`/api/doc/exists` route). When the popout's `/api/doc` request 404s the
dialog now surfaces "File not found in repo: <path>" instead of
silently swallowing the error.
Tests: `code-file.test.ts` extended with shape-filter and Next.js-route
cases; new `extract-code-paths.test.ts` covers extraction, dedup,
fenced/HTML/URL exclusion, and the URL-with-parens regression; new
`resolve-file.test.ts` covers the suffix-match strategy, leading `./`
handling, ambiguous results, and ignored-dir behavior.
* fix(ui): thread doc-base through code-path validator
Out-of-tree linked docs (and annotate-mode files outside cwd) reference
files relative to themselves. The validator was resolving against cwd
only, so those paths got marked missing and the renderer demoted them
to plain text — even though clicks still resolved correctly via base.
Also tightens the suffix-match's leading-segment strip so `../foo.ts`
no longer silently misresolves to an unrelated `foo.ts` in cwd.
Cleanup: delete unused extract-code-paths import in reference-handlers,
add the export entry to packages/shared so consumers don't rely on
Bun's lenient subpath resolution. Add TODO(security) comments at both
handleDocExists sites flagging that absolute paths bypass project-root
containment.
223 tests pass (3 new resolver cases for baseDir + ../ regression).
* refactor(editor): dedupe activeDocBaseDir; expand security TODO
Self-review fallout:
1. The doc-base expression `linkedDocHook.filepath ? dirname(...) :
imageBaseDir` lived in two places (click-time URL builder and Viewer
prop). If they drift, validator and click resolve against different
bases and we silently re-introduce the demote-correct-link bug.
Extract to a single useMemo.
2. The handleDocExists security TODO mentioned absolute paths in
`paths[]` but I just added `base` acceptance, which has the same
shape of leak (hostile sender supplies base=/secret/dir + relative
path). Both vectors flagged in one TODO, mirrored Bun + Pi.
223 tests pass; both builds clean.
* fix(ui): code-file popout shows real error; misc consistency
Review fallout:
- `CodeFilePopout` hardcoded "File not found in repo" regardless of
cause. The hook already captures the server's error string, so an
ambiguous-path 400 (which can happen if a user clicks an optimistic
link before validation completes) was surfacing as a misleading
not-found message. Render the actual `error` and only show the
planned/future-file caveat when the error matches "file not found".
- `InlineMarkdown` emitted demoted bare-prose paths as raw strings
while every other plain-text branch in `emitPlainTextWithBareUrls`
routes through `transformPlainText`. Cosmetic-only today since
paths rarely contain transformable content, but the divergence
invites copy-paste rot. Routed through the same helper.
- CLAUDE.md missed the new POST /api/doc/exists endpoint in both
Plan Server and Annotate Server tables. Added.
223 tests pass; both builds clean.
* fix(ui): demote paths the extractor excluded from validation
When the validator is ready but a candidate path has no entry in the
validated map, the extractor intentionally excluded it — e.g. inside
an HTML comment or fenced code block. The renderer was optimistically
linking these because gateCodePath returned 'link' for missing entries.
Found during manual testing: `<!-- packages/editor/App.tsx -->` inside
a paragraph (parser doesn't recognize HTML comments as block-level)
was rendered as a clickable link. The extractor correctly stripped the
comment, but the renderer's optimistic fallback overrode that.
Also adds manual test harness: tests/manual/path-detection/ with
sandbox setup + three launcher scripts (plan mode, annotate in-tree,
annotate out-of-tree) covering ~30 test cases.
223 tests pass; both builds clean.
* fix(ui): skip HTML comments in InlineMarkdown scanner
The parser doesn't recognize <!-- --> as block-level HTML, so comments
inside paragraphs fall through to InlineMarkdown. The scanner then
finds paths inside the comment text and linkifies them.
The previous gateCodePath fix (demote when not in validated map) didn't
help here because the same path appeared elsewhere in the document —
the map had an entry from the non-comment occurrence.
Fix: match <!-- ... --> at the top of the scanner loop and skip the
entire comment. HTML comments should be invisible per CommonMark spec.
* fix: handle unavailable variant in markdown resolve narrowing
The shared ResolveResult type gained an `unavailable` variant for code
files. The markdown resolver never returns it, but TS can't narrow
past it without an explicit guard. Both Bun and Pi handlers now guard
`not_found || unavailable` before accessing `result.path`.
* feat(ui): shortcut registry foundation
Salvages the foundation of #380 by @gjermundgaraba — the original PR sat
6 weeks waiting for review and was closed by the author. This lands the
engine, scope data, and auto-generated marketing docs page, deferring
per-component handler migration to follow-up PRs.
Layout:
- packages/ui/shortcuts/{core,runtime,index}.ts — engine + barrel
- packages/ui/shortcuts/plan-review/* — scopes for plan editor surfaces
- packages/ui/shortcuts/code-review/* — scopes for review editor surfaces
- packages/{editor,review-editor}/shortcuts.ts — per-app surface composition
- apps/marketing/src/{components,lib} — auto-generated docs page
App behavior is unchanged: existing ad-hoc keydown handlers keep running.
The registry is plumbing waiting for migration PRs to wire to it.
Co-authored-by: Gjermund Garaba <gjermund@garaba.net>
* feat(ui): add missing shortcuts; warn on hold-binding gap
Addresses code review on #652.
Adds five live shortcuts that were missing from the registry, so the
auto-generated /docs/reference/keyboard-shortcuts page is no longer
incomplete:
- Mod+P (print plan) — plan review + annotate
- Mod+B (toggle file tree) — code review (new "Layout" section)
- Mod+. (toggle review sidebar) — code review
- Mod+Shift+T (toggle demo tour, dev-only) — code review
- Mod+Shift+F (focus PR comments search) — new code-review/prComments scope
Also whitelists `.` as a valid normalized binding token in the registry
validator (keeps catching typos like Cmd/Ctrl while allowing legitimate
punctuation keys).
Hold-binding gap: the registry's `Alt hold` advertises a shortcut that
`useShortcutScope` cannot dispatch. Today this is harmless because the
existing `useInputMethodSwitch` hook handles it bespoke. Added warning
comments at the binding declaration and next to `useDoubleTapShortcuts`
so future migrators don't fall in. A `useHoldShortcuts` hook is the
proper fix and will land with the App.tsx migration that needs it.
* feat(ui): comprehensive shortcut registry coverage
Two-Sonnet inventory audit found 19 more user-facing shortcuts not in
the registry, on top of the 5 added in the previous commit. Adding all
of them so the auto-generated /docs/reference/keyboard-shortcuts page
ships honest and the in-app help modal (once migrated to read from the
registry) is comprehensive.
Plan / annotate surface (4 added):
- Escape closes image lightbox (Viewer)
- Mod+Enter saves annotation edit (AnnotationPanel)
- Escape cancels annotation edit (AnnotationPanel)
- Enter confirms image name (ImageAnnotator name field)
Code review surface (15 added):
- v / a in single-file diff mode (toggle viewed / stage)
- v, a, x, z, c, [, ] in all-files (new AllFilesDiff scope)
- Mod+Enter / Escape / Tab (new SuggestionModal scope)
- Mod+Enter / Escape (new AI Assistant scope: AITab + AskAIInput)
- Escape closes tour dialog (new TourDialog scope)
Engine: extended NAMED_TOKENS with `[` and `]` for the all-files
navigation bindings.
Tests updated for the new section ordering. App behavior unchanged —
this is pure declarative metadata. The existing ad-hoc keydown handlers
keep running; the registry is plumbing for the migration follow-up PRs.
* fix(ui): suggestion modal scope advertised non-existent submit shortcut
Self-review caught: SuggestionModal.tsx only has Escape (close) and Tab
(indent) handlers — no Mod+Enter. The Mod+Enter the inventory agent
referenced lives in AnnotationToolbar's suggested-code textarea, which
is already covered by reviewAnnotationToolbarShortcuts.submitComment.
Removed the bogus submit action from the scope. Renamed cancel→close
since the modal preserves suggestion state on Escape (it's not a
discard).
* fix(review): remove broken Cmd+Shift+C copy-diff shortcut
The handler at App.tsx:695 checked `e.key === 'c'` but Shift forces
`e.key` to uppercase `'C'`, so the shortcut never fired. The in-app
help modal also advertised it incorrectly as "Toggle comment mode".
Removing the broken shortcut entirely rather than fixing the case
mismatch — the toolbar's Copy button already does the same thing,
and there's no evidence anyone was relying on the keyboard shortcut.
Three deletions:
- packages/review-editor/App.tsx — handler block
- packages/review-editor/shortcuts.ts — copyDiff registry action
- packages/ui/components/KeyboardShortcuts.tsx — help modal entry
* chore(ui): address review nits
- F3: refresh AGENTS.md scope inventory (added scopes were missing)
- F5: getShortcutPlatform() now delegates to packages/ui/utils/platform
isMac instead of duplicating the regex+SSR guard
- F7: drop the stand-alone imageAnnotator.confirmName entry; the
behaviour is folded into the existing save action's hint so the
marketing docs page no longer renders two Enter rows with
different descriptions
- F8: remove the unused listenerOptions parameter from useShortcutScope
(no callers; inline objects in the dep array would have caused
re-render loops if anyone tried to use it)
TODOs left for the migration follow-up:
- F2: cross-scope arbitration — dispatchShortcutEvent should bail on
event.defaultPrevented; needs preventDefault to be claimed
consistently across scopes first
- F6: matchesKeyToken doesn't actually match the Space token; needs a
special case before any scope binds Space
App behaviour unchanged.
---------
Co-authored-by: Gjermund Garaba <gjermund@garaba.net>
* fix(review): stop diff list re-rendering on every comment keystroke (#659)
Move the useAnnotationToolbar hook + AnnotationToolbar/SuggestionModal
into a new ToolbarHost wrapper so per-keystroke form-state changes no
longer re-render AllFilesDiffView / DiffViewer (and therefore not every
LazyFileDiff and Pierre <FileDiff> beneath them). Parents talk to the
host through an imperative handle for selection/edit/token-click.
Verified with a console.count probe at the top of AllFilesDiffView:
typing 20+ chars no longer increments the render counter.
Out of scope (tracked in #659): React.memo on LazyFileDiff with stable
props, React Compiler, server-side prerendering of the review diff,
throttling capture-phase scroll listeners.
* fix(review): drop type cast on ToolbarHost mousemove handler
Widen useAnnotationToolbar's handleMouseMove signature to accept the
structural { clientX, clientY } shape — the implementation only reads
those two fields, and this lets ToolbarHost pass it directly to
window.addEventListener without an `as unknown as React.MouseEvent`
cast. Function parameters are contravariant, so any caller passing a
React.MouseEvent or native MouseEvent remains valid.
No runtime change.
* feat(editor): auto-close left sidebar when TOC is empty
When the parsed document has no level 1-3 headings (common for annotate
and annotate-last sessions), keep the sidebar closed regardless of the
"Auto-open Sidebar" preference. The collapsed flag strip stays
available so users can still open it manually; manual re-open is not
overridden until the document changes again.
* fix(editor): hoist hasTocEntries above the effects that read it
Adding hasTocEntries to the tocEnabled sync effect's dep array while
the const itself was declared 300+ lines later threw a TDZ error at
render ("Cannot access 'Pe' before initialization" in the minified
bundle). Move the useMemo above the effects so dep-array evaluation
sees an initialized binding.
* feat(review): right-click file tree row to copy path or filename
* feat(review): add "Copy full path" to file tree context menu
Resolves to activeWorktreePath ?? agentCwd ?? gitContext.cwd; hidden in PR-review mode where files aren't on local disk.
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
* fix(ui): code file and linked doc resolution in annotate-last mode
In annotate-last mode, imageBaseDir is set to "last-message" (a
non-path string), which gets sent as the base query param to /api/doc
and causes a 404. Guard both the code file popout and linked doc
URL builders to skip non-path imageBaseDir values and fall back to
projectRoot instead.
* fix(ui): drop projectRoot fallback to preserve server-side sandbox
Sending base=projectRoot bypasses the isWithinProjectRoot check in
/api/doc for code files (line 80) and skips containment entirely for
markdown (lines 40-57). Fall back to undefined instead, which omits
the base param and lets the server resolve paths against its own cwd
with sandbox checks intact — matching plan mode behavior.
Add file-scoped comment support to the all-files diff view via
onAddFileCommentForFile callback and CommentPopover. Remap keyboard
shortcuts: C now opens file comment, X collapses/expands files.
For provenance purposes, this commit was AI assisted.
* feat(review): all-files diff view with lazy mounting
Renders all diffs in a single scrollable pane using individual FileDiff
components with sticky file headers, collapse toggles, and viewport-based
lazy mounting via IntersectionObserver.
For provenance purposes, this commit was AI assisted.
* feat(review): convert all-files view to dock panel + fixes
- Register AllFilesDiffView as a proper dock panel so it coexists
with single-file tabs, AI chat, PR panels
- Remove viewMode toggle in favor of dock panel lifecycle
- Fix annotation toolbar positioning (mouse tracking)
- Fix annotation PR/scope filtering for stacked PRs
- Fix stale patch on file switch (deferred toolbar open)
- Fix conventional comments boolean comparison
- Fix SuggestionModal props to match DiffViewer pattern
- Wire reviewBase for correct hunk expansion context
- Fix pre-existing CSS bug on .has-annotations background
- Add Shift+V/A keyboard shortcuts for viewed/stage toggle
- Add [/] keyboard shortcuts for file-to-file scrolling
- Add keyboard shortcuts to settings panel
- Restore scroll-based file tree highlight via dock panel state
For provenance purposes, this commit was AI assisted.
* feat(review): sidebar toggles, keyboard shortcuts, click-drag selection
- Move sidebar tab buttons (annotations/AI/agents) to header bar
- Generalize useSidebar hook for both plan and review editors
- Remove internal tab state from ReviewSidebar, receive from parent
- Add file tree toggle button at far-left of header
- Add click-and-drag line selection in diff content area
- Change keyboard shortcuts from Shift+V/A to bare v/a/c
v = toggle viewed (+ collapse in all-files mode)
a = toggle git add/stage
c = collapse/expand file (all-files mode)
- Widen DiffOptionsPopover to prevent word-alt line break
- Fix Shift+V/A targeting wrong file in all-files mode
- Default to all-files panel on initial load
- Hide dock headers when all-files panel is the only panel
For provenance purposes, this commit was AI assisted.
* feat(review): click-and-drag line selection in diff content area
Extract shared selection helpers to utils/diffSelection.ts and add
mouseup handlers to both DiffViewer and AllFilesDiffView so users can
click and drag across code lines to open the annotation toolbar —
not just the gutter.
For provenance purposes, this commit was AI assisted.
* fix(review): address AI review findings
- Gate all-files keyboard shortcuts on isActive to prevent double-toggle
when a single-file tab is focused
- Fix getDiffSelection to iterate all diffs-container elements so
click-drag selection works on files beyond the first
- Clear needsInitialDiffPanel after opening all-files panel to prevent
forced reactivation on diff switches
- Clear activeFilePath and collapsedFiles when files change to prevent
stale state across PR/diff switches
- Add file.patch to LazyFileDiff fetch deps so file contents refetch
when the diff context changes
- Add toggleCollapse to keyboard effect dependency array
For provenance purposes, this commit was AI assisted.
* feat(review): add z to undo collapse with scroll-back
Press z to reopen the last collapsed file and scroll back to it.
Maintains a history stack so repeated z walks back through collapses.
Skips already-expanded entries. History clears on diff/PR switch.
For provenance purposes, this commit was AI assisted.
* fix(review): review findings + sidebar toggles + folder icon
- Fix stale context memo: add isAllFilesActive + handleAddAnnotationForFile
to useMemo deps
- Fix v handler: clear activeFilePath after collapse, only collapse when
marking viewed ON (not when toggling OFF)
- Fix click-drag copy regression: only trigger annotation toolbar for
multi-line selections, preserve single-line/word selection for Cmd+C
- Fix LazyFileDiff: clear stale file contents before refetch
- Clean up App.tsx v/a handler: early return when isAllFilesActive
- Add Cmd+B (file tree) and Cmd+. (sidebar) keyboard shortcuts
- Add toggle menu items with kbd badges to Options dropdown
- Replace file tree icon with simple folder
- Add z (undo collapse) to keyboard shortcuts panel
For provenance purposes, this commit was AI assisted.
* chore: remove dead code from iterations
- Remove unused FileTreeIcon import and component file
- Remove unused AnnotationsIcon component from ReviewHeaderMenu
- Remove unused onClose prop and close button from FileTree
For provenance purposes, this commit was AI assisted.
* fix(review): gate v/a on diff panel, keep annotation selection in all-files
- Track isDiffPanelActive so v/a shortcuts only fire when viewing a
single-file diff, not PR Comments/Summary/Agent panels
- Annotation selection stays in all-files mode instead of switching to
a single-file tab — lets the scroll-to-annotation effect handle it
For provenance purposes, this commit was AI assisted.
Inline code like `src/utils/foo.ts` and bare prose paths like
packages/server/index.ts are now clickable, opening the code file
popout viewer. Adds a subtle CSS shimmer and hover-to-primary
transition so users discover the interactivity without disrupting
the familiar inline-code look.
- Branch the backtick handler in InlineMarkdown to check isCodeFilePath()
- Extend emitPlainTextWithBareUrls() to also detect bare file paths
- Add isCodeFilePathStrict() requiring a / separator for bare prose
- Extract CodeFileIcon component to deduplicate the SVG across 3 sites
- Add code-file-link shimmer animation in theme.css with reduced-motion support
For provenance purposes, this commit was AI assisted.
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.
The diff library's ignoreWhitespace only trims leading/trailing whitespace,
so interior changes (alignment padding, extra spaces between tokens) still
showed as diffs. Normalize all whitespace before diffing to match git diff -w.
Also adds a real code change to the settings.ts demo so the file isn't fully
collapsed when hide-whitespace is on.
* 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>
The renderHoverUtility callback was gating on getHoveredLine() during
React render, but Pierre updates hover state imperatively — so the
button was permanently null. Always render the button; call
getHoveredLine() only in the click handler. Also aligned button
sizing/colors with Pierre's own [data-utility-button] spec.
For provenance purposes, this commit was AI assisted.
* feat(review): add "All files" diff type for reviewing entire codebases
Adds an "all" diff type that diffs the empty tree against HEAD, showing
every tracked file as an addition. Solves the case where a repo has only
committed changes and no working tree diffs — previously there was no way
to launch a review at all.
For provenance purposes, this commit was AI assisted.
* fix(review): add "All files" to Settings panel diff type options
The Settings panel (gear icon) had its own DEFAULT_DIFF_TYPE_OPTIONS list
that was missing the new "all" diff type, so users couldn't select or see
it as the default from the settings UI.
For provenance purposes, this commit was AI assisted.
* fix(review): rename "All files" to "All files (HEAD)" for clarity
Makes it clear this mode shows committed state at HEAD, not the working
tree. Updated tooltip to distinguish from "Committed changes" (which
diffs against a base branch).
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>
* feat(review): hide/de-emphasize merged PRs in stacked PR selector (#625)
Fetch PR state from GitHub GraphQL and surface it in the stack tree popover.
Adds a cookie-persisted "Hide merged" toggle: when on, merged nodes are removed
with a summary count; when off, they render dimmed with strikethrough and a
"merged" badge. Also reduces tree indentation to 2px/level to prevent overflow
on deep stacks.
Closes#625
For provenance purposes, this commit was AI assisted.
* feat(review): add hide-merged toggle to PR selector dropdown
Reuses the same micro-toggle pattern from the stack tree popover.
Adds a headerContent slot to SearchableSelect and filters merged PRs
from the list when the toggle is on. Separate cookie persistence from
the stack tree toggle.
For provenance purposes, this commit was AI assisted.
* fix(review): filter closed PRs alongside merged, improve count labels
Toggle ON now keeps only open PRs (filters both merged and closed).
Labels show "N open · N hidden" when filtering, "N open, N total" otherwise.
For provenance purposes, this commit was AI assisted.
Makes the image preview visible by splitting the dialog into options
(left) and a toolbar screenshot (right) with a CSS hover-to-zoom
effect so users can clearly see where the dropdown lives.
For provenance purposes, this commit was AI assisted.
Version-gates the cookie so all users see the dialog again. Reorders
options to match Settings, adds git command hints to descriptions, and
includes a screenshot showing the toolbar dropdown so users know they
can switch views at any time during a review.
For provenance purposes, this commit was AI assisted.
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.
The version-gated hint dot was permanently visible for users who never
matched the cookie version, with no way to dismiss it. Removes the
entire system: utility, state, prop plumbing, and pulsing dot JSX.
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.
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.
* fix(ui): in-page anchor navigation for headings
Intercept `#`-prefixed link clicks and smooth-scroll inside the sticky
scroll viewport instead of letting the browser jump (which broke when the
content lives inside a scrollable container). Headings now expose both a
canonical slug and a legacy slug via `data-anchor-aliases` so older shared
URLs keep resolving. Also listen for `hashchange` / initial hash so direct
links land on the right heading.
Adds `packages/ui/utils/anchors.ts` with `normalizeAnchorText`,
`decodeAnchorHash`, `slugifyHeadingAnchor`, `legacySlugifyHeadingAnchor`,
and `getHeadingAnchorAliases` (+ tests).
Also ignore `.serena/` tooling state.
* refactor(ui): drop unjustified slug aliasing
Anchor navigation didn't work before this branch, so no shared URLs exist
using any slug format — the legacy slug alias was preserving nothing.
The canonical slug was also a redefinition: heading ids are already
written by `utils/slugify.ts` (unicode-aware, `\p{L}\p{N}`). The new
ASCII-only `slugifyHeadingAnchor` would miss unicode headings ("Café"
writes `id="café"` but the nav code looked for `cafe`).
Nav path is now just: decode the hash and `getElementById`. The
sticky-header offset math — the part that actually mattered — stays.
* fix(ui): anchor-nav edge cases — share-hash collision and raw HTML blocks
Two issues flagged by cross-model review of 9978a40:
1. `useSharing` treated any non-empty `window.location.hash` as a share
payload. Plain anchor hashes like `#section-overview` fell into
`parseShareHash`, failed decompression, and popped the "Shared Plan
Could Not Be Loaded" dialog. Added an `isPlainAnchorHash` guard
(lowercase ASCII + digits + hyphen) that short-circuits before the
share path in both the initial load and the hashchange listener.
Share payloads are base64url-encoded deflate output, so they'll
always contain uppercase/`=`/`_` and never match the guard.
2. `HtmlBlock` (raw `<details>`, `<summary>`, etc.) skipped the new
nav handler. `rewriteRelativeRefs` returned early for any `#` href,
so anchors inside raw HTML fell back to native browser jump which
doesn't target the OverlayScrollArea. Now intercepts those clicks
and routes through `onNavigateAnchor` to match the `InlineMarkdown`
path. Threaded the callback through `BlockRenderer`.
* fix(ui): widen share-payload detection to cover Unicode + uppercase ids
Previous guard rejected any hash that wasn't lowercase ASCII + digits +
hyphen, so Unicode heading ids (`#café`, `#中文-标题`) and raw HTML ids
(`#MySection`) still fell into `parseShareHash` and popped the shared-
plan error dialog.
Flip the predicate: a share payload is base64url (`[A-Za-z0-9_-]`),
realistically ≥30 chars, and virtually always contains at least one
uppercase letter because deflate output has high entropy. Anything else
is handed back to Viewer to scroll to (or silently ignored).
Also add tests/test-fixtures/14-anchor-links.md exercising the full
character range — ASCII, Unicode (accented, CJK, Cyrillic), numeric
start, raw HTML ids — across paragraph, list, blockquote, alert,
directive, table, and raw `<details>` contexts.