@diegopetrucci/pi-extensions 0.1.74 → 0.1.76
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/LICENSE +2 -0
- package/README.md +9 -4
- package/extensions/agent-workflow-audit/index.ts +1398 -1009
- package/extensions/agent-workflow-audit/package.json +1 -1
- package/extensions/annotate-git-diff/README.md +3 -1
- package/extensions/annotate-git-diff/clipboard.ts +109 -100
- package/extensions/annotate-git-diff/git.ts +853 -767
- package/extensions/annotate-git-diff/glimpseui.d.ts +87 -78
- package/extensions/annotate-git-diff/index.ts +712 -671
- package/extensions/annotate-git-diff/package.json +1 -1
- package/extensions/annotate-git-diff/prompt.ts +45 -45
- package/extensions/annotate-git-diff/quiet-glimpse.ts +161 -155
- package/extensions/annotate-git-diff/review-server.ts +337 -311
- package/extensions/annotate-git-diff/types.ts +132 -132
- package/extensions/annotate-git-diff/ui.ts +27 -27
- package/extensions/annotate-git-diff/watch.ts +74 -74
- package/extensions/annotate-git-diff/web/app.js +1947 -1884
- package/extensions/annotate-git-diff/web/index.html +5 -6
- package/extensions/annotate-last-message/README.md +1 -1
- package/extensions/annotate-last-message/glimpseui.d.ts +87 -78
- package/extensions/annotate-last-message/index.ts +210 -206
- package/extensions/annotate-last-message/package.json +1 -1
- package/extensions/annotate-last-message/prompt.ts +87 -66
- package/extensions/annotate-last-message/quiet-glimpse.ts +161 -155
- package/extensions/annotate-last-message/session.ts +94 -89
- package/extensions/annotate-last-message/theme.ts +199 -193
- package/extensions/annotate-last-message/types.ts +23 -23
- package/extensions/annotate-last-message/ui.ts +27 -25
- package/extensions/annotate-last-message/web/app.js +4 -7
- package/extensions/annotate-last-message/web/md-renderer.js +1 -1
- package/extensions/brrr/README.md +1 -1
- package/extensions/brrr/index.ts +213 -191
- package/extensions/brrr/package.json +1 -1
- package/extensions/claude-fast/README.md +5 -79
- package/extensions/claude-fast/index.ts +31 -289
- package/extensions/claude-fast/package.json +2 -3
- package/extensions/code-reviewer/README.md +1 -1
- package/extensions/code-reviewer/index.ts +1598 -1350
- package/extensions/code-reviewer/package.json +1 -1
- package/extensions/confirm-destructive/README.md +1 -1
- package/extensions/confirm-destructive/index.ts +55 -51
- package/extensions/confirm-destructive/package.json +1 -1
- package/extensions/context-cap/index.ts +312 -304
- package/extensions/context-cap/package.json +1 -1
- package/extensions/context-inspector/index.ts +1211 -1064
- package/extensions/context-inspector/package.json +1 -1
- package/extensions/contrarian/index.ts +1854 -1735
- package/extensions/contrarian/package.json +1 -1
- package/extensions/dirty-repo-guard/index.ts +41 -41
- package/extensions/dynamic-context-pruning/README.md +6 -38
- package/extensions/dynamic-context-pruning/index.ts +2124 -1980
- package/extensions/dynamic-context-pruning/package.json +1 -1
- package/extensions/fast/README.md +50 -12
- package/extensions/fast/index.ts +513 -317
- package/extensions/fast/package.json +1 -1
- package/extensions/git-footer/README.md +1 -1
- package/extensions/git-footer/index.ts +458 -411
- package/extensions/git-footer/package.json +2 -3
- package/extensions/gnosis/index.ts +180 -130
- package/extensions/gnosis/package.json +1 -1
- package/extensions/inline-bash/index.ts +88 -88
- package/extensions/inline-bash/package.json +1 -1
- package/extensions/librarian/index.ts +1669 -1419
- package/extensions/librarian/package.json +1 -1
- package/extensions/minimal-footer/index.ts +994 -1012
- package/extensions/minimal-footer/openai-usage.ts +195 -190
- package/extensions/notify/index.ts +222 -230
- package/extensions/notify/package.json +1 -1
- package/extensions/openai-fast/README.md +5 -73
- package/extensions/openai-fast/index.ts +32 -272
- package/extensions/openai-fast/package.json +2 -3
- package/extensions/oracle/index.ts +1731 -1625
- package/extensions/oracle/package.json +1 -1
- package/extensions/permission-gate/index.ts +658 -608
- package/extensions/permission-gate/package.json +1 -1
- package/extensions/permission-gate/powershell-safety.ts +840 -793
- package/extensions/project-mcp-json/.pi-fleet-tested-version +1 -0
- package/extensions/project-mcp-json/README.md +199 -0
- package/extensions/project-mcp-json/config.ts +564 -0
- package/extensions/project-mcp-json/index.ts +119 -0
- package/extensions/project-mcp-json/package.json +38 -0
- package/extensions/quiet-tools/index.ts +282 -264
- package/extensions/review/README.md +2 -2
- package/extensions/review/index.ts +1745 -1872
- package/extensions/review/package.json +1 -1
- package/extensions/todo/index.ts +284 -265
- package/extensions/triage-comments/README.md +2 -2
- package/extensions/triage-comments/index.ts +2294 -1882
- package/extensions/triage-comments/package.json +1 -1
- package/package.json +6 -3
- package/assets/social-preview.png +0 -0
- package/extensions/claude-fast/claude-fast.example.json +0 -4
- package/extensions/openai-fast/openai-fast.example.json +0 -4
|
@@ -3,739 +3,780 @@
|
|
|
3
3
|
// badlogic/pi-diff-review. See ./README.md for attribution details.
|
|
4
4
|
import type { ExtensionAPI, ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
|
|
5
5
|
import { readSystemClipboard, writeSystemClipboard } from "./clipboard.js";
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
getCommitFiles,
|
|
8
|
+
getReviewWindowData,
|
|
9
|
+
isWorkingTreeCommitSha,
|
|
10
|
+
loadReviewFileContents,
|
|
11
|
+
} from "./git.js";
|
|
7
12
|
import { composeReviewPrompt } from "./prompt.js";
|
|
8
13
|
import { openQuietGlimpse, type QuietGlimpseWindow } from "./quiet-glimpse.js";
|
|
9
14
|
import { startReviewUiServer, type ReviewUiServer } from "./review-server.js";
|
|
10
15
|
import type {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
16
|
+
CommentSide,
|
|
17
|
+
DiffReviewComment,
|
|
18
|
+
ReviewCancelPayload,
|
|
19
|
+
ReviewClipboardReadPayload,
|
|
20
|
+
ReviewClipboardWritePayload,
|
|
21
|
+
ReviewCommitKind,
|
|
22
|
+
ReviewFile,
|
|
23
|
+
ReviewFileContents,
|
|
24
|
+
ReviewHostMessage,
|
|
25
|
+
ReviewRequestCommitPayload,
|
|
26
|
+
ReviewRequestFilePayload,
|
|
27
|
+
ReviewRequestReviewDataPayload,
|
|
28
|
+
ReviewScope,
|
|
29
|
+
ReviewSubmitPayload,
|
|
30
|
+
ReviewWindowMessage,
|
|
26
31
|
} from "./types.js";
|
|
27
32
|
import { createRepoChangeWatcher, type RepoChangeWatcher } from "./watch.js";
|
|
28
33
|
|
|
29
34
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
30
|
-
|
|
35
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
31
36
|
}
|
|
32
37
|
|
|
33
38
|
function isString(value: unknown): value is string {
|
|
34
|
-
|
|
39
|
+
return typeof value === "string";
|
|
35
40
|
}
|
|
36
41
|
|
|
37
42
|
function isNullableString(value: unknown): value is string | null | undefined {
|
|
38
|
-
|
|
43
|
+
return value == null || typeof value === "string";
|
|
39
44
|
}
|
|
40
45
|
|
|
41
46
|
function isCommentSide(value: unknown): value is CommentSide {
|
|
42
|
-
|
|
47
|
+
return value === "original" || value === "modified" || value === "file";
|
|
43
48
|
}
|
|
44
49
|
|
|
45
50
|
function isReviewScope(value: unknown): value is ReviewScope {
|
|
46
|
-
|
|
51
|
+
return value === "branch" || value === "commits" || value === "all";
|
|
47
52
|
}
|
|
48
53
|
|
|
49
54
|
function isReviewCommitKind(value: unknown): value is ReviewCommitKind | null | undefined {
|
|
50
|
-
|
|
55
|
+
return value == null || value === "commit" || value === "working-tree";
|
|
51
56
|
}
|
|
52
57
|
|
|
53
58
|
function hasNullableInteger(value: Record<string, unknown>, key: string): boolean {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
59
|
+
if (!Object.prototype.hasOwnProperty.call(value, key)) return false;
|
|
60
|
+
const field = value[key];
|
|
61
|
+
return field === null || (typeof field === "number" && Number.isInteger(field));
|
|
57
62
|
}
|
|
58
63
|
|
|
59
64
|
function isDiffReviewComment(value: unknown): value is DiffReviewComment {
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
65
|
+
if (!isRecord(value)) return false;
|
|
66
|
+
return (
|
|
67
|
+
isString(value.id) &&
|
|
68
|
+
isString(value.fileId) &&
|
|
69
|
+
isReviewScope(value.scope) &&
|
|
70
|
+
isNullableString(value.commitSha) &&
|
|
71
|
+
isNullableString(value.commitShort) &&
|
|
72
|
+
isReviewCommitKind(value.commitKind) &&
|
|
73
|
+
isCommentSide(value.side) &&
|
|
74
|
+
hasNullableInteger(value, "startLine") &&
|
|
75
|
+
hasNullableInteger(value, "endLine") &&
|
|
76
|
+
isString(value.body)
|
|
77
|
+
);
|
|
73
78
|
}
|
|
74
79
|
|
|
75
80
|
function parseSubmitPayload(value: unknown): ReviewSubmitPayload | null {
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
81
|
+
if (!isRecord(value) || value.type !== "submit") return null;
|
|
82
|
+
if (
|
|
83
|
+
!isString(value.overallComment) ||
|
|
84
|
+
!Array.isArray(value.comments) ||
|
|
85
|
+
!value.comments.every(isDiffReviewComment)
|
|
86
|
+
) {
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Fail-safe: only a literal false means the user explicitly clicked Submit.
|
|
91
|
+
// Missing, true, and malformed discriminator values remain editor-only drafts.
|
|
92
|
+
return {
|
|
93
|
+
type: "submit",
|
|
94
|
+
overallComment: value.overallComment,
|
|
95
|
+
comments: value.comments,
|
|
96
|
+
draft: value.draft !== false,
|
|
97
|
+
};
|
|
89
98
|
}
|
|
90
99
|
|
|
91
100
|
function isCancelPayload(value: unknown): value is ReviewCancelPayload {
|
|
92
|
-
|
|
101
|
+
return isRecord(value) && value.type === "cancel";
|
|
93
102
|
}
|
|
94
103
|
|
|
95
|
-
function hasMessageType(
|
|
96
|
-
|
|
104
|
+
function hasMessageType(
|
|
105
|
+
value: unknown,
|
|
106
|
+
type: ReviewWindowMessage["type"],
|
|
107
|
+
): value is Record<string, unknown> {
|
|
108
|
+
return isRecord(value) && value.type === type;
|
|
97
109
|
}
|
|
98
110
|
|
|
99
111
|
function isRequestFilePayload(value: unknown): value is ReviewRequestFilePayload {
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
112
|
+
return (
|
|
113
|
+
hasMessageType(value, "request-file") &&
|
|
114
|
+
isString(value.requestId) &&
|
|
115
|
+
isString(value.fileId) &&
|
|
116
|
+
isReviewScope(value.scope) &&
|
|
117
|
+
isNullableString(value.commitSha)
|
|
118
|
+
);
|
|
107
119
|
}
|
|
108
120
|
|
|
109
121
|
function isRequestCommitPayload(value: unknown): value is ReviewRequestCommitPayload {
|
|
110
|
-
|
|
122
|
+
return (
|
|
123
|
+
hasMessageType(value, "request-commit") && isString(value.requestId) && isString(value.sha)
|
|
124
|
+
);
|
|
111
125
|
}
|
|
112
126
|
|
|
113
127
|
function isRequestReviewDataPayload(value: unknown): value is ReviewRequestReviewDataPayload {
|
|
114
|
-
|
|
128
|
+
return hasMessageType(value, "request-review-data") && isString(value.requestId);
|
|
115
129
|
}
|
|
116
130
|
|
|
117
131
|
function isClipboardReadPayload(value: unknown): value is ReviewClipboardReadPayload {
|
|
118
|
-
|
|
132
|
+
return hasMessageType(value, "clipboard-read") && isString(value.requestId);
|
|
119
133
|
}
|
|
120
134
|
|
|
121
135
|
function isClipboardWritePayload(value: unknown): value is ReviewClipboardWritePayload {
|
|
122
|
-
|
|
136
|
+
return hasMessageType(value, "clipboard-write") && isString(value.text);
|
|
123
137
|
}
|
|
124
138
|
|
|
125
139
|
function escapeForInlineScript(value: string): string {
|
|
126
|
-
|
|
140
|
+
return value.replace(/</g, "\\u003c").replace(/>/g, "\\u003e").replace(/&/g, "\\u0026");
|
|
127
141
|
}
|
|
128
142
|
|
|
129
143
|
function hasReviewFeedback(payload: ReviewSubmitPayload): boolean {
|
|
130
|
-
|
|
144
|
+
return (
|
|
145
|
+
payload.overallComment.trim().length > 0 ||
|
|
146
|
+
payload.comments.some((comment) => comment.body.trim().length > 0)
|
|
147
|
+
);
|
|
131
148
|
}
|
|
132
149
|
|
|
133
150
|
function appendReviewPrompt(ctx: ExtensionCommandContext, prompt: string): void {
|
|
134
|
-
|
|
135
|
-
|
|
151
|
+
const prefix = ctx.ui.getEditorText().trim().length > 0 ? "\n\n" : "";
|
|
152
|
+
ctx.ui.pasteToEditor(`${prefix}${prompt}`);
|
|
136
153
|
}
|
|
137
154
|
|
|
138
155
|
export default function (pi: ExtensionAPI) {
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
156
|
+
let activeWindow: QuietGlimpseWindow | null = null;
|
|
157
|
+
let activeUiServer: ReviewUiServer | null = null;
|
|
158
|
+
let activeWatcher: RepoChangeWatcher | null = null;
|
|
159
|
+
let reviewOpening = false;
|
|
160
|
+
let reviewAttempt = 0;
|
|
161
|
+
const suppressedWindows = new WeakSet<QuietGlimpseWindow>();
|
|
162
|
+
|
|
163
|
+
function stopActiveWatcher(): void {
|
|
164
|
+
if (activeWatcher == null) return;
|
|
165
|
+
activeWatcher.dispose();
|
|
166
|
+
activeWatcher = null;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function closeActiveWindow(options: { suppressResults?: boolean } = {}): void {
|
|
170
|
+
reviewAttempt += 1;
|
|
171
|
+
const windowToClose = activeWindow;
|
|
172
|
+
activeWindow = null;
|
|
173
|
+
activeUiServer?.dispose();
|
|
174
|
+
activeUiServer = null;
|
|
175
|
+
stopActiveWatcher();
|
|
176
|
+
if (windowToClose == null) return;
|
|
177
|
+
if (options.suppressResults) {
|
|
178
|
+
suppressedWindows.add(windowToClose);
|
|
179
|
+
}
|
|
180
|
+
try {
|
|
181
|
+
windowToClose.close();
|
|
182
|
+
} catch {
|
|
183
|
+
// Window is already closing; ignore shutdown races.
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
async function reviewRepository(ctx: ExtensionCommandContext): Promise<void> {
|
|
188
|
+
if (ctx.mode !== "tui") {
|
|
189
|
+
ctx.ui.notify("annotate-git-diff requires interactive mode.", "error");
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if (reviewOpening || activeWindow != null || activeUiServer != null) {
|
|
194
|
+
ctx.ui.notify("A review window is already open.", "warning");
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
reviewOpening = true;
|
|
199
|
+
const attempt = ++reviewAttempt;
|
|
200
|
+
try {
|
|
201
|
+
let reviewData = await getReviewWindowData(pi, ctx.cwd);
|
|
202
|
+
if (attempt !== reviewAttempt) return;
|
|
203
|
+
const { repoRoot } = reviewData;
|
|
204
|
+
if (reviewData.files.length === 0 && reviewData.commits.length === 0) {
|
|
205
|
+
ctx.ui.notify("No reviewable files found.", "info");
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const uiServer = await startReviewUiServer(reviewData);
|
|
210
|
+
if (attempt !== reviewAttempt) {
|
|
211
|
+
uiServer.dispose();
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
activeUiServer = uiServer;
|
|
215
|
+
const window = await openQuietGlimpse(uiServer.html, {
|
|
216
|
+
width: 1680,
|
|
217
|
+
height: 1020,
|
|
218
|
+
title: "annotate-git-diff",
|
|
219
|
+
});
|
|
220
|
+
if (attempt !== reviewAttempt) {
|
|
221
|
+
suppressedWindows.add(window);
|
|
222
|
+
window.close();
|
|
223
|
+
uiServer.dispose();
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
activeWindow = window;
|
|
227
|
+
if (uiServer.failure != null) throw uiServer.failure;
|
|
228
|
+
if (window.failure != null) throw window.failure;
|
|
229
|
+
if (window.closed) throw new Error("Glimpse closed while the review window was starting.");
|
|
230
|
+
|
|
231
|
+
const historicalFiles = new Map<string, ReviewFile>();
|
|
232
|
+
const rememberFiles = (files: ReviewFile[]): void => {
|
|
233
|
+
for (const file of files) {
|
|
234
|
+
// A submitted comment may outlive a refresh. Keep the first metadata
|
|
235
|
+
// advertised for an id so refreshed authorization cannot rewrite its
|
|
236
|
+
// historical path in the composed prompt.
|
|
237
|
+
if (!historicalFiles.has(file.id)) historicalFiles.set(file.id, file);
|
|
238
|
+
}
|
|
239
|
+
};
|
|
240
|
+
rememberFiles(reviewData.files);
|
|
241
|
+
|
|
242
|
+
type ReviewReadSnapshot = {
|
|
243
|
+
generation: number;
|
|
244
|
+
data: typeof reviewData;
|
|
245
|
+
branchFiles: Map<string, ReviewFile>;
|
|
246
|
+
allFiles: Map<string, ReviewFile>;
|
|
247
|
+
commitFiles: Map<string, Map<string, ReviewFile>>;
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
const createReviewReadSnapshot = (
|
|
251
|
+
data: typeof reviewData,
|
|
252
|
+
generation: number,
|
|
253
|
+
previous: ReviewReadSnapshot | null,
|
|
254
|
+
): ReviewReadSnapshot => {
|
|
255
|
+
const branchFiles = new Map<string, ReviewFile>();
|
|
256
|
+
const allFiles = new Map<string, ReviewFile>();
|
|
257
|
+
for (const file of data.files) {
|
|
258
|
+
allFiles.set(file.id, file);
|
|
259
|
+
if (file.inGitDiff) branchFiles.set(file.id, file);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const commitFiles = new Map<string, Map<string, ReviewFile>>();
|
|
263
|
+
for (const commit of data.commits) {
|
|
264
|
+
if (commit.kind !== "commit") continue;
|
|
265
|
+
const retainedFiles = previous?.commitFiles.get(commit.sha);
|
|
266
|
+
if (retainedFiles != null) commitFiles.set(commit.sha, retainedFiles);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
return { generation, data, branchFiles, allFiles, commitFiles };
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
const commitFileCache = new Map<string, Promise<ReviewFile[]>>();
|
|
273
|
+
const contentCache = new Map<string, Promise<ReviewFileContents>>();
|
|
274
|
+
let currentSnapshot = createReviewReadSnapshot(reviewData, 0, null);
|
|
275
|
+
const isLiveWindow = (): boolean =>
|
|
276
|
+
activeWindow === window && attempt === reviewAttempt && !window.closed;
|
|
277
|
+
|
|
278
|
+
const cacheCommitSha = (cacheKey: string): string | null => {
|
|
279
|
+
if (!cacheKey.startsWith("commits:")) return null;
|
|
280
|
+
const rest = cacheKey.slice("commits:".length);
|
|
281
|
+
const separator = rest.indexOf(":");
|
|
282
|
+
return separator < 0 ? null : rest.slice(0, separator);
|
|
283
|
+
};
|
|
284
|
+
|
|
285
|
+
const pruneCachesForReviewData = (nextReviewData: typeof reviewData): void => {
|
|
286
|
+
const retainedImmutableCommits = new Set(
|
|
287
|
+
nextReviewData.commits
|
|
288
|
+
.filter((commit) => commit.kind === "commit")
|
|
289
|
+
.map((commit) => commit.sha),
|
|
290
|
+
);
|
|
291
|
+
for (const sha of commitFileCache.keys()) {
|
|
292
|
+
if (!retainedImmutableCommits.has(sha)) commitFileCache.delete(sha);
|
|
293
|
+
}
|
|
294
|
+
for (const cacheKey of contentCache.keys()) {
|
|
295
|
+
const commitSha = cacheCommitSha(cacheKey);
|
|
296
|
+
if (commitSha == null || !retainedImmutableCommits.has(commitSha)) {
|
|
297
|
+
contentCache.delete(cacheKey);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
};
|
|
301
|
+
|
|
302
|
+
const replaceReviewData = (nextReviewData: typeof reviewData): void => {
|
|
303
|
+
pruneCachesForReviewData(nextReviewData);
|
|
304
|
+
const previousSnapshot = currentSnapshot;
|
|
305
|
+
reviewData = nextReviewData;
|
|
306
|
+
rememberFiles(nextReviewData.files);
|
|
307
|
+
currentSnapshot = createReviewReadSnapshot(
|
|
308
|
+
nextReviewData,
|
|
309
|
+
previousSnapshot.generation + 1,
|
|
310
|
+
previousSnapshot,
|
|
311
|
+
);
|
|
312
|
+
};
|
|
313
|
+
|
|
314
|
+
const sendWindowMessage = (message: ReviewHostMessage, guard?: () => boolean): void => {
|
|
315
|
+
if (!isLiveWindow()) return;
|
|
316
|
+
if (guard != null && !guard()) return;
|
|
317
|
+
const payload = escapeForInlineScript(JSON.stringify(message));
|
|
318
|
+
window.send(`window.__reviewReceive(${payload});`);
|
|
319
|
+
};
|
|
320
|
+
|
|
321
|
+
let watcherWarningShown = false;
|
|
322
|
+
activeWatcher = createRepoChangeWatcher(
|
|
323
|
+
repoRoot,
|
|
324
|
+
() => {
|
|
325
|
+
sendWindowMessage({ type: "working-tree-changed", changedAt: Date.now() });
|
|
326
|
+
},
|
|
327
|
+
{
|
|
328
|
+
onError: (error) => {
|
|
329
|
+
if (watcherWarningShown || activeWindow !== window) return;
|
|
330
|
+
watcherWarningShown = true;
|
|
331
|
+
ctx.ui.notify(`Review change watcher failed: ${error.message}`, "warning");
|
|
332
|
+
},
|
|
333
|
+
},
|
|
334
|
+
);
|
|
335
|
+
|
|
336
|
+
const loadCommitFiles = (sha: string): Promise<ReviewFile[]> => {
|
|
337
|
+
const cached = commitFileCache.get(sha);
|
|
338
|
+
if (cached != null) return cached;
|
|
339
|
+
const pending = getCommitFiles(pi, repoRoot, sha);
|
|
340
|
+
commitFileCache.set(sha, pending);
|
|
341
|
+
pending
|
|
342
|
+
.then((commitFiles) => {
|
|
343
|
+
if (commitFileCache.get(sha) !== pending || !isLiveWindow()) return;
|
|
344
|
+
if (!currentSnapshot.data.commits.some((commit) => commit.sha === sha)) return;
|
|
345
|
+
rememberFiles(commitFiles);
|
|
346
|
+
currentSnapshot.commitFiles.set(
|
|
347
|
+
sha,
|
|
348
|
+
new Map(commitFiles.map((file) => [file.id, file])),
|
|
349
|
+
);
|
|
350
|
+
})
|
|
351
|
+
.catch(() => {
|
|
352
|
+
// A rejected promise is retryable, but an old rejection must not
|
|
353
|
+
// evict a replacement request for the same key.
|
|
354
|
+
if (isLiveWindow() && commitFileCache.get(sha) === pending) commitFileCache.delete(sha);
|
|
355
|
+
});
|
|
356
|
+
return pending;
|
|
357
|
+
};
|
|
358
|
+
|
|
359
|
+
const loadContents = (
|
|
360
|
+
file: ReviewFile,
|
|
361
|
+
scope: ReviewRequestFilePayload["scope"],
|
|
362
|
+
commitSha: string | null,
|
|
363
|
+
snapshot: ReviewReadSnapshot,
|
|
364
|
+
): Promise<ReviewFileContents> => {
|
|
365
|
+
const cacheKey = `${scope}:${commitSha ?? ""}:${file.id}`;
|
|
366
|
+
const cached = contentCache.get(cacheKey);
|
|
367
|
+
if (cached != null) return cached;
|
|
368
|
+
|
|
369
|
+
const pending = loadReviewFileContents(
|
|
370
|
+
pi,
|
|
371
|
+
repoRoot,
|
|
372
|
+
file,
|
|
373
|
+
scope,
|
|
374
|
+
commitSha,
|
|
375
|
+
snapshot.data.branchMergeBaseSha,
|
|
376
|
+
);
|
|
377
|
+
contentCache.set(cacheKey, pending);
|
|
378
|
+
pending.catch(() => {
|
|
379
|
+
// Do not let an older rejection remove a newer retry's promise.
|
|
380
|
+
if (isLiveWindow() && contentCache.get(cacheKey) === pending)
|
|
381
|
+
contentCache.delete(cacheKey);
|
|
382
|
+
});
|
|
383
|
+
return pending;
|
|
384
|
+
};
|
|
385
|
+
|
|
386
|
+
const isAdvertisedCommitSha = (snapshot: ReviewReadSnapshot, sha: string): boolean =>
|
|
387
|
+
snapshot.data.commits.some((commit) => commit.sha === sha);
|
|
388
|
+
|
|
389
|
+
const isCurrentCommitRequest = (
|
|
390
|
+
snapshot: ReviewReadSnapshot,
|
|
391
|
+
sha: string,
|
|
392
|
+
pending: Promise<ReviewFile[]> | null = null,
|
|
393
|
+
): boolean => {
|
|
394
|
+
if (!isLiveWindow()) return false;
|
|
395
|
+
const commit = currentSnapshot.data.commits.find((item) => item.sha === sha);
|
|
396
|
+
if (commit == null) return false;
|
|
397
|
+
if (currentSnapshot === snapshot) return true;
|
|
398
|
+
return commit.kind === "commit" && pending != null && commitFileCache.get(sha) === pending;
|
|
399
|
+
};
|
|
400
|
+
|
|
401
|
+
const isCurrentFileRequest = (
|
|
402
|
+
snapshot: ReviewReadSnapshot,
|
|
403
|
+
scope: ReviewRequestFilePayload["scope"],
|
|
404
|
+
commitSha: string | null,
|
|
405
|
+
file: ReviewFile,
|
|
406
|
+
): boolean => {
|
|
407
|
+
if (!isLiveWindow()) return false;
|
|
408
|
+
if (scope === "commits") {
|
|
409
|
+
if (commitSha == null || !isAdvertisedCommitSha(currentSnapshot, commitSha)) return false;
|
|
410
|
+
if (isWorkingTreeCommitSha(commitSha) && currentSnapshot !== snapshot) return false;
|
|
411
|
+
return currentSnapshot.commitFiles.get(commitSha)?.get(file.id) === file;
|
|
412
|
+
}
|
|
413
|
+
if (currentSnapshot !== snapshot) return false;
|
|
414
|
+
const files = scope === "branch" ? currentSnapshot.branchFiles : currentSnapshot.allFiles;
|
|
415
|
+
return files.get(file.id) === file;
|
|
416
|
+
};
|
|
417
|
+
|
|
418
|
+
const terminalMessagePromise = new Promise<ReviewSubmitPayload | ReviewCancelPayload | null>(
|
|
419
|
+
(resolve, reject) => {
|
|
420
|
+
let settled = false;
|
|
421
|
+
let closeTimer: ReturnType<typeof setTimeout> | null = null;
|
|
422
|
+
let removeUiServerErrorListener = (): void => {};
|
|
423
|
+
|
|
424
|
+
const requestWindowClose = (): void => {
|
|
425
|
+
try {
|
|
426
|
+
window.close();
|
|
427
|
+
} catch {
|
|
428
|
+
// Ignore races when the native process has already exited.
|
|
429
|
+
}
|
|
430
|
+
};
|
|
431
|
+
|
|
432
|
+
const cleanup = (): void => {
|
|
433
|
+
if (closeTimer != null) {
|
|
434
|
+
clearTimeout(closeTimer);
|
|
435
|
+
closeTimer = null;
|
|
436
|
+
}
|
|
437
|
+
window.removeListener("message", onMessage);
|
|
438
|
+
window.removeListener("closed", onClosed);
|
|
439
|
+
window.removeListener("error", onError);
|
|
440
|
+
removeUiServerErrorListener();
|
|
441
|
+
removeUiServerErrorListener = (): void => {};
|
|
442
|
+
if (activeWindow === window) {
|
|
443
|
+
activeWindow = null;
|
|
444
|
+
stopActiveWatcher();
|
|
445
|
+
}
|
|
446
|
+
if (activeUiServer === uiServer) activeUiServer = null;
|
|
447
|
+
uiServer.dispose();
|
|
448
|
+
};
|
|
449
|
+
|
|
450
|
+
const settle = (value: ReviewSubmitPayload | ReviewCancelPayload | null): void => {
|
|
451
|
+
if (settled) return;
|
|
452
|
+
settled = true;
|
|
453
|
+
cleanup();
|
|
454
|
+
resolve(value);
|
|
455
|
+
};
|
|
456
|
+
|
|
457
|
+
const handleRequestFile = async (message: ReviewRequestFilePayload): Promise<void> => {
|
|
458
|
+
const snapshot = currentSnapshot;
|
|
459
|
+
const currentGuard = (): boolean => isLiveWindow() && currentSnapshot === snapshot;
|
|
460
|
+
const commitSha = message.commitSha ?? null;
|
|
461
|
+
if (message.scope === "commits") {
|
|
462
|
+
if (commitSha == null) {
|
|
463
|
+
sendWindowMessage(
|
|
464
|
+
{
|
|
465
|
+
type: "file-error",
|
|
466
|
+
requestId: message.requestId,
|
|
467
|
+
fileId: message.fileId,
|
|
468
|
+
scope: message.scope,
|
|
469
|
+
commitSha: null,
|
|
470
|
+
message: "A commit SHA is required for commit-scoped file requests.",
|
|
471
|
+
},
|
|
472
|
+
currentGuard,
|
|
473
|
+
);
|
|
474
|
+
return;
|
|
475
|
+
}
|
|
476
|
+
if (!isAdvertisedCommitSha(snapshot, commitSha)) {
|
|
477
|
+
sendWindowMessage(
|
|
478
|
+
{
|
|
479
|
+
type: "file-error",
|
|
480
|
+
requestId: message.requestId,
|
|
481
|
+
fileId: message.fileId,
|
|
482
|
+
scope: message.scope,
|
|
483
|
+
commitSha,
|
|
484
|
+
message: "Unknown commit requested.",
|
|
485
|
+
},
|
|
486
|
+
currentGuard,
|
|
487
|
+
);
|
|
488
|
+
return;
|
|
489
|
+
}
|
|
490
|
+
} else if (commitSha != null) {
|
|
491
|
+
sendWindowMessage(
|
|
492
|
+
{
|
|
493
|
+
type: "file-error",
|
|
494
|
+
requestId: message.requestId,
|
|
495
|
+
fileId: message.fileId,
|
|
496
|
+
scope: message.scope,
|
|
497
|
+
commitSha,
|
|
498
|
+
message: "A commit SHA is only valid for commit-scoped file requests.",
|
|
499
|
+
},
|
|
500
|
+
currentGuard,
|
|
501
|
+
);
|
|
502
|
+
return;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
const fileMap =
|
|
506
|
+
message.scope === "branch"
|
|
507
|
+
? snapshot.branchFiles
|
|
508
|
+
: message.scope === "all"
|
|
509
|
+
? snapshot.allFiles
|
|
510
|
+
: (snapshot.commitFiles.get(commitSha as string) ??
|
|
511
|
+
new Map<string, ReviewFile>());
|
|
512
|
+
const file = fileMap.get(message.fileId);
|
|
513
|
+
if (file == null) {
|
|
514
|
+
sendWindowMessage(
|
|
515
|
+
{
|
|
516
|
+
type: "file-error",
|
|
517
|
+
requestId: message.requestId,
|
|
518
|
+
fileId: message.fileId,
|
|
519
|
+
scope: message.scope,
|
|
520
|
+
commitSha,
|
|
521
|
+
message: "Unknown file requested.",
|
|
522
|
+
},
|
|
523
|
+
currentGuard,
|
|
524
|
+
);
|
|
525
|
+
return;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
try {
|
|
529
|
+
const contents = await loadContents(file, message.scope, commitSha, snapshot);
|
|
530
|
+
if (!isCurrentFileRequest(snapshot, message.scope, commitSha, file)) return;
|
|
531
|
+
sendWindowMessage(
|
|
532
|
+
{
|
|
533
|
+
type: "file-data",
|
|
534
|
+
requestId: message.requestId,
|
|
535
|
+
fileId: message.fileId,
|
|
536
|
+
scope: message.scope,
|
|
537
|
+
commitSha,
|
|
538
|
+
originalContent: contents.originalContent,
|
|
539
|
+
modifiedContent: contents.modifiedContent,
|
|
540
|
+
kind: contents.kind,
|
|
541
|
+
mimeType: contents.mimeType,
|
|
542
|
+
originalExists: contents.originalExists,
|
|
543
|
+
modifiedExists: contents.modifiedExists,
|
|
544
|
+
originalPreviewUrl: contents.originalPreviewUrl,
|
|
545
|
+
modifiedPreviewUrl: contents.modifiedPreviewUrl,
|
|
546
|
+
},
|
|
547
|
+
() => isCurrentFileRequest(snapshot, message.scope, commitSha, file),
|
|
548
|
+
);
|
|
549
|
+
} catch (error) {
|
|
550
|
+
if (!isCurrentFileRequest(snapshot, message.scope, commitSha, file)) return;
|
|
551
|
+
const messageText = error instanceof Error ? error.message : String(error);
|
|
552
|
+
sendWindowMessage(
|
|
553
|
+
{
|
|
554
|
+
type: "file-error",
|
|
555
|
+
requestId: message.requestId,
|
|
556
|
+
fileId: message.fileId,
|
|
557
|
+
scope: message.scope,
|
|
558
|
+
commitSha,
|
|
559
|
+
message: messageText,
|
|
560
|
+
},
|
|
561
|
+
() => isCurrentFileRequest(snapshot, message.scope, commitSha, file),
|
|
562
|
+
);
|
|
563
|
+
}
|
|
564
|
+
};
|
|
565
|
+
|
|
566
|
+
const handleRequestCommit = async (
|
|
567
|
+
message: ReviewRequestCommitPayload,
|
|
568
|
+
): Promise<void> => {
|
|
569
|
+
const snapshot = currentSnapshot;
|
|
570
|
+
const currentGuard = (): boolean => isLiveWindow() && currentSnapshot === snapshot;
|
|
571
|
+
if (!isAdvertisedCommitSha(snapshot, message.sha)) {
|
|
572
|
+
sendWindowMessage(
|
|
573
|
+
{
|
|
574
|
+
type: "commit-error",
|
|
575
|
+
requestId: message.requestId,
|
|
576
|
+
sha: message.sha,
|
|
577
|
+
message: "Unknown commit requested.",
|
|
578
|
+
},
|
|
579
|
+
currentGuard,
|
|
580
|
+
);
|
|
581
|
+
return;
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
const pending = loadCommitFiles(message.sha);
|
|
585
|
+
try {
|
|
586
|
+
const commitFiles = await pending;
|
|
587
|
+
if (!isCurrentCommitRequest(snapshot, message.sha, pending)) return;
|
|
588
|
+
if (currentSnapshot.commitFiles.get(message.sha) == null) return;
|
|
589
|
+
sendWindowMessage(
|
|
590
|
+
{
|
|
591
|
+
type: "commit-data",
|
|
592
|
+
requestId: message.requestId,
|
|
593
|
+
sha: message.sha,
|
|
594
|
+
files: commitFiles,
|
|
595
|
+
},
|
|
596
|
+
() =>
|
|
597
|
+
isCurrentCommitRequest(snapshot, message.sha, pending) &&
|
|
598
|
+
currentSnapshot.commitFiles.get(message.sha) != null,
|
|
599
|
+
);
|
|
600
|
+
} catch (error) {
|
|
601
|
+
if (!isCurrentCommitRequest(snapshot, message.sha, pending)) return;
|
|
602
|
+
const messageText = error instanceof Error ? error.message : String(error);
|
|
603
|
+
sendWindowMessage({
|
|
604
|
+
type: "commit-error",
|
|
605
|
+
requestId: message.requestId,
|
|
606
|
+
sha: message.sha,
|
|
607
|
+
message: messageText,
|
|
608
|
+
});
|
|
609
|
+
}
|
|
610
|
+
};
|
|
611
|
+
|
|
612
|
+
let refreshRequestSequence = 0;
|
|
613
|
+
let newestRefreshRequest = 0;
|
|
614
|
+
const handleRequestReviewData = async (
|
|
615
|
+
message: ReviewRequestReviewDataPayload,
|
|
616
|
+
): Promise<void> => {
|
|
617
|
+
const refreshRequest = ++refreshRequestSequence;
|
|
618
|
+
newestRefreshRequest = refreshRequest;
|
|
619
|
+
try {
|
|
620
|
+
const nextReviewData = await getReviewWindowData(pi, repoRoot);
|
|
621
|
+
if (!isLiveWindow() || refreshRequest !== newestRefreshRequest) return;
|
|
622
|
+
replaceReviewData(nextReviewData);
|
|
623
|
+
sendWindowMessage(
|
|
624
|
+
{
|
|
625
|
+
type: "review-data",
|
|
626
|
+
requestId: message.requestId,
|
|
627
|
+
files: nextReviewData.files,
|
|
628
|
+
commits: nextReviewData.commits,
|
|
629
|
+
branchBaseRef: nextReviewData.branchBaseRef,
|
|
630
|
+
branchMergeBaseSha: nextReviewData.branchMergeBaseSha,
|
|
631
|
+
repositoryHasHead: nextReviewData.repositoryHasHead,
|
|
632
|
+
},
|
|
633
|
+
() => isLiveWindow() && refreshRequest === newestRefreshRequest,
|
|
634
|
+
);
|
|
635
|
+
} catch (error) {
|
|
636
|
+
if (!isLiveWindow() || refreshRequest !== newestRefreshRequest) return;
|
|
637
|
+
const messageText = error instanceof Error ? error.message : String(error);
|
|
638
|
+
sendWindowMessage({
|
|
639
|
+
type: "review-data-error",
|
|
640
|
+
requestId: message.requestId,
|
|
641
|
+
message: messageText,
|
|
642
|
+
});
|
|
643
|
+
}
|
|
644
|
+
};
|
|
645
|
+
|
|
646
|
+
const handleClipboardRead = (message: ReviewClipboardReadPayload): void => {
|
|
647
|
+
try {
|
|
648
|
+
sendWindowMessage({
|
|
649
|
+
type: "clipboard-data",
|
|
650
|
+
requestId: message.requestId,
|
|
651
|
+
text: readSystemClipboard(),
|
|
652
|
+
});
|
|
653
|
+
} catch (error) {
|
|
654
|
+
const messageText = error instanceof Error ? error.message : String(error);
|
|
655
|
+
sendWindowMessage({
|
|
656
|
+
type: "clipboard-data",
|
|
657
|
+
requestId: message.requestId,
|
|
658
|
+
text: "",
|
|
659
|
+
message: messageText,
|
|
660
|
+
});
|
|
661
|
+
}
|
|
662
|
+
};
|
|
663
|
+
|
|
664
|
+
const handleClipboardWrite = (message: ReviewClipboardWritePayload): void => {
|
|
665
|
+
try {
|
|
666
|
+
writeSystemClipboard(message.text);
|
|
667
|
+
} catch (error) {
|
|
668
|
+
const messageText = error instanceof Error ? error.message : String(error);
|
|
669
|
+
ctx.ui.notify(`Failed to copy from review window: ${messageText}`, "warning");
|
|
670
|
+
}
|
|
671
|
+
};
|
|
672
|
+
|
|
673
|
+
const onMessage = (message: unknown): void => {
|
|
674
|
+
// Terminal messages remain recoverable during the native close grace
|
|
675
|
+
// period; all other work must stop once the window is closed or retired.
|
|
676
|
+
const submit = parseSubmitPayload(message);
|
|
677
|
+
if (submit != null) {
|
|
678
|
+
settle(submit);
|
|
679
|
+
requestWindowClose();
|
|
680
|
+
return;
|
|
681
|
+
}
|
|
682
|
+
if (isCancelPayload(message)) {
|
|
683
|
+
settle(message);
|
|
684
|
+
requestWindowClose();
|
|
685
|
+
return;
|
|
686
|
+
}
|
|
687
|
+
if (!isLiveWindow()) return;
|
|
688
|
+
if (isRequestFilePayload(message)) {
|
|
689
|
+
void handleRequestFile(message);
|
|
690
|
+
return;
|
|
691
|
+
}
|
|
692
|
+
if (isRequestCommitPayload(message)) {
|
|
693
|
+
void handleRequestCommit(message);
|
|
694
|
+
return;
|
|
695
|
+
}
|
|
696
|
+
if (isRequestReviewDataPayload(message)) {
|
|
697
|
+
void handleRequestReviewData(message);
|
|
698
|
+
return;
|
|
699
|
+
}
|
|
700
|
+
if (isClipboardReadPayload(message)) {
|
|
701
|
+
handleClipboardRead(message);
|
|
702
|
+
return;
|
|
703
|
+
}
|
|
704
|
+
if (isClipboardWritePayload(message)) {
|
|
705
|
+
handleClipboardWrite(message);
|
|
706
|
+
}
|
|
707
|
+
};
|
|
708
|
+
|
|
709
|
+
const onClosed = (): void => {
|
|
710
|
+
if (settled || closeTimer != null) return;
|
|
711
|
+
closeTimer = setTimeout(() => {
|
|
712
|
+
closeTimer = null;
|
|
713
|
+
settle(null);
|
|
714
|
+
}, 250);
|
|
715
|
+
};
|
|
716
|
+
|
|
717
|
+
const onError = (error: Error): void => {
|
|
718
|
+
if (settled) return;
|
|
719
|
+
settled = true;
|
|
720
|
+
requestWindowClose();
|
|
721
|
+
cleanup();
|
|
722
|
+
reject(error);
|
|
723
|
+
};
|
|
724
|
+
|
|
725
|
+
window.on("message", onMessage);
|
|
726
|
+
window.on("closed", onClosed);
|
|
727
|
+
window.on("error", onError);
|
|
728
|
+
removeUiServerErrorListener = uiServer.onError(onError);
|
|
729
|
+
if (uiServer.failure != null) onError(uiServer.failure);
|
|
730
|
+
else if (window.failure != null) onError(window.failure);
|
|
731
|
+
else if (window.closed) onClosed();
|
|
732
|
+
},
|
|
733
|
+
);
|
|
734
|
+
|
|
735
|
+
void (async () => {
|
|
736
|
+
try {
|
|
737
|
+
const message = await terminalMessagePromise;
|
|
738
|
+
if (suppressedWindows.has(window) || attempt !== reviewAttempt) return;
|
|
739
|
+
if (message == null) return;
|
|
740
|
+
if (message.type === "cancel") {
|
|
741
|
+
ctx.ui.notify("Review cancelled.", "info");
|
|
742
|
+
return;
|
|
743
|
+
}
|
|
744
|
+
if (!hasReviewFeedback(message)) return;
|
|
745
|
+
|
|
746
|
+
const prompt = composeReviewPrompt([...historicalFiles.values()], message);
|
|
747
|
+
if (message.draft === true) {
|
|
748
|
+
appendReviewPrompt(ctx, prompt);
|
|
749
|
+
ctx.ui.notify("Appended review feedback to the editor.", "info");
|
|
750
|
+
} else {
|
|
751
|
+
pi.sendUserMessage(prompt, { deliverAs: "followUp" });
|
|
752
|
+
ctx.ui.notify("Review feedback sent to the agent.", "info");
|
|
753
|
+
}
|
|
754
|
+
} catch (error) {
|
|
755
|
+
if (suppressedWindows.has(window) || attempt !== reviewAttempt) return;
|
|
756
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
757
|
+
ctx.ui.notify(`Review failed: ${message}`, "error");
|
|
758
|
+
}
|
|
759
|
+
})();
|
|
760
|
+
|
|
761
|
+
ctx.ui.notify("Opened native review window.", "info");
|
|
762
|
+
} catch (error) {
|
|
763
|
+
if (attempt !== reviewAttempt) return;
|
|
764
|
+
closeActiveWindow({ suppressResults: true });
|
|
765
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
766
|
+
ctx.ui.notify(`Review failed: ${message}`, "error");
|
|
767
|
+
} finally {
|
|
768
|
+
reviewOpening = false;
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
pi.registerCommand("annotate-git-diff", {
|
|
773
|
+
description: "Open a native review window with branch, per-commit, and all-files scopes",
|
|
774
|
+
handler: async (_args, ctx) => {
|
|
775
|
+
await reviewRepository(ctx);
|
|
776
|
+
},
|
|
777
|
+
});
|
|
778
|
+
|
|
779
|
+
pi.on("session_shutdown", async () => {
|
|
780
|
+
closeActiveWindow({ suppressResults: true });
|
|
781
|
+
});
|
|
741
782
|
}
|