@diegopetrucci/pi-extensions 0.1.71 → 0.1.73
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.md +2 -2
- package/extensions/agent-workflow-audit/index.ts +41 -8
- package/extensions/agent-workflow-audit/package.json +1 -1
- package/extensions/annotate-git-diff/README.md +7 -3
- package/extensions/annotate-git-diff/git.ts +3 -2
- package/extensions/annotate-git-diff/index.ts +366 -81
- package/extensions/annotate-git-diff/package.json +2 -2
- package/extensions/annotate-git-diff/quiet-glimpse.ts +4 -2
- package/extensions/annotate-git-diff/types.ts +6 -0
- package/extensions/annotate-git-diff/web/app.js +54 -4
- package/extensions/annotate-git-diff/web/index.html +2 -2
- package/extensions/annotate-last-message/README.md +9 -1
- package/extensions/annotate-last-message/index.ts +53 -13
- package/extensions/annotate-last-message/package.json +2 -1
- package/extensions/annotate-last-message/quiet-glimpse.ts +4 -2
- package/extensions/annotate-last-message/theme.ts +247 -0
- package/extensions/annotate-last-message/ui.ts +28 -2
- package/extensions/annotate-last-message/web/app.js +351 -162
- package/extensions/annotate-last-message/web/index.html +153 -24
- package/extensions/annotate-last-message/web/md-renderer.js +458 -0
- package/extensions/code-reviewer/index.ts +12 -1
- package/extensions/code-reviewer/package.json +2 -1
- package/extensions/context-cap/README.md +33 -4
- package/extensions/context-cap/index.ts +271 -68
- package/extensions/context-cap/package.json +1 -1
- package/extensions/context-inspector/README.md +2 -0
- package/extensions/context-inspector/index.ts +90 -9
- package/extensions/context-inspector/package.json +1 -1
- package/extensions/contrarian/index.ts +21 -15
- package/extensions/contrarian/package.json +1 -1
- package/extensions/dynamic-context-pruning/README.md +17 -5
- package/extensions/dynamic-context-pruning/index.ts +17 -4
- package/extensions/dynamic-context-pruning/package.json +1 -1
- package/extensions/fast/README.md +25 -4
- package/extensions/fast/index.ts +4 -4
- package/extensions/fast/package.json +1 -1
- package/extensions/git-footer/README.md +1 -0
- package/extensions/git-footer/package.json +1 -1
- package/extensions/librarian/index.ts +7 -0
- package/extensions/librarian/package.json +2 -1
- package/extensions/minimal-footer/README.md +16 -4
- package/extensions/minimal-footer/index.ts +263 -14
- package/extensions/minimal-footer/openai-usage.ts +62 -8
- package/extensions/minimal-footer/package.json +1 -1
- package/extensions/oracle/index.ts +19 -18
- package/extensions/oracle/package.json +1 -1
- package/extensions/permission-gate/README.md +6 -0
- package/extensions/permission-gate/index.ts +37 -16
- package/extensions/permission-gate/package.json +1 -1
- package/extensions/quiet-tools/README.md +1 -1
- package/extensions/quiet-tools/package.json +1 -1
- package/extensions/triage-comments/package.json +2 -1
- package/package.json +4 -4
|
@@ -8,50 +8,118 @@ import { composeReviewPrompt } from "./prompt.js";
|
|
|
8
8
|
import { openQuietGlimpse, type QuietGlimpseWindow } from "./quiet-glimpse.js";
|
|
9
9
|
import { startReviewUiServer, type ReviewUiServer } from "./review-server.js";
|
|
10
10
|
import type {
|
|
11
|
+
CommentSide,
|
|
12
|
+
DiffReviewComment,
|
|
11
13
|
ReviewCancelPayload,
|
|
12
14
|
ReviewClipboardReadPayload,
|
|
13
15
|
ReviewClipboardWritePayload,
|
|
16
|
+
ReviewCommitKind,
|
|
14
17
|
ReviewFile,
|
|
15
18
|
ReviewFileContents,
|
|
16
19
|
ReviewHostMessage,
|
|
17
20
|
ReviewRequestCommitPayload,
|
|
18
21
|
ReviewRequestFilePayload,
|
|
19
22
|
ReviewRequestReviewDataPayload,
|
|
23
|
+
ReviewScope,
|
|
20
24
|
ReviewSubmitPayload,
|
|
21
25
|
ReviewWindowMessage,
|
|
22
26
|
} from "./types.js";
|
|
23
27
|
import { createRepoChangeWatcher, type RepoChangeWatcher } from "./watch.js";
|
|
24
28
|
|
|
25
|
-
function
|
|
26
|
-
return typeof value === "object" && value
|
|
29
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
30
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
27
31
|
}
|
|
28
32
|
|
|
29
|
-
function
|
|
30
|
-
return
|
|
33
|
+
function isString(value: unknown): value is string {
|
|
34
|
+
return typeof value === "string";
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function isNullableString(value: unknown): value is string | null | undefined {
|
|
38
|
+
return value == null || typeof value === "string";
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function isCommentSide(value: unknown): value is CommentSide {
|
|
42
|
+
return value === "original" || value === "modified" || value === "file";
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function isReviewScope(value: unknown): value is ReviewScope {
|
|
46
|
+
return value === "branch" || value === "commits" || value === "all";
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function isReviewCommitKind(value: unknown): value is ReviewCommitKind | null | undefined {
|
|
50
|
+
return value == null || value === "commit" || value === "working-tree";
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function hasNullableInteger(value: Record<string, unknown>, key: string): boolean {
|
|
54
|
+
if (!Object.prototype.hasOwnProperty.call(value, key)) return false;
|
|
55
|
+
const field = value[key];
|
|
56
|
+
return field === null || (typeof field === "number" && Number.isInteger(field));
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function isDiffReviewComment(value: unknown): value is DiffReviewComment {
|
|
60
|
+
if (!isRecord(value)) return false;
|
|
61
|
+
return (
|
|
62
|
+
isString(value.id) &&
|
|
63
|
+
isString(value.fileId) &&
|
|
64
|
+
isReviewScope(value.scope) &&
|
|
65
|
+
isNullableString(value.commitSha) &&
|
|
66
|
+
isNullableString(value.commitShort) &&
|
|
67
|
+
isReviewCommitKind(value.commitKind) &&
|
|
68
|
+
isCommentSide(value.side) &&
|
|
69
|
+
hasNullableInteger(value, "startLine") &&
|
|
70
|
+
hasNullableInteger(value, "endLine") &&
|
|
71
|
+
isString(value.body)
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function parseSubmitPayload(value: unknown): ReviewSubmitPayload | null {
|
|
76
|
+
if (!isRecord(value) || value.type !== "submit") return null;
|
|
77
|
+
if (!isString(value.overallComment) || !Array.isArray(value.comments) || !value.comments.every(isDiffReviewComment)) {
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Fail-safe: only a literal false means the user explicitly clicked Submit.
|
|
82
|
+
// Missing, true, and malformed discriminator values remain editor-only drafts.
|
|
83
|
+
return {
|
|
84
|
+
type: "submit",
|
|
85
|
+
overallComment: value.overallComment,
|
|
86
|
+
comments: value.comments,
|
|
87
|
+
draft: value.draft !== false,
|
|
88
|
+
};
|
|
31
89
|
}
|
|
32
90
|
|
|
33
91
|
function isCancelPayload(value: unknown): value is ReviewCancelPayload {
|
|
34
|
-
return
|
|
92
|
+
return isRecord(value) && value.type === "cancel";
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function hasMessageType(value: unknown, type: ReviewWindowMessage["type"]): value is Record<string, unknown> {
|
|
96
|
+
return isRecord(value) && value.type === type;
|
|
35
97
|
}
|
|
36
98
|
|
|
37
99
|
function isRequestFilePayload(value: unknown): value is ReviewRequestFilePayload {
|
|
38
|
-
return
|
|
100
|
+
return (
|
|
101
|
+
hasMessageType(value, "request-file") &&
|
|
102
|
+
isString(value.requestId) &&
|
|
103
|
+
isString(value.fileId) &&
|
|
104
|
+
isReviewScope(value.scope) &&
|
|
105
|
+
isNullableString(value.commitSha)
|
|
106
|
+
);
|
|
39
107
|
}
|
|
40
108
|
|
|
41
109
|
function isRequestCommitPayload(value: unknown): value is ReviewRequestCommitPayload {
|
|
42
|
-
return hasMessageType(value, "request-commit");
|
|
110
|
+
return hasMessageType(value, "request-commit") && isString(value.requestId) && isString(value.sha);
|
|
43
111
|
}
|
|
44
112
|
|
|
45
113
|
function isRequestReviewDataPayload(value: unknown): value is ReviewRequestReviewDataPayload {
|
|
46
|
-
return hasMessageType(value, "request-review-data");
|
|
114
|
+
return hasMessageType(value, "request-review-data") && isString(value.requestId);
|
|
47
115
|
}
|
|
48
116
|
|
|
49
117
|
function isClipboardReadPayload(value: unknown): value is ReviewClipboardReadPayload {
|
|
50
|
-
return hasMessageType(value, "clipboard-read");
|
|
118
|
+
return hasMessageType(value, "clipboard-read") && isString(value.requestId);
|
|
51
119
|
}
|
|
52
120
|
|
|
53
121
|
function isClipboardWritePayload(value: unknown): value is ReviewClipboardWritePayload {
|
|
54
|
-
return hasMessageType(value, "clipboard-write");
|
|
122
|
+
return hasMessageType(value, "clipboard-write") && isString(value.text);
|
|
55
123
|
}
|
|
56
124
|
|
|
57
125
|
function escapeForInlineScript(value: string): string {
|
|
@@ -143,21 +211,85 @@ export default function (pi: ExtensionAPI) {
|
|
|
143
211
|
if (window.failure != null) throw window.failure;
|
|
144
212
|
if (window.closed) throw new Error("Glimpse closed while the review window was starting.");
|
|
145
213
|
|
|
146
|
-
const
|
|
214
|
+
const historicalFiles = new Map<string, ReviewFile>();
|
|
215
|
+
const rememberFiles = (files: ReviewFile[]): void => {
|
|
216
|
+
for (const file of files) {
|
|
217
|
+
// A submitted comment may outlive a refresh. Keep the first metadata
|
|
218
|
+
// advertised for an id so refreshed authorization cannot rewrite its
|
|
219
|
+
// historical path in the composed prompt.
|
|
220
|
+
if (!historicalFiles.has(file.id)) historicalFiles.set(file.id, file);
|
|
221
|
+
}
|
|
222
|
+
};
|
|
223
|
+
rememberFiles(reviewData.files);
|
|
224
|
+
|
|
225
|
+
type ReviewReadSnapshot = {
|
|
226
|
+
generation: number;
|
|
227
|
+
data: typeof reviewData;
|
|
228
|
+
branchFiles: Map<string, ReviewFile>;
|
|
229
|
+
allFiles: Map<string, ReviewFile>;
|
|
230
|
+
commitFiles: Map<string, Map<string, ReviewFile>>;
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
const createReviewReadSnapshot = (
|
|
234
|
+
data: typeof reviewData,
|
|
235
|
+
generation: number,
|
|
236
|
+
previous: ReviewReadSnapshot | null,
|
|
237
|
+
): ReviewReadSnapshot => {
|
|
238
|
+
const branchFiles = new Map<string, ReviewFile>();
|
|
239
|
+
const allFiles = new Map<string, ReviewFile>();
|
|
240
|
+
for (const file of data.files) {
|
|
241
|
+
allFiles.set(file.id, file);
|
|
242
|
+
if (file.inGitDiff) branchFiles.set(file.id, file);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const commitFiles = new Map<string, Map<string, ReviewFile>>();
|
|
246
|
+
for (const commit of data.commits) {
|
|
247
|
+
if (commit.kind !== "commit") continue;
|
|
248
|
+
const retainedFiles = previous?.commitFiles.get(commit.sha);
|
|
249
|
+
if (retainedFiles != null) commitFiles.set(commit.sha, retainedFiles);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
return { generation, data, branchFiles, allFiles, commitFiles };
|
|
253
|
+
};
|
|
254
|
+
|
|
147
255
|
const commitFileCache = new Map<string, Promise<ReviewFile[]>>();
|
|
148
256
|
const contentCache = new Map<string, Promise<ReviewFileContents>>();
|
|
257
|
+
let currentSnapshot = createReviewReadSnapshot(reviewData, 0, null);
|
|
258
|
+
const isLiveWindow = (): boolean => activeWindow === window && attempt === reviewAttempt && !window.closed;
|
|
259
|
+
|
|
260
|
+
const cacheCommitSha = (cacheKey: string): string | null => {
|
|
261
|
+
if (!cacheKey.startsWith("commits:")) return null;
|
|
262
|
+
const rest = cacheKey.slice("commits:".length);
|
|
263
|
+
const separator = rest.indexOf(":");
|
|
264
|
+
return separator < 0 ? null : rest.slice(0, separator);
|
|
265
|
+
};
|
|
149
266
|
|
|
150
|
-
const
|
|
151
|
-
|
|
267
|
+
const pruneCachesForReviewData = (nextReviewData: typeof reviewData): void => {
|
|
268
|
+
const retainedImmutableCommits = new Set(
|
|
269
|
+
nextReviewData.commits.filter((commit) => commit.kind === "commit").map((commit) => commit.sha),
|
|
270
|
+
);
|
|
152
271
|
for (const sha of commitFileCache.keys()) {
|
|
153
|
-
if (
|
|
154
|
-
|
|
272
|
+
if (!retainedImmutableCommits.has(sha)) commitFileCache.delete(sha);
|
|
273
|
+
}
|
|
274
|
+
for (const cacheKey of contentCache.keys()) {
|
|
275
|
+
const commitSha = cacheCommitSha(cacheKey);
|
|
276
|
+
if (commitSha == null || !retainedImmutableCommits.has(commitSha)) {
|
|
277
|
+
contentCache.delete(cacheKey);
|
|
155
278
|
}
|
|
156
279
|
}
|
|
157
280
|
};
|
|
158
281
|
|
|
159
|
-
const
|
|
160
|
-
|
|
282
|
+
const replaceReviewData = (nextReviewData: typeof reviewData): void => {
|
|
283
|
+
pruneCachesForReviewData(nextReviewData);
|
|
284
|
+
const previousSnapshot = currentSnapshot;
|
|
285
|
+
reviewData = nextReviewData;
|
|
286
|
+
rememberFiles(nextReviewData.files);
|
|
287
|
+
currentSnapshot = createReviewReadSnapshot(nextReviewData, previousSnapshot.generation + 1, previousSnapshot);
|
|
288
|
+
};
|
|
289
|
+
|
|
290
|
+
const sendWindowMessage = (message: ReviewHostMessage, guard?: () => boolean): void => {
|
|
291
|
+
if (!isLiveWindow()) return;
|
|
292
|
+
if (guard != null && !guard()) return;
|
|
161
293
|
const payload = escapeForInlineScript(JSON.stringify(message));
|
|
162
294
|
window.send(`window.__reviewReceive(${payload});`);
|
|
163
295
|
};
|
|
@@ -184,9 +316,16 @@ export default function (pi: ExtensionAPI) {
|
|
|
184
316
|
commitFileCache.set(sha, pending);
|
|
185
317
|
pending
|
|
186
318
|
.then((commitFiles) => {
|
|
187
|
-
|
|
319
|
+
if (commitFileCache.get(sha) !== pending || !isLiveWindow()) return;
|
|
320
|
+
if (!currentSnapshot.data.commits.some((commit) => commit.sha === sha)) return;
|
|
321
|
+
rememberFiles(commitFiles);
|
|
322
|
+
currentSnapshot.commitFiles.set(sha, new Map(commitFiles.map((file) => [file.id, file])));
|
|
188
323
|
})
|
|
189
|
-
.catch(() => {
|
|
324
|
+
.catch(() => {
|
|
325
|
+
// A rejected promise is retryable, but an old rejection must not
|
|
326
|
+
// evict a replacement request for the same key.
|
|
327
|
+
if (isLiveWindow() && commitFileCache.get(sha) === pending) commitFileCache.delete(sha);
|
|
328
|
+
});
|
|
190
329
|
return pending;
|
|
191
330
|
};
|
|
192
331
|
|
|
@@ -194,16 +333,53 @@ export default function (pi: ExtensionAPI) {
|
|
|
194
333
|
file: ReviewFile,
|
|
195
334
|
scope: ReviewRequestFilePayload["scope"],
|
|
196
335
|
commitSha: string | null,
|
|
336
|
+
snapshot: ReviewReadSnapshot,
|
|
197
337
|
): Promise<ReviewFileContents> => {
|
|
198
338
|
const cacheKey = `${scope}:${commitSha ?? ""}:${file.id}`;
|
|
199
339
|
const cached = contentCache.get(cacheKey);
|
|
200
340
|
if (cached != null) return cached;
|
|
201
341
|
|
|
202
|
-
const pending = loadReviewFileContents(pi, repoRoot, file, scope, commitSha,
|
|
342
|
+
const pending = loadReviewFileContents(pi, repoRoot, file, scope, commitSha, snapshot.data.branchMergeBaseSha);
|
|
203
343
|
contentCache.set(cacheKey, pending);
|
|
344
|
+
pending.catch(() => {
|
|
345
|
+
// Do not let an older rejection remove a newer retry's promise.
|
|
346
|
+
if (isLiveWindow() && contentCache.get(cacheKey) === pending) contentCache.delete(cacheKey);
|
|
347
|
+
});
|
|
204
348
|
return pending;
|
|
205
349
|
};
|
|
206
350
|
|
|
351
|
+
const isAdvertisedCommitSha = (snapshot: ReviewReadSnapshot, sha: string): boolean =>
|
|
352
|
+
snapshot.data.commits.some((commit) => commit.sha === sha);
|
|
353
|
+
|
|
354
|
+
const isCurrentCommitRequest = (
|
|
355
|
+
snapshot: ReviewReadSnapshot,
|
|
356
|
+
sha: string,
|
|
357
|
+
pending: Promise<ReviewFile[]> | null = null,
|
|
358
|
+
): boolean => {
|
|
359
|
+
if (!isLiveWindow()) return false;
|
|
360
|
+
const commit = currentSnapshot.data.commits.find((item) => item.sha === sha);
|
|
361
|
+
if (commit == null) return false;
|
|
362
|
+
if (currentSnapshot === snapshot) return true;
|
|
363
|
+
return commit.kind === "commit" && pending != null && commitFileCache.get(sha) === pending;
|
|
364
|
+
};
|
|
365
|
+
|
|
366
|
+
const isCurrentFileRequest = (
|
|
367
|
+
snapshot: ReviewReadSnapshot,
|
|
368
|
+
scope: ReviewRequestFilePayload["scope"],
|
|
369
|
+
commitSha: string | null,
|
|
370
|
+
file: ReviewFile,
|
|
371
|
+
): boolean => {
|
|
372
|
+
if (!isLiveWindow()) return false;
|
|
373
|
+
if (scope === "commits") {
|
|
374
|
+
if (commitSha == null || !isAdvertisedCommitSha(currentSnapshot, commitSha)) return false;
|
|
375
|
+
if (isWorkingTreeCommitSha(commitSha) && currentSnapshot !== snapshot) return false;
|
|
376
|
+
return currentSnapshot.commitFiles.get(commitSha)?.get(file.id) === file;
|
|
377
|
+
}
|
|
378
|
+
if (currentSnapshot !== snapshot) return false;
|
|
379
|
+
const files = scope === "branch" ? currentSnapshot.branchFiles : currentSnapshot.allFiles;
|
|
380
|
+
return files.get(file.id) === file;
|
|
381
|
+
};
|
|
382
|
+
|
|
207
383
|
const terminalMessagePromise = new Promise<ReviewSubmitPayload | ReviewCancelPayload | null>(
|
|
208
384
|
(resolve, reject) => {
|
|
209
385
|
let settled = false;
|
|
@@ -244,59 +420,146 @@ export default function (pi: ExtensionAPI) {
|
|
|
244
420
|
};
|
|
245
421
|
|
|
246
422
|
const handleRequestFile = async (message: ReviewRequestFilePayload): Promise<void> => {
|
|
423
|
+
const snapshot = currentSnapshot;
|
|
424
|
+
const currentGuard = (): boolean => isLiveWindow() && currentSnapshot === snapshot;
|
|
425
|
+
const commitSha = message.commitSha ?? null;
|
|
426
|
+
if (message.scope === "commits") {
|
|
427
|
+
if (commitSha == null) {
|
|
428
|
+
sendWindowMessage(
|
|
429
|
+
{
|
|
430
|
+
type: "file-error",
|
|
431
|
+
requestId: message.requestId,
|
|
432
|
+
fileId: message.fileId,
|
|
433
|
+
scope: message.scope,
|
|
434
|
+
commitSha: null,
|
|
435
|
+
message: "A commit SHA is required for commit-scoped file requests.",
|
|
436
|
+
},
|
|
437
|
+
currentGuard,
|
|
438
|
+
);
|
|
439
|
+
return;
|
|
440
|
+
}
|
|
441
|
+
if (!isAdvertisedCommitSha(snapshot, commitSha)) {
|
|
442
|
+
sendWindowMessage(
|
|
443
|
+
{
|
|
444
|
+
type: "file-error",
|
|
445
|
+
requestId: message.requestId,
|
|
446
|
+
fileId: message.fileId,
|
|
447
|
+
scope: message.scope,
|
|
448
|
+
commitSha,
|
|
449
|
+
message: "Unknown commit requested.",
|
|
450
|
+
},
|
|
451
|
+
currentGuard,
|
|
452
|
+
);
|
|
453
|
+
return;
|
|
454
|
+
}
|
|
455
|
+
} else if (commitSha != null) {
|
|
456
|
+
sendWindowMessage(
|
|
457
|
+
{
|
|
458
|
+
type: "file-error",
|
|
459
|
+
requestId: message.requestId,
|
|
460
|
+
fileId: message.fileId,
|
|
461
|
+
scope: message.scope,
|
|
462
|
+
commitSha,
|
|
463
|
+
message: "A commit SHA is only valid for commit-scoped file requests.",
|
|
464
|
+
},
|
|
465
|
+
currentGuard,
|
|
466
|
+
);
|
|
467
|
+
return;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
const fileMap =
|
|
471
|
+
message.scope === "branch"
|
|
472
|
+
? snapshot.branchFiles
|
|
473
|
+
: message.scope === "all"
|
|
474
|
+
? snapshot.allFiles
|
|
475
|
+
: snapshot.commitFiles.get(commitSha as string) ?? new Map<string, ReviewFile>();
|
|
247
476
|
const file = fileMap.get(message.fileId);
|
|
248
477
|
if (file == null) {
|
|
249
|
-
sendWindowMessage(
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
478
|
+
sendWindowMessage(
|
|
479
|
+
{
|
|
480
|
+
type: "file-error",
|
|
481
|
+
requestId: message.requestId,
|
|
482
|
+
fileId: message.fileId,
|
|
483
|
+
scope: message.scope,
|
|
484
|
+
commitSha,
|
|
485
|
+
message: "Unknown file requested.",
|
|
486
|
+
},
|
|
487
|
+
currentGuard,
|
|
488
|
+
);
|
|
257
489
|
return;
|
|
258
490
|
}
|
|
259
491
|
|
|
260
492
|
try {
|
|
261
|
-
const contents = await loadContents(file, message.scope,
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
493
|
+
const contents = await loadContents(file, message.scope, commitSha, snapshot);
|
|
494
|
+
if (!isCurrentFileRequest(snapshot, message.scope, commitSha, file)) return;
|
|
495
|
+
sendWindowMessage(
|
|
496
|
+
{
|
|
497
|
+
type: "file-data",
|
|
498
|
+
requestId: message.requestId,
|
|
499
|
+
fileId: message.fileId,
|
|
500
|
+
scope: message.scope,
|
|
501
|
+
commitSha,
|
|
502
|
+
originalContent: contents.originalContent,
|
|
503
|
+
modifiedContent: contents.modifiedContent,
|
|
504
|
+
kind: contents.kind,
|
|
505
|
+
mimeType: contents.mimeType,
|
|
506
|
+
originalExists: contents.originalExists,
|
|
507
|
+
modifiedExists: contents.modifiedExists,
|
|
508
|
+
originalPreviewUrl: contents.originalPreviewUrl,
|
|
509
|
+
modifiedPreviewUrl: contents.modifiedPreviewUrl,
|
|
510
|
+
},
|
|
511
|
+
() => isCurrentFileRequest(snapshot, message.scope, commitSha, file),
|
|
512
|
+
);
|
|
277
513
|
} catch (error) {
|
|
514
|
+
if (!isCurrentFileRequest(snapshot, message.scope, commitSha, file)) return;
|
|
278
515
|
const messageText = error instanceof Error ? error.message : String(error);
|
|
279
|
-
sendWindowMessage(
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
516
|
+
sendWindowMessage(
|
|
517
|
+
{
|
|
518
|
+
type: "file-error",
|
|
519
|
+
requestId: message.requestId,
|
|
520
|
+
fileId: message.fileId,
|
|
521
|
+
scope: message.scope,
|
|
522
|
+
commitSha,
|
|
523
|
+
message: messageText,
|
|
524
|
+
},
|
|
525
|
+
() => isCurrentFileRequest(snapshot, message.scope, commitSha, file),
|
|
526
|
+
);
|
|
287
527
|
}
|
|
288
528
|
};
|
|
289
529
|
|
|
290
530
|
const handleRequestCommit = async (message: ReviewRequestCommitPayload): Promise<void> => {
|
|
531
|
+
const snapshot = currentSnapshot;
|
|
532
|
+
const currentGuard = (): boolean => isLiveWindow() && currentSnapshot === snapshot;
|
|
533
|
+
if (!isAdvertisedCommitSha(snapshot, message.sha)) {
|
|
534
|
+
sendWindowMessage(
|
|
535
|
+
{
|
|
536
|
+
type: "commit-error",
|
|
537
|
+
requestId: message.requestId,
|
|
538
|
+
sha: message.sha,
|
|
539
|
+
message: "Unknown commit requested.",
|
|
540
|
+
},
|
|
541
|
+
currentGuard,
|
|
542
|
+
);
|
|
543
|
+
return;
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
const pending = loadCommitFiles(message.sha);
|
|
291
547
|
try {
|
|
292
|
-
const commitFiles = await
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
548
|
+
const commitFiles = await pending;
|
|
549
|
+
if (!isCurrentCommitRequest(snapshot, message.sha, pending)) return;
|
|
550
|
+
if (currentSnapshot.commitFiles.get(message.sha) == null) return;
|
|
551
|
+
sendWindowMessage(
|
|
552
|
+
{
|
|
553
|
+
type: "commit-data",
|
|
554
|
+
requestId: message.requestId,
|
|
555
|
+
sha: message.sha,
|
|
556
|
+
files: commitFiles,
|
|
557
|
+
},
|
|
558
|
+
() =>
|
|
559
|
+
isCurrentCommitRequest(snapshot, message.sha, pending) && currentSnapshot.commitFiles.get(message.sha) != null,
|
|
560
|
+
);
|
|
299
561
|
} catch (error) {
|
|
562
|
+
if (!isCurrentCommitRequest(snapshot, message.sha, pending)) return;
|
|
300
563
|
const messageText = error instanceof Error ? error.message : String(error);
|
|
301
564
|
sendWindowMessage({
|
|
302
565
|
type: "commit-error",
|
|
@@ -307,22 +570,29 @@ export default function (pi: ExtensionAPI) {
|
|
|
307
570
|
}
|
|
308
571
|
};
|
|
309
572
|
|
|
573
|
+
let refreshRequestSequence = 0;
|
|
574
|
+
let newestRefreshRequest = 0;
|
|
310
575
|
const handleRequestReviewData = async (message: ReviewRequestReviewDataPayload): Promise<void> => {
|
|
576
|
+
const refreshRequest = ++refreshRequestSequence;
|
|
577
|
+
newestRefreshRequest = refreshRequest;
|
|
311
578
|
try {
|
|
312
579
|
const nextReviewData = await getReviewWindowData(pi, repoRoot);
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
580
|
+
if (!isLiveWindow() || refreshRequest !== newestRefreshRequest) return;
|
|
581
|
+
replaceReviewData(nextReviewData);
|
|
582
|
+
sendWindowMessage(
|
|
583
|
+
{
|
|
584
|
+
type: "review-data",
|
|
585
|
+
requestId: message.requestId,
|
|
586
|
+
files: nextReviewData.files,
|
|
587
|
+
commits: nextReviewData.commits,
|
|
588
|
+
branchBaseRef: nextReviewData.branchBaseRef,
|
|
589
|
+
branchMergeBaseSha: nextReviewData.branchMergeBaseSha,
|
|
590
|
+
repositoryHasHead: nextReviewData.repositoryHasHead,
|
|
591
|
+
},
|
|
592
|
+
() => isLiveWindow() && refreshRequest === newestRefreshRequest,
|
|
593
|
+
);
|
|
325
594
|
} catch (error) {
|
|
595
|
+
if (!isLiveWindow() || refreshRequest !== newestRefreshRequest) return;
|
|
326
596
|
const messageText = error instanceof Error ? error.message : String(error);
|
|
327
597
|
sendWindowMessage({
|
|
328
598
|
type: "review-data-error",
|
|
@@ -360,6 +630,20 @@ export default function (pi: ExtensionAPI) {
|
|
|
360
630
|
};
|
|
361
631
|
|
|
362
632
|
const onMessage = (message: unknown): void => {
|
|
633
|
+
// Terminal messages remain recoverable during the native close grace
|
|
634
|
+
// period; all other work must stop once the window is closed or retired.
|
|
635
|
+
const submit = parseSubmitPayload(message);
|
|
636
|
+
if (submit != null) {
|
|
637
|
+
settle(submit);
|
|
638
|
+
requestWindowClose();
|
|
639
|
+
return;
|
|
640
|
+
}
|
|
641
|
+
if (isCancelPayload(message)) {
|
|
642
|
+
settle(message);
|
|
643
|
+
requestWindowClose();
|
|
644
|
+
return;
|
|
645
|
+
}
|
|
646
|
+
if (!isLiveWindow()) return;
|
|
363
647
|
if (isRequestFilePayload(message)) {
|
|
364
648
|
void handleRequestFile(message);
|
|
365
649
|
return;
|
|
@@ -378,11 +662,6 @@ export default function (pi: ExtensionAPI) {
|
|
|
378
662
|
}
|
|
379
663
|
if (isClipboardWritePayload(message)) {
|
|
380
664
|
handleClipboardWrite(message);
|
|
381
|
-
return;
|
|
382
|
-
}
|
|
383
|
-
if (isSubmitPayload(message) || isCancelPayload(message)) {
|
|
384
|
-
requestWindowClose();
|
|
385
|
-
settle(message);
|
|
386
665
|
}
|
|
387
666
|
};
|
|
388
667
|
|
|
@@ -415,7 +694,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
415
694
|
void (async () => {
|
|
416
695
|
try {
|
|
417
696
|
const message = await terminalMessagePromise;
|
|
418
|
-
if (suppressedWindows.has(window)) return;
|
|
697
|
+
if (suppressedWindows.has(window) || attempt !== reviewAttempt) return;
|
|
419
698
|
if (message == null) return;
|
|
420
699
|
if (message.type === "cancel") {
|
|
421
700
|
ctx.ui.notify("Review cancelled.", "info");
|
|
@@ -423,11 +702,16 @@ export default function (pi: ExtensionAPI) {
|
|
|
423
702
|
}
|
|
424
703
|
if (!hasReviewFeedback(message)) return;
|
|
425
704
|
|
|
426
|
-
const prompt = composeReviewPrompt([...
|
|
427
|
-
|
|
428
|
-
|
|
705
|
+
const prompt = composeReviewPrompt([...historicalFiles.values()], message);
|
|
706
|
+
if (message.draft === true) {
|
|
707
|
+
appendReviewPrompt(ctx, prompt);
|
|
708
|
+
ctx.ui.notify("Appended review feedback to the editor.", "info");
|
|
709
|
+
} else {
|
|
710
|
+
pi.sendUserMessage(prompt, { deliverAs: "followUp" });
|
|
711
|
+
ctx.ui.notify("Review feedback sent to the agent.", "info");
|
|
712
|
+
}
|
|
429
713
|
} catch (error) {
|
|
430
|
-
if (suppressedWindows.has(window)) return;
|
|
714
|
+
if (suppressedWindows.has(window) || attempt !== reviewAttempt) return;
|
|
431
715
|
const message = error instanceof Error ? error.message : String(error);
|
|
432
716
|
ctx.ui.notify(`Review failed: ${message}`, "error");
|
|
433
717
|
}
|
|
@@ -435,6 +719,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
435
719
|
|
|
436
720
|
ctx.ui.notify("Opened native review window.", "info");
|
|
437
721
|
} catch (error) {
|
|
722
|
+
if (attempt !== reviewAttempt) return;
|
|
438
723
|
closeActiveWindow({ suppressResults: true });
|
|
439
724
|
const message = error instanceof Error ? error.message : String(error);
|
|
440
725
|
ctx.ui.notify(`Review failed: ${message}`, "error");
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@diegopetrucci/pi-annotate-git-diff",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "A standalone pi extension that adds /annotate-git-diff, a native Glimpse UI for annotating git diffs and sending feedback to the editor.",
|
|
3
|
+
"version": "0.1.12",
|
|
4
|
+
"description": "A standalone pi extension that adds /annotate-git-diff, a native Glimpse UI for annotating git diffs and sending explicit feedback directly to the agent; close recovery keeps unsent drafts in the editor.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi-package",
|
|
7
7
|
"pi",
|
|
@@ -57,13 +57,15 @@ class QuietGlimpseWindowImpl extends EventEmitter implements QuietGlimpseWindow
|
|
|
57
57
|
|
|
58
58
|
const rl = createInterface({ input: proc.stdout, crlfDelay: Infinity });
|
|
59
59
|
rl.on("line", (line) => {
|
|
60
|
-
let
|
|
60
|
+
let parsed: unknown;
|
|
61
61
|
try {
|
|
62
|
-
|
|
62
|
+
parsed = JSON.parse(line);
|
|
63
63
|
} catch {
|
|
64
64
|
this.#reportError(new Error(`Malformed glimpse protocol line: ${line}`));
|
|
65
65
|
return;
|
|
66
66
|
}
|
|
67
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) return;
|
|
68
|
+
const message = parsed as GlimpseProtocolMessage;
|
|
67
69
|
|
|
68
70
|
switch (message.type) {
|
|
69
71
|
case "ready":
|
|
@@ -71,6 +71,12 @@ export interface ReviewSubmitPayload {
|
|
|
71
71
|
type: "submit";
|
|
72
72
|
overallComment: string;
|
|
73
73
|
comments: DiffReviewComment[];
|
|
74
|
+
/**
|
|
75
|
+
* True means this is a close-recovery draft and must stay editor-only.
|
|
76
|
+
* Only literal false permits an explicit submission to trigger an agent turn;
|
|
77
|
+
* malformed or missing values are normalized to true by the host.
|
|
78
|
+
*/
|
|
79
|
+
draft: boolean;
|
|
74
80
|
}
|
|
75
81
|
|
|
76
82
|
export interface ReviewCancelPayload {
|