Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .changeset/quiet-row-facade.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
---
---
2 changes: 2 additions & 0 deletions .changeset/tidy-mounted-rows.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
---
---
4 changes: 2 additions & 2 deletions docs/browser-review-seam-audit.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
11 changes: 7 additions & 4 deletions docs/extension-architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -128,10 +128,13 @@ clusters, with context and gap lines sharing one range list under both side
keys — and the one span transform that repaints backgrounds without changing
text. `src/ui/diff/rowStyle.ts` resolves tones against the actual line
background with the word-diff minimum-contrast guarantee.
`src/ui/diff/renderRows.tsx` applies the transform per rendered cell, 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.
`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.
`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
Expand Down
2 changes: 1 addition & 1 deletion src/opentui/HunkDiffBody.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
3 changes: 2 additions & 1 deletion src/ui/components/panes/DiffPane.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@ import {
} from "../../../core/review/state";
import type { FileSourceStatus } from "../../diff/expandCollapsedRows";
import type { ActiveAddNoteAffordance } from "../../diff/DiffSectionBody";
import { isNestedRowMouseAction, type CursorHighlight } from "../../diff/renderRows";
import type { CursorHighlight } from "../../diff/cursorHighlight";
import { isNestedRowMouseAction } from "../../diff/rowMouseActions";
import type { DraftReviewNote } from "../../lib/reviewNoteMapping";
import {
createVisibleAgentNote,
Expand Down
2 changes: 1 addition & 1 deletion src/ui/components/panes/DiffSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
2 changes: 1 addition & 1 deletion src/ui/components/panes/FileView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
2 changes: 1 addition & 1 deletion src/ui/components/ui-components.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
2 changes: 1 addition & 1 deletion src/ui/diff/CodeCellView.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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. */
Expand Down
71 changes: 71 additions & 0 deletions src/ui/diff/CodeRowView.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { expect, test } from "bun:test";
Comment thread
benvinegar marked this conversation as resolved.
import { testRender } from "@opentui/react/test-utils";
import { act } from "react";
import { capturedTestColorToHex } from "../../../test/helpers/test-color-helpers";
import { resolveTheme } from "../themes";
import { CodeRowView, type PlannedCodeReviewRow } from "./CodeRowView";
import { cursorLineHighlightBg, selectionHighlightBg, stackCellPalette } from "./rowStyle";

/** Return the normalized background painted behind matching captured text. */
function backgroundForText(
capture: ReturnType<Awaited<ReturnType<typeof testRender>>["captureSpans"]>,
text: string,
) {
const span = capture.lines
.flatMap((line) => line.spans)
.find((candidate) => candidate.text.includes(text));
return capturedTestColorToHex(span?.bg)?.toLowerCase();
}

test("CodeRowView gives copy selection precedence over cursor paint", async () => {
const theme = resolveTheme("github-dark-default", null);
const plannedRow: PlannedCodeReviewRow = {
kind: "diff-row",
key: "diff-row:precedence",
stableKey: "line:0:new:1",
fileId: "paint",
hunkIndex: 0,
row: {
type: "stack-line",
key: "precedence",
fileId: "paint",
hunkIndex: 0,
cell: {
kind: "addition",
sign: "+",
newLineNumber: 1,
spans: [{ text: "selected" }],
},
},
};
const setup = await testRender(
<CodeRowView
plannedRow={plannedRow}
width={16}
lineNumberDigits={1}
showLineNumbers={false}
wrapLines={false}
codeHorizontalOffset={0}
theme={theme}
selected={false}
copySelectedRowRange={{ startCol: 0, endCol: Number.MAX_SAFE_INTEGER }}
cursorHighlight={{ stableKey: plannedRow.stableKey, side: "new", style: "row" }}
/>,
{ width: 20, height: 2 },
);

try {
await act(async () => {
await setup.renderOnce();
});
const background = backgroundForText(setup.captureSpans(), "selected");
const baseBackground = stackCellPalette("addition", theme).contentBg;

expect(background).toBe(selectionHighlightBg(baseBackground, theme).toLowerCase());
expect(background).not.toBe(cursorLineHighlightBg(baseBackground, theme).toLowerCase());
} finally {
await act(async () => {
setup.renderer.destroy();
});
}
});
Loading
Loading