difit 3.1.18 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.ja.md +30 -3
- package/README.ko.md +30 -3
- package/README.md +30 -3
- package/README.zh.md +30 -3
- package/dist/cli/github.d.ts +65 -0
- package/dist/cli/github.js +296 -0
- package/dist/cli/github.test.d.ts +1 -0
- package/dist/cli/github.test.js +341 -0
- package/dist/cli/index.js +26 -1
- package/dist/cli/index.test.js +206 -4
- package/dist/cli/utils.d.ts +2 -8
- package/dist/cli/utils.js +4 -43
- package/dist/cli/utils.test.js +50 -67
- package/dist/client/assets/{_basePickBy-DyiQWUmK.js → _basePickBy-B9N-f0iT.js} +1 -1
- package/dist/client/assets/{_baseUniq-DivSZEOF.js → _baseUniq-tbL7nVvN.js} +1 -1
- package/dist/client/assets/{arc-c0kacVOL.js → arc-BOY-7mep.js} +1 -1
- package/dist/client/assets/{architectureDiagram-2XIMDMQ5-ubymLNEe.js → architectureDiagram-2XIMDMQ5-59AvHaSB.js} +1 -1
- package/dist/client/assets/{blockDiagram-WCTKOSBZ-F9D8w4_S.js → blockDiagram-WCTKOSBZ-DXIlumQk.js} +1 -1
- package/dist/client/assets/{c4Diagram-IC4MRINW-JE9Kx4yQ.js → c4Diagram-IC4MRINW-BbfZ0uRn.js} +1 -1
- package/dist/client/assets/channel-cZXsTJxA.js +1 -0
- package/dist/client/assets/{chunk-4BX2VUAB-CYOCoDMc.js → chunk-4BX2VUAB-l7rcB2IW.js} +1 -1
- package/dist/client/assets/{chunk-55IACEB6-PRBuiJg9.js → chunk-55IACEB6-CrZL3qv9.js} +1 -1
- package/dist/client/assets/{chunk-FMBD7UC4-C0eJ7JsI.js → chunk-FMBD7UC4-CrKv7ndg.js} +1 -1
- package/dist/client/assets/{chunk-JSJVCQXG-QZotPSqo.js → chunk-JSJVCQXG-DyBDhAEM.js} +1 -1
- package/dist/client/assets/{chunk-KX2RTZJC-B8du3tt8.js → chunk-KX2RTZJC-By5mkZmU.js} +1 -1
- package/dist/client/assets/{chunk-NQ4KR5QH-B10ldi5m.js → chunk-NQ4KR5QH-C30p9xRx.js} +1 -1
- package/dist/client/assets/{chunk-QZHKN3VN-CpwW9rUQ.js → chunk-QZHKN3VN-DVlhR2wU.js} +1 -1
- package/dist/client/assets/{chunk-WL4C6EOR-DwKPHpbL.js → chunk-WL4C6EOR-Cn7a6CO3.js} +1 -1
- package/dist/client/assets/classDiagram-VBA2DB6C-B_coIPEy.js +1 -0
- package/dist/client/assets/classDiagram-v2-RAHNMMFH-B_coIPEy.js +1 -0
- package/dist/client/assets/clone-BjaT2HOk.js +1 -0
- package/dist/client/assets/{cose-bilkent-S5V4N54A-p76yal75.js → cose-bilkent-S5V4N54A-LyauIk_9.js} +1 -1
- package/dist/client/assets/{dagre-KLK3FWXG-CdDyed3V.js → dagre-KLK3FWXG-DRWb2KE3.js} +1 -1
- package/dist/client/assets/{diagram-E7M64L7V-BaC8dXuW.js → diagram-E7M64L7V-ChT6mNWK.js} +1 -1
- package/dist/client/assets/{diagram-IFDJBPK2-BGf8xwJI.js → diagram-IFDJBPK2-CqbTduoP.js} +1 -1
- package/dist/client/assets/{diagram-P4PSJMXO-D3j16gBZ.js → diagram-P4PSJMXO-Bzv5Z3ri.js} +1 -1
- package/dist/client/assets/{erDiagram-INFDFZHY-DFpDdocf.js → erDiagram-INFDFZHY-CvXfUZ4L.js} +1 -1
- package/dist/client/assets/{flowDiagram-PKNHOUZH-Cz4mb4IF.js → flowDiagram-PKNHOUZH-CxmpNUKq.js} +1 -1
- package/dist/client/assets/{ganttDiagram-A5KZAMGK-CNzY9ua5.js → ganttDiagram-A5KZAMGK-9LpZCsg6.js} +1 -1
- package/dist/client/assets/{gitGraphDiagram-K3NZZRJ6-DCSxL8EQ.js → gitGraphDiagram-K3NZZRJ6-C6yZOrQJ.js} +1 -1
- package/dist/client/assets/{graph-BC2BV1-T.js → graph-bUZ7uHLW.js} +1 -1
- package/dist/client/assets/index-BLNN1bfE.js +98 -0
- package/dist/client/assets/index-VxkpzDXr.css +1 -0
- package/dist/client/assets/{infoDiagram-LFFYTUFH-BKSspZbH.js → infoDiagram-LFFYTUFH-Djdy3W21.js} +1 -1
- package/dist/client/assets/{ishikawaDiagram-PHBUUO56-DZ2IRYwc.js → ishikawaDiagram-PHBUUO56-oOdwCpeS.js} +1 -1
- package/dist/client/assets/{journeyDiagram-4ABVD52K-BrjXAkii.js → journeyDiagram-4ABVD52K-DTb_nGAw.js} +1 -1
- package/dist/client/assets/{kanban-definition-K7BYSVSG-B1mfOekw.js → kanban-definition-K7BYSVSG-CMtP7pHA.js} +1 -1
- package/dist/client/assets/{layout-CWTG02uT.js → layout-CXr5MatK.js} +1 -1
- package/dist/client/assets/{linear-CGgOKp1d.js → linear-pOMS9pjV.js} +1 -1
- package/dist/client/assets/{mermaid.core-DTPtVBG7.js → mermaid.core-DV5JJ1Ie.js} +4 -4
- package/dist/client/assets/{mindmap-definition-YRQLILUH-DByVRPFT.js → mindmap-definition-YRQLILUH-DN-sbonc.js} +1 -1
- package/dist/client/assets/{pieDiagram-SKSYHLDU-DEgvAxAy.js → pieDiagram-SKSYHLDU-tAHCkgh1.js} +1 -1
- package/dist/client/assets/{prism-csharp-DqTrHqwJ.js → prism-csharp-5CQ0RcEE.js} +1 -1
- package/dist/client/assets/{prism-elixir-DEJaM00V.js → prism-elixir-BSOTyVg2.js} +1 -1
- package/dist/client/assets/{prism-hcl-HvJ0aPiH.js → prism-hcl-BYvi1mtM.js} +1 -1
- package/dist/client/assets/{prism-java-DDUFERTh.js → prism-java-DMU2FM4X.js} +1 -1
- package/dist/client/assets/{prism-perl-CNA3SNC9.js → prism-perl-CpfvaEQk.js} +1 -1
- package/dist/client/assets/{prism-php-hBQuhE2A.js → prism-php-SC920LoD.js} +1 -1
- package/dist/client/assets/{prism-ruby-BKap8imy.js → prism-ruby-DZph-YiO.js} +1 -1
- package/dist/client/assets/{prism-solidity-DHc7LZHq.js → prism-solidity-qTLbmiAT.js} +1 -1
- package/dist/client/assets/{quadrantDiagram-337W2JSQ-DTtikTvc.js → quadrantDiagram-337W2JSQ-B0wODmgR.js} +1 -1
- package/dist/client/assets/{requirementDiagram-Z7DCOOCP-B34R-xD0.js → requirementDiagram-Z7DCOOCP-A3aeHC06.js} +1 -1
- package/dist/client/assets/{sankeyDiagram-WA2Y5GQK-Dts1ZXRC.js → sankeyDiagram-WA2Y5GQK-BWa6kZhG.js} +1 -1
- package/dist/client/assets/{sequenceDiagram-2WXFIKYE-DzM3WhEY.js → sequenceDiagram-2WXFIKYE-Cx_COX9G.js} +1 -1
- package/dist/client/assets/{stateDiagram-RAJIS63D-B2dF8YnK.js → stateDiagram-RAJIS63D-BXGnN6rZ.js} +1 -1
- package/dist/client/assets/stateDiagram-v2-FVOUBMTO-CMw3xNha.js +1 -0
- package/dist/client/assets/{timeline-definition-YZTLITO2-BO4OtcEm.js → timeline-definition-YZTLITO2-DbqaUm9k.js} +1 -1
- package/dist/client/assets/{treemap-KZPCXAKY-DaXnvVRH.js → treemap-KZPCXAKY-CfEujPCR.js} +1 -1
- package/dist/client/assets/{vennDiagram-LZ73GAT5-AIMhd8Js.js → vennDiagram-LZ73GAT5-CqJE8CAD.js} +1 -1
- package/dist/client/assets/{xychartDiagram-JWTSCODW-Ch6W1f7P.js → xychartDiagram-JWTSCODW-CfdDvzHC.js} +1 -1
- package/dist/client/index.html +2 -2
- package/dist/server/generated-file-check.js +113 -58
- package/dist/server/generated-file-check.test.js +2 -0
- package/dist/server/git-diff.d.ts +1 -0
- package/dist/server/git-diff.js +33 -6
- package/dist/server/git-diff.test.js +26 -0
- package/dist/server/server.d.ts +2 -0
- package/dist/server/server.js +107 -34
- package/dist/server/server.test.js +120 -0
- package/dist/types/diff.d.ts +73 -14
- package/dist/utils/commentFormatting.d.ts +4 -2
- package/dist/utils/commentFormatting.js +57 -19
- package/dist/utils/commentImports.d.ts +9 -0
- package/dist/utils/commentImports.js +264 -0
- package/dist/utils/commentImports.test.d.ts +1 -0
- package/dist/utils/commentImports.test.js +197 -0
- package/package.json +1 -1
- package/dist/client/assets/channel-Ca4c0q8d.js +0 -1
- package/dist/client/assets/classDiagram-VBA2DB6C-CJLw9sK7.js +0 -1
- package/dist/client/assets/classDiagram-v2-RAHNMMFH-CJLw9sK7.js +0 -1
- package/dist/client/assets/clone-D0mDLEir.js +0 -1
- package/dist/client/assets/index-DHt9OwVU.css +0 -1
- package/dist/client/assets/index-mE8CA51x.js +0 -95
- package/dist/client/assets/stateDiagram-v2-FVOUBMTO-ReD0hBzH.js +0 -1
|
@@ -59,6 +59,7 @@ vi.mock('./git-diff.js', () => {
|
|
|
59
59
|
isEmpty: false,
|
|
60
60
|
});
|
|
61
61
|
getBlobContent = vi.fn().mockResolvedValue(Buffer.from('mock image data'));
|
|
62
|
+
getLineCount = vi.fn().mockResolvedValue(42);
|
|
62
63
|
getGeneratedStatus = vi.fn().mockResolvedValue({
|
|
63
64
|
isGenerated: true,
|
|
64
65
|
source: 'content',
|
|
@@ -255,6 +256,74 @@ describe('Server Integration Tests', () => {
|
|
|
255
256
|
expect(response.ok).toBe(true);
|
|
256
257
|
expect(data).toHaveProperty('ignoreWhitespace', true);
|
|
257
258
|
});
|
|
259
|
+
it('GET /api/diff returns comment import payload when configured', async () => {
|
|
260
|
+
const importedComments = [
|
|
261
|
+
{
|
|
262
|
+
type: 'thread',
|
|
263
|
+
filePath: 'test.js',
|
|
264
|
+
position: { side: 'new', line: 10 },
|
|
265
|
+
body: 'Imported comment',
|
|
266
|
+
},
|
|
267
|
+
];
|
|
268
|
+
const importServer = await startServer({
|
|
269
|
+
targetCommitish: 'HEAD',
|
|
270
|
+
baseCommitish: 'HEAD^',
|
|
271
|
+
preferredPort: 9034,
|
|
272
|
+
commentImports: importedComments,
|
|
273
|
+
});
|
|
274
|
+
servers.push(importServer.server);
|
|
275
|
+
const response = await fetch(`http://localhost:${importServer.port}/api/diff`);
|
|
276
|
+
const data = (await response.json());
|
|
277
|
+
expect(response.ok).toBe(true);
|
|
278
|
+
expect(data.commentImports).toEqual(importedComments);
|
|
279
|
+
expect(data.commentImportId).toEqual(expect.any(String));
|
|
280
|
+
});
|
|
281
|
+
it('GET /api/diff returns clearComments together with comment import payload', async () => {
|
|
282
|
+
const importedComments = [
|
|
283
|
+
{
|
|
284
|
+
type: 'thread',
|
|
285
|
+
filePath: 'test.js',
|
|
286
|
+
position: { side: 'new', line: 10 },
|
|
287
|
+
body: 'Imported comment',
|
|
288
|
+
},
|
|
289
|
+
];
|
|
290
|
+
const importServer = await startServer({
|
|
291
|
+
targetCommitish: 'HEAD',
|
|
292
|
+
baseCommitish: 'HEAD^',
|
|
293
|
+
preferredPort: 9037,
|
|
294
|
+
clearComments: true,
|
|
295
|
+
commentImports: importedComments,
|
|
296
|
+
});
|
|
297
|
+
servers.push(importServer.server);
|
|
298
|
+
const response = await fetch(`http://localhost:${importServer.port}/api/diff`);
|
|
299
|
+
const data = (await response.json());
|
|
300
|
+
expect(response.ok).toBe(true);
|
|
301
|
+
expect(data.clearComments).toBe(true);
|
|
302
|
+
expect(data.commentImports).toEqual(importedComments);
|
|
303
|
+
expect(data.commentImportId).toEqual(expect.any(String));
|
|
304
|
+
});
|
|
305
|
+
it('GET /api/diff omits comment import payload after revision changes', async () => {
|
|
306
|
+
const importedComments = [
|
|
307
|
+
{
|
|
308
|
+
type: 'thread',
|
|
309
|
+
filePath: 'test.js',
|
|
310
|
+
position: { side: 'new', line: 10 },
|
|
311
|
+
body: 'Imported comment',
|
|
312
|
+
},
|
|
313
|
+
];
|
|
314
|
+
const importServer = await startServer({
|
|
315
|
+
targetCommitish: 'HEAD',
|
|
316
|
+
baseCommitish: 'HEAD^',
|
|
317
|
+
preferredPort: 9038,
|
|
318
|
+
commentImports: importedComments,
|
|
319
|
+
});
|
|
320
|
+
servers.push(importServer.server);
|
|
321
|
+
const response = await fetch(`http://localhost:${importServer.port}/api/diff?base=main&target=feature`);
|
|
322
|
+
const data = (await response.json());
|
|
323
|
+
expect(response.ok).toBe(true);
|
|
324
|
+
expect(data.commentImports).toBeUndefined();
|
|
325
|
+
expect(data.commentImportId).toBeUndefined();
|
|
326
|
+
});
|
|
258
327
|
it('GET /api/generated-status/* returns generated status', async () => {
|
|
259
328
|
const response = await fetch(`http://localhost:${port}/api/generated-status/src/query.ts?ref=HEAD`);
|
|
260
329
|
const data = (await response.json());
|
|
@@ -329,6 +398,7 @@ describe('Server Integration Tests', () => {
|
|
|
329
398
|
const response = await fetch(`http://localhost:${port}/api/comments-output`);
|
|
330
399
|
const output = await response.text();
|
|
331
400
|
expect(response.ok).toBe(true);
|
|
401
|
+
expect(response.headers.get('Content-Type')).toContain('text/plain');
|
|
332
402
|
expect(output).toContain('Comments from review session');
|
|
333
403
|
expect(output).toContain('test.js:L10');
|
|
334
404
|
expect(output).toContain('First comment');
|
|
@@ -427,6 +497,17 @@ describe('Server Integration Tests', () => {
|
|
|
427
497
|
// But the server should not crash
|
|
428
498
|
expect([200, 404]).toContain(response.status);
|
|
429
499
|
});
|
|
500
|
+
it('returns 404 for unknown paths in production mode', async () => {
|
|
501
|
+
process.env.NODE_ENV = 'production';
|
|
502
|
+
const result = await startServer({
|
|
503
|
+
targetCommitish: 'HEAD',
|
|
504
|
+
baseCommitish: 'HEAD^',
|
|
505
|
+
preferredPort: 9055,
|
|
506
|
+
});
|
|
507
|
+
servers.push(result.server);
|
|
508
|
+
const response = await fetch(`http://localhost:${result.port}/not-a-route`);
|
|
509
|
+
expect(response.status).toBe(404);
|
|
510
|
+
});
|
|
430
511
|
});
|
|
431
512
|
describe('Mode option handling', () => {
|
|
432
513
|
it('accepts mode option in server configuration', async () => {
|
|
@@ -529,6 +610,39 @@ describe('Server Integration Tests', () => {
|
|
|
529
610
|
expect(response.headers.get('Access-Control-Allow-Headers')).toBe('Origin, X-Requested-With, Content-Type, Accept');
|
|
530
611
|
});
|
|
531
612
|
});
|
|
613
|
+
describe('Line count API', () => {
|
|
614
|
+
let port;
|
|
615
|
+
beforeEach(async () => {
|
|
616
|
+
const result = await startServer({
|
|
617
|
+
targetCommitish: 'HEAD',
|
|
618
|
+
baseCommitish: 'HEAD^',
|
|
619
|
+
preferredPort: 9050,
|
|
620
|
+
});
|
|
621
|
+
servers.push(result.server);
|
|
622
|
+
port = result.port;
|
|
623
|
+
});
|
|
624
|
+
it('returns line counts for repository files', async () => {
|
|
625
|
+
const response = await fetch(`http://localhost:${port}/api/line-count/src%2Findex.ts?oldRef=HEAD~1&newRef=HEAD`);
|
|
626
|
+
const data = (await response.json());
|
|
627
|
+
expect(response.ok).toBe(true);
|
|
628
|
+
expect(data).toEqual({
|
|
629
|
+
oldLineCount: 42,
|
|
630
|
+
newLineCount: 42,
|
|
631
|
+
});
|
|
632
|
+
});
|
|
633
|
+
it('rejects paths outside repository', async () => {
|
|
634
|
+
const response = await fetch(`http://localhost:${port}/api/line-count/..%2Foutside.txt`);
|
|
635
|
+
const data = (await response.json());
|
|
636
|
+
expect(response.status).toBe(400);
|
|
637
|
+
expect(data).toHaveProperty('error', 'File path outside repository');
|
|
638
|
+
});
|
|
639
|
+
it('rejects oldPath values outside repository', async () => {
|
|
640
|
+
const response = await fetch(`http://localhost:${port}/api/line-count/src%2Findex.ts?oldRef=HEAD~1&oldPath=..%2Foutside.txt`);
|
|
641
|
+
const data = (await response.json());
|
|
642
|
+
expect(response.status).toBe(400);
|
|
643
|
+
expect(data).toHaveProperty('error', 'File path outside repository');
|
|
644
|
+
});
|
|
645
|
+
});
|
|
532
646
|
describe('Blob API endpoints', () => {
|
|
533
647
|
let port;
|
|
534
648
|
beforeEach(async () => {
|
|
@@ -609,6 +723,12 @@ describe('Server Integration Tests', () => {
|
|
|
609
723
|
expect(response.ok).toBe(true);
|
|
610
724
|
}
|
|
611
725
|
});
|
|
726
|
+
it('rejects paths outside repository', async () => {
|
|
727
|
+
const response = await fetch(`http://localhost:${port}/api/blob/..%2Foutside.txt?ref=HEAD`);
|
|
728
|
+
const data = (await response.json());
|
|
729
|
+
expect(response.status).toBe(400);
|
|
730
|
+
expect(data).toHaveProperty('error', 'File path outside repository');
|
|
731
|
+
});
|
|
612
732
|
});
|
|
613
733
|
describe('Keep-alive option', () => {
|
|
614
734
|
it('accepts keepAlive option without error', async () => {
|
package/dist/types/diff.d.ts
CHANGED
|
@@ -34,6 +34,18 @@ export interface ParsedDiff {
|
|
|
34
34
|
export type DiffViewMode = 'split' | 'unified';
|
|
35
35
|
export type LegacyDiffViewMode = 'side-by-side' | 'inline';
|
|
36
36
|
export type DiffSide = 'old' | 'new';
|
|
37
|
+
export type DiffLineRange = number | {
|
|
38
|
+
start: number;
|
|
39
|
+
end: number;
|
|
40
|
+
};
|
|
41
|
+
export interface DiffCommentPosition {
|
|
42
|
+
side: DiffSide;
|
|
43
|
+
line: DiffLineRange;
|
|
44
|
+
}
|
|
45
|
+
export interface DiffCommentCodeSnapshot {
|
|
46
|
+
content: string;
|
|
47
|
+
language?: string;
|
|
48
|
+
}
|
|
37
49
|
export interface DiffResponse {
|
|
38
50
|
commit: string;
|
|
39
51
|
files: DiffFile[];
|
|
@@ -47,6 +59,8 @@ export interface DiffResponse {
|
|
|
47
59
|
requestedTargetCommitish?: string;
|
|
48
60
|
clearComments?: boolean;
|
|
49
61
|
repositoryId?: string;
|
|
62
|
+
commentImports?: CommentImport[];
|
|
63
|
+
commentImportId?: string;
|
|
50
64
|
}
|
|
51
65
|
export interface GeneratedStatusResponse {
|
|
52
66
|
path: string;
|
|
@@ -69,38 +83,82 @@ export interface LineSelection {
|
|
|
69
83
|
side: DiffSide;
|
|
70
84
|
lineNumber: number;
|
|
71
85
|
}
|
|
72
|
-
export interface
|
|
86
|
+
export interface LegacyDiffComment {
|
|
73
87
|
id: string;
|
|
74
88
|
filePath: string;
|
|
75
89
|
body: string;
|
|
76
90
|
author?: string;
|
|
77
91
|
createdAt: string;
|
|
78
92
|
updatedAt: string;
|
|
79
|
-
position:
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
93
|
+
position: DiffCommentPosition;
|
|
94
|
+
codeSnapshot?: DiffCommentCodeSnapshot;
|
|
95
|
+
}
|
|
96
|
+
export interface DiffCommentMessage {
|
|
97
|
+
id: string;
|
|
98
|
+
body: string;
|
|
99
|
+
author?: string;
|
|
100
|
+
createdAt: string;
|
|
101
|
+
updatedAt: string;
|
|
102
|
+
}
|
|
103
|
+
export interface DiffCommentThread {
|
|
104
|
+
id: string;
|
|
105
|
+
filePath: string;
|
|
106
|
+
createdAt: string;
|
|
107
|
+
updatedAt: string;
|
|
108
|
+
position: DiffCommentPosition;
|
|
109
|
+
codeSnapshot?: DiffCommentCodeSnapshot;
|
|
110
|
+
messages: DiffCommentMessage[];
|
|
111
|
+
}
|
|
112
|
+
interface CommentImportBase {
|
|
113
|
+
id?: string;
|
|
114
|
+
filePath: string;
|
|
115
|
+
position: DiffCommentPosition;
|
|
116
|
+
body: string;
|
|
117
|
+
author?: string;
|
|
118
|
+
createdAt?: string;
|
|
119
|
+
updatedAt?: string;
|
|
120
|
+
codeSnapshot?: DiffCommentCodeSnapshot;
|
|
90
121
|
}
|
|
122
|
+
export interface ThreadCommentImport extends CommentImportBase {
|
|
123
|
+
type: 'thread';
|
|
124
|
+
}
|
|
125
|
+
export interface ReplyCommentImport extends CommentImportBase {
|
|
126
|
+
type: 'reply';
|
|
127
|
+
}
|
|
128
|
+
export type CommentImport = ThreadCommentImport | ReplyCommentImport;
|
|
91
129
|
export interface ViewedFileRecord {
|
|
92
130
|
filePath: string;
|
|
93
131
|
viewedAt: string;
|
|
94
132
|
diffContentHash: string;
|
|
95
133
|
}
|
|
96
|
-
export interface
|
|
134
|
+
export interface LegacyDiffContextStorage {
|
|
97
135
|
version: 1;
|
|
98
136
|
baseCommitish: string;
|
|
99
137
|
targetCommitish: string;
|
|
100
138
|
createdAt: string;
|
|
101
139
|
lastModifiedAt: string;
|
|
102
|
-
comments:
|
|
140
|
+
comments: LegacyDiffComment[];
|
|
141
|
+
viewedFiles: ViewedFileRecord[];
|
|
142
|
+
}
|
|
143
|
+
export interface DiffContextStorage {
|
|
144
|
+
version: 2;
|
|
145
|
+
baseCommitish: string;
|
|
146
|
+
targetCommitish: string;
|
|
147
|
+
createdAt: string;
|
|
148
|
+
lastModifiedAt: string;
|
|
149
|
+
threads: DiffCommentThread[];
|
|
103
150
|
viewedFiles: ViewedFileRecord[];
|
|
151
|
+
appliedCommentImportIds: string[];
|
|
152
|
+
}
|
|
153
|
+
export interface CommentThread {
|
|
154
|
+
id: string;
|
|
155
|
+
file: string;
|
|
156
|
+
line: LineNumber;
|
|
157
|
+
side?: DiffSide;
|
|
158
|
+
createdAt: string;
|
|
159
|
+
updatedAt: string;
|
|
160
|
+
codeContent?: string;
|
|
161
|
+
messages: DiffCommentMessage[];
|
|
104
162
|
}
|
|
105
163
|
export interface RevisionOption {
|
|
106
164
|
value: string;
|
|
@@ -140,3 +198,4 @@ export interface ExpandedRange {
|
|
|
140
198
|
export interface ExpandedLine extends DiffLine {
|
|
141
199
|
isExpanded?: boolean;
|
|
142
200
|
}
|
|
201
|
+
export {};
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import type { Comment } from '../types/diff';
|
|
1
|
+
import type { Comment, CommentThread } from '../types/diff';
|
|
2
2
|
export declare function formatCommentPrompt(file: string, line: number | number[], body: string, codeContent?: string): string;
|
|
3
3
|
export declare function formatAllCommentsPrompt(comments: Comment[]): string;
|
|
4
|
-
export declare function
|
|
4
|
+
export declare function formatCommentThreadPrompt(thread: CommentThread): string;
|
|
5
|
+
export declare function formatAllCommentThreadsPrompt(threads: CommentThread[]): string;
|
|
6
|
+
export declare function formatCommentsOutput(input: Comment[] | CommentThread[]): string;
|
|
@@ -1,38 +1,56 @@
|
|
|
1
1
|
import { hasSuggestionBlock, parseSuggestionBlocks } from './suggestionUtils.js';
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
// Check if body contains suggestion blocks
|
|
2
|
+
function getLineInfo(line) {
|
|
3
|
+
return typeof line === 'number' ? `L${line}` : `L${line[0]}-L${line[1]}`;
|
|
4
|
+
}
|
|
5
|
+
function formatCommentContent(body, codeContent) {
|
|
7
6
|
if (hasSuggestionBlock(body)) {
|
|
8
7
|
const suggestions = parseSuggestionBlocks(body);
|
|
9
8
|
if (suggestions.length > 0) {
|
|
10
|
-
let result =
|
|
11
|
-
// Walk through body preserving text between suggestion blocks
|
|
9
|
+
let result = '';
|
|
12
10
|
let lastIndex = 0;
|
|
13
11
|
for (const suggestion of suggestions) {
|
|
14
|
-
// Add text before this suggestion block
|
|
15
12
|
const textBefore = body.slice(lastIndex, suggestion.startIndex).trim();
|
|
16
13
|
if (textBefore) {
|
|
17
|
-
result +=
|
|
14
|
+
result += `${result ? '\n' : ''}${textBefore}`;
|
|
18
15
|
}
|
|
19
|
-
// Add structured ORIGINAL/SUGGESTED format
|
|
20
16
|
if (codeContent) {
|
|
21
|
-
result +=
|
|
17
|
+
result += `${result ? '\n' : ''}ORIGINAL:\n\`\`\`\n${codeContent}\n\`\`\``;
|
|
22
18
|
}
|
|
23
|
-
result +=
|
|
19
|
+
result += `${result ? '\n' : ''}SUGGESTED:\n\`\`\`\n${suggestion.suggestedCode}\n\`\`\``;
|
|
24
20
|
lastIndex = suggestion.endIndex;
|
|
25
21
|
}
|
|
26
|
-
// Add remaining text after the last suggestion block
|
|
27
22
|
const textAfter = body.slice(lastIndex).trim();
|
|
28
23
|
if (textAfter) {
|
|
29
|
-
result +=
|
|
24
|
+
result += `${result ? '\n' : ''}${textAfter}`;
|
|
30
25
|
}
|
|
31
26
|
return result;
|
|
32
27
|
}
|
|
33
28
|
}
|
|
34
|
-
|
|
35
|
-
|
|
29
|
+
return body;
|
|
30
|
+
}
|
|
31
|
+
function normalizeLegacyComment(comment) {
|
|
32
|
+
return {
|
|
33
|
+
id: comment.id,
|
|
34
|
+
file: comment.file,
|
|
35
|
+
line: comment.line,
|
|
36
|
+
side: comment.side,
|
|
37
|
+
createdAt: comment.timestamp,
|
|
38
|
+
updatedAt: comment.timestamp,
|
|
39
|
+
codeContent: comment.codeContent,
|
|
40
|
+
messages: [
|
|
41
|
+
{
|
|
42
|
+
id: comment.id,
|
|
43
|
+
body: comment.body,
|
|
44
|
+
author: comment.author,
|
|
45
|
+
createdAt: comment.timestamp,
|
|
46
|
+
updatedAt: comment.timestamp,
|
|
47
|
+
},
|
|
48
|
+
],
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
export function formatCommentPrompt(file, line, body, codeContent) {
|
|
52
|
+
const filePath = file || '<unknown file>';
|
|
53
|
+
return `${filePath}:${getLineInfo(line)}\n${formatCommentContent(body, codeContent)}`;
|
|
36
54
|
}
|
|
37
55
|
export function formatAllCommentsPrompt(comments) {
|
|
38
56
|
if (comments.length === 0)
|
|
@@ -40,13 +58,33 @@ export function formatAllCommentsPrompt(comments) {
|
|
|
40
58
|
const prompts = comments.map((comment) => formatCommentPrompt(comment.file, comment.line, comment.body, comment.codeContent));
|
|
41
59
|
return prompts.join('\n=====\n');
|
|
42
60
|
}
|
|
43
|
-
export function
|
|
44
|
-
const
|
|
61
|
+
export function formatCommentThreadPrompt(thread) {
|
|
62
|
+
const sections = [`${thread.file || '<unknown file>'}:${getLineInfo(thread.line)}`];
|
|
63
|
+
thread.messages.forEach((message, index) => {
|
|
64
|
+
if (index === 0) {
|
|
65
|
+
sections.push(formatCommentContent(message.body, thread.codeContent));
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
const replyIndex = index;
|
|
69
|
+
const authorLabel = message.author?.trim() || 'Unknown';
|
|
70
|
+
sections.push(`Reply ${replyIndex} (${authorLabel})`);
|
|
71
|
+
sections.push(formatCommentContent(message.body, thread.codeContent));
|
|
72
|
+
});
|
|
73
|
+
return sections.filter(Boolean).join('\n');
|
|
74
|
+
}
|
|
75
|
+
export function formatAllCommentThreadsPrompt(threads) {
|
|
76
|
+
if (threads.length === 0)
|
|
77
|
+
return '';
|
|
78
|
+
return threads.map((thread) => formatCommentThreadPrompt(thread)).join('\n=====\n');
|
|
79
|
+
}
|
|
80
|
+
export function formatCommentsOutput(input) {
|
|
81
|
+
const threads = input.map((item) => ('messages' in item ? item : normalizeLegacyComment(item)));
|
|
82
|
+
const allPrompts = formatAllCommentThreadsPrompt(threads);
|
|
45
83
|
return [
|
|
46
84
|
'\n📝 Comments from review session:',
|
|
47
85
|
'='.repeat(50),
|
|
48
86
|
allPrompts,
|
|
49
87
|
'='.repeat(50),
|
|
50
|
-
`Total comments: ${
|
|
88
|
+
`Total comments: ${threads.length}\n`,
|
|
51
89
|
].join('\n');
|
|
52
90
|
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { CommentImport, DiffCommentThread } from '../types/diff.js';
|
|
2
|
+
interface MergeCommentImportsResult {
|
|
3
|
+
threads: DiffCommentThread[];
|
|
4
|
+
warnings: string[];
|
|
5
|
+
}
|
|
6
|
+
export declare function parseCommentImportValue(value: string): CommentImport[];
|
|
7
|
+
export declare function serializeCommentImports(commentImports: CommentImport[]): string;
|
|
8
|
+
export declare function mergeCommentImports(existingThreads: DiffCommentThread[], commentImports: CommentImport[]): MergeCommentImportsResult;
|
|
9
|
+
export {};
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
function isRecord(value) {
|
|
2
|
+
return typeof value === 'object' && value !== null;
|
|
3
|
+
}
|
|
4
|
+
function isValidIsoTimestamp(value) {
|
|
5
|
+
return Number.isFinite(Date.parse(value));
|
|
6
|
+
}
|
|
7
|
+
function normalizeTimestamp(value, fieldName) {
|
|
8
|
+
if (value === undefined) {
|
|
9
|
+
return undefined;
|
|
10
|
+
}
|
|
11
|
+
if (typeof value !== 'string' || value.trim().length === 0 || !isValidIsoTimestamp(value)) {
|
|
12
|
+
throw new Error(`Invalid comment import field: ${fieldName}`);
|
|
13
|
+
}
|
|
14
|
+
return value;
|
|
15
|
+
}
|
|
16
|
+
function normalizeLineRange(value) {
|
|
17
|
+
if (typeof value === 'number' && Number.isInteger(value) && value > 0) {
|
|
18
|
+
return value;
|
|
19
|
+
}
|
|
20
|
+
if (!isRecord(value)) {
|
|
21
|
+
throw new Error('Invalid comment import field: position.line');
|
|
22
|
+
}
|
|
23
|
+
const start = value.start;
|
|
24
|
+
const end = value.end;
|
|
25
|
+
if (typeof start !== 'number' ||
|
|
26
|
+
typeof end !== 'number' ||
|
|
27
|
+
!Number.isInteger(start) ||
|
|
28
|
+
!Number.isInteger(end) ||
|
|
29
|
+
start <= 0 ||
|
|
30
|
+
end <= 0 ||
|
|
31
|
+
start > end) {
|
|
32
|
+
throw new Error('Invalid comment import field: position.line');
|
|
33
|
+
}
|
|
34
|
+
return { start, end };
|
|
35
|
+
}
|
|
36
|
+
function normalizePosition(value) {
|
|
37
|
+
if (!isRecord(value)) {
|
|
38
|
+
throw new Error('Invalid comment import field: position');
|
|
39
|
+
}
|
|
40
|
+
if (value.side !== 'old' && value.side !== 'new') {
|
|
41
|
+
throw new Error('Invalid comment import field: position.side');
|
|
42
|
+
}
|
|
43
|
+
return {
|
|
44
|
+
side: value.side,
|
|
45
|
+
line: normalizeLineRange(value.line),
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
function normalizeCodeSnapshot(value) {
|
|
49
|
+
if (value === undefined) {
|
|
50
|
+
return undefined;
|
|
51
|
+
}
|
|
52
|
+
if (!isRecord(value) || typeof value.content !== 'string') {
|
|
53
|
+
throw new Error('Invalid comment import field: codeSnapshot');
|
|
54
|
+
}
|
|
55
|
+
if (value.language !== undefined && typeof value.language !== 'string') {
|
|
56
|
+
throw new Error('Invalid comment import field: codeSnapshot.language');
|
|
57
|
+
}
|
|
58
|
+
return {
|
|
59
|
+
content: value.content,
|
|
60
|
+
language: value.language,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
function normalizeOptionalString(value, fieldName) {
|
|
64
|
+
if (value === undefined) {
|
|
65
|
+
return undefined;
|
|
66
|
+
}
|
|
67
|
+
if (typeof value !== 'string') {
|
|
68
|
+
throw new Error(`Invalid comment import field: ${fieldName}`);
|
|
69
|
+
}
|
|
70
|
+
return value;
|
|
71
|
+
}
|
|
72
|
+
function normalizeRequiredBody(value) {
|
|
73
|
+
if (typeof value !== 'string') {
|
|
74
|
+
throw new Error('Invalid comment import field: body');
|
|
75
|
+
}
|
|
76
|
+
const trimmed = value.trim();
|
|
77
|
+
if (trimmed.length === 0) {
|
|
78
|
+
throw new Error('Invalid comment import field: body');
|
|
79
|
+
}
|
|
80
|
+
return trimmed;
|
|
81
|
+
}
|
|
82
|
+
function normalizeCommentImportEntry(value) {
|
|
83
|
+
if (!isRecord(value)) {
|
|
84
|
+
throw new Error('Comment import must be an object');
|
|
85
|
+
}
|
|
86
|
+
if (value.type !== 'thread' && value.type !== 'reply') {
|
|
87
|
+
throw new Error('Invalid comment import field: type');
|
|
88
|
+
}
|
|
89
|
+
if (typeof value.filePath !== 'string' || value.filePath.trim().length === 0) {
|
|
90
|
+
throw new Error('Invalid comment import field: filePath');
|
|
91
|
+
}
|
|
92
|
+
const createdAt = normalizeTimestamp(value.createdAt, 'createdAt');
|
|
93
|
+
const updatedAt = normalizeTimestamp(value.updatedAt, 'updatedAt');
|
|
94
|
+
const normalized = {
|
|
95
|
+
type: value.type,
|
|
96
|
+
id: normalizeOptionalString(value.id, 'id'),
|
|
97
|
+
filePath: value.filePath,
|
|
98
|
+
position: normalizePosition(value.position),
|
|
99
|
+
body: normalizeRequiredBody(value.body),
|
|
100
|
+
author: normalizeOptionalString(value.author, 'author'),
|
|
101
|
+
createdAt,
|
|
102
|
+
updatedAt,
|
|
103
|
+
codeSnapshot: normalizeCodeSnapshot(value.codeSnapshot),
|
|
104
|
+
};
|
|
105
|
+
return normalized;
|
|
106
|
+
}
|
|
107
|
+
function normalizeCommentImports(input) {
|
|
108
|
+
if (Array.isArray(input)) {
|
|
109
|
+
return input.map((entry) => normalizeCommentImportEntry(entry));
|
|
110
|
+
}
|
|
111
|
+
return [normalizeCommentImportEntry(input)];
|
|
112
|
+
}
|
|
113
|
+
export function parseCommentImportValue(value) {
|
|
114
|
+
let parsed;
|
|
115
|
+
try {
|
|
116
|
+
parsed = JSON.parse(value);
|
|
117
|
+
}
|
|
118
|
+
catch {
|
|
119
|
+
throw new Error('Invalid --comment JSON');
|
|
120
|
+
}
|
|
121
|
+
return normalizeCommentImports(parsed);
|
|
122
|
+
}
|
|
123
|
+
function getPositionKey(position) {
|
|
124
|
+
if (typeof position.line === 'number') {
|
|
125
|
+
return `${position.side}:${position.line}`;
|
|
126
|
+
}
|
|
127
|
+
return `${position.side}:${position.line.start}-${position.line.end}`;
|
|
128
|
+
}
|
|
129
|
+
function positionsMatch(left, right) {
|
|
130
|
+
return getPositionKey(left) === getPositionKey(right);
|
|
131
|
+
}
|
|
132
|
+
function normalizeAuthor(author) {
|
|
133
|
+
return author?.trim() || '';
|
|
134
|
+
}
|
|
135
|
+
function messageMatchesImport(message, commentImport, includeId = true) {
|
|
136
|
+
if (includeId && commentImport.id && message.id === commentImport.id) {
|
|
137
|
+
return true;
|
|
138
|
+
}
|
|
139
|
+
if (normalizeAuthor(message.author) !== normalizeAuthor(commentImport.author)) {
|
|
140
|
+
return false;
|
|
141
|
+
}
|
|
142
|
+
if (message.body !== commentImport.body) {
|
|
143
|
+
return false;
|
|
144
|
+
}
|
|
145
|
+
if (commentImport.createdAt && message.createdAt !== commentImport.createdAt) {
|
|
146
|
+
return false;
|
|
147
|
+
}
|
|
148
|
+
return true;
|
|
149
|
+
}
|
|
150
|
+
function rootThreadMatchesImport(thread, commentImport) {
|
|
151
|
+
if (commentImport.id && thread.id === commentImport.id) {
|
|
152
|
+
return true;
|
|
153
|
+
}
|
|
154
|
+
if (thread.filePath !== commentImport.filePath ||
|
|
155
|
+
!positionsMatch(thread.position, commentImport.position)) {
|
|
156
|
+
return false;
|
|
157
|
+
}
|
|
158
|
+
const rootMessage = thread.messages[0];
|
|
159
|
+
if (!rootMessage) {
|
|
160
|
+
return false;
|
|
161
|
+
}
|
|
162
|
+
return messageMatchesImport(rootMessage, commentImport, true);
|
|
163
|
+
}
|
|
164
|
+
function toThreadTimestamp(commentImport, now) {
|
|
165
|
+
const createdAt = commentImport.createdAt ?? now;
|
|
166
|
+
const updatedAt = commentImport.updatedAt ?? createdAt;
|
|
167
|
+
return { createdAt, updatedAt };
|
|
168
|
+
}
|
|
169
|
+
function maxIsoTimestamp(left, right) {
|
|
170
|
+
return left.localeCompare(right) >= 0 ? left : right;
|
|
171
|
+
}
|
|
172
|
+
function sortByNewestThread(left, right) {
|
|
173
|
+
return right.updatedAt.localeCompare(left.updatedAt);
|
|
174
|
+
}
|
|
175
|
+
function createImportedThread(commentImport, now) {
|
|
176
|
+
const { createdAt, updatedAt } = toThreadTimestamp(commentImport, now);
|
|
177
|
+
const threadId = commentImport.id ?? crypto.randomUUID();
|
|
178
|
+
return {
|
|
179
|
+
id: threadId,
|
|
180
|
+
filePath: commentImport.filePath,
|
|
181
|
+
createdAt,
|
|
182
|
+
updatedAt,
|
|
183
|
+
position: commentImport.position,
|
|
184
|
+
codeSnapshot: commentImport.codeSnapshot,
|
|
185
|
+
messages: [
|
|
186
|
+
{
|
|
187
|
+
id: commentImport.id ?? threadId,
|
|
188
|
+
body: commentImport.body,
|
|
189
|
+
author: commentImport.author,
|
|
190
|
+
createdAt,
|
|
191
|
+
updatedAt,
|
|
192
|
+
},
|
|
193
|
+
],
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
function createImportedReply(commentImport, now) {
|
|
197
|
+
const { createdAt, updatedAt } = toThreadTimestamp(commentImport, now);
|
|
198
|
+
return {
|
|
199
|
+
id: commentImport.id ?? crypto.randomUUID(),
|
|
200
|
+
body: commentImport.body,
|
|
201
|
+
author: commentImport.author,
|
|
202
|
+
createdAt,
|
|
203
|
+
updatedAt,
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
function serializeLineRange(line) {
|
|
207
|
+
if (typeof line === 'number') {
|
|
208
|
+
return line;
|
|
209
|
+
}
|
|
210
|
+
return { start: line.start, end: line.end };
|
|
211
|
+
}
|
|
212
|
+
export function serializeCommentImports(commentImports) {
|
|
213
|
+
return JSON.stringify(commentImports.map((commentImport) => ({
|
|
214
|
+
type: commentImport.type,
|
|
215
|
+
id: commentImport.id,
|
|
216
|
+
filePath: commentImport.filePath,
|
|
217
|
+
position: {
|
|
218
|
+
side: commentImport.position.side,
|
|
219
|
+
line: serializeLineRange(commentImport.position.line),
|
|
220
|
+
},
|
|
221
|
+
body: commentImport.body,
|
|
222
|
+
author: commentImport.author,
|
|
223
|
+
createdAt: commentImport.createdAt,
|
|
224
|
+
updatedAt: commentImport.updatedAt,
|
|
225
|
+
codeSnapshot: commentImport.codeSnapshot
|
|
226
|
+
? {
|
|
227
|
+
content: commentImport.codeSnapshot.content,
|
|
228
|
+
language: commentImport.codeSnapshot.language,
|
|
229
|
+
}
|
|
230
|
+
: undefined,
|
|
231
|
+
})));
|
|
232
|
+
}
|
|
233
|
+
export function mergeCommentImports(existingThreads, commentImports) {
|
|
234
|
+
const threads = [...existingThreads];
|
|
235
|
+
const warnings = [];
|
|
236
|
+
for (const commentImport of commentImports) {
|
|
237
|
+
const now = new Date().toISOString();
|
|
238
|
+
if (commentImport.type === 'thread') {
|
|
239
|
+
const hasDuplicate = threads.some((thread) => rootThreadMatchesImport(thread, commentImport));
|
|
240
|
+
if (hasDuplicate) {
|
|
241
|
+
continue;
|
|
242
|
+
}
|
|
243
|
+
threads.push(createImportedThread(commentImport, now));
|
|
244
|
+
continue;
|
|
245
|
+
}
|
|
246
|
+
const targetThreads = threads
|
|
247
|
+
.filter((thread) => thread.filePath === commentImport.filePath &&
|
|
248
|
+
positionsMatch(thread.position, commentImport.position))
|
|
249
|
+
.sort(sortByNewestThread);
|
|
250
|
+
const targetThread = targetThreads[0];
|
|
251
|
+
if (!targetThread) {
|
|
252
|
+
warnings.push(`Skipped reply import for ${commentImport.filePath}:${getPositionKey(commentImport.position)} because no matching thread was found.`);
|
|
253
|
+
continue;
|
|
254
|
+
}
|
|
255
|
+
const hasDuplicateReply = targetThread.messages.some((message) => messageMatchesImport(message, commentImport, true));
|
|
256
|
+
if (hasDuplicateReply) {
|
|
257
|
+
continue;
|
|
258
|
+
}
|
|
259
|
+
const importedReply = createImportedReply(commentImport, now);
|
|
260
|
+
targetThread.messages = [...targetThread.messages, importedReply];
|
|
261
|
+
targetThread.updatedAt = maxIsoTimestamp(targetThread.updatedAt, importedReply.updatedAt);
|
|
262
|
+
}
|
|
263
|
+
return { threads, warnings };
|
|
264
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|