Add inline review comments to git diff pane (#530)
* Add inline git diff review comments
Adds a review draft store keyed per diff target, inline gutter + thread
rendering in the diff pane, and a generated review attachment that flows
through the composer and agent providers.
* Strip generated review attachments before saving to draft store
Drafts persist only UserComposerAttachment (no review kind), so filter the
auto-generated review attachment out when seeding the draft or restoring
after a failed auto-submit.
* Fix inline review typecheck and test harness drift
* Unslop inline review feature
- Fix stale-closure bug in composer queueMessage: clear drafts for the
actually-queued attachments, not the latest props
- Drop dead useEffect mirroring initialBody in inline review editor
(parent already remounts via key)
- Restore typed AgentAttachment signature on renderPromptAttachmentAsText;
delete schema redispatch helper and drop null-checks across 5 providers
- Inline single-use review prompt formatters and replace marker switch
with const map
- Drop normalizeComment field-copy redundancy after type guard
- Remove __reviewDraftStoreTestUtils export; test migration via
rehydrate() public path
- Refactor findTarget to return the target line directly (no non-null
assertion at call site)
- Drop redundant isGeneratedReviewAttachment helper; inline kind check
* Update review pill to withUnistyles after rebase
Main's unistyles refactor stripped useUnistyles in favor of withUnistyles +
StyleSheet. Convert ReviewAttachmentPill to use ThemedCircleDot + ICON_SIZE so
the icon picks up the foreground-muted mapping like the other pills.
* Fix lint and typecheck after rebase onto main
Resolve drift after main's withUnistyles refactor:
- Composer complexity: dedup voice?.isVoiceSwitching to drop to 20
- Move per-comment Pressable handlers in git-diff-inline-review into
CommentRow subcomponent so callbacks are stable
- Hoist style/onPress factories to module level (iconButton,
iconButtonDestructive, ghostButton)
- Memoize style arrays in git-diff-pane review wrappers
- Replace inline {} placeholders with useMemo styles
- review-draft-store: extract applyCommentUpdates to avoid map-spread,
include input.key in useMemo deps
- Test fixtures: hoist EMPTY_COMMENTS, add buildReviewActions helper
* Drop GitDiffPane default param to keep complexity at 20
After the rebase, main's `enabled = true` default param combined with the
inline review additions pushed cyclomatic complexity to 21. Drop the
default and resolve `enabled !== false` at the call site (comparison
doesn't count toward complexity).
* Reshape inline review behind @/review module
Drop the `generated: true` attachment flag and consolidate inline-review
logic behind a deep `@/review` module so composer and git-diff-pane stop
hand-composing review primitives. Workspace-scoped review snapshots are
now derived at submit/queue time rather than persisted in the draft
store. Composer review references drop from 51 to 4; git-diff-pane drops
~108 lines and now consumes a controller hook + slot components.
* Fix optimistic review attachments
This commit is contained in:
@@ -1,8 +1,18 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { buildSplitDiffRows, buildUnifiedDiffLines } from "./diff-layout";
|
||||
import {
|
||||
buildReviewableDiffTargetKey,
|
||||
buildSplitDiffRows,
|
||||
buildUnifiedDiffLines,
|
||||
} from "./diff-layout";
|
||||
import type { ParsedDiffFile } from "@/hooks/use-checkout-diff-query";
|
||||
import type { ReviewableDiffTarget } from "./diff-layout";
|
||||
|
||||
function makeFile(lines: ParsedDiffFile["hunks"][number]["lines"]): ParsedDiffFile {
|
||||
function makeFile(
|
||||
lines: ParsedDiffFile["hunks"][number]["lines"],
|
||||
options: { oldStart?: number; newStart?: number } = {},
|
||||
): ParsedDiffFile {
|
||||
const oldStart = options.oldStart ?? 10;
|
||||
const newStart = options.newStart ?? 10;
|
||||
return {
|
||||
path: "example.ts",
|
||||
isNew: false,
|
||||
@@ -12,9 +22,9 @@ function makeFile(lines: ParsedDiffFile["hunks"][number]["lines"]): ParsedDiffFi
|
||||
status: "ok",
|
||||
hunks: [
|
||||
{
|
||||
oldStart: 10,
|
||||
oldStart,
|
||||
oldCount: 4,
|
||||
newStart: 10,
|
||||
newStart,
|
||||
newCount: 5,
|
||||
lines,
|
||||
},
|
||||
@@ -22,7 +32,39 @@ function makeFile(lines: ParsedDiffFile["hunks"][number]["lines"]): ParsedDiffFi
|
||||
};
|
||||
}
|
||||
|
||||
function expectReviewTarget(
|
||||
target: ReviewableDiffTarget | null | undefined,
|
||||
expected: Omit<ReviewableDiffTarget, "key" | "filePath">,
|
||||
) {
|
||||
expect(target).toMatchObject({
|
||||
filePath: "example.ts",
|
||||
...expected,
|
||||
});
|
||||
expect(target?.key).toBe(
|
||||
buildReviewableDiffTargetKey({
|
||||
filePath: "example.ts",
|
||||
side: expected.side,
|
||||
lineNumber: expected.lineNumber,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
describe("buildSplitDiffRows", () => {
|
||||
it("uses one canonical persisted key for rendered review targets", () => {
|
||||
const rows = buildSplitDiffRows(
|
||||
makeFile([
|
||||
{ type: "header", content: "@@ -10,1 +20,1 @@" },
|
||||
{ type: "context", content: "same line" },
|
||||
]),
|
||||
);
|
||||
|
||||
if (rows[1]?.kind !== "pair") {
|
||||
throw new Error("Expected split pair row");
|
||||
}
|
||||
expect(rows[1].left?.reviewTarget?.key).toBe("example.ts:old:10");
|
||||
expect(rows[1].right?.reviewTarget?.key).toBe("example.ts:new:10");
|
||||
});
|
||||
|
||||
it("pairs replacement runs by index", () => {
|
||||
const rows = buildSplitDiffRows(
|
||||
makeFile([
|
||||
@@ -78,6 +120,86 @@ describe("buildSplitDiffRows", () => {
|
||||
right: { type: "context", content: "same line", lineNumber: 10 },
|
||||
});
|
||||
});
|
||||
|
||||
it("emits old and new review targets for split context cells", () => {
|
||||
const rows = buildSplitDiffRows(
|
||||
makeFile(
|
||||
[
|
||||
{ type: "header", content: "@@ -10,1 +20,1 @@" },
|
||||
{ type: "context", content: "same line" },
|
||||
],
|
||||
{ newStart: 20 },
|
||||
),
|
||||
);
|
||||
|
||||
expect(rows[0]).toEqual({
|
||||
kind: "header",
|
||||
content: "@@ -10,1 +20,1 @@",
|
||||
});
|
||||
expect(rows[1]).toMatchObject({
|
||||
kind: "pair",
|
||||
left: { type: "context", content: "same line", lineNumber: 10 },
|
||||
right: { type: "context", content: "same line", lineNumber: 20 },
|
||||
});
|
||||
if (rows[1]?.kind !== "pair") {
|
||||
throw new Error("Expected split pair row");
|
||||
}
|
||||
expectReviewTarget(rows[1].left?.reviewTarget, {
|
||||
hunkHeader: "@@ -10,1 +20,1 @@",
|
||||
hunkIndex: 0,
|
||||
lineIndex: 1,
|
||||
oldLineNumber: 10,
|
||||
newLineNumber: 20,
|
||||
side: "old",
|
||||
lineNumber: 10,
|
||||
lineType: "context",
|
||||
content: "same line",
|
||||
});
|
||||
expectReviewTarget(rows[1].right?.reviewTarget, {
|
||||
hunkHeader: "@@ -10,1 +20,1 @@",
|
||||
hunkIndex: 0,
|
||||
lineIndex: 1,
|
||||
oldLineNumber: 10,
|
||||
newLineNumber: 20,
|
||||
side: "new",
|
||||
lineNumber: 20,
|
||||
lineType: "context",
|
||||
content: "same line",
|
||||
});
|
||||
});
|
||||
|
||||
it("does not emit review targets for split headers or empty cells", () => {
|
||||
const rows = buildSplitDiffRows(
|
||||
makeFile([
|
||||
{ type: "header", content: "@@ -10,1 +10,2 @@" },
|
||||
{ type: "add", content: "after" },
|
||||
]),
|
||||
);
|
||||
|
||||
expect(rows[0]).toEqual({
|
||||
kind: "header",
|
||||
content: "@@ -10,1 +10,2 @@",
|
||||
});
|
||||
expect(rows[1]).toMatchObject({
|
||||
kind: "pair",
|
||||
left: null,
|
||||
right: { type: "add", content: "after", lineNumber: 10 },
|
||||
});
|
||||
if (rows[1]?.kind !== "pair") {
|
||||
throw new Error("Expected split pair row");
|
||||
}
|
||||
expectReviewTarget(rows[1].right?.reviewTarget, {
|
||||
hunkHeader: "@@ -10,1 +10,2 @@",
|
||||
hunkIndex: 0,
|
||||
lineIndex: 1,
|
||||
oldLineNumber: null,
|
||||
newLineNumber: 10,
|
||||
side: "new",
|
||||
lineNumber: 10,
|
||||
lineType: "add",
|
||||
content: "after",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("buildUnifiedDiffLines", () => {
|
||||
@@ -151,4 +273,53 @@ describe("buildUnifiedDiffLines", () => {
|
||||
expect(lines[5]?.lineNumber).toBe(166);
|
||||
expect(lines[6]?.lineNumber).toBe(167);
|
||||
});
|
||||
|
||||
it("emits canonical review targets for unified add, remove, and context lines", () => {
|
||||
const lines = buildUnifiedDiffLines(
|
||||
makeFile(
|
||||
[
|
||||
{ type: "header", content: "@@ -10,2 +20,2 @@" },
|
||||
{ type: "context", content: "before" },
|
||||
{ type: "remove", content: "removed" },
|
||||
{ type: "add", content: "inserted" },
|
||||
],
|
||||
{ newStart: 20 },
|
||||
),
|
||||
);
|
||||
|
||||
expect(lines[0]?.reviewTarget).toBeNull();
|
||||
expectReviewTarget(lines[1]?.reviewTarget, {
|
||||
hunkHeader: "@@ -10,2 +20,2 @@",
|
||||
hunkIndex: 0,
|
||||
lineIndex: 1,
|
||||
oldLineNumber: 10,
|
||||
newLineNumber: 20,
|
||||
side: "new",
|
||||
lineNumber: 20,
|
||||
lineType: "context",
|
||||
content: "before",
|
||||
});
|
||||
expectReviewTarget(lines[2]?.reviewTarget, {
|
||||
hunkHeader: "@@ -10,2 +20,2 @@",
|
||||
hunkIndex: 0,
|
||||
lineIndex: 2,
|
||||
oldLineNumber: 11,
|
||||
newLineNumber: null,
|
||||
side: "old",
|
||||
lineNumber: 11,
|
||||
lineType: "remove",
|
||||
content: "removed",
|
||||
});
|
||||
expectReviewTarget(lines[3]?.reviewTarget, {
|
||||
hunkHeader: "@@ -10,2 +20,2 @@",
|
||||
hunkIndex: 0,
|
||||
lineIndex: 3,
|
||||
oldLineNumber: null,
|
||||
newLineNumber: 21,
|
||||
side: "new",
|
||||
lineNumber: 21,
|
||||
lineType: "add",
|
||||
content: "inserted",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user