Commit Graph

370 Commits

Author SHA1 Message Date
Michael Ramos afac44b940 feat(review): migrate all-files code review to Pierre CodeView (#885)
Replaces the all-files renderer with Pierre's virtualized CodeView (one scroller, identical look, scales to huge diffs); deletes the legacy AllFilesDiffView/LazyFileDiff and their flag; single-file panels stay on FileDiff. @pierre/diffs pinned exact 1.2.8 (1.2.9's tree is broken on npm — see #880).

Also ships: diff staleness detection ('Diff out of date · Refresh' toolbar notice backed by shared per-VCS fingerprints + GET /api/diff/fresh on both Bun and Pi servers), a stale-content guard so files edited mid-review degrade to raw-patch view instead of breaking virtualization, the Ask AI wrong-file fix (pre-existing), and toolbar/header UI polish (global settings cog, aligned sem badges, no all-files split dragger).

Hardened through five review waves; tested with 1384 passing tests incl. real-git fingerprint and patch-consistency suites. Known minor gaps documented in the PR.
2026-06-10 21:07:58 -07:00
Devin 9b936aa299 fix(markdown): keep custom angle autolinks literal (#867)
Co-authored-by: ishowman <ishowman@users.noreply.github.com>
2026-06-10 16:16:27 -07:00
Michael Ramos 4e841e2124 fix(deps): pin @pierre/diffs to exact 1.1.20
Upstream @pierre/diffs@1.2.x depends on @pierre/theming@0.0.1, which is
not published on npm, so any fresh install resolving our ^1.1.12 range
floated to 1.2.9 and failed with a 404. Our lockfile masked this for
repo builds, but npm consumers of @plannotator/pi-extension resolve
fresh and hit it (#880). Pin every declaration to exact 1.1.20, whose
dependency tree is fully resolvable (@pierre/theme@0.0.28).
2026-06-10 09:25:40 -07:00
Michael Ramos cbf891ba40 feat(review): add Claude Fable 5 to the code-review model dropdown
Fable 5 dropped today; add it to the top of CLAUDE_MODELS (the review
agents model picker) alongside the existing Opus 4.8 entry. No [1m]
variant — Fable 5 has no separate 1M-context toggle. Server forwards
--model verbatim, so no allowlist change needed.
2026-06-09 11:05:03 -07:00
Michael Ramos 6573f2f519 chore(release): fix README command count + point dialog at v0.20.0 notes
- apps/hook/README.md: 'four' -> 'three' slash commands (archive row was
  removed in #873 but the count wasn't reverted).
- LookAndFeelAnnouncementDialog: 'Full release notes' now deep-links to the
  v0.20.0 release tag instead of the generic releases list.
2026-06-09 10:58:35 -07:00
Michael Ramos 0be4295a2e Plan-look chooser + 0.20.0 release dialog (with GitLab / sem / install fixes) (#879)
* feat(plan): default to the grid look + look-and-feel image chooser

- Flip gridEnabled default back to true (classic grid / floating-card look),
  reverting #863's flat-by-default so existing users without an explicit
  choice return to the grid view they're used to.
- Rewrite the look-and-feel announcement into a two-image chooser: Grid
  (classic, now the default) vs Clean (the new flat look), each a clickable
  screenshot that hover-zooms to preview — mirroring the code-review
  DiffTypeSetupDialog pattern.
- Bump the announcement version (1 -> 2) so existing users are re-offered the
  choice once.
- Add look-grid.png / look-flat.png preview assets (resized app screenshots).

* fix(review): restore GitLab /diffs fallback for MR review

#871's pr-gitlab refactor moved to the raw_diffs endpoint and deleted the
paginated /diffs path, hard-breaking MR review on self-hosted GitLab too old to
expose raw_diffs (opaque "Failed to fetch MR diff") and silently rendering zero
files when raw_diffs returns empty for oversized MRs.

Keep raw_diffs as the primary path (it preserves binary markers + collapsed
content), but fall back to the restored paginated /diffs + reconstructPatch when
raw_diffs fails or is empty, and throw a clear "diff is empty / too large" error
if both come back empty. Restores parsePaginatedArray / reconstructPatch and
adds tests for the fallback paths.

* fix(install): bound the optional sem sidecar download

The semantic-diff 'sem' download (#871) ran curl / Invoke-WebRequest with no
timeout. The sidecar installs after plannotator itself, so a slow or hung fetch
could wedge an otherwise-complete install. Add --connect-timeout 10
--max-time 120/60 (sh, cmd) and -TimeoutSec 120/60 (ps1) so it times out and
skips gracefully, and document the PLANNOTATOR_SKIP_SEM_INSTALL opt-out in the
installer help.

* chore(install): remove the Glimpse install option from the installers

The installers offered to `npm install -g glimpseui` (a wizard prompt +
--glimpse/--no-glimpse flags + saved pref). Drop all of it across install.sh,
install.ps1, and install.cmd: flags, usage text, the wizard question, the
glimpse_present detection, the saved-pref read/write, the persistence clauses,
and the install block. The guided wizard is now two questions (extras,
model-invocable).

Runtime Glimpse support is unchanged: the app still opens in the native window
if glimpseui is on PATH (packages/server/browser.ts), still gated by
PLANNOTATOR_GLIMPSE. Updated install.test.ts (asserts the installers are
glimpse-free) and the installation docs.

* feat(plan): 0.20.0 release announcement dialog

Turn the first-run look chooser into a two-page 0.20.0 announcement.

- Page 1: header with a "Full release notes" link, a four-up feature
  grid (fresh look, semantic review, multi-repo review, leaner install),
  and the grid/clean plan-look chooser.
- Page 2: "Workspaces are coming" teaser with the waitlist image + link.
- Footer actions grouped bottom-right on both pages (shimmering
  "Workspaces are coming" teaser via TextShimmer + the primary action)
  so focus stays in one corner across the page turn.
- Reshoot the grid/clean chooser screenshots on a clean prose plan and
  add the workspaces teaser image.

* feat(plan): add full-page HTML feature card, lead with leaner install

Add a fifth what's-new card for --render-html (annotate HTML reports and
explainers rendered full-screen) and move Leaner install to the front of
the grid.
2026-06-09 09:45:20 -07:00
Michael Ramos c23df4db43 UI 2.0 visual refresh + HTML-render annotate (strictly UI, off main) (#863)
Extracts the UI 2.0 visual refresh and the HTML-render annotate feature onto main, standalone (no daemon). Faithful copy of feat's UI/HTML logic with the standalone transport kept.
2026-06-08 17:03:41 -07:00
Nikolai Perlt 13a6adde18 Add full-file diff context setting (#858)
Co-authored-by: Nikolai Perlt <perlt@Nikolais-MacBook-Pro.local>
2026-06-05 08:59:59 -07:00
Graham Lipsman be2c81fa3b annotate-last: pick which message to annotate (fixes #800) (#809)
* feat: message picker for annotate-last (#800)

When running /plannotator-last after /rewind, the newest transcript
entry is no longer the message the user intended to annotate, and there
was no affordance to pick a different one.

Adds a picker UI that surfaces the recent assistant messages so the
user can choose which one to annotate:

- A "Message N of M" button in the Viewer's sticky-top action bar
  (alongside Copy / Global comment / Attachments), so it stays
  accessible while scrolling.
- A "Messages" tab in the left sidebar with the full list
  (newest-first, preview + timestamp, default ★), mirroring the
  existing Files / Versions / Archive tab pattern.

Wired for Claude Code, Codex, and Droid (all share apps/hook/server).
OpenCode, Pi, and Copilot still get the original single-message
behavior — they don't emit recentMessages, so the picker affordances
hide cleanly.

Default selection (index 0) matches today's "last message" behavior,
so users who don't interact with the picker see no change.

* feat: extend annotate-last picker to Copilot and OpenCode

The picker UI from #800 was wired for Claude / Codex / Droid only. Pull
Copilot and OpenCode onto the same shape so users on those harnesses
also get the recent-messages picker when annotating the last assistant
message.

- Copilot: replace getLastCopilotMessage with getRecentCopilotMessages,
  walking events.jsonl newest-first up to 25 assistant.message events.
- OpenCode: rewrite the session walk to collect up to 25 messages
  (newest first) instead of bailing on the first hit; normalize the SDK
  time.created (ms epoch) to ISO to match the shared picker contract.
- Both pass recentMessages to startAnnotateServer only when length > 1,
  matching the existing Claude/Codex/Droid behavior.

Also trims a leftover narrating comment in MessagesBrowser and refreshes
the stale Copilot session-parser header.

Pi parity follows in the next commit (needs round-trip of the picker
selection through /api/feedback so its post-submit anchoring quotes the
right message).

* feat(pi): wire annotate-last picker with feedback round-trip

Extends the picker UI (#800) to Pi and fixes a Pi-specific anchoring bug
the picker would otherwise introduce.

Picker plumbing
- assistant-message: getRecentAssistantMessages walks the active branch
  newest-first, returning { messageId, text, timestamp? } in the same
  shape the other harnesses produce.
- Plumbed through plannotator-browser / plannotator-events so the Bun
  server's recentMessages option is populated when the branch has more
  than one assistant message.

Anchoring fix
- Pi quotes the targeted assistant message back to the agent because its
  UX is async — the conversation may have moved on by feedback time.
  With the picker, that target is no longer guaranteed to be the
  snapshot taken when the UI opened. The editor now sends the user's
  selectedMessageId with /api/feedback; Pi looks it up in the current
  branch via findAssistantMessageByEntryId and quotes that message
  instead. Falls back to the original snapshot if the entry is gone.
- The round-trip field is optional and only meaningful in annotate-last
  mode; other harnesses (and other modes) ignore it.

Timestamp safety
- Pi's SDK currently types SessionEntryBase.timestamp as string, but the
  picker contract everywhere else is ISO. Treat the value as unknown and
  normalize string/number(ms)/Date to ISO; drop anything else, rather
  than blind-casting and risking silent drift if the SDK changes.

* chore: strip issue-number references from comments

Comments shouldn't rely on external references — issue numbers age out
of context, link rot is a thing, and a reader shouldn't need to open
GitHub to understand why a line exists. Strip the `(#800)` and `(#570)`
parentheticals from comments and doc strings across the picker and
review-gate code; the surrounding "why" content is preserved.

* fix: prevent removeChild crash when switching annotate-last messages

Switching the picked message remounted nothing, so React reconciled new
content against DOM that web-highlighter had mutated with <mark> nodes,
throwing removeChild. Drive the Viewer key (and StickyHeaderLane's
remount token) off a shared viewerContentKey so a message switch fully
remounts the Viewer and re-anchors the sticky-header observer.

Also cap MessagesBrowser row previews via previewText() and drop the
redundant 'block' class that was overriding line-clamp-2.

* feat: persist annotate-last feedback across messages

---------

Co-authored-by: Michael Ramos <mdramos8@gmail.com>
2026-06-03 13:01:36 -07:00
Benjamin Jesuiter 96aad592af Open Plannotator in Glimpse when available (#840)
* feat: open plannotator in glimpse

* fix: harden Glimpse browser integration

* chore: refresh lockfile after rebase

---------

Co-authored-by: Michael Ramos <mdramos8@gmail.com>
2026-06-03 12:17:11 -07:00
Robert Dailey cbc6186a15 fix(ui): align list markers to first line in clean diff view (#838)
* fix(ui): align list marker to first line in clean diff view

* refactor(ui): extract list-item marker and body into ListItemBody

Three render paths (BlockRenderer and two in PlanCleanDiffView)
duplicated the same marker-plus-paragraph scaffold. A recent alignment
fix had to be applied to each surface independently, which exposed the
duplication. The shared structure now lives in ListItemBody; call sites
keep their own row wrapper with surface-specific concerns (indent, data
attributes, hover props, interactivity).
2026-06-02 11:18:52 -07:00
Michael Ramos ffe2be504f feat: improve update notification with toast, shimmer, and prominent button
Add a sonner toast when a new version is detected, apply TextShimmer to
the Options button label, and restyle the copy-update button as a filled
primary action. Applied to both plan and review editors.

For provenance purposes, this commit was AI assisted.
2026-05-31 20:04:33 -07:00
J-token 2a8eb9d647 Fix flickering AI-tip input in label settings (#830)
The "Add AI instruction tip" editor in Settings > Labels flickered
intensely on open. Its `tip-slide-open` keyframe animated `max-height`
(a layout property) to 60px -- larger than the input's natural height --
with no animation-fill-mode. Inside the OverlayScrollbars viewport and a
flex `min-h-0` container this formed a layout feedback loop that
restarted the animation ~40x/second, jittering the editor height.

Animate `opacity` and `transform` instead, so no layout property is
animated. Measured on the live demo, animation restarts and height
reversals dropped from ~17/10 to 0, with height stable at its natural
value.

Fixes #829

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-31 09:58:00 -07:00
HyunmoAhn b00ddfa16d Fix Mermaid block sizing, zoom retention, and pinpoint drag (#819)
* Pin Mermaid SVG sizing attrs into markup so they survive re-injection

Mermaid 11.x emits the root <svg> with style="max-width: <natural-px>;"
and no preserveAspectRatio / height attributes. The component fixed this
via imperative setAttribute in a useEffect, but React's repeated
dangerouslySetInnerHTML re-injections (4-5 times during mount) wipe the
imperative attrs because the useEffect's `svg` state dep does not change.

Result: short flowcharts (e.g. `X --> Y --> Z`) render at their natural
85x66 px "dot" size, long flowcharts overflow vertically past their
container (~1667 px), and drag feels broken because pan-on-clipped-svg
has nowhere to go.

Bake max-width:none, preserveAspectRatio="xMidYMid meet", and
height="100%" into the SVG markup string itself before setSvg(). Since
the attributes live in the markup, every re-injection carries them
along. The imperative useEffect is left in place as defense-in-depth.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* Memoize MermaidBlock to preserve zoomed viewBox across pinpoint hover

After zooming a Mermaid diagram and toggling pinpoint mode on, hovering
the viewer would reset the visible viewport back to the full diagram.
The zoom ref / badge stayed at e.g. 150%, but the SVG snapped back to
its natural extents.

Root cause: the pinpoint hook calls setHoverTarget on every mousemove,
which re-renders the parent Viewer, which re-runs MermaidBlock's
dangerouslySetInnerHTML and replaces the live SVG with the cached markup
string. The prior commit's markup normalization pins three sizing
attrs, but viewBox is not in the markup -- zoom mutates viewBox via
setAttribute, so each re-injection drops the zoomed value.

Wrap MermaidBlock with React.memo and a custom comparator on block.id +
block.content (matching HtmlBlock's convention). Upstream blocks come
from useMemo([markdown]) in the editor App, so referential equality
holds whenever content is unchanged -- memo skips the re-render and the
live SVG keeps its zoomed viewBox across pinpoint mousemove storms.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* Skip pinpoint targeting inside Mermaid grab area to keep drag clean

In pinpoint mode, releasing a Mermaid pan-drag created a whole-block
annotation. mouseup is followed by a click event, and the pinpoint
overlay's click handler caught it and called highlighter.fromRange()
on the entire Mermaid block.

Separate annoyance: the dashed hover border drawn by the pinpoint
overlay was visually distracting while the user was clearly trying to
drag the diagram.

Add data-pinpoint-ignore="" to the grab container div. usePinpoint's
existing SKIP_SELECTORS in blockTargeting.ts already matches that
attribute, so this is a one-attribute opt-out using a public extension
point -- no changes to the pinpoint hook itself.

Trade-off: Mermaid blocks no longer support whole-block-annotation via
pinpoint. Annotations on diagrams must go through standard selection.
This is the deliberate behavior chosen for diagrams where drag/zoom
ergonomics matter more than whole-block targeting.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* Extract normalizeMermaidSvgMarkup to a pure module

The MermaidBlock test imported the component, which runs mermaid.initialize()
at module load. That throws 'Unsupported color format: "#333"' in the headless
CI environment, failing the test on import before any assertion runs.

Move the pure SVG-normalization helper into mermaidSvg.ts (no React, no mermaid
import) and have both the component and the test import it from there. App
behavior is unchanged; the test no longer loads mermaid.

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: Michael Ramos <mdramos8@gmail.com>
2026-05-29 15:58:04 -07:00
Hrand Liu e0aee7451b feat: add PLANNOTATOR_DATA_DIR env var to customize data directory (#795)
* feat: add PLANNOTATOR_DATA_DIR env var to customize data directory

* fix: update missed hardcoded paths to use PLANNOTATOR_DATA_DIR

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

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

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

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

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

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

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

* fix: resolve relative PLANNOTATOR_DATA_DIR to absolute path

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

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

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

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

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

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

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

* fix: remove remaining hardcoded ~/.plannotator assumptions

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

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

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

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

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

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

---------

Co-authored-by: Michael Ramos <mdramos8@gmail.com>
Co-authored-by: Chris Werner Rau <14326070+cwrau@users.noreply.github.com>
Co-authored-by: João O. Santos <34689526+Joao-O-Santos@users.noreply.github.com>
2026-05-26 14:56:07 -07:00
Nawapat Buakoet 92efe6fdef Add Auto Mode option to Permission Mode dropdown (#796)
Claude Code shipped Auto Mode in March 2026 as a new permission mode
value (`auto`) — autonomous execution gated by a Sonnet 4.6 safety
classifier. It sits between `acceptEdits` and `bypassPermissions` in
risk profile.

Extend the `PermissionMode` union and `PERMISSION_MODE_OPTIONS` array
with the `auto` entry. The Settings dropdown and first-time setup
modal both iterate the options array, so no UI component changes are
needed. The hook stdout payload (`updatedPermissions[].mode`) is
already generic over mode value, so server-side plumbing also requires
no changes.

Refs:
- https://www.anthropic.com/engineering/claude-code-auto-mode
- https://code.claude.com/docs/en/permission-modes
2026-05-26 07:31:32 -07:00
Michael Ramos dbe2d6e4cb Replace update banner with passive dot indicator in Options menu (#793)
* Replace persistent update banner with passive dot indicator in Options menu (#790)

The floating update banner that appeared every session is removed. Update
availability is now surfaced as a small dot on the Options button that
auto-dismisses (cookie-persisted per version) when the menu is opened.
The version section inside the menu shows install command and release
notes when an update exists.

* fix: restore ActionMenuSectionLabel import in both header menus

* Add TextShimmer to update notice and simplify menu version section

- Add reusable TextShimmer component (motion-primitives adaptation)
- Add reusable BorderTrail component for future use
- Shimmer "New update available!" text in the Options menu
- Simplify button to plain text link in emerald
- Remove Project repo link, make "Plannotator" header link to repo
2026-05-25 12:09:42 -07:00
Michael Ramos 05392eb99e Fix copy (Cmd+C) broken in Safari on plan viewer (#785)
* Fix copy (Cmd+C) broken in Safari on plan viewer

Safari rejects async navigator.clipboard.writeText() outside the
immediate user-gesture window.  The old keydown handler broke the
gesture chain with an await, and web-highlighter already clears
the DOM selection on mouseup so native copy had nothing to grab.

Switch Viewer to the synchronous copy event + clipboardData.setData()
which works across all browsers.  Add an execCommand fallback to the
AnnotationToolbar copy button for extra safety.

* Remove copy button from annotation toolbar

Cmd+C now works reliably via the native copy event, so the toolbar
copy button is redundant. Code block and table copy buttons are kept.

* Show copy button on touch devices only

Touch users have no Cmd+C fallback since web-highlighter clears the
native selection. Detect (pointer: coarse) and conditionally render
the copy button + execCommand fallback for iPad/phone users.

* Reset copied state when selection changes on touch devices

* Keep copy button on code block hover toolbar for desktop users

* Scope copy handler to viewer container

Prevents the copy handler from hijacking Cmd+C when the target is
outside the viewer (sidebar, annotations panel, code popout, etc.).

* Remove overly aggressive container scope check from copy handler

* Add copy event handler to HtmlViewer for Safari support

* Keep copy button visible in HtmlViewer toolbar

The iframe copy event can't reach the parent document handler, so the
toolbar button is the only copy path for HTML annotation mode.

* Use hideCopyButton prop instead of overloading copyText

Separates "what text to copy" from "whether to show the button."
Only Viewer's text selection toolbar hides it (desktop only, has
keyboard handler). HtmlViewer and code block toolbars always show it.

* Memoize touch device check in Viewer
2026-05-23 23:11:58 -07:00
Michael Ramos 2a5528693f Add Ask AI to plan and annotate reviews (#763)
* Add Ask AI to plan and annotate reviews

- mount shared AI runtime on plan, annotate, review, and Pi servers

- add shared document chat UI and comment-popover Ask AI entry point

- default providers from detected agent origin and document first-run announcement

- reuse shared code-review AI hook and document setup/docs

* Address Ask AI review followups

Stabilize AI chat session resets and callbacks, preserve per-origin provider defaults, share AI chat formatting utilities, and avoid blocking startup on model discovery.

* Fix AI permission details and Pi CLI lookup

Show formatted tool input in document AI permission cards and make Pi AI provider CLI detection work on Windows.

* Fix Pi model discovery in AI capabilities

Keep AI runtime startup non-blocking while making the capabilities endpoint await pending Pi/OpenCode model discovery before returning provider metadata.
2026-05-21 01:32:17 -07:00
Michael Ramos 82636e1286 Add interactive goal setup UI (#731)
* Add interactive goal setup UI

* Refine goal interview skip and question flow

* Fix review findings: recommendation combo, option-only recs, single deselect

* Persist goal setup working JSON files

* Refine goal setup copy and facts controls

* Remove generated goal package from PR

* Address goal setup review issues

* Remove goal setup slash command adapters

* Disable fact comment attachments

* Fix goal setup fact comment state

* Address goal setup review cleanup

* Fix goal setup fact submission edge cases
2026-05-18 08:39:29 -07:00
Andrea Arturo Venti Fuentes 1ec5243448 chore(deps): drop stub @types/dompurify
dompurify ships its own types since v3.x — the @types package is an
empty stub. Removes it from both root and packages/ui devDependencies.

Credit to @av1155 who identified this in #726.

For provenance purposes, this commit was AI assisted.
2026-05-17 23:24:41 -07:00
Cody Partington b04c9e1f30 fix(review): persist file comment drafts across close/reopen (#721)
* fix(review): persist file comment drafts across close/reopen

File comment popover lost in-progress text when dismissed via click-outside / Escape / X — local state died with the unmount. Line-comment toolbar already does this via a module-level draft map; mirror the pattern in `CommentPopover` behind an opt-in `draftKey` prop and wire both file-comment call sites (`AllFilesDiffView`, `DiffViewer`).

* fix(review): scope file-comment draft key by PR identity

File-path-only key leaked drafts across in-place PR switches.

* fix(review): remount all-files comment popover on context change

The all-files panel shares one CommentPopover instance across every
file. Switching files (or PR/scope) batches close+open into a single
state transition, so React reuses the instance with stale text in
state and the draft-sync effect writes it under the new draftKey.
Key the popover by the same expression as draftKey so any change in
file, PR, or scope forces a fresh mount that reads its own draft.
Also clear the anchor when the file list reloads so a popover
anchored to a removed DOM node doesn't survive a diff swap.

---------

Co-authored-by: Michael Ramos <mdramos8@gmail.com>
2026-05-13 06:54:33 -07:00
Michael Ramos 4c8338f53f feat(ui): copyable hook path + guidance in Settings Hooks tab (#707)
* feat(ui): show copyable file path and guidance in Hooks settings tab

Always return the improvement hook file path from /api/hooks/status
(actual path when present, expected path when absent) so the UI can
display it in both states. Add CopyPathButton with tilde-shortened
display and full-path clipboard copy. When active, guide users to
edit directly or regenerate via /plannotator-compound. When absent,
show expected path and both creation options (auto-generate or manual).

* fix(ui): anchor displayPath on .plannotator instead of guessing homedir

The regex assumed home directories are always two segments deep
(/Users/x), which breaks for /root on Linux — it would capture
/root/.plannotator as the home prefix and display ~/hooks/... instead
of ~/.plannotator/hooks/..., leading users to create the file in the
wrong location.
2026-05-11 21:07:58 -07:00
Michael Ramos fcf2ba4cf5 fix: indent loose list continuation content under parent bullet (#705)
Closes #704
2026-05-11 16:41:26 -07:00
Michael Ramos 1eb561551c feat: standalone skills package + HTML render-annotate mode (#687)
Add --render-html flag to plannotator annotate that renders HTML files
as-is in an iframe instead of converting to markdown. Includes annotation
support via postMessage bridge, sharing via paste service, and theme
inheritance from Plannotator's 30+ themes.

New skill: plannotator-visual-explainer — wraps nicobailon/visual-explainer
with Plannotator theme tokens, extended patterns (timelines, SVG diagrams,
code blocks, risk tables, Pierre diffs via CDN), and plan/PR-specific guidance.

All three servers (Bun, Pi, OpenCode) support the new flag.
2026-05-11 10:30:49 -04:00
Andrey Skripalschikov 50191e6c1d Add diff line background intensity option (#691) 2026-05-11 10:18:27 -04:00
Michael Ramos 347663a4de feat(pfm): code line range references, hover preview, sketch Graphviz (#692)
Code file line range references with hover preview + Graphviz improvements.

Line ranges: `file.ts:42` and `file.ts:10-20` are fully supported with
syntax-highlighted hover preview popover (150ms delay, GitHub-style
persistence). New parseCodePath() utility, server-side line suffix
stripping on both Bun and Pi servers, ambiguous picker preserves line
suffix. useCodeFilePopout moved to hooks/pfm/.

Graphviz: responsive container height from SVG aspect ratio, white
background polygon removed, default colors (black, lightgrey) replaced
with theme tokens via SVG post-processing. User-specified colors
preserved.
2026-05-11 10:13:47 -04:00
Michael Ramos f4493fc6ff Optimize plan editor rendering (#696)
- Replace blocks/frontmatter useState + useEffect with useMemo (eliminates mount cascade)
- Migrate toast notifications from React state to Sonner (App no longer re-renders for toasts)
- Extract AppHeader component with React.memo (header no longer re-renders on unrelated state changes)
- Fix color transition flash on initial load (suppress global * transition until mount settles)
- Add Tailwind @source path for new editor components directory
2026-05-11 08:55:18 -04:00
Michael Ramos 579127e2c9 feat(ui): add Hooks visibility tab to Settings dialog (#695)
Add Hooks tab to plan review Settings dialog for visibility into the
improve-context system. PFM Reminder toggle card with config persistence,
Improvement Hook status card with collapsible content preview. New
GET /api/hooks/status endpoint and pfmReminder in POST /api/config
on both Bun and Pi servers. Dev mock API wired up.
2026-05-11 08:26:58 -04:00
Andrey Skripalschikov 7329efb980 Add diff tab size option (#685) 2026-05-08 11:45:47 -07:00
Michael Ramos 36948e8572 chore: bump version to 0.19.11
For provenance purposes, this commit was AI assisted.
2026-05-07 23:24:00 -04:00
Michael Ramos 49c55e6e25 Revert "Expose bypass clear reminder permission mode (#668)"
This reverts commit 3b88415aeb.
2026-05-06 16:10:31 -07:00
Michael Ramos a22ee3fbe5 fix(ui): remove shimmer animation from clickable file paths (#676)
The repeating shimmer animation on code file links was distracting and
stole focus while reading plans. Closes #672.
2026-05-06 13:51:30 -07:00
Nehr 3b88415aeb Expose bypass clear reminder permission mode (#668)
* 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>
2026-05-05 12:27:16 -07:00
Michael Ramos 308e272b10 feat(ui): 49 themes with matched syntax highlighting + preview mode (#664)
* 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
2026-05-04 23:25:30 -07:00
Michael Ramos 84a0b434f9 fix(ui): smart resolution + existence-validation for code-file paths (#654)
* 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`.
2026-05-04 14:21:16 -07:00
Michael Ramos 5bc57fc5bd feat(ui): shortcut registry foundation (#652)
* 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>
2026-05-04 10:15:31 -07:00
Michael Ramos 5aabace2ee feat(opencode): agent picker dropdown on Approve button (#648)
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
2026-05-01 18:05:50 -07:00
Michael Ramos ddcca6e0ee feat(review): file comment button in all-files view + remap C/X shortcuts
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.
2026-05-01 00:04:14 -07:00
Michael Ramos 0fd9826c2c feat(review): all-files diff view with unified scrolling (#640)
* 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.
2026-04-30 23:39:43 -07:00
Michael Ramos 108a61b7d8 feat(ui): detect code file paths in backtick spans and bare prose (#639)
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.
2026-04-30 18:08:52 -07:00
Michael Ramos a11bf802bd feat(ui): code file viewer with syntax highlighting and annotations (#634)
* 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
2026-04-30 10:42:36 -07:00
Michael Ramos b4b1d432c8 feat(review): diff display options — hide whitespace + quick-settings popover (#631)
* 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>
2026-04-29 23:51:17 -07:00
Michael Ramos a68463b6a4 feat(review): add "All files" diff type (#629)
* 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.
2026-04-29 16:43:06 -07:00
Michael Ramos 93c844021d feat(review): hide/de-emphasize merged PRs in stacked PR selector (#626)
* 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.
2026-04-28 13:18:28 -07:00
Michael Ramos 03d4e8b214 fix: widen diff type dialog with 60/40 split layout and hover-to-zoom image
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.
2026-04-27 23:25:58 -07:00
Michael Ramos aaad89ee3f fix: re-show diff type setup dialog with clearer options and toolbar hint
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.
2026-04-27 23:15:19 -07:00
Michael Ramos fdc4bc4656 feat(plan,annotate): include source line numbers in exported feedback (#623)
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.
2026-04-27 23:13:52 -07:00
Michael Ramos 7ab2d8f868 fix: remove ghost dot on Options menu (new-settings-hint system)
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.
2026-04-27 22:47:02 -07:00
Michael Ramos bb404f8d14 feat: stacked PR review — PR switching, scope toggling, multi-PR posting (#620)
* 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.
2026-04-27 22:07:55 -07:00