Skip to content

Commit 63cd9f6

Browse files
committed
Add review file model and tests
1 parent 0434778 commit 63cd9f6

3 files changed

Lines changed: 173 additions & 1 deletion

File tree

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@
2828
"dev": "node scripts/dev-server.mjs",
2929
"postinstall": "node scripts/fix-node-pty-permissions.mjs",
3030
"start": "node dist/cli.js serve",
31-
"test": "tsx src/config.test.ts && tsx src/ui/card-types.test.ts && tsx src/ui/patch-display.test.ts && tsx src/apply-patch.test.ts && tsx src/process-platform.test.ts && tsx src/process-sessions.test.ts && tsx src/local-agent-runtime.test.ts && tsx src/local-agent-adapters.test.ts && tsx src/local-agent-availability.test.ts && tsx src/local-agent-profiles.test.ts && tsx src/local-agent-targets.test.ts && tsx src/local-agent-store.test.ts && tsx src/roots.test.ts && tsx src/skills.test.ts && tsx src/workspaces.test.ts && tsx src/review-checkpoints.test.ts && tsx src/oauth-store.test.ts && tsx src/cli.test.ts",
31+
"test": "tsx src/config.test.ts && tsx src/ui/card-types.test.ts && tsx src/ui/patch-display.test.ts && tsx src/ui/review-model.test.ts && tsx src/apply-patch.test.ts && tsx src/process-platform.test.ts && tsx src/process-sessions.test.ts && tsx src/local-agent-runtime.test.ts && tsx src/local-agent-adapters.test.ts && tsx src/local-agent-availability.test.ts && tsx src/local-agent-profiles.test.ts && tsx src/local-agent-targets.test.ts && tsx src/local-agent-store.test.ts && tsx src/roots.test.ts && tsx src/skills.test.ts && tsx src/workspaces.test.ts && tsx src/review-checkpoints.test.ts && tsx src/oauth-store.test.ts && tsx src/cli.test.ts",
3232
"typecheck": "tsc -p tsconfig.json --noEmit"
3333
},
3434
"keywords": [],

src/ui/review-model.test.ts

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import assert from "node:assert/strict";
2+
import {
3+
buildReviewFileEntries,
4+
initialReviewPath,
5+
reviewFileStatus,
6+
} from "./review-model.js";
7+
8+
const patch = [
9+
"diff --git a/src/old.ts b/src/new.ts",
10+
"similarity index 80%",
11+
"rename from src/old.ts",
12+
"rename to src/new.ts",
13+
"index 1111111..2222222 100644",
14+
"--- a/src/old.ts",
15+
"+++ b/src/new.ts",
16+
"@@ -1 +1 @@",
17+
"-old value",
18+
"+new value",
19+
"diff --git a/README.md b/README.md",
20+
"index 3333333..4444444 100644",
21+
"--- a/README.md",
22+
"+++ b/README.md",
23+
"@@ -1 +1,2 @@",
24+
" title",
25+
"+details",
26+
"",
27+
].join("\n");
28+
29+
const entries = buildReviewFileEntries({
30+
tool: "show_changes",
31+
files: [
32+
{
33+
path: "src/new.ts",
34+
previousPath: "src/old.ts",
35+
type: "rename-changed",
36+
additions: 1,
37+
removals: 1,
38+
},
39+
],
40+
payload: { patch },
41+
});
42+
43+
assert.equal(entries.length, 2);
44+
assert.deepEqual(
45+
entries.map(({ path, previousPath, additions, removals, status }) => ({
46+
path,
47+
previousPath,
48+
additions,
49+
removals,
50+
status,
51+
})),
52+
[
53+
{
54+
path: "src/new.ts",
55+
previousPath: "src/old.ts",
56+
additions: 1,
57+
removals: 1,
58+
status: "renamed",
59+
},
60+
{
61+
path: "README.md",
62+
previousPath: undefined,
63+
additions: 1,
64+
removals: 0,
65+
status: "modified",
66+
},
67+
],
68+
);
69+
assert.ok(entries.every((entry) => entry.fileDiff));
70+
71+
assert.equal(reviewFileStatus("new"), "added");
72+
assert.equal(reviewFileStatus("deleted"), "deleted");
73+
assert.equal(reviewFileStatus("rename-pure"), "renamed");
74+
assert.equal(reviewFileStatus("change"), "modified");
75+
76+
assert.equal(initialReviewPath(entries), "src/new.ts");
77+
assert.equal(initialReviewPath(entries, "README.md"), "README.md");
78+
assert.equal(initialReviewPath(entries, "missing.ts"), "src/new.ts");
79+
assert.equal(initialReviewPath([]), undefined);

