refactor(review): extract a renderer-neutral review store into core (Phase 1 PR 1) - #716
Conversation
Sharing a review with a second surface needs one semantic model of a live review — selection, reveal intent, filter, note visibility, notes, drafts, and gap expansion — instead of each surface keeping its own copy and drifting. This lands that model in src/core/review as state, actions, reducer, store, selectors, and intents, where the source-boundary suite keeps it free of renderers and platform runtimes. The split follows the seam: the reducer is the one place state changes, the selectors are the one place it is interpreted, and intents are the one place a request is validated and lowered into actions. Rules that were previously inline conditionals get names and documentation — reveal requests, stale-note visibility, single-line anchoring, bulk-clear scope, content retirement, and the empty-note-body rule the terminal and agent surfaces answer differently while agreeing on what blank means. Scope is only what the terminal already has today: the document carries file identity and hunk counts, so hunk geometry, note anchoring over parsed ranges, navigation intents, and generations arrive with the phases that need them. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018L6h5GBz6RAxRXbgUS4mx4
useReviewController owned the review's semantic state in a dozen useState hooks, plus mirroring refs that existed only so a callback could read state its own setter had not committed yet. That made the terminal the owner of a model three more consumers are about to need. It now projects the shared store instead: the store answers synchronously, so the mirroring refs and the saved-draft guard go away, and note visibility moves out of App state into the store beside the notes it governs. Behavior is unchanged by design. The controller's public API is the same, the existing hook and pane suites pass untouched, and the store is observed through React state rather than useSyncExternalStore so a held key still drains as one burst against one committed snapshot instead of re-entering key routing per press. src/ui/lib/reviewProjection.ts is the one boundary between the semantic model and the terminal's diff-file model: document projection, note and draft adaptation both ways, and the transitional file key and source identity that the document-projection phase replaces with content-derived values. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018L6h5GBz6RAxRXbgUS4mx4
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
Greptile SummaryThis PR extracts semantic review state into a renderer-neutral core store and adapts the terminal review controller to project between that store and DiffFile values.
Confidence Score: 4/5The PR appears safe to merge, with only a non-blocking formatting inconsistency against the mounted repository guidance. The store extraction preserves the investigated navigation, reload, and note-projection behavior, while the remaining accepted concern is limited to code-format consistency. Files Needing Attention: src/core/review/actions.ts and the other newly added review-store modules Important Files Changed
Flowchart%%{init: {'theme': 'neutral'}}%%
flowchart LR
Terminal["App / useReviewController"] --> Projection["reviewProjection"]
Projection --> Store["ReviewStore"]
Store --> Reducer["reduceReviewState"]
Intents["plan/applyReviewIntent"] --> Store
Reducer --> State["ReviewState"]
State --> Projection
Projection --> Panes["DiffPane and session bridge"]
Prompt To Fix All With AI### Issue 1
src/core/review/actions.ts:13
**Align new review-store formatting**
The newly added review-store modules use two-space indentation and double-quoted strings, conflicting with the repository guidance requiring four spaces and single quotes. Applying the repository convention across this new subsystem will avoid ongoing formatting churn and inconsistent examples.
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.Reviews (1): Last reviewed commit: "refactor(ui): move terminal review state..." | Re-trigger Greptile |
| ReviewDraftNote, | ||
| ReviewRevealRequest, | ||
| ReviewSourceStatus, | ||
| ReviewStoredNote, |
There was a problem hiding this comment.
Align new review-store formatting
The newly added review-store modules use two-space indentation and double-quoted strings, conflicting with the repository guidance requiring four spaces and single quotes. Applying the repository convention across this new subsystem will avoid ongoing formatting churn and inconsistent examples.
Context Used: guidelines.mdc Cursor rule (source)
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/core/review/actions.ts
Line: 13
Comment:
**Align new review-store formatting**
The newly added review-store modules use two-space indentation and double-quoted strings, conflicting with the repository guidance requiring four spaces and single quotes. Applying the repository convention across this new subsystem will avoid ongoing formatting churn and inconsistent examples.
**Context Used:** guidelines.mdc Cursor rule ([source](https://github.com/modem-dev/modem/blob/main/.cursor/rules/guidelines.mdc))
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
…ter a rule Rewrite the review store's module headers to lead with what each module does, and add the register to AGENTS.md: first sentence names the behavior, invariants follow, no passive or self-important framing. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018L6h5GBz6RAxRXbgUS4mx4
Phase 1 PR 1 of the browser-review rebuild (
docs/browser-review-rebuild.md). Stacked on the Phase 0 guardrails branch.What
Extracts a renderer-neutral review store into
src/core/review/—types / state / actions / reducer / store / selectors / intents, each with colocated tests — and refactors the terminal (useReviewController,App,DiffPanetype import) onto it. Behavior-neutral: no user-visible change, empty changeset.ReviewStateplus the value policies that read it (isRenderableStoredReviewNote,reviewLineAnchor,reviewNoteOwnerHunkIndex,reviewNoteAnchorLine,ReviewRevealRequest) — one named answer per policy instead of inline conditionals per consumer.getSnapshot/subscribe/dispatch).planReviewIntent/applyReviewIntent) with a typed outcome map and planning errors.src/ui/lib/reviewProjection.tsowns projection between the terminal'sDiffFilemodel and the semantic store (UserReviewNote/DraftReviewNotemoved here).useReviewControllerdrops from 14useStatehooks to 3; the 3 mirroring refs are deleted;showAgentNotesmoves fromAppstate into shared review state (G1 classification: shared).Implemented fresh against the plan and audit — the prototype branch was used as a semantic reference only; deliberate design divergences are documented in the plan's terms (named reveal requests instead of flag fields, reuse of existing
ReviewNoteSource, trimmed store surface, no dead exports).What changed, in diagrams
State ownership: before
Fourteen
useStatehooks in one controller, three refs mirroring state for synchronous reads, one review-relevant flag owned byApp, and follow-up effects reconciling it all after the fact:flowchart TB subgraph app["App.tsx"] SAN["showAgentNotes"] end subgraph urc["useReviewController — 14 useState hooks"] SEL["selectedFileId · selectedHunkIndex"] FIL["filter"] REV["fileTopAlignRequestId · hunkRevealRequestId · scrollToNote"] NOTES["liveCommentsByFileId · userNotesByFileId · draftNote"] EXP["expandedGapsByFileId"] SRC["sourceStatusByFileId"] SNAP["filesSnapshot"] CUR["lineCursor · lineCursorRevealRequestId"] end subgraph refs["Mirroring refs (sync reads)"] R1["sourceStatusRef"] R2["expandedGapsRef"] R3["savedDraftIdRef"] end SRC -. "mirrored each render" .-> R1 EXP -. "mirrored each render" .-> R2 NOTES -. "mirrored on save" .-> R3 SNAP -. "follow-up effect clamps" .-> SEL FIL -. "follow-up effect re-selects" .-> SELState ownership: after
One semantic
ReviewStatebehind a three-method store; the controller keeps only the store snapshot plus the two renderer-measured cursor values (terminal row geometry stays with the renderer that measures it, per the audit's do-not-unify list):flowchart TB subgraph core["src/core/review — renderer-free, boundary-gated"] STATE["ReviewState<br/>selection · filter · reveal · notes<br/>expansion · source status · showAgentNotes"] STORE["store: getSnapshot / subscribe / dispatch"] STATE --- STORE end subgraph urc2["useReviewController — 3 useState hooks"] SNAP2["snapshot (subscribed)"] CUR2["lineCursor · lineCursorRevealRequestId<br/>(renderer-measured, stays terminal-side)"] end subgraph app2["App.tsx"] VIEW["view preferences (per-client)"] end STORE -- "subscribe → one snapshot" --> SNAP2 SNAP2 --> app2How a change flows now
Validation and clamping happen in the intent planner before the state changes, replacing scattered
setXcalls plus after-the-fact reconciliation effects:sequenceDiagram participant H as key/mouse handler participant C as useReviewController participant I as intents (plan + apply) participant S as store participant R as reducer H->>C: select hunk / edit note / set filter C->>I: ReviewIntent Note over I: planReviewIntent —<br/>validate file, clamp hunk,<br/>typed planning errors I->>S: dispatch(ReviewAction) S->>R: reduce(state, action) R-->>S: next ReviewState (identity-stable no-ops) S-->>C: notify → single snapshot renderModule seam
flowchart LR subgraph core3["src/core/review (imports: core-internal + @pierre/diffs only)"] T3["types"] --> ST3["state"] ST3 --> A3["actions"] --> RD3["reducer"] --> STO3["store"] ST3 --> SE3["selectors"] ST3 --> IN3["intents"] end subgraph ui3["src/ui (terminal renderer)"] RP3["lib/reviewProjection.ts<br/>DiffFile ⇄ semantic adapter"] URC3["hooks/useReviewController"] APP3["App.tsx"] DP3["DiffPane"] end URC3 --> STO3 URC3 --> IN3 RP3 --> T3 APP3 --> URC3 DP3 --> RP3 GATE(["scripts/source-boundaries.test.ts<br/>gates every arrow crossing the seam"]) -.- core3Scope boundaries
Document projection, geometry primitives, and the conformance harness are Phase 1 PR 2; navigation intents and the command catalog are PR 3. Trimmed prototype surface (generations,
expandedLineProof, filter selectors, additional note intents) is tracked per-phase in the plan. Nothing deleted yet → no tombstone entries.Gates
bun run typecheckclean;bun run lintclean (deny-warnings); formatted.bun test: 2311 pass / 4 fail — all four failures reproduce on the untouched base branch (missing@axe-core/playwrightdev dep ×2, one pre-existingui-componentsfailure, one known-flaky PTY test); branch failures are a strict subset of baseline failures.bun run test:integration(PTY): 105/106, sole failure is the pre-existing flake, reproduced on baseline.src/core/review/(contained in core,@pierre/diffs-only externals, node-debt map untouched).test/pty/,test/cli/,test/session/modified;useReviewController.test.tsx(38 tests) andAppHost.*suites pass unedited.Repays (partial, per the spanning rule): D2 core+terminal sites (
isBlankReviewNoteBody; fixture + conformance registration land with PR 2's harness).