Commit Graph

12 Commits

Author SHA1 Message Date
Michael Ramos 8e7b5ce300 feat(review): refine Call Flow navigation and annotations (#1277)
* feat(review): refine Call Flow navigation and annotations

* fix(review): align viewed controls with panel navigation

* feat(review): add Call Flow path search controls

* fix(ui): wrap long tooltip identifiers

* feat(review): annotate raw Call Flow output

* feat(review): refine call flow lens context

* fix(review): keep call flow lens search accessible

* fix(review): scope call flow find shortcuts
2026-08-12 11:18:29 -07:00
Michael Ramos 463f6ed57c fix(ui): fall back to legacy copy in insecure browser contexts (#1174)
* fix(ui): fall back to legacy copy in insecure browser contexts

navigator.clipboard only exists in secure contexts. Remote mode serves
plain HTTP on a non-localhost host, so every bare
navigator.clipboard.writeText call threw TypeError and copy buttons
silently broke.

Add copyTextToClipboard(text): Promise<boolean> to
packages/ui/utils/clipboard.ts: it tries the async Clipboard API
(guarded against synchronous throws), falls back to the existing
copy-event plus execCommand path, reports success as a boolean, and
never throws. copyTextWithFallback now returns whether the copy
happened and accepts an optional focusOwner; copyTextPreservingFocus
keeps its exported signature and behavior unchanged.

Route all bare call sites through the helper, preserving each site's
UX: Copied states only flip on success, error toasts and console
errors remain for the failure case, fire-and-forget sites stay
fire-and-forget. GoalSetupSurface gains the fallback and keeps its
error surface for the all-strategies-failed case.

Add DOM-gated unit tests for the helper and register them in CI.

Closes #1173

* fix(ui): address clipboard fallback review findings

Review follow-ups for the insecure-context clipboard fallback:

1. Tag the fallback textarea with data-clipboard-fallback and whitelist
   it in PopoutDialog's ANNOTATION_SELECTORS so the transient focus
   shift during a fallback copy no longer closes popout dialogs
   (TablePopout copy buttons, CodeFilePopout copy contents).

2. Widen AnnotationPanel's onQuickCopy prop to Promise<void | boolean>.
   A false resolution now suppresses the Copied flash; void resolution
   stays success so existing hosts keep today's behavior. The editor
   quick-copy site returns the helper's boolean.

3. In copyTextWithFallback, only flag the copy-event path as success
   when clipboardData was present and setData actually ran, and only
   when execCommand also reported success. A null clipboardData no
   longer calls preventDefault, so the textarea retry still runs.

4. Add tests pinning that the fallback runs synchronously when
   navigator.clipboard is absent (execCommand fires before the call
   returns, keeping it inside the user-gesture window), that a copy
   event without clipboardData is not treated as success, and that the
   fallback textarea carries the PopoutDialog focus-out marker and is
   removed after the copy resolves.

5. GoalSetupSurface surfaces the real writeText rejection message when
   the Clipboard API exists but fails and the fallback also fails; the
   generic unavailable message is reserved for the API-absent case.
2026-08-03 10:14:34 -07:00
Michael Ramos 58f87500f1 Migrate packages/review-editor to Base UI (#1014)
* refactor(review-editor): migrate tour QA checkbox to Base UI

First component of the Radix→Base UI migration (internal package).
@base-ui/react added alongside Radix; Radix deps removed in the final
commit. Root typecheck, bun test (1955 pass), build:review all green.

Note: bun.lock also absorbs pre-existing manifest drift (the committed
lock was stale relative to several package.jsons at HEAD — any install
regenerates it identically).

* chore(review-editor): give the package its own typecheck gate

The package had no tsconfig and is absent from the root typecheck
chain (it is typechecked only through apps/review, whose own tsc run
is broken for unrelated reasons: it never declares @types/react).
Adds a package tsconfig modeled on apps/review's compilerOptions plus
@types/react(-dom) devDeps so migration transforms are type-verified.
53 pre-existing errors surfaced (import.meta.env, response-type
mismatches) — recorded, untouched; gate for the migration is zero
errors in migrated files.

* refactor(review-editor): migrate all 7 popover files to Base UI

Portal>Content becomes Portal>Positioner>Popup (positioning props and
z-index on Positioner), asChild triggers become render props, state
classes move to Base UI presence attributes (data-popup-open on
triggers), --radix-* vars become --transform-origin, and keyframe
enter animations restate as data-starting/ending-style transitions.
Focus interception (onOpenAutoFocus) maps to Popup initialFocus,
including WorktreePicker's conditional search-input focus.

Flagged, not patched: collision defaults differ slightly at viewport
edges; exit fade now animates where Radix unmounted instantly. See
.migration/popover.md. Tests (1955) and build:review green; zero tsc
errors in migrated files.

* refactor(review-editor): migrate DiffTypePicker to Base UI Menu

Radix DropdownMenu is renamed Menu in Base UI; same restructure as the
popovers (Positioner/Popup split, render triggers, --anchor-width for
trigger-width matching). Item onSelect becomes onClick. Flagged: focus
loop now wraps by default (Base UI default). See
.migration/dropdown-menu.md.

* refactor(review-editor): migrate FileTreeNode context menu to Base UI

Same menu anatomy via @base-ui/react/context-menu (which re-exports
Menu's Positioner/Popup/Item parts). Row button keeps left-click
select / double-click open; items switch onSelect→onClick. See
.migration/context-menu.md for the hand-verify checklist — this is
the file tree's core interaction.

* refactor(review-editor): drop all Radix deps, finish Base UI migration

Removes the 7 @radix-ui/* dependencies — three of them (collapsible,
dialog, tooltip) were never imported anywhere and go without a code
change. Also fixes two keyboard-nav guards in FileTree/SectionsPanel
that matched Radix's [data-radix-popper-content-wrapper] portal
attribute (Base UI popups carry ARIA roles directly, so the role
selectors already cover every floating surface — verified against the
shipped popup implementations).

Zero radix references remain in the package (deps, imports, class
strings, CSS vars, DOM selectors). Root typecheck, 1955 tests, and
build:review all green. packages/ui untouched. Full summary in
.migration/project.md.

* fix(review-editor): initialFocus fallback semantics in pickers

Self-review catch: Base UI's initialFocus function form treats a
returned undefined as 'do nothing' (focus stays on the trigger) while
null falls back to the default focus. The '?? undefined' I introduced
would strand focus on the trigger for short worktree lists — the exact
arrow-keys-leak-to-file-tree bug the original Radix code's comment
warned about. Returning searchRef.current directly (element or null)
matches the original semantics. Also corrects the popover report and
adds a Tooltip-prop coordination note for the parallel ui migration.

* docs(review-editor): record automated QA results in migration reports

Playwright pass against the demo review session: context menu (full
flow incl. real clipboard), tour checkboxes (all three toggle paths,
incl. the span+hidden-input label association), diff-options popover,
and DOM invariants (roles, data-popup-open, no data-state on migrated
primitives) all pass. Demo mode never populates gitContext, so the
diff-type dropdown and worktree/base pickers don't mount there —
their checks remain on the manual list against a real session.

* docs(review-editor): annotate QA checklist items untestable from the standalone entrypoint

* chore: regenerate bun.lock after rebase onto Base UI ui migration
2026-07-07 12:42:41 -07:00
Michael Ramos 795f381ebe feat(review): "All changes" git-status review view (#990)
* feat(review): "Since main" git-status review view + correctness fixes

Adds a composite `since-base` diff — merge-base(origin/main, HEAD) vs the
working tree, plus untracked — as the default code-review view, rendered as a
three-section "git status" panel (Committed / Changes / Untracked) with a
Sections|Tree toggle, a first-run setup chooser, and a "baseline behind GitHub"
fetch banner. Everything normalizes to one git patch, so the diff viewer,
annotations, and feedback path are unchanged.

New diff type wired through runGitDiff / fingerprint / file-content / context /
staging / agent-context in packages/shared + both server runtimes (Bun +
Pi mirror). Default flipped from `unstaged` to `since-base`; the old
DiffTypeSetupDialog is replaced by ReviewSetupDialog (view + default-diff
chooser, screenshots, Settings access).

Includes a reviewed batch of correctness fixes:
- P0: baseBehindRemote was permanently true (rev-parse missing --verify)
- fingerprint blind to quoted/unicode untracked paths (unquote)
- sections parser: record both sides of a rename; staged-delete wins over
  untracked (rm --cached collision)
- graceful degrade when merge-base can't resolve (trunk/no-remote repos)
- /api/fetch-base re-queries remote (narrow-refspec honesty)
- /api/diff/switch concurrency guard (diffSwitchEpoch) + draft rekey
- decouple remote-staleness probe from initialBase (Pi parity)
- keyboard file nav in the sections view; banner gated to base-relative modes

See adr/decisions/005-since-base-github-view-default-20260701-223706.md

* fix(review): since-base review-round hardening (11 fixes, both runtimes)

Addresses the multi-agent + PR-990 review findings:

- Sections rename parser split ` -> ` on the raw quoted porcelain token, so a
  filename containing ` -> ` tore into garbage and a dirty file could render as
  "Committed". Now quote-aware (splitPorcelainRename + unit tests).
- /api/diff/switch captured the epoch AFTER await req.json(), letting a
  slow-body older switch overwrite a newer confirmed one. Epoch now captured
  before any await; hideWhitespace committed only on win. Both runtimes.
- Unresolvable base (trunk / no origin/HEAD) no longer auto-defaults to a
  degraded since-base that hides committed work — getGitContext only offers
  since-base when the base ref resolves, so the default falls through to
  uncommitted. Fingerprint + file-content degrade to HEAD to match the diff.
- git rm --cached no longer yields two diff entries for one path (untracked
  files already in the tracked patch are dropped) — fixes wrong-file-open and
  j/k nav looping in the Sections panel; also fixes latent uncommitted/unstaged.
- Settings' "Default Diff View" list now preserves the sections<->since-base
  coupling (can't leave an invalid sections+classic pair).
- "Behind GitHub" banner only treats origin/* as fetchable; a bare local base
  ("main") is upgraded to its tracking ref at startup so Fetch can clear it.
- hashUntracked resolves untracked paths against the repo toplevel, not cwd, so
  a review launched from a subdirectory isn't blind to untracked edits.
- Agent review instruction now tells agents to enumerate/inspect untracked files.
- Terminology: one "Committed changes" label; the live "Since <base>" label is
  dynamic (matches the header); "Since main" kept only as product copy.
- Docs: CLAUDE.md endpoints/fields + since-base view section; ADR recap.

Verified: typecheck (all projects), bun test 1793 pass, and empirical repro of
the rm --cached dedup, trunk->uncommitted fallback, and rename parser.

* fix(review): restore "(PR view)" on the Committed-changes label (unify toward it, not away)

* fix(review): close since-base coverage gaps from PR-990 review round 2

- First-run setup no longer forces since-base on repos where it isn't available
  (base ref can't resolve). getGitContext omits since-base there; the first-run
  block now checks the same availability before resetting the default or showing
  the chooser, so committed work isn't silently hidden on trunk/no-origin repos.
- reviewBase now includes 'since-base', so changing the base while in the
  git-status view passes the selected base to /api/file-content — expandable
  context is fetched from the right merge-base (was falling back to default).
- Settings now mirror ReviewSetupDialog's coupling exactly: Sections ⇒ force
  since-base; Tree ⇒ leave the diff (Tree + since-base is valid and now
  saveable); classic diff ⇒ Tree; since-base diff ⇒ leave the view. Removes both
  coercions that previously discarded a supported preference.

* fix(review): PR-990 review round 3 — fingerprint/dedup/base-canonicalization

- Fingerprint now uses `git status --porcelain -uall`, so editing a file inside
  a brand-new untracked directory changes the fingerprint and the "Diff out of
  date" banner fires (was collapsed to `?? dir/` and hashed as unreadable).
- extractTrackedPatchPaths pairs `---`/`+++` and excludes only the file's KEYED
  path (new side, or old side for a pure deletion) — no longer drops a recreated
  rename-source file (`git mv a b && touch a`). Verified rm --cached still
  dedups to one entry and the recreated file still shows.
- resolveReviewBase canonicalizes a bare local default name ("main") to its
  tracking ref ("origin/main") on every call, so a client that loaded before the
  startup upgrade resolved can't revert the server to the stale local base on the
  next refresh/switch. Both runtimes (new Pi resolveReviewBase helper).
- Removed the dead activeDiffLabel prop chain (FileTree -> DiffTypePicker).
- ADR recap: documented the staleness-banner scope limitation (default base only).

Verified: typecheck (all projects), bun test 23/23 review-core+fingerprint pass,
empirical repro of the untracked-dir fingerprint, rm --cached dedup, and
rename-recreate cases.

* fix(review): PR-990 review round 4 — dedup content, staging gate, banner timing

- Dedup rewrite: instead of dropping the untracked side of a same-path collision
  (which hid content), drop the tracked DELETION block and keep the untracked
  working-tree content. `git rm --cached f` + edit now shows the new content, not
  a phantom deletion — still one entry per path (no dock/nav collision). New
  stripHeaderPath also strips git's trailing-tab metadata, so space-named files
  dedup correctly. Verified: rm --cached+modify shows content (1 entry),
  space-named (1 entry), rename+recreate still shows both.
- All-files (and the `a` shortcut on the focused file) now gate staging per-file:
  committed files in since-base mode are not stageable, matching SectionsPanel /
  FileTreeNode. Shared isPathStageable helper threaded via ReviewStateContext →
  AllFilesCodeView. Stops the confusing `git add` no-op that still flipped local
  staged/viewed state.
- /api/diff/switch now awaits recomputeBaseBehindRemote before building the
  response, so the "Baseline behind GitHub" banner reflects the new base
  immediately (no ~5s lag switching in, no stale banner switching away). Both
  runtimes.
- ReviewSetupDialog re-applies the recommended default only on first-run dismiss,
  not when reopened from the header menu (was snapping a mid-session diff back).
- SectionsPanel "N added" header now counts sidecar-staged files too, so it
  matches the staged dots on rows.

Verified: typecheck (all projects), bun test 1830 pass, empirical dedup repro,
both bundles build.

* refactor(review): reuse parsePatchPathToken in removeTrackedDeletions (self-review)

Drop the duplicated stripHeaderPath helper — the shared diff-paths
parsePatchPathToken already strips a/|b/ prefix + C-quoting + git trailing-tab,
and verifies the prefix instead of blindly slicing two chars. Documents the
binary-deletion edge (no --- line, so a binary rm --cached is not deduped).

* fix(review): PR-990 review round 5 — mixed-base + staging/sort/flicker nits

- Atomic base upgrade: the startup origin/* canonicalization swapped currentBase
  without rebuilding the patch, so /api/diff could advertise origin/main while
  the served hunks came from local main (mixed-base review on origin/HEAD-absent
  repos). Now rebuilds the diff for the new base and commits base+patch+ref+
  fingerprint together (only if no user switch happened); the fingerprint change
  makes the client's freshness poll pick it up. Both runtimes.
- isPathStageable: gate staging OFF when since-base is active but the sidecar
  hasn't loaded (was falling through to true, allowing a git-add no-op on a
  committed file).
- SectionsPanel: session-staged files now float to the top of Changes — the sort
  key was `false ?? stagedFiles.has(...)` which short-circuits; now ORs them.
- /api/diff/fresh: early returns now carry baseBehindRemote, so a snapshot change
  mid-probe no longer clears the "behind GitHub" banner for one poll. Both runtimes.
- Removed the now-dead activeLabelFallback prop from DiffTypePicker (FileTree
  stopped forwarding it in round 3).

Skipped (agreed): the fetch-base input-validation nit (no realistic attacker)
and the diff-switch TOCTOU (concurrent switches aren't UI-reachable).

Verified: typecheck (all projects), bun test 1830 pass, both bundles build.

* refactor(review): drop redundant staged-OR in SectionsRow (self-review)

item.staged (the grouping key) already ORs in stagedFiles as of the round-5
sort fix, so the row-level `|| stagedFiles.has(...)` is dead weight.

* fix(review): surface the startup base upgrade to already-loaded clients

Review round 6 fixes:

- The startup main -> origin/main upgrade re-baselined the freshness
  fingerprint, so a client that fetched /api/diff before the rebuild kept
  the old patch and every /api/diff/fresh probe reported fresh (the probe
  compares server state to itself, never to what the client renders). Now
  the fingerprint is only re-baselined when no client has loaded the
  pre-upgrade snapshot; otherwise the stale baseline trips the normal
  "Diff out of date - Refresh" banner. Bun + Pi.
- preserveFile refreshes (staleness Refresh, post-Fetch) now adopt the
  server's returned base — exactly the paths where the server may have
  canonicalized main -> origin/main; keeping the old name sent
  /api/file-content and Ask AI context against the wrong base.
- ReviewSetupDialog: clamp the fixed 800px height to the viewport so the
  dialog fits on small laptop screens.
- Docs: /api/diff/fresh response also carries baseBehindRemote/agentCwd.
- Documented the committed-deletion + untracked-recreation dedupe edge as
  accepted (code comment + ADR) — fixing it needs two same-path diff
  entries, which the path-keyed UI cannot represent.

* fix(review): stop the Git-status default from silently reverting to tree

Users with reviewPanelView=sections could keep opening in the tree view on
a stale diff type despite their cookie saying Git status. Three causes:

- The header Sections toggle persisted the view but not defaultDiffType,
  creating a conflicted pair (sections + non-since-base default) that every
  UI writer is supposed to prevent. It now couples the diff default like
  the setup dialog and Settings do.
- configStore's debounced POST /api/config could be lost when a session
  closed within 300ms of a change, leaving cookie and config.json split.
  Pending writes now flush on pagehide with a keepalive fetch.
- configStore.init() applies config.json over the cookie without the UI
  coupling, so a stale server value re-corrupted the pair on every load.
  The app now self-heals at load: if the view says sections but the diff
  default isn't since-base, it repairs the default (cookie + config.json)
  and switches the live session to since-base.

* fix(ui): keep the stage (+) button border visible on the active file row

The button's --border border has no contrast against the active row's 30%
primary tint, so it vanished on the selected row until hovered. Tint the
border with the row's primary color on active rows (both sections and tree
views); the button's own hover border still applies.

* ui(review): spell out the panel view toggle — 'Git status | Tree' text instead of icons

* fix(review): PR-990 review round 7 — export label, diff snapshot race, unicode paths

- exportFeedback describeDiff(): add the missing since-base case — every
  feedback export in the new default mode read "**Diff:** since-base".
- /api/diff GET: snapshot patch/base/ref/error BEFORE the sections-sidecar
  await and pass the pinned base into buildSectionsSidecar. The startup base
  upgrade landing mid-await could pair a rebuilt patch with sections grouped
  against the old base, with initialDiffServed still false so no refresh
  banner ever came. Bun + Pi.
- SectionsPanel: a session-staged untracked file now moves to the Changes
  section immediately (anticipating the server's next sidecar) instead of
  sitting in Untracked with a staged dot until refresh.
- unquoteGitPath: real C-style unquoting with octal (UTF-8 byte) escape
  decoding. JSON.parse rejects octal escapes, so non-ASCII names kept their
  literal \303\251 form — an untracked "café.txt" was silently ABSENT from
  the review (git diff --no-index could not access the quoted name) and the
  deletion dedupe Set lookup could never match. getUntrackedFileDiffs now
  unquotes ls-files output. Unit tests: octal decoding + end-to-end unicode
  untracked file in a real repo.
- review-core.test: worktree subtype round-trip now covers since-base and
  all (was 6 of 8).

Parked (deliberate): SectionsPanel/FileTree keyboard-nav dedup refactor.

* fix(shared): unquoteGitPath keeps literal unicode intact (self-review)

The byte-collector treated literal non-ASCII code units as single bytes,
which would mojibake headers synthesized by our own quoteGitPath
(JSON.stringify leaves unicode unescaped inside quotes — workspace-mode
prefixed headers round-trip through the same parser). Literal chars now
append as string code units (surrogate-safe); only octal escapes go
through the UTF-8 byte decoder.

* fix(review): PR-990 review round 8 — pre-staged toggles, explicit base, AI context race

- useGitAdd: session Set replaced with a tri-state override map folded over
  the sections sidecar; stagedFiles is now the EFFECTIVE staged set (sidecar
  + session stages - session unstages). Fixes pre-staged files: the first
  `a` press actually unstages (was a git-add no-op), the sidebar dot clears
  after a real unstage (was stuck via sidecar OR), and the All-files header
  agrees with the sidebar on load. Overrides reset whenever a fresh sidecar
  arrives (switch, preserveFile refresh, PR response) so stale session
  intent can't fight new porcelain truth. SectionsPanel drops its own
  sidecar OR; stagedCount = effective size.
- Explicit base picks are honored verbatim: the picker sends explicitBase,
  and the server permanently disables local-name -> origin/* canonicalization
  once set (the local/remote groups are distinct choices). The behind-GitHub
  banner also exempts an explicitly-picked local name — Fetch advances
  origin/*, so the banner would be un-clearable nagging. Bun + Pi.
- buildCurrentAiReviewContext(patch, base): GET /api/diff builds Ask AI
  context from the same served snapshot as the patch — the startup base
  upgrade could hand Ask AI a different changeset than the screen. Bun + Pi.
- recomputeBaseBehindRemote: capture remoteDefaultInfo once — a concurrent
  refresh nulling it mid-await threw. Bun + Pi.
- Revert stray "Status Update / Updated!" edit to adr/0001 (test debris
  swept into the round-2 commit).
- splitPorcelainRename comment corrected (porcelain v1 does NOT quote plain
  spaces; a name containing " -> " is ambiguous without -z) + ADR notes for
  the index-only-changes semantics and the rename edge.

* fix(review): FileTreeNode uses the effective staged set — round 9

Round 8 made stagedFiles the effective set (sidecar + session overrides)
and removed SectionsPanel's sidecar OR, but missed the same OR in
FileTreeNode's since-base row (sectionStaged). In the Tree fallback a
pre-staged file unstaged this session kept its dot and the next toggle
re-staged it. Grep-swept: this was the last surviving sidecar-staged OR.

* refactor(review): make the staged-display invariant unrepresentable

The round-8/9 bug class (sidecar staged flag ORed over the effective set)
existed because surfaces had a second staging source to reach for. Remove
it: stagedFiles is now a REQUIRED prop on SectionsPanel/FileTree/
FileTreeNode and the optional-prop fallback branches reading
sectionEntry.staged for display are deleted — a future surface cannot
reintroduce the OR because the pattern no longer exists to copy. The
sidecar type's staged field and AGENTS.md now document the invariant at
the point of temptation. Grep for display reads of .staged now hits only
useGitAdd (owner) and the sidecar builder (producer).

* refactor(review): self-review cleanups on the staged-invariant hardening

- orderFilesBySections: document why its snapshot .staged read is safe
  (only called at sidecar-fresh moments where snapshot = effective) and
  that it must not be reused mid-session — the one remaining display-side
  snapshot read the invariant sweep surfaced.
- FileTreeNode: drop the now-pure sectionStaged alias; use isStaged.

* fix(review): PR-990 review round 10 — header staging gate, fingerprint cap, single-writer coupling

- Single-file diff header now uses the per-path staging gate (canStagePath),
  closing the last ungated staging trigger: committed-only files in
  since-base offered a no-op Git Add that flipped local staged/viewed state.
  Full trigger inventory swept: App `a` shortcut, all-files `a` + header,
  SectionsPanel rows, FileTreeNode rows, single-file header — all six now
  per-path gated or group-gated; no context-menu staging exists.
- Fingerprint circuit-breaker: the freshness poll's `git status --porcelain
  -uall` degrades permanently (per cwd, per process) to collapsed -unormal
  once its output exceeds 2MB — a forgotten node_modules/ no longer burns
  CPU every 5s for the whole session. Costs untracked-dir edit sensitivity
  only on such repos; one possibly-spurious staleness banner at the switch.
- The sections ⟺ since-base coupling now has a single writer:
  setReviewPanelView/setReviewDefaultDiffType in @plannotator/ui/config.
  All five call sites (setup dialog x2, Settings x2, header toggle,
  first-run reset, self-heal) converted; grep for direct writes of either
  setting now hits only reviewView.ts.
- /api/diff/switch responses pass snapshot args to the AI context builder
  (both branches, Bun + Pi) — correct today, now robust against future
  awaits between the epoch check and the response.
- Docs: explicitBase in the /api/diff/switch body.

Parked per discussion: SectionsPanel/FileTree nav+search+footer dedup
(extract when the commit-list view adds a third panel), querySelector row
measurement, fetch-base stderr passthrough (standing decision), Pi
hasAgentLocalAccess (pre-existing follow-up list).

* fix(review): PR-990 review round 11 — subdirectory launches, escape decode, settings note

- Repo-root-relative patch paths now resolve against the git toplevel
  everywhere they meet a filesystem path or pathspec, via a shared
  resolveRepoToplevel helper: file-content working-tree reads (hunk
  expansion returned null from a subdirectory launch) and gitAddFile/
  gitResetFile (stage/unstage failed with pathspec errors). Both bugs
  pre-existed for uncommitted/unstaged; since-base made them the default
  experience. The two existing inline toplevel resolutions (untracked
  diffs, fingerprint) now use the same helper. Shared code — Pi inherits
  via vendoring. Real-repo subdirectory tests for both.
- unquoteGitPath decodes \uXXXX (JSON.stringify emits it for control
  chars without a short escape; our synthesized workspace headers
  round-trip through this decoder). Malformed \u stays literal. Tests.
- Pi explicitBase guard matches Bun byte-for-byte on empty-string base.
- Settings Git tab notes when the CURRENT repo can't serve the Git-status
  view (base ref unresolvable) instead of letting the preference look
  silently broken — it's a global preference, so the options stay.

Same-class items left parked (pre-existing, untouched by this PR):
open-in root resolution and code-nav file reads from subdirectory
launches — on the follow-up list with the Pi divergences.

* fix(review): PR-990 review round 12 — per-client freshness, keyboard-operable row controls

- Freshness is now judged PER CLIENT: every patch-carrying response
  (/api/diff, /api/diff/switch, pr-switch, pr-diff-scope) includes
  snapshotId (the server's draftKey), and the client echoes it on
  /api/diff/fresh probes. A mismatch reports stale for THAT client
  regardless of the VCS fingerprint. This fixes the round-12 finding —
  reloads/second tabs after the startup base upgrade got a permanently
  bogus staleness banner from the shared pre-upgrade baseline — and
  deletes the round-6 conditional re-baseline hack entirely (the
  fingerprint recaptures unconditionally again; the old client's banner
  now comes from its snapshot mismatch, not a deliberately stale
  baseline). Also gives unfingerprintable modes (P4, PR layer) snapshot-
  level staleness for free. Bun + Pi + client hook.
- StageControl and ViewedControl (which had the identical gap) are
  keyboard-operable: tabIndex + Enter/Space activation + focus outline.
  They're spans inside the row <button> (nested real buttons are invalid
  HTML), so they need their own focus stop; the a/v shortcuts remain the
  power path.
- Docs: snapshotId on /api/diff, ?snapshot= on /api/diff/fresh.

* fix(review): PR-990 review round 13 — re-key snapshots on scope switch, PR-tab refresh

- The PR scope switch now re-keys draftKey (= snapshotId + draft storage
  key) at BOTH commit points, unconditionally — matching every other
  snapshot commit site. The full-stack branch previously kept the layer
  patch's key: stale layer tabs never got the banner after a cross-tab
  scope switch (the exact case snapshotId exists for), and full-stack
  drafts collided with layer drafts (pre-existing). The layer branch's
  !layerPatchIncomplete conditional is gone — it only stayed consistent
  because full-stack never re-keyed. Invariant now: every currentPatch
  commit is followed by a re-key. Bun + Pi.
- Refresh works for any stale PR tab: re-selects the CURRENT scope
  instead of no-opping for layer (only full-stack could go stale in the
  fingerprint-only world; snapshot mismatch changed that). Accepted
  residual (documented in code): after a cross-tab PR switch, refresh
  updates the patch but not prMetadata — the scope endpoint doesn't
  carry it, and the full-rehydrate refactor isn't worth the two-tab edge.

* fix(review): stale incomplete-layer Refresh uses the non-blocking upgrade path (self-review)

Round 13 made Refresh re-select the current scope for stale PR tabs; for
an INCOMPLETE layer patch that POST triggers the server's local recompute,
which can park for minutes behind checkout warmup — and
handlePRDiffScopeSelect renders the full-screen switch overlay the whole
time. Route that case through handleLoadFullDiff (same POST, progress
notice instead of modal), which exists for exactly this slow path.

* fix(review): round 14 — composite snapshot id, fully-pinned /api/diff, "All changes" label

- snapshotId is now content hash + diff type (+ PR scope), built by a
  single currentSnapshotId() helper used at every response site and the
  freshness compare. A cross-tab MODE switch with a byte-identical patch
  (layer vs full-stack on a single-PR stack) now flags old tabs; the base
  is deliberately excluded so a same-commit main -> origin/main
  canonicalization stays banner-silent (round-12 noise-avoidance kept).
  draftKey stays a pure content hash — drafts survive content-identical
  round-trips. Bun + Pi.
- GET /api/diff pins ALL served fields (diffType, hideWhitespace,
  prDiffScope join patch/base/ref/error/snapshotId) and the sidecar + AI
  context builders take the pinned type instead of reading globals — a
  concurrent tab switch during the sidecar await can no longer produce a
  since-base patch labeled with another mode. Bun + Pi.
- First-user feedback: the since-base label is now plain English —
  "All changes since origin/main" (dynamic, follows the picked base);
  dialog/Settings short form "All changes"; feedback exports match.
  uncommitted reverts to "Uncommitted" where it had borrowed "All
  changes", so the two stay distinguishable side by side.

* docs(review): finish the Since-main -> All-changes terminology sweep in comments (self-review)

* fix(review): PR-990 review round 15 — base-revert race, freshness reset, unstage regroup

- resolveReviewBase gains a second, probe-independent rule: a non-explicit
  echo of the bare local name of the CURRENT origin/* base stays on the
  tracking ref. The existing rule keys off remoteDefaultInfo, which comes
  from a second network probe that can lag the startup upgrade by seconds;
  in that window a diff-type/whitespace switch echoing "main" committed
  the session back onto the stale local branch and set baseEverSwitched,
  permanently blocking the upgrade. Bun + Pi.
- useDiffFreshness resets stale/dismissed state on snapshotId too — since
  round 14 a new snapshot can reuse identical patch text with a different
  id, and the old banner state wrongly carried over until the next poll.
- SectionsPanel: unstaging a PRE-staged add moves it back to Untracked
  (mirror of the round-7 stage regroup). Detection deliberately uses the
  sidecar's snapshot flag to recognize "was pre-staged"; staged
  modifications stay in Changes, staged renames remain a refresh-heals
  edge.

* fix(review): PR-990 review round 16 — rename staged count, guarded fetch-base replay

- "N added" no longer double-counts staged renames: the sidecar truthfully
  marks BOTH porcelain sides staged, but an above-threshold rename renders
  as ONE file — the hidden old path inflated the count and left a phantom
  effective-staged entry that unstaging the visible row couldn't clear.
  The client now filters sidecar-staged paths to rendered files, which is
  correct in both patch shapes (below-threshold renames render delete+add
  as two rows and both sides pass the filter). Client-only; the sidecar
  stays faithful to porcelain.
- The fetch-base completion only replays the diff refresh if the user's
  diff type/base selection is unchanged since the click — a slow fetch no
  longer yanks the review back to the view captured at click time.

Declined with reasons (in review thread): first-run persist-before-confirm
(approved forced default, second flagging), explicit-base flag ordering
(early-set preserves user intent; commit-after-win would leave picker and
server disagreeing), base-picker revert target (near-unreachable compound
race, deferred).

* fix(review): PR-990 review round 17 — find origin/main on feature-only clones

getDefaultBranch's chain (origin/HEAD -> local main -> blind "master")
skipped the fetched remote-tracking ref entirely. On checkouts with no
origin/HEAD symref and no local main/master — CI checkouts, `clone
--branch feature`, extra worktrees — it guessed "master", the base
didn't resolve, getGitContext suppressed since-base, and the flagship
"All changes" view silently disabled itself for the whole session even
though origin/main was fetched and diffable. The startup upgrade can't
rescue it either (its guard reads the bogus "master" as a deliberate
non-default base).

Chain is now: origin/HEAD (verified) -> origin/main -> local main ->
origin/master -> "master" — remote-tracking refs preferred, matching the
function's stated prefer-upstream intent. Shared core (Pi inherits);
real-repo test reproducing the exact clone shape.
2026-07-04 09:53:49 -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 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
Michael Ramos b0ed4be063 feat(review): right-click file tree row to copy path or filename (#650)
* feat(review): right-click file tree row to copy path or filename

* feat(review): add "Copy full path" to file tree context menu

Resolves to activeWorktreePath ?? agentCwd ?? gitContext.cwd; hidden in PR-review mode where files aren't on local disk.
2026-05-02 12:28:57 -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 b375a804b2 feat(review): AI review agents, local worktree, and UI polish (#491)
* feat(review): add Codex AI review agent with live logs, --local worktree, and panel UI

Hook up Codex as the first AI review agent in the code review system:

- Spawn `codex exec` with Codex's native review prompt and output schema
- Parse structured findings (ReviewOutputEvent) and push as external annotations
- Annotations appear inline in the diff viewer pinned to specific lines
- Review verdict (correct/incorrect + confidence + explanation) displayed in panel

Agent job infrastructure enhancements:
- Server-side command building via `buildCommand` callback (providers don't need frontend commands)
- Result ingestion via `onJobComplete` callback (reads output file, transforms findings)
- `addAnnotations` method on external annotation handler (bypasses HTTP for server-internal producers)
- Live stderr streaming via `job:log` SSE events with 200ms buffer-and-flush
- `cwd` and `summary` fields on AgentJobInfo

PR review with --local worktree:
- `plannotator review <PR_URL> --local` creates a temp git worktree with the PR branch
- Agent gets full local file access without touching the user's working tree
- Hybrid server mode: both prMetadata (platform features) and gitContext (local access)
- Automatic worktree cleanup on session end
- Runtime-agnostic worktree primitives in packages/shared/worktree.ts

Panel UI redesign:
- Findings | Logs tab system with underline-style tabs
- LiveLogViewer component with auto-scroll, truncation, and copy
- Review verdict card (correct/incorrect with confidence and explanation)
- Pending state with labeled "Review Verdict — Pending..." (not skeleton bars)
- Job card click opens detail panel directly (removed separate icon button)
- Dismissed annotation tracking (deleted annotations persist as "dismissed" in panel)
- Copy all annotations as formatted markdown
- Worktree badge in header with info dialog showing path
- CopyButton extended with inline variant for reuse
- ConfirmDialog extended with wide option

For provenance purposes, this commit was AI assisted.

* style(review): UX polish pass — visual quality improvements across code review UI

- VerdictCard: remove AI-template left-border, use background-only tint
- Inline annotations: 6px radius, subtle shadow, hover elevation, action button scale
- File tree: tighter indentation (4 + depth*10), reduced container padding
- Select dropdowns: normalized to 4px border-radius matching pierre diffs
- Dockview tabs: close button pushed to far right with margin-left auto, visible at 0.25 opacity
- PR icons moved from sidebar to header (next to PR link)
- AnnotationRow: translate-x hover feedback
- Border-radius normalized to `rounded` (4px) across all components
- Type scale consolidated: text-[8px]/[9px] → text-[10px], text-[11px] → text-xs
- Sidebar tab hit targets increased (px-2.5 py-1.5, w-4 icons)
- Sticky file group headers in annotation sidebar
- FileTree controls collapsed (worktree + diff selectors share one row)
- Tab micro-animations (transition-all duration-150)
- ScrollFade component for gradient indicators on scrollable containers
- Prose containment: max-w-2xl + px-6 padding on PR Summary/Comments/Checks panels
- MarkdownBody: leading-relaxed for comfortable reading
- PR Comments: surface lift (bg-muted/10), hover feedback, author font-semibold
- PR Checks: link affordance (text-primary, underline on hover, external link icon)

For provenance purposes, this commit was AI assisted.

* feat(review): PR comments panel — search, filter, collapse, navigation, and polish

Comments panel enhancements:
- Search: real-time text filter across author and body, match count display
- Keyboard navigation: j/k to move between comments, scroll-to-selected
- Sort: toggle between oldest/newest first
- Collapsible comments: click header to collapse, collapse/expand all controls
- Author exclusion filter: click authors to hide their comments (not inclusion)
- Comment actions: hover-reveal "View on GitHub" link + copy button (bottom-right)
- Review URLs: PRReview type now includes optional url field, populated from GitHub API

PR Summary fixes:
- Label contrast: use theme foreground color for label text instead of raw GitHub hex
- Linked issues: replaced broken hardcoded SVG with proper GitHub Octicons issue-opened icon

Data plumbing:
- platformUser exposed through ReviewStateContext for "Mine" filtering
- Panel wrapper changed to overflow-hidden for sticky toolbar support
- "Commented" review badge hidden (noise — only show Approved/Changes Requested/Dismissed)

For provenance purposes, this commit was AI assisted.

* feat(review): inline review threads with outdated/resolved state and diff hunk previews

PR Review Threads:
- Fetch inline code review comments via GitHub GraphQL (reviewThreads query)
- PRReviewThread and PRThreadComment types with isResolved, isOutdated, path, line, diffSide
- ThreadCard component: file/line context, Outdated/Resolved badges, nested replies
- Resolved/outdated threads: gradient fade on body with "Show full comment" expand
- GitLab: reviewThreads placeholder (TODO: parse DiffNote positions from notes)

DiffHunkPreview:
- Renders diff hunks using @pierre/diffs FileDiff component (read-only, compact)
- Full theme integration: reads computed CSS vars, injects via unsafeCSS (same as main DiffViewer)
- Respects user font settings from ReviewState context
- Handles bare GitHub diffHunk format (prepends synthetic file headers for pierre parsing)

Comments Panel Polish:
- Comment cards: bg-card + subtle shadow for depth and isolation from panel background
- Hover: shadow elevation (0_2px_6px) for interactive feedback
- Thread cards: dimmed shadow for resolved/outdated, full shadow for active
- Prose padding: px-8 (32px) across all dockview panels (Summary, Comments, Checks, Findings)

For provenance purposes, this commit was AI assisted.

* feat(review): Claude Code agent, cross-repo --local, render fixes

Claude Code review agent:
- claude-review.ts: prompt (adapted from code-review plugin), command builder
  (dontAsk + granular allowedTools/disallowedTools), JSONL stream output parser
- Prompt sent via stdin (not argv) to avoid quoting/variadic flag conflicts
- stream-json --verbose for live JSONL streaming + final structured_output
- Same schema as Codex — transformReviewFindings is now provider-agnostic

Agent jobs infrastructure:
- stdout capture (captureStdout option) for providers that return results on stdout
- stdin prompt writing (stdinPrompt option) for providers that read prompt from stdin
- cwd override in buildCommand return for providers without -C flag
- await stdoutDone before onJobComplete to prevent drain race condition
- job.prompt field for transparent prompt display in detail panel

Cross-repo --local:
- Detect same-repo vs cross-repo via parseRemoteUrl comparison
- Cross-repo: shallow clone via gh/glab repo clone (--depth 1 --no-checkout + targeted fetch)
- Cross-repo uses platform diff (gh pr diff) for display, clone for agent file access
- Same-repo: existing worktree path unchanged
- Cleanup: rmSync for clones, worktree remove for same-repo

Performance: jobLogs context split
- Separate JobLogsContext to prevent high-frequency log SSE from re-rendering all panels
- Only ReviewAgentJobDetailPanel subscribes to JobLogsProvider
- Standard React pattern: split contexts by update frequency

Image error handling:
- SafeHtmlBlock component wraps dangerouslySetInnerHTML with img onerror handlers
- Broken images (expired GitHub JWTs) hide on first 404 instead of flickering
- Prevents console 404 flood from re-render retry loops

For provenance purposes, this commit was AI assisted.

* fix(review): decontaminate --local from diff pipeline, fix worktree setup, default local for PRs

The --local flag was setting gitContext from the worktree, which contaminated
the diff rendering pipeline — causing pierre "trailing context mismatch" errors
because /api/file-content read worktree files instead of using the GitHub API.

Root cause: gitContext serves two purposes — diff pipeline (file contents, diff
switching, staging) and agent sandbox (cwd for agent processes). These are now
properly separated via a new agentCwd option on ReviewServerOptions.

Changes:
- Add agentCwd to ReviewServerOptions, independent of gitContext
- Agent handler (getCwd, buildCommand, onJobComplete) prefers agentCwd
- Stop setting gitContext in --local PR path — diff pipeline untouched
- Revert band-aid !isPRMode guards (no longer needed)
- Fix same-repo worktree: fetch origin/<baseBranch> so agents see correct diff
- Fix cross-repo clone: create local branch at baseSha for git diff accuracy
- Fix FETCH_HEAD ordering: fetch base branch before PR head (createWorktree needs PR tip)
- Fix macOS path mismatch: realpathSync(tmpdir()) so agent paths strip correctly
- Change buildCodexReviewUserMessage signature from GitContext to focused options
- Make --local the default for PR/MR reviews (--no-local to opt out)
- Pass agentCwd to client for worktree badge display

For provenance purposes, this commit was AI assisted.

* style(review): unify panel headers, responsive buttons, dockview polish

- Unify all panel headers at 33px via --panel-header-h CSS variable
- FileHeader now uses shared variable instead of hardcoded 30px
- Dockview tab bar height, font size, and padding aligned with sidebars
- FileTree header uses fixed height instead of padding-based sizing
- Consistent border opacity (border-border/50) across all panels
- Consistent font weight (font-semibold) on all header labels
- Remove dockview tab focus outline (::after pseudo-element)
- Dockview tab close button pushed to right edge
- Tab bar void area uses muted background
- Top app header compacted from h-12 to py-1
- Sidebar footer: copy button and diff stats side by side
- FeedbackButton responsive labels (Send/Post at md, full labels at lg)
- Move ReviewAgentsIcon to packages/ui for shared use
- Agent empty state uses shared ReviewAgentsIcon instead of hardcoded SVG
- Sidebar header label truncates when narrow (tabs never clip)
- Detail panel prompt disclosures get proper spacing
- React.memo on PR tab components (PRSummaryTab, PRCommentsTab)
- Inline onerror on img tags for broken GitHub image handling

For provenance purposes, this commit was AI assisted.

* fix: type assertion for Bun stdin FileSink

Bun's proc.stdin is typed as `number | FileSink` but we need to call
.write() and .end() on it. Cast to FileSink to satisfy tsc --noEmit.

For provenance purposes, this commit was AI assisted.

* fix(review): XSS in sanitizeHtml, git flag injection, cross-repo ref mismatch

Security:
- Remove `onerror` from DOMPurify ALLOWED_ATTR — was allowing arbitrary JS
  execution via PR descriptions containing `<img onerror="...">`. Replace
  with SafeHtml component that attaches error handlers via useEffect + ref.
- Add `--` end-of-options separator to git fetch and git branch calls to
  prevent flag injection via crafted branch names from API responses.

Bug fix:
- Cross-repo clones now create both local branch AND remote-tracking ref
  (`refs/remotes/origin/<baseBranch>`) at baseSha, so agents can use either
  `git diff main...HEAD` or `git diff origin/main...HEAD`.

Polish:
- Add copy button to verdict card in agent detail panel
- Remove hover:translate-x animation from finding rows
- Reduce file tree indent per level, remove extra file offset
- Add pr-action endpoint logging for debugging submit failures

For provenance purposes, this commit was AI assisted.

* chore: upgrade @pierre/diffs from 1.1.0-beta.19 to ^1.1.12

The beta pin was needed for processFile() API (expandable diff context),
which shipped in 1.1.0 stable on March 14. We were 13 releases behind.

Notable fixes in the upgrade path:
- 1.1.5: Fix diffAcceptRejectHunk with partial FileDiffMetadata
- 1.1.6: Patch parsing fix for renames and dotfiles
- 1.1.8: Fix maxLineDiffLength regression

May resolve intermittent "trailing context mismatch" errors in diff rendering.

For provenance purposes, this commit was AI assisted.

* fix(review): remove shell provider, flag injection, Claude log formatting, copy UX

Security:
- Remove shell provider from agent capabilities (unauthenticated RCE vector)
- Move `--` before prRepo in `gh repo clone` to prevent flag injection

Features:
- Wire up formatClaudeLogEvent — Claude live logs now show readable text
  instead of raw JSONL
- Sidebar annotations: copy + delete buttons appear on hover (no overlap)
- Agent finding rows: copy button on hover (progressive disclosure)
- Verdict card: copy button pushed to the right
- File tree: reduced indent per level, files aligned with folders

Infra:
- PR action endpoint logging for debugging submit failures

For provenance purposes, this commit was AI assisted.

* fix: validate repo identifier to prevent flag injection in gh repo clone

The `--` separator in `gh repo clone` separates gh args from git args,
not positional args from flags. Using `--` before prRepo would break
the git flags. Instead, validate that the repo identifier doesn't start
with `-` to prevent flag injection via crafted PR URLs.

For provenance purposes, this commit was AI assisted.

* feat(review): Claude-specific review model with severity, reasoning, and multi-agent prompt

Claude review agent now has its own schema, prompt, and transform — separate
from Codex's P0-P3 priority model. Each provider uses its natural review style.

Schema changes:
- Claude findings use severity (important/nit/pre_existing) instead of priority (0-3)
- Flat structure: file, line, end_line instead of nested code_location
- description (single field) instead of title + body
- reasoning field captures the validation chain per finding
- summary with counts instead of overall_correctness/confidence

Prompt: Converges the open-source Claude Code review prompt with the remote
review service model. 4 parallel agents (Bug+Regression at Opus, Security at
Opus, Code Quality at Sonnet, Guideline Compliance at Haiku), validation step,
deduplication, severity classification. CLAUDE.md and REVIEW.md awareness.

UI: Severity markers (colored dots) on finding rows. Collapsible reasoning
section via <details>. New optional severity/reasoning fields on CodeAnnotation
and the external annotation store — backward compatible, only set by Claude.

Transform: transformClaudeFindings normalizes Claude output into the shared
annotation format. Codex path (transformReviewFindings) is completely untouched.

For provenance purposes, this commit was AI assisted.

* fix: use bg-amber-500 for nit severity dot (bg-warning may not be defined)

For provenance purposes, this commit was AI assisted.

* fix: security hardening, debug cleanup, deduplication, Pi mirroring, findings UX

Security:
- Add -- separator to ensureObjectAvailable git fetch (worktree.ts)
- Validate baseBranch against path traversal (..) before git ref operations
- Use process.once('exit') instead of process.on for worktree cleanup

Debug:
- Gate debugLog behind PLANNOTATOR_DEBUG env var (no more unconditional writes)
- Remove PARSE_OUTPUT_RAW dump that logged full JSON output to disk

Deduplication:
- Extract toRelativePath to packages/server/path-utils.ts (was duplicated
  in codex-review.ts and claude-review.ts)

Pi extension:
- Remove shell provider from capabilities
- Add buildCommand callback to AgentJobHandlerOptions
- POST handler calls buildCommand for server-side command synthesis

UI:
- Findings sorted by severity (important → nit → pre_existing)
- Severity legend under findings header (colored dots)
- Reasoning always visible (not collapsible) — fixes click navigation bug
  where <details> captured click events and broke annotation linkage
- Full finding text shown (removed line-clamp-2 truncation)
- Sidebar annotation hover actions aligned to the right
- DiffHunkPreview: cancel requestAnimationFrame on unmount

For provenance purposes, this commit was AI assisted.

* fix: move toRelativePath import to top of claude-review.ts

For provenance purposes, this commit was AI assisted.

* fix: same-repo detection compares host, platform-aware comment links, remove design docs

- Same-repo detection now compares both owner/repo AND hostname from the
  git remote URL against prMetadata.host. Prevents false positives on
  GitHub Enterprise where different instances share org/repo names.
- "View on GitHub" label in PR comments tab now shows "View on GitLab"
  for GitLab MR comments based on the comment URL.
- Remove internal design docs (PR_LOCAL_WORKTREE.md, AGENT_LIVE_LOGS.md)
  that were development artifacts, not user-facing documentation.

For provenance purposes, this commit was AI assisted.

* fix(review): show severity markers and reasoning in inline diff annotations

The severity and reasoning fields from Claude findings were only visible in
the agent detail panel, not in the inline diff annotations. Now:

- DiffAnnotationMetadata carries severity and reasoning fields
- DiffViewer passes them through when mapping annotations
- InlineAnnotation renders colored severity dot and reasoning text

For provenance purposes, this commit was AI assisted.

* fix: prefix Claude findings text with [severity] tag

Findings now show as "[important] description", "[nit] description",
"[pre_existing] description" — consistent with Codex's [P0]/[P1] tags.

For provenance purposes, this commit was AI assisted.

* feat(pi): full agent review mirroring — stdin, stdout, live logs, result ingestion

Pi extension agent-jobs handler now mirrors the Bun server's full capabilities:
- stdin piping for Claude prompt delivery
- stdout capture for Claude JSONL stream parsing
- Live stderr streaming with 200ms buffer-and-flush for job:log events
- Claude JSONL formatting via vendored formatClaudeLogEvent
- onJobComplete callback for result parsing and annotation push
- Full buildCommand integration in POST handler
- jobOutputPaths tracking with cleanup on kill

Pi serverReview.ts now wires buildCommand and onJobComplete with the same
logic as the Bun review server — Codex and Claude commands are built
server-side, results are parsed and transformed into external annotations.

Runtime compatibility: replaced Bun.file/Bun.write in codex-review.ts with
node:fs/promises equivalents (writeFile, readFile, existsSync) that work on
both Bun and Node. Verified Bun build passes.

Vendoring: vendor.sh now copies codex-review.ts, claude-review.ts, and
path-utils.ts from packages/server/ with import path rewriting for the
generated/ layout.

Also: Review Prompt label, px-8 padding on agent detail header/tabs/logs.

For provenance purposes, this commit was AI assisted.

* fix: remove duplicate isPRMode declaration in Pi serverReview.ts

For provenance purposes, this commit was AI assisted.

* fix: include reasoning in all copy and feedback export paths

- exportReviewFeedback: appends **Reasoning:** after finding text
- Per-finding copy button: appends reasoning to copy text
- Sidebar annotation copy: appends reasoning to copy text

This ensures reasoning flows through Copy All, Send Feedback, and
individual copy actions — not just the visual rendering.

For provenance purposes, this commit was AI assisted.

* fix: six verified findings — navigation, dedup, cleanup, copy, diff match, Windows paths

1. openDiffFile: clicking a finding now navigates to the correct file
   before selecting the annotation (was silently selecting in wrong file)

2. SEVERITY_STYLES: extracted to packages/ui/types.ts as shared constant,
   imported in both ReviewAgentJobDetailPanel and InlineAnnotation
   (was duplicated with per-render rebuild in InlineAnnotation)

3. killJob: added jobOutputPaths.delete calls to match Pi's version
   (was leaking two strings per killed job)

4. CommentActions: replaced hand-rolled copy with CopyButton inline
   variant (was reimplementing useState/clipboard/setTimeout pattern)

5. Branch mode prompt: changed from three-dot to two-dot to match
   the UI's actual diff computation (agent was reviewing different diff)

6. toRelativePath: uses path.relative + forward-slash normalization
   for Windows compatibility (was string prefix matching with / only)

For provenance purposes, this commit was AI assisted.

* perf: wrap ReviewSidebar in React.memo to prevent re-renders during log streaming

Every job:log SSE event triggers setJobLogs in useAgentJobs, which re-renders
App.tsx. Without memo, the sidebar re-renders on every event (~5/sec) even
though its props (agentJobs.jobs, capabilities, callbacks) haven't changed.
This caused visible flickering when a review tab was open during agent runs.

React.memo shallow-compares props — all sidebar props are stable references
(jobs array only changes on status events, callbacks are useCallback-wrapped),
so the sidebar correctly skips re-renders during log streaming.

For provenance purposes, this commit was AI assisted.

* fix(security): remove find/ls/cat from Claude allowed tools, add glab CLI

Security: Bash(find:*) allowed find -exec to spawn arbitrary subprocesses
that bypassed --disallowedTools. Removed find, ls, and cat — Claude has
Glob, Read, and Grep built-in which cover file access without shell exec.

Feature: Added glab mr view/diff/list and glab api to allowed tools so
Claude can inspect GitLab MR context in remote-mode reviews.

For provenance purposes, this commit was AI assisted.

* fix: Pi addAnnotations, Pi stdout drain, cross-repo exit codes

Pi extension:
- Add addAnnotations() to external-annotations.ts return object —
  serverReview.ts calls it when agent jobs complete but the method
  was missing (build/runtime error)
- Change proc.on('exit') to proc.on('close') in agent-jobs.ts —
  Node's 'exit' fires before stdio streams drain, so stdoutBuf could
  be incomplete when onJobComplete parses Claude's JSONL result

Cross-repo --local:
- Check git checkout FETCH_HEAD exit code — throw if it fails so the
  outer catch falls back to remote-only with a clear warning
- Log warning if baseSha fetch fails (non-fatal, agents just can't
  diff locally)

For provenance purposes, this commit was AI assisted.

* fix: FETCH_HEAD ordering, Pi worktree-aware cwd, SEVERITY_ORDER hoisted

Critical:
- Move ensureObjectAvailable before PR head fetch — it can overwrite
  FETCH_HEAD if baseSha needs fetching, causing createWorktree to
  check out the base commit instead of the PR head
- Pi serverReview.ts: extract resolveAgentCwd() helper used by getCwd,
  buildCommand, and onJobComplete — was bypassing worktree-aware path
  resolution, causing agents to run in wrong directory

Cleanup:
- Hoist SEVERITY_ORDER to module scope in ReviewAgentJobDetailPanel
  (was recreated inside component body on every render)

For provenance purposes, this commit was AI assisted.

* fix: Bun/Pi parity — provider default, annotation error logging

- Bun agent-jobs: change provider default from "shell" to "" (shell was
  removed from capabilities, default should match Pi)
- Pi serverReview: log errors from addAnnotations in onJobComplete
  (Bun logs them, Pi was silently ignoring)

For provenance purposes, this commit was AI assisted.

* docs: add AI Code Review Agents guide with full prompt transparency

New docs page covering:
- Overview of Codex and Claude review agents
- How findings work (severity/priority, reasoning, navigation)
- Local worktree behavior (same-repo vs cross-repo)
- Full transparency section with:
  - Claude multi-agent pipeline prompt (all 6 steps)
  - Claude command and allowed/blocked tools
  - Codex review prompt and command
  - Both output schemas (Claude severity + Codex priority)
- Security notes (read-only, no network, local execution, no commenting)
- Customization via CLAUDE.md and REVIEW.md

For provenance purposes, this commit was AI assisted.

* docs: add provenance links for Claude and Codex review integrations

Credit Anthropic's Claude Code Review service, the open-source
code-review plugin, and OpenAI's Codex CLI as the foundations
for our review agent integrations.

For provenance purposes, this commit was AI assisted.

* fix: temp clone leak, j/k key conflict, thread header null line

- Cross-repo: clean up localPath in catch block when fetch/checkout
  fails after clone succeeds (directory was leaking in /tmp)
- Remove j/k/arrow keyboard navigation from PR comments panel —
  these shortcuts belong to the file tree only, both registering
  global handlers caused double-navigation
- Thread header null guard: check thread.line before building range
  label to prevent "L12–null" for outdated GitHub threads

For provenance purposes, this commit was AI assisted.

* fix: hoist localPath for catch-block scope, validate baseSha format

The previous rmSync(localPath) in the catch block was dead code —
const declarations inside try are not in scope in catch (separate
lexical environments per ECMAScript spec). The ReferenceError was
silently swallowed by the inner try/catch, so temp directories
still leaked on failed fetch/checkout.

Fix: hoist `let localPath` before the try block so it's accessible
in catch. Guard with `if (localPath)` since the error could occur
before assignment.

Also: validate baseSha is a hex SHA (40-64 chars) to prevent git
flag injection via crafted API responses. Validate baseBranch
rejects both '..' and '-' prefixes.

For provenance purposes, this commit was AI assisted.

* docs: rewrite AI Code Review guide for clarity and readability

Restructured for a technical audience: shorter paragraphs, cleaner
tables, removed em-dashes and filler prose, tightened the pipeline
diagram, streamlined security section into scannable single-line items.

For provenance purposes, this commit was AI assisted.

* docs: rewrite transparency section with exact prompts and commands

Replaced summarized/paraphrased transparency section with the actual
prompts, commands, schemas, and tool allowlists as they exist in the
code. One short security note at the top, then raw content.

For provenance purposes, this commit was AI assisted.

* docs: add mini TOC to transparency section

For provenance purposes, this commit was AI assisted.

* fix: stdout drain hang, Codex verdict override, Claude parse logging, memo removal

Critical:
- Race stdoutDone against 2s timeout after proc.exited — prevents
  permanent job hang when Bun's ReadableStream doesn't close after
  process exit. The process is dead; 2s is a cleanup deadline.

Bug fix:
- Codex verdict: override to "Issues Found" when P0/P1 findings exist,
  regardless of the freeform overall_correctness string. Prevents green
  "Correct" badge when Codex says "mostly correct but has issues."
  P2/P3-only findings still trust Codex's verdict.

Observability:
- Log Claude parse failures with buffer size and last 200 bytes so we
  can diagnose empty-findings cases.

Performance:
- Remove React.memo from ReviewSidebar — was blocking legitimate
  re-renders (job status, findings) to prevent cosmetic log flickering.
  The tradeoff was wrong.

Docs:
- Remove "shell" from CLAUDE.md capabilities table (provider was removed).

For provenance purposes, this commit was AI assisted.

* fix: type assertions for Bun ReadableStream async iteration

Bun's proc.stdout/stderr support for-await at runtime but TypeScript's
ReadableStream type doesn't declare [Symbol.asyncIterator]. Cast through
unknown to AsyncIterable<Uint8Array> — standard Bun workaround, same
pattern as the FileSink cast for stdin.

For provenance purposes, this commit was AI assisted.
2026-04-06 12:15:27 -07:00
Michael Ramos 68418e2a35 feat(review): add dockview center workspace (#453)
* chore(review): add dockview-react dependency

* feat(review): add dockview center workspace

* fix(review): stabilize diff search navigation

* feat(review): add progressive file search UI

* feat(review): streamline header controls

* fix(review): portal annotation toolbar

* refactor(review): streamline header and action menus

* fix(review): make file header responsive

* fix(review): align split diff drag handle

* chore(review): hide review agents sidebar

* fix(review): remount diff viewer on file switch

* fix(review): wrap inline diff comments
2026-04-01 14:58:45 -07:00
Michael Ramos d29dd08c13 feat: git add files from code review UI (#257)
* feat: add per-file git add from code review UI (#254)

Adds the ability to `git add` individual files directly from the code
review interface. Users can stage approved files then switch to the new
"Unstaged changes" diff view to see only remaining work.

- POST /api/git-add endpoint with worktree support
- gitAddFile/gitResetFile utilities in packages/server/git.ts
- "Staged changes" and "Unstaged changes" added to diff type dropdown
- useGitAdd hook encapsulating all staging state and API logic
- "Git Add" / "Added" toggle button in FileHeader (next to Viewed)
- Visual indicator (left border) for staged files in sidebar FileTree

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

* style: use green background tint for staged files instead of left border

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

* fix: support staged/unstaged diffs in worktrees and display stage errors

Adds "staged" and "unstaged" to worktree sub-type allowlists (server parser,
client parser, and runGitDiff switch), displays stageError in FileHeader,
and documents /api/git-add in CLAUDE.md.

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

* refactor: clean up git add feature — fix stale closure, timeout leak, dedup validation

- useGitAdd: use ref for stagedFiles to stabilize stageFile callback,
  clear error timeout on reset, drop unnecessary useMemo
- App.tsx: wrap onFileViewed in useCallback
- FileTree: merge duplicate staged count conditionals
- review.ts: reuse exported validateFilePath for /api/file-content

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

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 07:31:17 -07:00
Michael Ramos 47bfc4bfd4 feat: hierarchical folder tree in code review sidebar (#246)
Group changed files by directory with collapsible folders, aggregate
+/- stats per folder, and auto-expand on keyboard navigation.
Single-child directories collapse (e.g., src/utils/ as one node).
Falls back to flat list for single-directory diffs.

Closes #161

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 17:20:08 -08:00