5 Commits

Author SHA1 Message Date
Michael Ramos 3d435184dc fix(review): keep large-diff memory bound when the object-size probe fails (#1205)
A failed `cat-file --batch-check` used to map every changed object to
infinity, replacing the ENTIRE review diff with `Binary files ... differ`
stubs and no visible error, and silently degrading the staleness
fingerprint.

The memory bound is now probe-independent: every rendered diff carries
`core.bigFileThreshold=<MAX_REVIEW_FILE_CONTENT_BYTES>` injected through
`GIT_CONFIG_*` environment variables (never `-c` argv flags, so argv stays
byte-identical), making git itself stub oversized blobs. On probe failure
blob sizes read as unknown-but-bounded and files render normally; the
stat-based exclusion door for oversized working-tree files (which git's
threshold does not cover) never depended on the probe and keeps working.
Per-object doors (missing / unparseable size) stay conservative when the
probe ran.

The probe itself gains timeoutMs + interaction:"forbid" so a hung git
cannot stall the review server. Three existing mocks that returned
non-batch-check output (and passed only because of the all-infinity bug)
now return well-formed batch-check lines.
2026-08-04 22:21:56 -07:00
Raúl bc5470b90d fix(review): bound server memory for large tracked-file diffs (#1167)
* fix(review): bound server memory for large tracked-file diffs

PR #1118 renders large untracked files as binary additions, but staging
one moves it into the tracked `git diff` path, which had no size guard and
buffered the full multi-megabyte patch (~240 MB RSS on a 51 MB text
artifact). Any large tracked text file modified in the working tree hits
the same unguarded path.

Add a per-invocation `git -c core.bigFileThreshold=<MAX_REVIEW_FILE_CONTENT_BYTES>`
prefix to every content-producing git diff, so git renders oversized blobs
as "Binary files ... differ" instead of a text patch. Their bytes never
enter git's diff machinery or the server's buffered stdout, mirroring the
untracked-file guard. The flag is a no-op at or below the threshold, so
smaller files are byte-for-byte unaffected, and the blob hash git emits in
the binary diff still changes with content, so staleness detection holds.

The guard is applied in the shared cores, so the Bun and Pi runtimes
inherit it identically: `review-core.ts` covers the ordinary git provider
(working-tree, staged, commit, and the freshness fingerprint) and
`gitbutler-core.ts` covers the GitButler object diff. The jj provider runs
`jj diff`, which has no `core.bigFileThreshold` equivalent, so it is out of
scope here and stays unbounded as before.

* fix(review): preflight oversized tracked diffs

* fix(review): batch tracked diff preflight

* fix(review): restore browser-safe diff core

* fix(review): preserve gitlinks and textconv

* fix(review): require filesystem runtime seam

Fail compilation when a runtime omits file metadata or symlink support instead of silently disabling bounded reads and expansion.
2026-08-03 13:25:35 -07:00
Michael Ramos 6ec1a66c9b feat(review): large-PR pipeline, instant-open checkout, scroll perf, and worker-pool highlighting (#893)
* feat(review): large GitHub PR fallback + non-blocking PR checkout

Two PR-mode improvements:

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Self-review findings:

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

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

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

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

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

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

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

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

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

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

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

Adopts two diffshub practices identified in the architecture comparison:

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

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

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

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

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

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

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

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

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

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

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

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

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

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

--local is a CLI remedy; OpenCode sessions have no such flag. Visible
text states the fact, the tooltip carries the CLI guidance.
2026-06-12 13:50:09 -07:00
Michael Ramos 3fb0b9cf03 fix(gitlab): persist unposted inline comments + split pr-provider from browser-safe pr-types (#719)
Closes #680. Two changes that landed together because the persistence fix
exposed a hidden architectural constraint.

1. GitLab inline comments: when one or more discussion POSTs failed
   (e.g. transient `i/o timeout`), the failed comment bodies were lost.
   Now `submitGlMRReview` writes them to
   `~/.plannotator/failed-comments/{host}-{project}-mr{iid}-{ts}.json`
   in both the all-fail and partial-fail branches. The throw-vs-warn
   split is preserved deliberately: all-fail throws so the UI retries
   from a clean state, partial-fail warns so the UI doesn't resubmit
   already-posted content.

2. Split `packages/shared/pr-provider.ts` into `pr-types.ts`
   (browser-safe types + pure label/URL helpers) and `pr-provider.ts`
   (server-only dispatch that imports pr-github / pr-gitlab). The
   review-editor browser bundle previously dragged pr-gitlab.ts in as
   dead code via static imports, which silently constrained the file
   to never use Node built-ins. Adding `fs`/`os`/`path` for (1) broke
   the review build until we routed browser imports to pr-types and
   left server callers on the now server-only pr-provider facade.

Server-only `pr-provider.ts` re-exports `pr-types` so existing
server-side imports keep working unchanged.
2026-05-13 05:27:28 -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