src/ui/review-model.ts

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
import { parsePatchFiles, type FileDiffMetadata } from "@pierre/diffs";
2+
import type { ToolResultCard } from "./card-types.js";
3+
4+
export type ReviewFileStatus =
5+
| "added"
6+
| "deleted"
7+
| "modified"
8+
| "renamed";
9+
10+
export interface ReviewFileEntry {
11+
path: string;
12+
previousPath?: string;
13+
type?: string;
14+
additions: number;
15+
removals: number;
16+
status: ReviewFileStatus;
17+
fileDiff?: FileDiffMetadata;
18+
}
19+
20+
export function buildReviewFileEntries(card: ToolResultCard): ReviewFileEntry[] {
21+
const parsedFiles = parseReviewPatch(card.payload?.patch);
22+
const parsedByPath = new Map(parsedFiles.map((fileDiff) => [fileDiff.name, fileDiff]));
23+
const entries: ReviewFileEntry[] = [];
24+
25+
for (const file of card.files ?? []) {
26+
if (!file.path) continue;
27+
28+
const fileDiff = parsedByPath.get(file.path);
29+
const stats = fileDiff ? reviewFileStats(fileDiff) : undefined;
30+
entries.push({
31+
path: file.path,
32+
previousPath: file.previousPath ?? fileDiff?.prevName,
33+
type: file.type,
34+
additions: file.additions ?? stats?.additions ?? 0,
35+
removals: file.removals ?? stats?.removals ?? 0,
36+
status: reviewFileStatus(file.type),
37+
fileDiff,
38+
});
39+
parsedByPath.delete(file.path);
40+
}
41+
42+
for (const fileDiff of parsedFiles) {
43+
if (!parsedByPath.has(fileDiff.name)) continue;
44+
45+
const stats = reviewFileStats(fileDiff);
46+
entries.push({
47+
path: fileDiff.name,
48+
previousPath: fileDiff.prevName,
49+
additions: stats.additions,
50+
removals: stats.removals,
51+
status: fileDiff.prevName && fileDiff.prevName !== fileDiff.name
52+
? "renamed"
53+
: "modified",
54+
fileDiff,
55+
});
56+
}
57+
58+
return entries;
59+
}
60+
61+
export function parseReviewPatch(patch: string | undefined): FileDiffMetadata[] {
62+
if (!patch) return [];
63+
return parsePatchFiles(patch, "review", true).flatMap((parsedPatch) => parsedPatch.files);
64+
}
65+
66+
export function reviewFileStats(
67+
fileDiff: FileDiffMetadata,
68+
): { additions: number; removals: number } {
69+
return fileDiff.hunks.reduce(
70+
(stats, hunk) => ({
71+
additions: stats.additions + hunk.additionLines,
72+
removals: stats.removals + hunk.deletionLines,
73+
}),
74+
{ additions: 0, removals: 0 },
75+
);
76+
}
77+
78+
export function reviewFileStatus(type: string | undefined): ReviewFileStatus {
79+
if (type === "new") return "added";
80+
if (type === "deleted") return "deleted";
81+
if (type === "rename-pure" || type === "rename-changed") return "renamed";
82+
return "modified";
83+
}
84+
85+
export function initialReviewPath(
86+
entries: ReviewFileEntry[],
87+
selectedPath?: string,
88+
): string | undefined {
89+
if (selectedPath && entries.some((entry) => entry.path === selectedPath)) {
90+
return selectedPath;
91+
}
92+
return entries[0]?.path;
93+
}

0 commit comments

Comments
 (0)