Review: a global review destination, from queue to submitted PR - #7
Conversation
Review stops being a per-session sub-tab and becomes a destination that spans repositories. The old shape gated Review off for remote repositories -- but reviewing a remote PR is the primary use case -- and a per-session tab cannot show a queue that spans repos at all. OpenSessionTab.SubTab is now CLAUDE - TERMINAL - EXPLORER, and Cmd-4 is a navigation command that lands on Review scoped to the current session's checkout. Domain: - ReviewScope / ReviewScopeRegistry mint opaque rs_ handles (MCP schema section 0), with grants so a human can hand one session's agent another worktree's scope. Minting is idempotent on identity, because the queue is reassembled on every worktree change and a fresh id per assembly would orphan everything keyed by (scopeId, ...). - ReviewQueueService assembles MINE/AGENTS/REQUESTED/STACK from WorktreeService, GitStatusService and a new GhCliService.listReviewRequests. A failing repository or a missing gh contributes nothing and is logged; the rest of the queue still assembles. Review degrades, never blocks. UI: - ReviewDestinationView (36px title bar, two-row item header, a Host seam where the diff column lands) and ReviewQueueRail (236/206/44px, animated like the Explorer's search rail). Every row is a real focusable Button -- the prototype having zero focusable controls was a defect, not a style. - Sidebar destination row above the tree with a count badge, and a per- worktree finding badge that jumps into Review scoped to that worktree. - Showing Review hides every native terminal, through one writer over three independent conditions, so a modal closing over Review cannot un-hide a terminal through it. Theme and fonts: - Three chrome greys and ten semantic tokens added to BOTH theme sheets. Semantic names stay separate from same-valued existing ones: a MED-risk intent is not a dirty worktree, and collapsing them would make a future palette change to one silently move the other. - All new CSS is structure-only with px font literals, so the interface-size slider scales it. ThemeTokenContractTest now pins both invariants, which nothing guarded before. Testing: - A headless JavaFX harness (TestFX + Monocle), which this repository did not have: view tests show a real Stage, apply the real stylesheets and drive real key events with no window server. headless.geometry is load-bearing -- Monocle's default 1280x800 screen overflows the software pixel buffer for a wider Scene. - ReviewDestinationViewTest covers the keyboard table M1 binds and the focus-traversability rule; verified non-vacuous by mutating clamp-to-wrap and clearing focusTraversable, which failed exactly those two tests. - Queue assembly is tested against real temporary git repositories, including the missing-gh path. ReviewView (the per-session diff tab) is deleted rather than parked; its hunk-row model is kept for the diff column. docs/ui-redesign.md records the structure and eleven known deviations, and flags the five open decisions in section 10 of the handoff rather than inventing answers. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Hunk cards over a virtualized row list: gutter, sign column, syntax- highlighted source, add/delete tints, collapsed unchanged runs, three densities, and a jump into the Explorer. Cards are drawn from a per-row edge (top/body/bottom) rather than by nesting rows inside real card containers, because the column has to stay virtualized -- a 21-file diff of this repository renders 853 rows across 28 cards. Nothing sets a cell height and no cell graphic is bound to the cell's height: a fixed row height was the bug that hid code with no scrollbar to reveal it. Review asks git for a wider context window (-U12, not git's default -U3). Found by testing rather than by reading: with a three-line window no unchanged run is ever longer than the fold threshold, so the "N unchanged" fold would have rendered correctly and simply never appeared. Showing more context and folding it is the whole point of the feature. DiffService gained an explicit contextLines parameter; every other caller keeps git's default. On this repository the wider window produces 51 folded runs. The lexer moves to app.drydock.ui.code now that two surfaces render code, with two output shapes over one set of language rules: plain span data for the diff column's per-line TextFlow, and the RichTextFX StyleSpans the Explorer's CodeArea already consumed. One editor control per diff line would have been thousands of controls for read-only text. Density is a style class, and its measurements are px literals in app.css -- so the interface-size slider still scales them and density stays a relative choice on top of an absolute one. The Explorer jump reports when it has nowhere to go (no bound session, or a closed tab) and explains why in its tooltip, instead of appearing to do nothing. Testing: the row model is pure and covered directly (card edges, fold threshold, run keys surviving a re-diff that shifts line numbers, the render cap, context-free mode). The column is covered against real temporary git repositories through the headless harness, including that rows keep their natural height and that density actually changes it. The per-session diff tab's old row model is deleted; this replaces it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Everything in the review store is keyed by (scopeId, id), not by id alone. Finding and intent ids are agent-chosen and stable across re-runs, so the SAME id genuinely appears in two worktrees at once -- a reviewer re-run against two branches produces f_leak_1 in both. Keying on the id made resolving a finding in one worktree resolve it in another. The regression test for that is the first test in AnnotationStoreTest. ReviewAnnotation is now the full finding of the MCP schema: severity, confidence, intent, evidence, a proposed patch, deviates-from, ASK chips, a thread, and a human severity override that records the reviewer's original opinion alongside the human's. Verdicts are keyed by (scopeId, intentId) for the same reason findings are. Annotations written before scope handles existed are migrated rather than dropped. A v1 file names a session and a DiffScope, neither of which can identify a handle minted this process, so those entries are parked under a deterministic placeholder scope and adopted the first time the matching scope is minted -- and a finding the live scope already has always wins. Dropping them would have silently thrown away real review comments on upgrade. The margin sits beside the code, never inline, so the diff stays continuous; the verdict bar sits below both columns and is always in the layout, so collapsing every rail cannot take the primary action with it. Approving an intent with an open blocking finding is refused inline -- with the reason beside the button and the two ways out (resolve, or downgrade after a discussion) in the margin next to it. Reply drafts are held by the margin, keyed by (scopeId, id), not by the card node. Found by testing: a card is rebuilt whenever its finding changes -- an agent appending a reply, say -- and a draft living inside the node went with it. The scoped key is also what keeps a draft out of an identically-named finding in another worktree. Apply patch is a hand-off, not an edit: drydock sends the proposal to the scope's live session as every other worktree action does, and records only that the hand-off happened. An agent that writes to the tree behind the review is a bug, not a feature. Intents come from review_intents, or from the by-file fallback the schema specifies -- which is what keeps the verdict bar and the submit flow working on a plain diff with no reviewer configured. Review must never require an agent. review_reply now refuses an ambiguous finding id, listing the candidate scopes and asking for scopeId rather than picking one. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
review_scope, review_intents, review_finding, review_answer and review_state, plus the rail that renders what review_intents supplies. review_scope pages on a byte budget and returns a cursor. A hunk larger than the whole budget comes back truncated and flagged rather than dropped or failing the call -- one generated file must not make a scope unreadable. Every line carries the stable o/n line key that every anchor in every other call refers to, which is what lets findings survive a re-diff. Three rules keep the human's work theirs: - review_finding upserts on the agent-chosen id, so a re-run keeps the thread, the human's severity override and their resolution. The reviewer's opening statement refreshes; nothing else the human touched does. - review_answer's proposeSeverity and proposeResolve are recorded in the thread as labelled suggestions and never applied. An agent cannot downgrade or resolve its own finding. - The human-side calls in schema section 5 are deliberately NOT exposed as agent-callable tools. Making them tools would let an agent approve its own work; they are the UI's own actions against the store, and agents observe them through review_state. This narrows the schema on purpose and is flagged in docs/ui-redesign.md for a second look. A findings batch is decoded in full before anything is stored, so one malformed entry writes nothing rather than half a review. Unknown and forbidden scope handles produce the same refusal, so probing tells an agent nothing. PromptSafety gained checkInboundText, which is a different check from the outbound one and says so: it refuses control characters (a finding's words can later be typed into a live terminal by "Ask the agent to fix it") but allows a leading ! / #, because those rules are about what the claude TUI does with a typed line and a finding body is not typed as a line. Markup is allowed because the margin renders findings as text -- there is no renderer to confuse. Notifications (schema section 7) are not implemented: they need a server-initiated channel McpServer does not have, and agents poll review_state instead. Flagged rather than faked. Verified in the real app: 5 queue items, 26 hunk cards, 61 folded runs, no exceptions. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Checking a pull request out into its own worktree, a read-only patch path for when you do not want one, and handing a submitted review to the Finish flow that already exists. The handoff's literal command does not exist. It gives the sequence as `gh pr checkout <n> --worktree`; gh has no such flag -- checked against 2.96, where `gh pr checkout` takes --branch, --detach, --force and --recurse-submodules and always operates on the working tree it is run in. Running it in the repository root would therefore move the reviewer's MAIN checkout onto the PR branch, mid-task, which is the one thing a review must never do to someone. What drydock does instead, to the same end: git worktree add --detach <dir> (main checkout untouched) gh pr checkout <n> --branch pr-<n> (run INSIDE that worktree) claude --cwd <dir> The gate prints that sequence rather than the handoff's, because the reader may well run it themselves and the handoff's would fail. A failure at the second step removes the worktree the first step made: a half-made checkout is worse than none, because the next attempt collides with what was left behind. The test for that also pins that the main checkout's branch and HEAD do not move. Checking out lives in its own service rather than GhCliService, whose contract is read-only observation. `gh pr diff` -- the "Read the patch only" path -- is read-only and does belong there; its output goes through the same UnifiedDiff parser as a local git diff, so a PR read without a worktree renders exactly like one with, under a banner saying the agent actions are unavailable, because everything that needs a session is. A successful checkout starts a session on the new worktree and grants it the scope handle -- the human action the MCP schema requires before an agent may address a scope. Submitting a review hands the worktree to the Finish flow. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Two of the three extras. The third is deliberately not built; see below. The symbol lens is a local lexical index of the diff, never MCP -- "where is this symbol used" is a question about the code in front of the reader, and asking an agent would make it slow, non-deterministic, and unavailable exactly when Review has no reviewer configured, which it must always survive. Identifiers the index knows get a dotted underline; the popover chips every occurrence `in diff` or `not touched` and says plainly that these are occurrences, not resolved references, because that is what a lexical index can honestly promise. Keywords, identifiers under three characters, and symbols appearing once are excluded -- an underline on every word would mean nothing. The MCP activity panel renders a bounded ring buffer written by the one place every tool call passes through. Bounded because a long review makes thousands of calls and a panel nobody has open must not leak; the counters behind the budget bar are not bounded, so the bar still reflects the whole session. A hidden panel unsubscribes, so a closed panel costs nothing. The header names the scope and says when no session is bound -- an empty log then means "nothing can call", not "nothing has". Escape now unwinds topmost-first through Review: the lens popover, then the activity panel, then Review itself. The agent step timeline is NOT implemented, and promptHistory returns empty. It needs the session transcript (cheap), per-step line attribution (reconstructing per-turn diffs), and a survives-to-HEAD calculation (diffing each against the current tree). The last two are real work, and a plausible-looking approximation would be worse than nothing: the entire value of that panel is answering "which of the agent's edits actually made it" correctly. Flagged in docs/ui-redesign.md rather than faked. Found while writing the tests: Set.of rejected a duplicate stop word, which failed every view test at class-initialization time. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Scope handles were minted at random per process, so a finding persisted under a scopeId was orphaned on the next launch -- the store kept it, and nothing could address it again. Handles are now an HMAC of the scope's identity (kind, repo root, worktree, base, head, PR) under a per-profile secret persisted beside the findings, bumping the annotations schema to 3. The same worktree therefore resolves to the same handle across restarts, while the handle still exposes no repository paths. Identity paths are normalized first, so a symlinked or relative root cannot produce a second handle for the same scope. Also: - PrCheckoutService removes the worktree it created when the checkout step fails by timeout, interrupt or launch failure, not only on a non-zero exit -- all of those happen after step 1 and stranded the worktree. - The checkout gate shows progress for "Read the patch only" too; reading a patch is a network call and the click has to visibly do something. - `ReviewScopeRegistry::normalized` did not resolve (the helper lives on the nested Identity record); corrected to Identity::normalized. 911 tests pass. Verified in the app: 5 queue items, 26 hunk cards, 62 folded runs, no exceptions.
Scope identity included base and head. Findings and verdicts are keyed by (scopeId, ...), so anything that moved the base re-derived every worktree's handle -- and that worktree's review vanished from the UI while sitting untouched in the store under the old handle. Nothing on screen said so; the queue simply showed a fresh handle with no findings against it. The trigger was not exotic. The base was the main checkout's CURRENT branch, so `git switch` in another terminal did it. So did checking out a pull request. A reviewer part-way through `feat/sidebar-resize` -- three findings, one blocking, two intents approved -- would come back to an empty margin and a renumbered intent list, and the surviving line-key anchors would resolve against a diff computed from a different base, pinning "this event filter leaks" to whatever now sits at that line. Two fixes, because there were two halves to it: - Scope identity is now a PLACE, plus which pull request it is: kind, repo root, worktree, PR number. Base and head are properties of a review, not what makes it that review -- a commit inside a worktree moves its head, and that is not a different review. - The base is the repository's default branch (origin/HEAD, then the conventional names, then the current branch as a last resort) instead of whatever happens to be checked out, so the diff itself stops moving too. Found while doing this: GitStatusService.runLines throws on any non-zero exit, and `symbolic-ref --quiet refs/remotes/origin/HEAD` exits 1 whenever origin/HEAD is absent -- normal for a local-only repository. That turned a routine "no default branch" into a failed scan, which also flipped the scan-completeness flag and silently disabled scope revocation. Probes where "not there" is a legitimate answer now use a variant that tolerates it. Both fixes are mutation-checked: restoring base/head to the identity fails the two registry tests, and restoring the current-branch base fails the two queue tests, and nothing else. 911 tests pass. Verified in the app: 5 queue items, no exceptions. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Fixed: a branch switch orphaned every in-progress reviewFollow-up on the
What a reviewer would have experienced: part-way through Two fixes, for the two halves
Found on the way
VerificationFour regression tests, mutation-checked: putting base/head back into the identity fails the two registry tests, restoring the current-branch base fails the two queue tests, and nothing else fails either way. 911 tests, 0 failures. Verified in the app — 5 queue items, no exceptions.
|
Follow-up to the base change, and a regression it introduced. origin/HEAD names a REMOTE default. `git clone -b feat/x`, and deleting a local main after a merge, both leave origin/HEAD -> origin/main with no local branch of that name -- so handing the bare name to the diff failed every worktree item with "fatal: ambiguous argument 'main...HEAD': unknown revision". The previous behaviour could not hit this, because the main checkout's current branch always resolves. Verified against a real clone before fixing. defaultBranch now returns the local branch when there is one and the remote-tracking ref otherwise, and the conventional-name fallbacks are only offered when the ref exists. Testing: a new case builds the exact repository shape (clone, switch to a feature branch, delete local main) and asserts the base is origin/main. Both that and a broader one run the REAL diff for every assembled queue item rather than only checking the base string -- a base is only useful if git can resolve it, and asserting on the name would not have caught this. Mutation- checked: dropping the resolvability check fails the new test alone. Also adds a diagnostic that walks every queue item and reports its diff, so the visual pass can prove each item resolves its base rather than only the selected one. Against this repository: 4 items diff correctly against base=main, and the fifth fails for an unrelated pre-existing reason -- a worktree whose .git file points into another repository's missing admin directory, so `git -C` there fails before any base is considered. 913 tests pass. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Checked: the base change did not break the other items' diffs — but it had introduced a different regressionVerified by running the real diff for every queue item, not by asserting on the base string. Against this repository: Four of five resolve correctly. The fifth is unrelated and pre-existing: that worktree's The regression the check did find
The old behaviour couldn't hit this, because the main checkout's current branch always resolves. I reproduced it against a real clone before fixing it.
Why the earlier tests missed itThey asserted on the base string ( 919 tests, 0 failures.
|
Adds -Dapp.drydock.diag.screenshot=<path>, which renders the scene graph to
a PNG from inside the process on the FX thread. macOS screen capture is
unavailable here ("could not create image from display"), so the FX layer had
no visual evidence at all -- every check so far proved data flowed, not that
the UI was right. An in-process snapshot is better than a screen capture
anyway: no OS permission, and it cannot accidentally photograph another
window. It uses java.desktop, already on the jlink module list, rather than
javafx.swing, which is not.
Looking at the result found two bugs immediately.
The verdict bar sat on "no intent" for every freshly opened item, so Approve,
Request change and Submit were all dead. Intents fall back to one-per-file
and are therefore derived FROM the diff, and the diff arrives on a background
thread: the bar rendered once against an empty diff, correctly concluded
there were no intents, and never re-rendered. Every existing test handed the
view a diff synchronously, so none of them could reach it.
ReviewIntentFallbackTest drives the real asynchronous path against a real
repository, and fails without the fix.
Diff rows were almost twice their intended height. Section 4.8's "row padding
8 / 6 / 4px" is the prototype's `ipad`, which it applies to the QUEUE and
INTENT cards -- code line height there is font-size x line-height. Applying
the padding to code rows as well gave 36px rows where the design wants 20px.
Verified against the prototype source rather than guessed.
Also fixes a latent flake this surfaced: aScopeWithNoChangesSaysSo waited for
any Message row, and "Diffing..." is one, so it raced the placeholder instead
of the result. It passed by timing luck until the machine was loaded.
Every test that asserts on a base string now also runs the real diff, for the
reason the previous commit gave: a base is only useful if git can resolve it.
920 tests pass.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Visual verification, and two bugs only looking could findmacOS screen capture is unavailable in this environment ( Looking at the first image found two bugs in about ten seconds. The verdict bar was dead on every freshly opened itemIt showed Intents fall back to one-per-file, so they are derived from the diff — and the diff arrives on a background thread. The bar rendered once against an empty diff, correctly concluded there were no intents, and never re-rendered. No existing test could have caught it: every one of them handed the view a diff synchronously. Diff rows were almost twice their intended height~36px where the design wants 20px. §4.8's "row padding 8 / 6 / 4px" is the prototype's Before → after, same window: rows went 33px → 20px (= 12.5 × 1.6, exactly the spec), and the bar went from AlsoA latent flake this surfaced: 920 tests, 0 failures.
|
Photographing the surface with every rail expanded found three more things that no test and no row count could have shown. The ◆n pins rendered correctly and were never visible. The source column had Hgrow, so it filled the row and pushed each pin to the right edge of the CONTENT -- which on a diff whose widest line is hundreds of characters is far outside the viewport. They now sit immediately after the code. The design right-aligns them to the card edge, which a virtualized row as wide as the longest line in the whole diff cannot do; noted as a deviation. A pin whose finding is filtered out rendered as "◆0". The numbers are the margin's render order, so a finding it is not rendering has no number -- 0 was invented. It keeps a bare dimmed diamond now: the line still carries a finding, and a made-up number is a lie. Pins also had no background, so they sat directly on the add/delete row tint and were hard to pick out; they now carry the code background and a border. Two diagnostic flags make the surface photographable at all: windowSize, because the rails collapse at documented widths and showing them expanded means asking for a wide enough window; and seedFindings, which seeds representative findings anchored to lines genuinely in the diff, because the margin's severity pills, patch blocks and ASK chips cannot be seen -- or shown to a reviewer -- without findings to render. What the expanded shot confirms, first time any of it has been seen: queue rail with group headers and count badges, intent rail with numbered cards and risk heat bars, findings margin with severity pills, coloured pins, a patch block, ASK chips and per-card reply boxes, the `open` filter correctly hiding the resolved nit (4 threads, 3 open), and the verdict bar refusing approval with "a blocking finding is still open". 920 tests pass. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Every rail expanded — and three more bugs only the picture showedPhotographed at 1800×1000 with representative findings seeded, so the intent rail and findings margin are both expanded and populated. Two new diagnostic flags make that possible: What it confirms — first time any of this has actually been seen
The three bugsThe A filtered-out pin rendered as Pins had no background, so they sat directly on the add/delete row tint and were hard to pick out. They now carry the code background and a border. 920 tests, 0 failures.
|
Five conflicts, and one integration defect the merge surfaced. - ReviewView.java: modified on main, deleted here. Kept deleted -- the per-session diff tab is what this branch replaces. Main's change to it was part of its "stop the UI claiming Claude" pass, whose intent is carried into the new code instead (below). - ShortcutsOverlay: kept the sectioned table this branch introduced, adopting main's "Agent view" rename for the first sub-tab. - OpenSessionTab: kept both sides -- main's per-button shortcut hints and the agent-named sub-tab, and this branch's updateGeometryNow(), which restores the native surface's geometry after Review gives the centre back. Dropped main's showKeyHint for the Review sub-tab: there is no such button any more, and Cmd-4 is advertised in the overlay and on the sidebar row. - MainWorkspace / DrydockApplication: took main's agentRegistry wiring alongside this branch's Review wiring. The defect: McpToolRouter attributed every agent-written finding and answer to the literal string "Claude", which is precisely what main had just finished removing from the rest of the UI -- a Codex session's findings would have been signed with the wrong agent's name. The router now asks the context for the caller's reviewer name, and the context derives it through main's own AgentLabels rather than a second copy of that logic; AgentLabels is public for that reason. 924 tests pass. Verified in the merged app: 5 queue items, 21 intents, the findings margin and verdict bar rendering, no exceptions. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Two images, generated by the in-process snapshot hook rather than a screen capture: every rail expanded on a 75-file worktree review, and the same review narrowed past the section 4.9 collapse thresholds so the queue and intent rails fold to their icon strips while the findings margin and the verdict bar stay. Also adds -Dapp.drydock.diag.reviewItem=<n>, which selects a queue item by index. Without it the visual pass could only ever photograph whatever sorted first -- on a clean tree that is the working-tree item, whose diff is empty, which illustrates nothing.
They belong in the pull request, not in the repository -- 456 KB of PNG that main would carry forever to illustrate one review. The images move to an orphan branch that never merges; the pull request references them by commit-pinned raw URL. The -Dapp.drydock.diag.reviewItem flag added alongside them stays: it is what lets the visual pass photograph a chosen queue item rather than whichever sorts first.
Rebuilds Review as a global destination that spans repositories, per the design handoff.
Reviewing a 75-file worktree: queue rail, intent rail, diff column with folded unchanged runs and
◆npins, findings margin, and the verdict bar refusing approval while a blocking finding is open.Narrowed past the collapse thresholds (§4.9)
Queue and intents fold to their icon strips; the findings margin and the verdict bar stay. Collapsing a rail never removes an action.
Review is no longer a per-session sub-tab. The old shape gated Review off for remote repositories — but reviewing a remote PR is the primary use case — and a per-session tab cannot show a queue that spans repos at all.
OpenSessionTab.SubTabis nowCLAUDE · TERMINAL · EXPLORER, and⌘4navigates.8d77f06⌘4as navigation9d6bbcc29196c4◆npins, verdict bar, submit7831e946f32e2271bdeb450d1ac9–e5bfd4ca2a5739origin/mainThree things worth a close look
1. The handoff specifies a
ghflag that does not exist. It gives the checkout asgh pr checkout <n> --worktree. There is no such flag (checked against gh 2.96), andgh pr checkoutalways operates on the working tree it runs in — so following it literally would move the reviewer's main checkout onto the PR branch mid-task. drydock instead doesgit worktree add --detach <dir>and then runsgh pr checkout <n> --branch pr-<n>inside that worktree. The gate prints that sequence rather than the handoff's, because the reader may run it themselves.2. The MCP schema is deliberately narrowed in one place. §5 lists
review_verdict/review_resolve/review_submitunder "human-side writes". Exposing them as agent-callable tools would let an agent approve its own work and resolve its own findings, so they are the UI's actions against the store and agents observe them read-only throughreview_state. This is the one intentional departure from the schema's surface — flagged for a decision.3. The agent step timeline is not built. It needs per-step line attribution and a survives-to-HEAD calculation; both are real work, and a plausible-looking approximation would be worse than nothing when the panel's entire value is answering "which of the agent's edits actually made it" correctly.
promptHistoryreturns empty andtis unbound.Bugs found by testing and by looking, rather than by reading
(scopeId, id).⋯ N unchangedcould never appear. With git's default 3-line context no unchanged run is ever longer than the fold threshold, so the feature rendered correctly and never showed. Review asks for-U12.git switchin another terminal discarded every in-progress review.basewas part of scope identity, so anything moving it re-derived every worktree's handle and its findings vanished from the UI while sitting untouched in the store. Identity is now a place, plus which PR it is; the base is the repository's default branch.◆npins were never on screen. They rendered correctly, but the source column grew and pushed each pin to the right edge of the content — far outside the viewport on a wide diff.ipad, applied to the queue and intent cards, not to code lines.Keying
Findings, verdicts, threads and drafts are keyed by
(scopeId, id), never byidalone: finding ids are agent-chosen and repeat across scopes, so a reviewer re-run against two worktrees producesf_leak_1in both. The regression test is the first test inAnnotationStoreTest.review_replyrefuses an ambiguous id rather than guessing. Annotations written before scope handles existed are migrated rather than dropped.Testing
924 tests, 0 failures, 0 skipped (from 828 before this branch).
This adds a headless JavaFX harness the repository did not have — TestFX + Monocle, so view tests show a real
Stage, apply the real stylesheets and drive real key events with no window server, and run identically on themacos-14CI runner. Mutation-checked to confirm it is not vacuous.It also adds in-process visual verification:
-Dapp.drydock.diag.screenshot=<path>renders the scene graph to a PNG on the FX thread, needing no screen-recording permission. The images above came from it. Three of the bugs listed were invisible to the test suite by construction, because they were about what reaches the screen.Theme and font integration is enforced by
ThemeTokenContractTest: both theme sheets define identical token names,app.cssreferences no undefined token, and every font size is apxliteral so the interface-size slider scales it.Merge with main
a2a5739mergesorigin/main. Five conflicts, plus one integration defect it surfaced:McpToolRoutersigned every agent-written finding with the literal string"Claude"— precisely what main had just finished removing from the rest of the UI, and wrong for a Codex session. It now derives the name through main's ownAgentLabels.Review notes
docs/ui-redesign.mdcarries the new structure plus ~45 known deviations grouped by milestone, and flags the five open decisions in §10 of the handoff rather than inventing answers.Not implemented, and flagged: MCP notifications (§7) need a server-initiated channel
McpServerdoes not have — agents pollreview_state; and theSTACKqueue group is modelled but never produced, since stacked PRs are open decision §10.4.🤖 Generated with Claude Code