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
31 changes: 30 additions & 1 deletion bun.lock
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
"@ai-sdk/openai": "^2.0.52",
"@emotion/react": "^11.14.0",
"@emotion/styled": "^11.14.1",
"@types/react-syntax-highlighter": "^15.5.13",
"ai": "^5.0.72",
"ai-tokenizer": "^1.0.3",
"cmdk": "^1.0.0",
Expand All @@ -32,6 +33,7 @@
"rehype-sanitize": "^6.0.0",
"remark-gfm": "^4.0.1",
"remark-math": "^6.0.0",
"shiki": "^3.13.0",
"source-map-support": "^0.5.21",
"undici": "^7.16.0",
"write-file-atomic": "^6.0.0",
Expand All @@ -58,7 +60,6 @@
"@types/minimist": "^1.2.5",
"@types/react": "^18.2.0",
"@types/react-dom": "^18.2.0",
"@types/react-syntax-highlighter": "^15.5.13",
"@types/write-file-atomic": "^4.0.3",
"@typescript-eslint/eslint-plugin": "^8.44.1",
"@typescript-eslint/parser": "^8.44.1",
Expand Down Expand Up @@ -445,6 +446,20 @@

"@rollup/pluginutils": ["@rollup/pluginutils@5.3.0", "", { "dependencies": { "@types/estree": "^1.0.0", "estree-walker": "^2.0.2", "picomatch": "^4.0.2" }, "peerDependencies": { "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q=="],

"@shikijs/core": ["@shikijs/core@3.13.0", "", { "dependencies": { "@shikijs/types": "3.13.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-3P8rGsg2Eh2qIHekwuQjzWhKI4jV97PhvYjYUzGqjvJfqdQPz+nMlfWahU24GZAyW1FxFI1sYjyhfh5CoLmIUA=="],

"@shikijs/engine-javascript": ["@shikijs/engine-javascript@3.13.0", "", { "dependencies": { "@shikijs/types": "3.13.0", "@shikijs/vscode-textmate": "^10.0.2", "oniguruma-to-es": "^4.3.3" } }, "sha512-Ty7xv32XCp8u0eQt8rItpMs6rU9Ki6LJ1dQOW3V/56PKDcpvfHPnYFbsx5FFUP2Yim34m/UkazidamMNVR4vKg=="],

"@shikijs/engine-oniguruma": ["@shikijs/engine-oniguruma@3.13.0", "", { "dependencies": { "@shikijs/types": "3.13.0", "@shikijs/vscode-textmate": "^10.0.2" } }, "sha512-O42rBGr4UDSlhT2ZFMxqM7QzIU+IcpoTMzb3W7AlziI1ZF7R8eS2M0yt5Ry35nnnTX/LTLXFPUjRFCIW+Operg=="],

"@shikijs/langs": ["@shikijs/langs@3.13.0", "", { "dependencies": { "@shikijs/types": "3.13.0" } }, "sha512-672c3WAETDYHwrRP0yLy3W1QYB89Hbpj+pO4KhxK6FzIrDI2FoEXNiNCut6BQmEApYLfuYfpgOZaqbY+E9b8wQ=="],

"@shikijs/themes": ["@shikijs/themes@3.13.0", "", { "dependencies": { "@shikijs/types": "3.13.0" } }, "sha512-Vxw1Nm1/Od8jyA7QuAenaV78BG2nSr3/gCGdBkLpfLscddCkzkL36Q5b67SrLLfvAJTOUzW39x4FHVCFriPVgg=="],

"@shikijs/types": ["@shikijs/types@3.13.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-oM9P+NCFri/mmQ8LoFGVfVyemm5Hi27330zuOBp0annwJdKH1kOLndw3zCtAVDehPLg9fKqoEx3Ht/wNZxolfw=="],

"@shikijs/vscode-textmate": ["@shikijs/vscode-textmate@10.0.2", "", {}, "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg=="],

"@sideway/address": ["@sideway/address@4.1.5", "", { "dependencies": { "@hapi/hoek": "^9.0.0" } }, "sha512-IqO/DUQHUkPeixNQ8n0JA6102hT9CmaljNTPmQ1u8MEhBo/R4Q8eKLN/vGZxuebwOroDB4cbpjheD4+/sKFK4Q=="],

"@sideway/formula": ["@sideway/formula@3.0.1", "", {}, "sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg=="],
Expand Down Expand Up @@ -1509,6 +1524,8 @@

"hast-util-sanitize": ["hast-util-sanitize@5.0.2", "", { "dependencies": { "@types/hast": "^3.0.0", "@ungap/structured-clone": "^1.0.0", "unist-util-position": "^5.0.0" } }, "sha512-3yTWghByc50aGS7JlGhk61SPenfE/p1oaFeNwkOOyrscaOkMGrcW9+Cy/QAIOBpZxP1yqDIzFMR0+Np0i0+usg=="],

"hast-util-to-html": ["hast-util-to-html@9.0.5", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "ccount": "^2.0.0", "comma-separated-tokens": "^2.0.0", "hast-util-whitespace": "^3.0.0", "html-void-elements": "^3.0.0", "mdast-util-to-hast": "^13.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "stringify-entities": "^4.0.0", "zwitch": "^2.0.4" } }, "sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw=="],

"hast-util-to-jsx-runtime": ["hast-util-to-jsx-runtime@2.3.6", "", { "dependencies": { "@types/estree": "^1.0.0", "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "comma-separated-tokens": "^2.0.0", "devlop": "^1.0.0", "estree-util-is-identifier-name": "^3.0.0", "hast-util-whitespace": "^3.0.0", "mdast-util-mdx-expression": "^2.0.0", "mdast-util-mdx-jsx": "^3.0.0", "mdast-util-mdxjs-esm": "^2.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "style-to-js": "^1.0.0", "unist-util-position": "^5.0.0", "vfile-message": "^4.0.0" } }, "sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg=="],

"hast-util-to-parse5": ["hast-util-to-parse5@8.0.0", "", { "dependencies": { "@types/hast": "^3.0.0", "comma-separated-tokens": "^2.0.0", "devlop": "^1.0.0", "property-information": "^6.0.0", "space-separated-tokens": "^2.0.0", "web-namespaces": "^2.0.0", "zwitch": "^2.0.0" } }, "sha512-3KKrV5ZVI8if87DVSi1vDeByYrkGzg4mEfeu4alwgmmIeARiBLKCZS2uw5Gb6nU9x9Yufyj3iudm6i7nl52PFw=="],
Expand Down Expand Up @@ -2043,6 +2060,10 @@

"onetime": ["onetime@5.1.2", "", { "dependencies": { "mimic-fn": "^2.1.0" } }, "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg=="],

"oniguruma-parser": ["oniguruma-parser@0.12.1", "", {}, "sha512-8Unqkvk1RYc6yq2WBYRj4hdnsAxVze8i7iPfQr8e4uSP3tRv0rpZcbGUDvxfQQcdwHt/e9PrMvGCsa8OqG9X3w=="],

"oniguruma-to-es": ["oniguruma-to-es@4.3.3", "", { "dependencies": { "oniguruma-parser": "^0.12.1", "regex": "^6.0.1", "regex-recursion": "^6.0.2" } }, "sha512-rPiZhzC3wXwE59YQMRDodUwwT9FZ9nNBwQQfsd1wfdtlKEyCdRV0avrTcSZ5xlIvGRVPd/cx6ZN45ECmS39xvg=="],

"open": ["open@8.4.2", "", { "dependencies": { "define-lazy-prop": "^2.0.0", "is-docker": "^2.1.1", "is-wsl": "^2.2.0" } }, "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ=="],

"optionator": ["optionator@0.9.4", "", { "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", "type-check": "^0.4.0", "word-wrap": "^1.2.5" } }, "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g=="],
Expand Down Expand Up @@ -2219,6 +2240,12 @@

"refractor": ["refractor@3.6.0", "", { "dependencies": { "hastscript": "^6.0.0", "parse-entities": "^2.0.0", "prismjs": "~1.27.0" } }, "sha512-MY9W41IOWxxk31o+YvFCNyNzdkc9M20NoZK5vq6jkv4I/uh2zkWcfudj0Q1fovjUQJrNewS9NMzeTtqPf+n5EA=="],

"regex": ["regex@6.0.1", "", { "dependencies": { "regex-utilities": "^2.3.0" } }, "sha512-uorlqlzAKjKQZ5P+kTJr3eeJGSVroLKoHmquUj4zHWuR+hEyNqlXsSKlYYF5F4NI6nl7tWCs0apKJ0lmfsXAPA=="],

"regex-recursion": ["regex-recursion@6.0.2", "", { "dependencies": { "regex-utilities": "^2.3.0" } }, "sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg=="],

"regex-utilities": ["regex-utilities@2.3.0", "", {}, "sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng=="],

"regexp.prototype.flags": ["regexp.prototype.flags@1.5.4", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-errors": "^1.3.0", "get-proto": "^1.0.1", "gopd": "^1.2.0", "set-function-name": "^2.0.2" } }, "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA=="],

"rehype-katex": ["rehype-katex@7.0.1", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/katex": "^0.16.0", "hast-util-from-html-isomorphic": "^2.0.0", "hast-util-to-text": "^4.0.0", "katex": "^0.16.0", "unist-util-visit-parents": "^6.0.0", "vfile": "^6.0.0" } }, "sha512-OiM2wrZ/wuhKkigASodFoo8wimG3H12LWQaH8qSPVJn9apWKFSH3YOCtbKpBorTVw/eI7cuT21XBbvwEswbIOA=="],
Expand Down Expand Up @@ -2317,6 +2344,8 @@

"shell-quote": ["shell-quote@1.8.3", "", {}, "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw=="],

"shiki": ["shiki@3.13.0", "", { "dependencies": { "@shikijs/core": "3.13.0", "@shikijs/engine-javascript": "3.13.0", "@shikijs/engine-oniguruma": "3.13.0", "@shikijs/langs": "3.13.0", "@shikijs/themes": "3.13.0", "@shikijs/types": "3.13.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-aZW4l8Og16CokuCLf8CF8kq+KK2yOygapU5m3+hoGw0Mdosc6fPitjM+ujYarppj5ZIKGyPDPP1vqmQhr+5/0g=="],

"side-channel": ["side-channel@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3", "side-channel-list": "^1.0.0", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" } }, "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw=="],

"side-channel-list": ["side-channel-list@1.0.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3" } }, "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA=="],
Expand Down
2 changes: 2 additions & 0 deletions jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ module.exports = {
},
],
},
// Transform ESM modules (like shiki) to CommonJS for Jest
transformIgnorePatterns: ["node_modules/(?!(shiki)/)"],
// Run tests in parallel (use 50% of available cores, or 4 minimum)
maxWorkers: "50%",
// Force exit after tests complete to avoid hanging on lingering handles
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
"@ai-sdk/openai": "^2.0.52",
"@emotion/react": "^11.14.0",
"@emotion/styled": "^11.14.1",
"@types/react-syntax-highlighter": "^15.5.13",
"ai": "^5.0.72",
"ai-tokenizer": "^1.0.3",
"cmdk": "^1.0.0",
Expand All @@ -61,6 +62,7 @@
"rehype-sanitize": "^6.0.0",
"remark-gfm": "^4.0.1",
"remark-math": "^6.0.0",
"shiki": "^3.13.0",
"source-map-support": "^0.5.21",
"undici": "^7.16.0",
"write-file-atomic": "^6.0.0",
Expand All @@ -87,7 +89,6 @@
"@types/minimist": "^1.2.5",
"@types/react": "^18.2.0",
"@types/react-dom": "^18.2.0",
"@types/react-syntax-highlighter": "^15.5.13",
"@types/write-file-atomic": "^4.0.3",
"@typescript-eslint/eslint-plugin": "^8.44.1",
"@typescript-eslint/parser": "^8.44.1",
Expand Down
1 change: 0 additions & 1 deletion src/components/RightSidebar/CodeReview/UntrackedStatus.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,6 @@ export const UntrackedStatus: React.FC<UntrackedStatusProps> = ({
return () => {
cancelled = true;
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [workspaceId, workspacePath, refreshTrigger]);

// Close tooltip when clicking outside
Expand Down
207 changes: 207 additions & 0 deletions src/components/shared/DiffRenderer.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@
/**
* Tests for DiffRenderer components
*
* These are integration tests that verify the review note feature works end-to-end.
* We test the line extraction and formatting logic that ReviewNoteInput uses internally.
*/

describe("SelectableDiffRenderer review notes", () => {
it("should extract correct line content for review notes", () => {
// Simulate the internal review note building logic
// This is what happens when user clicks comment button and submits
const content = "+const x = 1;\n+const y = 2;\n const z = 3;";
const lines = content.split("\n").filter((line) => line.length > 0);

// Simulate what ReviewNoteInput does
const lineData = [
{ index: 0, type: "add" as const, lineNum: 1 },
{ index: 1, type: "add" as const, lineNum: 2 },
{ index: 2, type: "context" as const, lineNum: 3 },
];

// Simulate selecting first two lines (the + lines)
const selectedLines = lineData
.slice(0, 2)
.map((lineInfo) => {
const line = lines[lineInfo.index];
const indicator = line[0];
const lineContent = line.slice(1);
return `${lineInfo.lineNum} ${indicator} ${lineContent}`;
})
.join("\n");

// Verify the extracted content is correct
expect(selectedLines).toContain("const x = 1");
expect(selectedLines).toContain("const y = 2");
expect(selectedLines).not.toContain("const z = 3");

// Verify format includes line numbers and indicators
expect(selectedLines).toMatch(/1 \+ const x = 1/);
expect(selectedLines).toMatch(/2 \+ const y = 2/);
});

it("should handle removal lines correctly", () => {
const content = "-const old = 1;\n+const new = 2;";
const lines = content.split("\n").filter((line) => line.length > 0);

const lineData = [
{ index: 0, type: "remove" as const, lineNum: 10 },
{ index: 1, type: "add" as const, lineNum: 10 },
];

// Extract first line (removal)
const line = lines[lineData[0].index];
const indicator = line[0];
const lineContent = line.slice(1);
const formattedLine = `${lineData[0].lineNum} ${indicator} ${lineContent}`;

expect(formattedLine).toBe("10 - const old = 1;");
expect(lineContent).toBe("const old = 1;");
});

it("should handle context lines correctly", () => {
const content = " unchanged line\n+new line";
const lines = content.split("\n").filter((line) => line.length > 0);

const lineData = [
{ index: 0, type: "context" as const, lineNum: 5 },
{ index: 1, type: "add" as const, lineNum: 6 },
];

// Extract context line
const line = lines[lineData[0].index];
const indicator = line[0]; // Should be space
const lineContent = line.slice(1);
const formattedLine = `${lineData[0].lineNum} ${indicator} ${lineContent}`;

expect(formattedLine).toBe("5 unchanged line");
expect(indicator).toBe(" ");
});

it("should handle multiline selection correctly", () => {
const content = "+line1\n+line2\n+line3\n line4";
const lines = content.split("\n").filter((line) => line.length > 0);

const lineData = [
{ index: 0, type: "add" as const, lineNum: 1 },
{ index: 1, type: "add" as const, lineNum: 2 },
{ index: 2, type: "add" as const, lineNum: 3 },
{ index: 3, type: "context" as const, lineNum: 4 },
];

// Simulate selecting lines 0-2 (first 3 additions)
const selectedLines = lineData
.slice(0, 3)
.map((lineInfo) => {
const line = lines[lineInfo.index];
const indicator = line[0];
const lineContent = line.slice(1);
return `${lineInfo.lineNum} ${indicator} ${lineContent}`;
})
.join("\n");

expect(selectedLines.split("\n")).toHaveLength(3);
expect(selectedLines).toContain("line1");
expect(selectedLines).toContain("line2");
expect(selectedLines).toContain("line3");
expect(selectedLines).not.toContain("line4");
});

it("should format review note with proper structure", () => {
const filePath = "src/test.ts";
const lineRange = "10-12";
const selectedLines = "10 + const x = 1;\n11 + const y = 2;\n12 + const z = 3;";
const noteText = "These variables should be renamed";

// This is the format that ReviewNoteInput creates
const reviewNote = `<review>\nRe ${filePath}:${lineRange}\n\`\`\`\n${selectedLines}\n\`\`\`\n> ${noteText.trim()}\n</review>`;

expect(reviewNote).toContain("<review>");
expect(reviewNote).toContain("Re src/test.ts:10-12");
expect(reviewNote).toContain("const x = 1");
expect(reviewNote).toContain("const y = 2");
expect(reviewNote).toContain("const z = 3");
expect(reviewNote).toContain("These variables should be renamed");
expect(reviewNote).toContain("</review>");
});

describe("line elision for long selections", () => {
it("should show all lines when selection is ≤3 lines", () => {
const allLines = ["1 + line1", "2 + line2", "3 + line3"];

// No elision for 3 lines
const selectedLines = allLines.join("\n");

expect(selectedLines).toContain("line1");
expect(selectedLines).toContain("line2");
expect(selectedLines).toContain("line3");
expect(selectedLines).not.toContain("omitted");
});

it("should elide middle lines when selection is >3 lines", () => {
const allLines = ["1 + line1", "2 + line2", "3 + line3", "4 + line4", "5 + line5"];

// Elide middle 3 lines, show first and last
const omittedCount = allLines.length - 2;
const selectedLines = [
allLines[0],
` (${omittedCount} lines omitted)`,
allLines[allLines.length - 1],
].join("\n");

expect(selectedLines).toContain("line1");
expect(selectedLines).not.toContain("line2");
expect(selectedLines).not.toContain("line3");
expect(selectedLines).not.toContain("line4");
expect(selectedLines).toContain("line5");
expect(selectedLines).toContain("(3 lines omitted)");
});

it("should handle exactly 4 lines (edge case)", () => {
const allLines = [
"10 + const a = 1;",
"11 + const b = 2;",
"12 + const c = 3;",
"13 + const d = 4;",
];

// Should elide 2 middle lines
const omittedCount = allLines.length - 2;
const selectedLines = [
allLines[0],
` (${omittedCount} lines omitted)`,
allLines[allLines.length - 1],
].join("\n");

expect(selectedLines).toBe("10 + const a = 1;\n (2 lines omitted)\n13 + const d = 4;");
expect(selectedLines).toContain("const a = 1");
expect(selectedLines).toContain("const d = 4");
expect(selectedLines).not.toContain("const b = 2");
expect(selectedLines).not.toContain("const c = 3");
expect(selectedLines).toContain("(2 lines omitted)");
});

it("should format elision message correctly in review note", () => {
const filePath = "src/large.ts";
const lineRange = "10-20";
const allLines = Array.from({ length: 11 }, (_, i) => `${10 + i} + line${i + 1}`);

// Elide middle lines
const omittedCount = allLines.length - 2;
const selectedLines = [
allLines[0],
` (${omittedCount} lines omitted)`,
allLines[allLines.length - 1],
].join("\n");

const noteText = "Review this section";
const reviewNote = `<review>\nRe ${filePath}:${lineRange}\n\`\`\`\n${selectedLines}\n\`\`\`\n> ${noteText.trim()}\n</review>`;

expect(reviewNote).toContain("10 + line1");
expect(reviewNote).toContain("(9 lines omitted)");
expect(reviewNote).toContain("20 + line11");
expect(reviewNote).not.toContain("line2");
expect(reviewNote).not.toContain("line10");
});
});
});
Loading