diff --git a/.changeset/quiet-row-facade.md b/.changeset/quiet-row-facade.md new file mode 100644 index 000000000..a845151cc --- /dev/null +++ b/.changeset/quiet-row-facade.md @@ -0,0 +1,2 @@ +--- +--- diff --git a/docs/browser-review-seam-audit.md b/docs/browser-review-seam-audit.md index ba63fdd05..c490c38c8 100644 --- a/docs/browser-review-seam-audit.md +++ b/docs/browser-review-seam-audit.md @@ -109,13 +109,13 @@ whether a client needs more than that is Phase 5's first question. _Repaid (Phase 1 PR 2, model side)_: `splitLineCount`/`unifiedLineCount` carried on `ReviewFileV1` by `core/review/document.ts`; the browser consumes them in Phase 5. - **A8. Empty-diff explanation — 3 variants with different precedence.** Terminal - `renderRows.tsx` `diffMessage` (rename-pure first), web `ReviewStream.tsx` (binary first), + `plannedRowText.ts` `diffMessage` (rename-pure first), web `ReviewStream.tsx` (binary first), `staticDiffPager.ts` (extra cases). Same file can explain itself differently per client. Fix: `reviewEmptyDiffReason(file)` in core. _Repaid (Phase 1 PR 2)_: `reviewEmptyDiffReason` in `core/review/document.ts` with one canonical precedence — what the change _is_ outranks how it is stored (`rename-only` -> `binary` -> `too-large` -> `new-file` -> `deleted-file` -> `no-hunks`), the review stream's existing order. - `renderRows.tsx` and `staticDiffPager.ts` keep their own wording and share the reason; fixture + `plannedRowText.ts` and `staticDiffPager.ts` keep their own wording and share the reason; fixture `binary-rename-with-no-rows`. The static pager's own order put storage first, so a renamed binary or oversized rename now reports as a rename there too. - **A9. STML tag vocabulary — parse shared, tag semantics forked.** Terminal diff --git a/docs/extension-architecture.md b/docs/extension-architecture.md index 03d45f17c..571d89a16 100644 --- a/docs/extension-architecture.md +++ b/docs/extension-architecture.md @@ -130,8 +130,11 @@ text. `src/ui/diff/rowStyle.ts` resolves tones against the actual line background with the word-diff minimum-contrast guarantee. `src/ui/diff/CodeRowView.tsx` applies the transform through the cell painter, which keeps highlights out of `buildDiffSectionRowPlan`, its caches, and every -geometry measurement: a highlight change is a repaint, never a re-plan. The -static pager never runs extension code, so highlights are interactive-only. +geometry measurement: a highlight change is a repaint, never a re-plan. +`src/ui/diff/DiffRowView.tsx` remains only the memoized dispatch facade; raw-row +adaptation there supports the public OpenTUI and extension current-line surfaces, while +`src/ui/diff/cursorHighlight.ts` owns stable-key cursor matching. The static pager never +runs extension code, so highlights are interactive-only. Agent attention marks (`hunk session highlight add` / `clear`) join this same pipeline rather than growing a second one: `useTerminalReview.ts` validates diff --git a/src/opentui/HunkDiffBody.tsx b/src/opentui/HunkDiffBody.tsx index d500d6a88..90e38defa 100644 --- a/src/opentui/HunkDiffBody.tsx +++ b/src/opentui/HunkDiffBody.tsx @@ -3,7 +3,7 @@ import { DEFAULT_TAB_WIDTH } from "../core/run/tabWidth"; import { findMaxLineNumber } from "../ui/diff/codeColumns"; import { buildSplitRows, buildStackRows } from "../ui/diff/diffRows"; import { diffMessage, fitText } from "../ui/diff/plannedRowText"; -import { DiffRowView } from "../ui/diff/renderRows"; +import { DiffRowView } from "../ui/diff/DiffRowView"; import { useHighlightedDiff } from "../ui/diff/useHighlightedDiff"; import { resolveTheme } from "../ui/themes"; import { toInternalDiffFile } from "./model"; diff --git a/src/ui/components/panes/DiffPane.tsx b/src/ui/components/panes/DiffPane.tsx index c47607985..ab9e39e6f 100644 --- a/src/ui/components/panes/DiffPane.tsx +++ b/src/ui/components/panes/DiffPane.tsx @@ -27,7 +27,7 @@ import { } from "../../../core/review/state"; import type { FileSourceStatus } from "../../diff/expandCollapsedRows"; import type { ActiveAddNoteAffordance } from "../../diff/DiffSectionBody"; -import type { CursorHighlight } from "../../diff/renderRows"; +import type { CursorHighlight } from "../../diff/cursorHighlight"; import { isNestedRowMouseAction } from "../../diff/rowMouseActions"; import type { DraftReviewNote } from "../../lib/reviewNoteMapping"; import { diff --git a/src/ui/components/panes/DiffSection.tsx b/src/ui/components/panes/DiffSection.tsx index 3302365a5..8d96afaa7 100644 --- a/src/ui/components/panes/DiffSection.tsx +++ b/src/ui/components/panes/DiffSection.tsx @@ -4,7 +4,7 @@ import type { LayoutMode } from "../../../core/run/commandInputs"; import type { UserNoteLineTarget } from "../../../core/liveComments"; import type { FileSourceStatus } from "../../diff/expandCollapsedRows"; import { DiffSectionBody, type ActiveAddNoteAffordance } from "../../diff/DiffSectionBody"; -import type { CursorHighlight } from "../../diff/renderRows"; +import type { CursorHighlight } from "../../diff/cursorHighlight"; import type { VisibleBodyBounds } from "../../diff/rowWindowing"; import type { DiffSectionGeometry } from "../../diff/diffSectionGeometry"; import type { DiffSectionRowPlan } from "../../diff/diffSectionRowPlan"; diff --git a/src/ui/components/panes/FileView.tsx b/src/ui/components/panes/FileView.tsx index 39601e231..4548201a9 100644 --- a/src/ui/components/panes/FileView.tsx +++ b/src/ui/components/panes/FileView.tsx @@ -9,7 +9,7 @@ import type { } from "../../../extension-api/types"; import type { AppTheme } from "../../themes"; import type { DiffSectionGeometry } from "../../diff/diffSectionGeometry"; -import { plannedRowMatchesCursor, type CursorHighlight } from "../../diff/renderRows"; +import { plannedRowMatchesCursor, type CursorHighlight } from "../../diff/cursorHighlight"; import { cursorLineHighlightBg } from "../../diff/rowStyle"; import { resolveVisibleRowIndexWindow, type VisibleBodyBounds } from "../../diff/rowWindowing"; import { reviewRowId } from "../../lib/ids"; diff --git a/src/ui/components/ui-components.test.tsx b/src/ui/components/ui-components.test.tsx index 2c47cc137..54d694819 100644 --- a/src/ui/components/ui-components.test.tsx +++ b/src/ui/components/ui-components.test.tsx @@ -36,7 +36,7 @@ const { DiffFileHeaderRow } = await import("./panes/DiffFileHeaderRow"); const { DiffSectionBody } = await import("../diff/DiffSectionBody"); const { measurePlannedRenderedRowHeight, measureRenderedRowHeight } = await import("../diff/codeRowLayout"); -const { DiffRowView } = await import("../diff/renderRows"); +const { DiffRowView } = await import("../diff/DiffRowView"); function createTestDiffFile( id: string, diff --git a/src/ui/diff/CodeCellView.test.tsx b/src/ui/diff/CodeCellView.test.tsx index 7c570d6b9..49360ab32 100644 --- a/src/ui/diff/CodeCellView.test.tsx +++ b/src/ui/diff/CodeCellView.test.tsx @@ -11,7 +11,7 @@ import { import { legacyPlannedDiffRow, planCodeRowLayout } from "./codeRowLayout"; import type { DiffRow } from "./diffRows"; import { lineHighlightPaintKey, type LineHighlightPaintIndex } from "./lineHighlightPaint"; -import { DiffRowView } from "./renderRows"; +import { DiffRowView } from "./DiffRowView"; import { resolveTheme, withTransparentSurfaces } from "../themes"; /** Capture one code-row component and always release its OpenTUI renderer. */ diff --git a/src/ui/diff/CodeRowView.tsx b/src/ui/diff/CodeRowView.tsx index be0efcb4e..88c466349 100644 --- a/src/ui/diff/CodeRowView.tsx +++ b/src/ui/diff/CodeRowView.tsx @@ -1,6 +1,5 @@ /** Mounts split and stack code rows from the canonical code-row layout and paint plans. */ import type { UserNoteLineTarget } from "../../core/liveComments"; -import type { CursorLine } from "../../core/run/commandInputs"; import type { CopySelectedRowRange } from "../lib/diffSpatial"; import type { AppTheme } from "../themes"; import { CODE_ROW_ADD_NOTE_BADGE_TEXT, CODE_ROW_ADD_NOTE_BADGE_WIDTH } from "./codeRowAffordance"; @@ -10,6 +9,7 @@ import { type PlannedDiffReviewRow, } from "./codeRowLayout"; import { codeCellView, FULL_CODE_CELL_COL_RANGE, type CodeCellHighlight } from "./CodeCellView"; +import type { CursorHighlight } from "./cursorHighlight"; import type { DiffRow } from "./diffRows"; import type { LineHighlightPaintIndex } from "./lineHighlightPaint"; import { @@ -29,13 +29,6 @@ export type PlannedCodeReviewRow = Omit & { row: CodeDiffRow; }; -/** Cursor paint inputs consumed by mounted code rows. */ -export interface CodeRowCursorHighlight { - stableKey: string; - style: Exclude; - side: "old" | "new"; -} - export interface CodeRowViewProps { plannedRow: PlannedCodeReviewRow; width: number; @@ -47,7 +40,7 @@ export interface CodeRowViewProps { selected: boolean; copySelectedRowRange?: CopySelectedRowRange; copySelectedSide?: "left" | "right"; - cursorHighlight?: CodeRowCursorHighlight; + cursorHighlight?: CursorHighlight; lineHighlights?: LineHighlightPaintIndex; showAddNoteBadge?: boolean; onHoverRow?: (rowKey: string) => void; diff --git a/src/ui/diff/renderRows.tsx b/src/ui/diff/DiffRowView.tsx similarity index 87% rename from src/ui/diff/renderRows.tsx rename to src/ui/diff/DiffRowView.tsx index 02dd0e05f..1fb02a2bd 100644 --- a/src/ui/diff/renderRows.tsx +++ b/src/ui/diff/DiffRowView.tsx @@ -1,35 +1,15 @@ +/** Adapts and dispatches diff rows to their focused mounted row views. */ import { memo } from "react"; import type { UserNoteLineTarget } from "../../core/liveComments"; -import type { CursorLine } from "../../core/run/commandInputs"; import type { CopySelectedRowRange } from "../lib/diffSpatial"; import type { AppTheme } from "../themes"; import { CodeRowView, type PlannedCodeReviewRow } from "./CodeRowView"; import { legacyPlannedDiffRow, type PlannedDiffReviewRow } from "./codeRowLayout"; +import type { CursorHighlight } from "./cursorHighlight"; import { DiffMetaRowView, type PlannedDiffMetaReviewRow } from "./DiffMetaRowView"; import type { DiffRow } from "./diffRows"; import type { LineHighlightPaintIndex } from "./lineHighlightPaint"; -export { isNestedRowMouseAction, markNestedRowMouseAction } from "./rowMouseActions"; - -export interface CursorHighlight { - /** The render plan anchor of the row the cursor rests on, shared with reveal lookups. */ - stableKey: string; - style: Exclude; - /** Which half of a split row the cursor sits on, and where a note would anchor. */ - side: "old" | "new"; -} - -/** Report whether one planned row carries the anchor the cursor rests on. */ -export function plannedRowMatchesCursor( - row: { stableKey: string; stableAliasKeys?: readonly string[] }, - cursor: CursorHighlight | undefined, -) { - return ( - cursor !== undefined && - (row.stableKey === cursor.stableKey || row.stableAliasKeys?.includes(cursor.stableKey) === true) - ); -} - /** Dispatch one planned diff row to its focused metadata or code view. */ function renderRow( plannedRow: PlannedDiffReviewRow, @@ -95,7 +75,8 @@ function renderRow( ); } -interface DiffRowViewProps { +/** Inputs accepted by the memoized diff-row facade. */ +export interface DiffRowViewProps { /** Complete review-stream row; preferred when the caller owns the shared render plan. */ plannedRow?: PlannedDiffReviewRow; /** Raw row fallback for renderer-only surfaces outside the shared review stream. */ diff --git a/src/ui/diff/DiffSectionBody.tsx b/src/ui/diff/DiffSectionBody.tsx index 7528068f0..2aec54724 100644 --- a/src/ui/diff/DiffSectionBody.tsx +++ b/src/ui/diff/DiffSectionBody.tsx @@ -24,7 +24,8 @@ import { plannedReviewRowVisible } from "./reviewRowGeometry"; import { buildDiffSectionRowPlan, type DiffSectionRowPlan } from "./diffSectionRowPlan"; import { resolveVisiblePlannedRowWindow, type VisibleBodyBounds } from "./rowWindowing"; import { diffMessage, fitText } from "./plannedRowText"; -import { DiffRowView, plannedRowMatchesCursor, type CursorHighlight } from "./renderRows"; +import { DiffRowView } from "./DiffRowView"; +import { plannedRowMatchesCursor, type CursorHighlight } from "./cursorHighlight"; import { useHighlightedDiff } from "./useHighlightedDiff"; import { useHighlightedSource } from "./useHighlightedSource"; diff --git a/src/ui/diff/cursorHighlight.test.ts b/src/ui/diff/cursorHighlight.test.ts new file mode 100644 index 000000000..41e0b590e --- /dev/null +++ b/src/ui/diff/cursorHighlight.test.ts @@ -0,0 +1,25 @@ +import { describe, expect, test } from "bun:test"; +import { plannedRowMatchesCursor, type CursorHighlight } from "./cursorHighlight"; + +const CURSOR: CursorHighlight = { + stableKey: "line:new:2", + style: "row", + side: "new", +}; + +describe("plannedRowMatchesCursor", () => { + test("matches canonical and alias stable keys", () => { + expect(plannedRowMatchesCursor({ stableKey: CURSOR.stableKey }, CURSOR)).toBe(true); + expect( + plannedRowMatchesCursor( + { stableKey: "line:old:2", stableAliasKeys: [CURSOR.stableKey] }, + CURSOR, + ), + ).toBe(true); + }); + + test("rejects absent and unrelated cursors", () => { + expect(plannedRowMatchesCursor({ stableKey: "line:new:3" }, CURSOR)).toBe(false); + expect(plannedRowMatchesCursor({ stableKey: CURSOR.stableKey }, undefined)).toBe(false); + }); +}); diff --git a/src/ui/diff/cursorHighlight.ts b/src/ui/diff/cursorHighlight.ts new file mode 100644 index 000000000..bf9fc44a8 --- /dev/null +++ b/src/ui/diff/cursorHighlight.ts @@ -0,0 +1,22 @@ +/** Defines cursor paint inputs and matches them to stable planned-row identities. */ +import type { CursorLine } from "../../core/run/commandInputs"; + +/** Cursor paint inputs shared by review planning and mounted code rows. */ +export interface CursorHighlight { + /** The render plan anchor of the row the cursor rests on, shared with reveal lookups. */ + stableKey: string; + style: Exclude; + /** Which half of a split row the cursor sits on, and where a note would anchor. */ + side: "old" | "new"; +} + +/** Report whether one planned row carries the anchor the cursor rests on. */ +export function plannedRowMatchesCursor( + row: { stableKey: string; stableAliasKeys?: readonly string[] }, + cursor: CursorHighlight | undefined, +) { + return ( + cursor !== undefined && + (row.stableKey === cursor.stableKey || row.stableAliasKeys?.includes(cursor.stableKey) === true) + ); +} diff --git a/src/ui/lib/extensionCurrentLine.tsx b/src/ui/lib/extensionCurrentLine.tsx index c1bab0765..42bf4b10f 100644 --- a/src/ui/lib/extensionCurrentLine.tsx +++ b/src/ui/lib/extensionCurrentLine.tsx @@ -1,6 +1,6 @@ import type { ExtensionCurrentLinePaint } from "../../extension-api/types"; import type { DiffRow, SplitLineCell, StackLineCell } from "../diff/diffRows"; -import { DiffRowView } from "../diff/renderRows"; +import { DiffRowView } from "../diff/DiffRowView"; import type { DiffSectionRowPlan } from "../diff/diffSectionRowPlan"; import type { LineCursor } from "./lineCursors"; import type { AppTheme } from "../themes";