pi-diff-review 0.1.19 → 0.1.20
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 +12 -1
- package/extensions/review.ts +5 -1
- package/package.json +1 -1
- package/src/diff/source.ts +6 -41
- package/src/explanation/controller.ts +34 -1
- package/src/explanation/explainer.ts +22 -2
- package/src/index.ts +170 -226
- package/src/review/cache.ts +171 -0
- package/src/review/component.ts +41 -6
- package/src/review/prompt.ts +25 -2
- package/src/review/types.ts +8 -0
- package/src/review/workspace-comments.ts +493 -0
- package/src/shared/args.ts +44 -0
- package/src/view/parser.ts +54 -0
- package/src/view/source.ts +132 -0
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ExtensionAPI,
|
|
3
|
+
ExtensionCommandContext,
|
|
4
|
+
} from "@earendil-works/pi-coding-agent";
|
|
5
|
+
import type { PersistedAsk, ReviewComment } from "./types.ts";
|
|
6
|
+
|
|
7
|
+
const REVIEW_COMMENT_CACHE_ENTRY = "pi-diff-review-cache";
|
|
8
|
+
const REVIEW_EXPLANATION_CACHE_ENTRY = "pi-diff-review-explanation-cache";
|
|
9
|
+
const REVIEW_ASK_CACHE_ENTRY = "pi-diff-review-ask-cache";
|
|
10
|
+
|
|
11
|
+
type ReviewCommentCacheEntry = {
|
|
12
|
+
cacheKey: string;
|
|
13
|
+
comments: ReviewComment[];
|
|
14
|
+
updatedAt: number;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
type ReviewExplanationCacheEntry = {
|
|
18
|
+
cacheKey: string;
|
|
19
|
+
explanations: Record<string, string>;
|
|
20
|
+
updatedAt: number;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
type ReviewAskCacheEntry = {
|
|
24
|
+
cacheKey: string;
|
|
25
|
+
ask?: PersistedAsk;
|
|
26
|
+
updatedAt: number;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export function getCachedComments(
|
|
30
|
+
ctx: ExtensionCommandContext,
|
|
31
|
+
cacheKey: string,
|
|
32
|
+
): Map<string, ReviewComment> {
|
|
33
|
+
let latest: ReviewCommentCacheEntry | undefined;
|
|
34
|
+
for (const entry of ctx.sessionManager.getEntries()) {
|
|
35
|
+
if (
|
|
36
|
+
entry.type !== "custom" ||
|
|
37
|
+
entry.customType !== REVIEW_COMMENT_CACHE_ENTRY
|
|
38
|
+
) {
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const data = entry.data as Partial<ReviewCommentCacheEntry> | undefined;
|
|
43
|
+
if (data?.cacheKey !== cacheKey || !Array.isArray(data.comments)) continue;
|
|
44
|
+
|
|
45
|
+
if (!latest || (data.updatedAt ?? 0) >= latest.updatedAt) {
|
|
46
|
+
latest = {
|
|
47
|
+
cacheKey: data.cacheKey,
|
|
48
|
+
comments: data.comments,
|
|
49
|
+
updatedAt: data.updatedAt ?? 0,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return new Map(
|
|
55
|
+
(latest?.comments ?? []).map((comment) => [comment.id, comment]),
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function persistCachedComments(
|
|
60
|
+
pi: ExtensionAPI,
|
|
61
|
+
cacheKey: string,
|
|
62
|
+
comments: Iterable<ReviewComment>,
|
|
63
|
+
): void {
|
|
64
|
+
pi.appendEntry(REVIEW_COMMENT_CACHE_ENTRY, {
|
|
65
|
+
cacheKey,
|
|
66
|
+
comments: [...comments],
|
|
67
|
+
updatedAt: Date.now(),
|
|
68
|
+
} satisfies ReviewCommentCacheEntry);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function getCachedExplanations(
|
|
72
|
+
ctx: ExtensionCommandContext,
|
|
73
|
+
cacheKey: string,
|
|
74
|
+
): Map<string, string> {
|
|
75
|
+
let latest: ReviewExplanationCacheEntry | undefined;
|
|
76
|
+
for (const entry of ctx.sessionManager.getEntries()) {
|
|
77
|
+
if (
|
|
78
|
+
entry.type !== "custom" ||
|
|
79
|
+
entry.customType !== REVIEW_EXPLANATION_CACHE_ENTRY
|
|
80
|
+
) {
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const data = entry.data as Partial<ReviewExplanationCacheEntry> | undefined;
|
|
85
|
+
if (
|
|
86
|
+
data?.cacheKey !== cacheKey ||
|
|
87
|
+
!data.explanations ||
|
|
88
|
+
typeof data.explanations !== "object" ||
|
|
89
|
+
Array.isArray(data.explanations)
|
|
90
|
+
) {
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (!latest || (data.updatedAt ?? 0) >= latest.updatedAt) {
|
|
95
|
+
latest = {
|
|
96
|
+
cacheKey: data.cacheKey,
|
|
97
|
+
explanations: Object.fromEntries(
|
|
98
|
+
Object.entries(data.explanations).filter(
|
|
99
|
+
(entry): entry is [string, string] =>
|
|
100
|
+
typeof entry[0] === "string" && typeof entry[1] === "string",
|
|
101
|
+
),
|
|
102
|
+
),
|
|
103
|
+
updatedAt: data.updatedAt ?? 0,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return new Map(Object.entries(latest?.explanations ?? {}));
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function persistCachedExplanations(
|
|
112
|
+
pi: ExtensionAPI,
|
|
113
|
+
cacheKey: string,
|
|
114
|
+
explanations: Map<string, string>,
|
|
115
|
+
): void {
|
|
116
|
+
pi.appendEntry(REVIEW_EXPLANATION_CACHE_ENTRY, {
|
|
117
|
+
cacheKey,
|
|
118
|
+
explanations: Object.fromEntries(explanations),
|
|
119
|
+
updatedAt: Date.now(),
|
|
120
|
+
} satisfies ReviewExplanationCacheEntry);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export function getCachedAsk(
|
|
124
|
+
ctx: ExtensionCommandContext,
|
|
125
|
+
cacheKey: string,
|
|
126
|
+
): PersistedAsk | undefined {
|
|
127
|
+
let latest: ReviewAskCacheEntry | undefined;
|
|
128
|
+
for (const entry of ctx.sessionManager.getEntries()) {
|
|
129
|
+
if (
|
|
130
|
+
entry.type !== "custom" ||
|
|
131
|
+
entry.customType !== REVIEW_ASK_CACHE_ENTRY
|
|
132
|
+
) {
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const data = entry.data as Partial<ReviewAskCacheEntry> | undefined;
|
|
137
|
+
if (data?.cacheKey !== cacheKey) continue;
|
|
138
|
+
|
|
139
|
+
const ask = data.ask;
|
|
140
|
+
const validAsk =
|
|
141
|
+
ask &&
|
|
142
|
+
typeof ask === "object" &&
|
|
143
|
+
typeof ask.scopeKey === "string" &&
|
|
144
|
+
typeof ask.anchorLineId === "string" &&
|
|
145
|
+
typeof ask.text === "string"
|
|
146
|
+
? ask
|
|
147
|
+
: undefined;
|
|
148
|
+
|
|
149
|
+
if (!latest || (data.updatedAt ?? 0) >= latest.updatedAt) {
|
|
150
|
+
latest = {
|
|
151
|
+
cacheKey: data.cacheKey,
|
|
152
|
+
ask: validAsk,
|
|
153
|
+
updatedAt: data.updatedAt ?? 0,
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return latest?.ask;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export function persistCachedAsk(
|
|
162
|
+
pi: ExtensionAPI,
|
|
163
|
+
cacheKey: string,
|
|
164
|
+
ask?: PersistedAsk,
|
|
165
|
+
): void {
|
|
166
|
+
pi.appendEntry(REVIEW_ASK_CACHE_ENTRY, {
|
|
167
|
+
cacheKey,
|
|
168
|
+
ask,
|
|
169
|
+
updatedAt: Date.now(),
|
|
170
|
+
} satisfies ReviewAskCacheEntry);
|
|
171
|
+
}
|
package/src/review/component.ts
CHANGED
|
@@ -41,6 +41,7 @@ import type {
|
|
|
41
41
|
SelectionBounds,
|
|
42
42
|
SplitDiffCell,
|
|
43
43
|
SplitDiffRow,
|
|
44
|
+
WorkspaceCommentSummary,
|
|
44
45
|
} from "./types.ts";
|
|
45
46
|
|
|
46
47
|
type InlineBoxPart = "top" | "body" | "bottom";
|
|
@@ -122,6 +123,9 @@ export class ReviewComponent {
|
|
|
122
123
|
private onExplanationsChanged?: (explanations: Map<string, string>) => void,
|
|
123
124
|
cachedAsk?: PersistedAsk,
|
|
124
125
|
private onAskChanged?: (ask?: PersistedAsk) => void,
|
|
126
|
+
private getWorkspaceCommentSummary?: (
|
|
127
|
+
comments: Map<string, ReviewComment>,
|
|
128
|
+
) => WorkspaceCommentSummary | undefined,
|
|
125
129
|
) {
|
|
126
130
|
const firstCommentable = this.lines.findIndex((line) => line.commentable);
|
|
127
131
|
this.navigation = new ReviewNavigationState(
|
|
@@ -394,11 +398,12 @@ export class ReviewComponent {
|
|
|
394
398
|
render(width: number): string[] {
|
|
395
399
|
const viewportHeight = this.getContentHeight();
|
|
396
400
|
const selectedLine = this.lines[this.selected];
|
|
401
|
+
const workspaceSummary = this.getWorkspaceCommentSummary?.(this.comments);
|
|
397
402
|
const output: string[] = [];
|
|
398
403
|
|
|
399
404
|
output.push(
|
|
400
405
|
this.renderStatusLine(
|
|
401
|
-
this.getHeaderText(selectedLine),
|
|
406
|
+
this.getHeaderText(selectedLine, workspaceSummary),
|
|
402
407
|
"Press h for help",
|
|
403
408
|
width,
|
|
404
409
|
),
|
|
@@ -409,15 +414,21 @@ export class ReviewComponent {
|
|
|
409
414
|
|
|
410
415
|
output.push(
|
|
411
416
|
truncateToWidth(
|
|
412
|
-
this.theme.fg(
|
|
417
|
+
this.theme.fg(
|
|
418
|
+
"muted",
|
|
419
|
+
this.getFooterText(selectedLine, workspaceSummary),
|
|
420
|
+
),
|
|
413
421
|
width,
|
|
414
422
|
),
|
|
415
423
|
);
|
|
416
424
|
return this.helpVisible ? this.renderHelpModal(output, width) : output;
|
|
417
425
|
}
|
|
418
426
|
|
|
419
|
-
private getHeaderText(
|
|
420
|
-
|
|
427
|
+
private getHeaderText(
|
|
428
|
+
selectedLine?: ReviewLine,
|
|
429
|
+
workspaceSummary?: WorkspaceCommentSummary,
|
|
430
|
+
): string {
|
|
431
|
+
const base = `${this.title} • ${this.lines.length} lines • ${this.comments.size} comments${this.formatWorkspaceSummary(workspaceSummary)}`;
|
|
421
432
|
|
|
422
433
|
if (this.editMode) {
|
|
423
434
|
return `${base} • editing ${this.editingCommentKey === GLOBAL_COMMENT_KEY ? "overall comment" : "inline comment"}`;
|
|
@@ -1209,7 +1220,10 @@ export class ReviewComponent {
|
|
|
1209
1220
|
: position;
|
|
1210
1221
|
}
|
|
1211
1222
|
|
|
1212
|
-
private getFooterText(
|
|
1223
|
+
private getFooterText(
|
|
1224
|
+
selectedLine?: ReviewLine,
|
|
1225
|
+
workspaceSummary?: WorkspaceCommentSummary,
|
|
1226
|
+
): string {
|
|
1213
1227
|
if (this.search.mode) {
|
|
1214
1228
|
return `Search: /${this.search.draftQuery} • Enter jump • Esc cancel`;
|
|
1215
1229
|
}
|
|
@@ -1224,7 +1238,28 @@ export class ReviewComponent {
|
|
|
1224
1238
|
const endLine = this.lines[selection.end]!;
|
|
1225
1239
|
return `Selected ${count} lines: ${formatLocation(startLine)} -> ${formatLocation(endLine)}`;
|
|
1226
1240
|
}
|
|
1227
|
-
|
|
1241
|
+
|
|
1242
|
+
const selectedText = `Selected: ${selectedLine ? formatLocation(selectedLine) : "(no selection)"}`;
|
|
1243
|
+
const workspaceText = this.formatWorkspaceSummary(workspaceSummary, false);
|
|
1244
|
+
return workspaceText ? `${selectedText} • ${workspaceText}` : selectedText;
|
|
1245
|
+
}
|
|
1246
|
+
|
|
1247
|
+
private formatWorkspaceSummary(
|
|
1248
|
+
summary?: WorkspaceCommentSummary,
|
|
1249
|
+
includeVisible = true,
|
|
1250
|
+
): string {
|
|
1251
|
+
if (!summary) return "";
|
|
1252
|
+
|
|
1253
|
+
const parts: string[] = [];
|
|
1254
|
+
if (includeVisible && summary.visible > 0)
|
|
1255
|
+
parts.push(`${summary.visible} visible persisted`);
|
|
1256
|
+
if (summary.hiddenInCurrentFiles > 0) {
|
|
1257
|
+
parts.push(`${summary.hiddenInCurrentFiles} hidden in current files`);
|
|
1258
|
+
}
|
|
1259
|
+
if (summary.elsewhere > 0) parts.push(`${summary.elsewhere} elsewhere`);
|
|
1260
|
+
if (summary.stale > 0) parts.push(`${summary.stale} stale`);
|
|
1261
|
+
if (summary.orphaned > 0) parts.push(`${summary.orphaned} orphaned`);
|
|
1262
|
+
return parts.length > 0 ? ` • ${parts.join(" • ")}` : "";
|
|
1228
1263
|
}
|
|
1229
1264
|
|
|
1230
1265
|
private jumpHunk(direction: 1 | -1): void {
|
package/src/review/prompt.ts
CHANGED
|
@@ -36,16 +36,39 @@ export function formatCommentLocation(comment: ReviewComment): string {
|
|
|
36
36
|
export function buildReviewPrompt(
|
|
37
37
|
comments: ReviewComment[],
|
|
38
38
|
promptLabel: string,
|
|
39
|
+
): string {
|
|
40
|
+
return buildCommentPrompt(
|
|
41
|
+
comments,
|
|
42
|
+
`Address this local code review feedback for ${promptLabel}.`,
|
|
43
|
+
"diff",
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function buildViewReviewPrompt(
|
|
48
|
+
comments: ReviewComment[],
|
|
49
|
+
promptLabel: string,
|
|
50
|
+
): string {
|
|
51
|
+
return buildCommentPrompt(
|
|
52
|
+
comments,
|
|
53
|
+
`Address this code review feedback for ${promptLabel}.`,
|
|
54
|
+
"ts",
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function buildCommentPrompt(
|
|
59
|
+
comments: ReviewComment[],
|
|
60
|
+
intro: string,
|
|
61
|
+
codeFenceLanguage: string,
|
|
39
62
|
): string {
|
|
40
63
|
const body = comments
|
|
41
64
|
.map((comment) => {
|
|
42
65
|
const location = formatCommentLocation(comment);
|
|
43
66
|
const excerpt = comment.lineText.trim()
|
|
44
|
-
? `\n Excerpt:\n\n
|
|
67
|
+
? `\n Excerpt:\n\n\`\`\`${codeFenceLanguage}\n${comment.lineText}\n\`\`\``
|
|
45
68
|
: "";
|
|
46
69
|
return `- \`${location}\` — ${comment.text}${excerpt}`;
|
|
47
70
|
})
|
|
48
71
|
.join("\n");
|
|
49
72
|
|
|
50
|
-
return
|
|
73
|
+
return `${intro}\n\n## Review comments\n${body}\n\nPlease apply the feedback and summarize what changed.`;
|
|
51
74
|
}
|
package/src/review/types.ts
CHANGED
|
@@ -48,6 +48,14 @@ export type PersistedAsk = {
|
|
|
48
48
|
text: string;
|
|
49
49
|
};
|
|
50
50
|
|
|
51
|
+
export type WorkspaceCommentSummary = {
|
|
52
|
+
visible: number;
|
|
53
|
+
hiddenInCurrentFiles: number;
|
|
54
|
+
elsewhere: number;
|
|
55
|
+
stale: number;
|
|
56
|
+
orphaned: number;
|
|
57
|
+
};
|
|
58
|
+
|
|
51
59
|
export type DiffRenderMode = "unified" | "split";
|
|
52
60
|
|
|
53
61
|
export type SplitDiffCell = {
|