pi-diff-review 0.1.18 → 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 +46 -1
- package/src/explanation/explainer.ts +22 -2
- package/src/index.ts +170 -160
- package/src/review/cache.ts +171 -0
- package/src/review/component.ts +134 -16
- package/src/review/prompt.ts +25 -2
- package/src/review/types.ts +14 -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
|
@@ -32,6 +32,7 @@ import { padToWidth, lineNumberCell } from "../render/utils.ts";
|
|
|
32
32
|
import { buildSplitDiffRows } from "../diff/split.ts";
|
|
33
33
|
import type {
|
|
34
34
|
DiffRenderMode,
|
|
35
|
+
PersistedAsk,
|
|
35
36
|
ReviewComment,
|
|
36
37
|
ReviewLine,
|
|
37
38
|
ReviewResult,
|
|
@@ -40,6 +41,7 @@ import type {
|
|
|
40
41
|
SelectionBounds,
|
|
41
42
|
SplitDiffCell,
|
|
42
43
|
SplitDiffRow,
|
|
44
|
+
WorkspaceCommentSummary,
|
|
43
45
|
} from "./types.ts";
|
|
44
46
|
|
|
45
47
|
type InlineBoxPart = "top" | "body" | "bottom";
|
|
@@ -58,6 +60,7 @@ type AnnotatedDiffRow =
|
|
|
58
60
|
const HELP_COMMANDS = [
|
|
59
61
|
["h", "show or hide this help"],
|
|
60
62
|
["j/k or arrows", "move selection"],
|
|
63
|
+
["PgUp / PgDown", "move up or down half a page"],
|
|
61
64
|
["ctrl-u / ctrl-d", "move up or down half a page"],
|
|
62
65
|
["g / G", "jump to top or bottom"],
|
|
63
66
|
["n / p", "jump to next or previous hunk"],
|
|
@@ -85,8 +88,10 @@ export class ReviewComponent {
|
|
|
85
88
|
|
|
86
89
|
private inlineAnnotationsVisible = true;
|
|
87
90
|
private visibleExplanationKeys = new Set<string>();
|
|
91
|
+
private explanationAnchorByScope = new Map<string, number>();
|
|
88
92
|
private askInputMode = false;
|
|
89
93
|
private askScope?: ExplanationScope;
|
|
94
|
+
private askAnchorIndex?: number;
|
|
90
95
|
private explanationController: ExplanationController;
|
|
91
96
|
private editor: Editor;
|
|
92
97
|
private splitRows?: SplitDiffRow[];
|
|
@@ -116,6 +121,11 @@ export class ReviewComponent {
|
|
|
116
121
|
private onCommentsChanged?: (comments: Map<string, ReviewComment>) => void,
|
|
117
122
|
cachedExplanations?: Map<string, string>,
|
|
118
123
|
private onExplanationsChanged?: (explanations: Map<string, string>) => void,
|
|
124
|
+
cachedAsk?: PersistedAsk,
|
|
125
|
+
private onAskChanged?: (ask?: PersistedAsk) => void,
|
|
126
|
+
private getWorkspaceCommentSummary?: (
|
|
127
|
+
comments: Map<string, ReviewComment>,
|
|
128
|
+
) => WorkspaceCommentSummary | undefined,
|
|
119
129
|
) {
|
|
120
130
|
const firstCommentable = this.lines.findIndex((line) => line.commentable);
|
|
121
131
|
this.navigation = new ReviewNavigationState(
|
|
@@ -124,11 +134,36 @@ export class ReviewComponent {
|
|
|
124
134
|
);
|
|
125
135
|
this.lines.forEach((line, index) => this.lineIndexById.set(line.id, index));
|
|
126
136
|
this.search = new ReviewSearchState(this.lines);
|
|
137
|
+
|
|
138
|
+
const restoredAsk = cachedAsk
|
|
139
|
+
? this.restorePersistedAsk(cachedAsk)
|
|
140
|
+
: undefined;
|
|
141
|
+
if (restoredAsk) {
|
|
142
|
+
this.askScope = restoredAsk.scope;
|
|
143
|
+
this.askAnchorIndex = restoredAsk.anchorIndex;
|
|
144
|
+
}
|
|
145
|
+
|
|
127
146
|
this.explanationController = new ExplanationController(
|
|
128
147
|
tui,
|
|
129
148
|
explainer,
|
|
130
149
|
cachedExplanations,
|
|
131
150
|
onExplanationsChanged,
|
|
151
|
+
restoredAsk ? cachedAsk?.text : undefined,
|
|
152
|
+
(state) => {
|
|
153
|
+
if (!state) {
|
|
154
|
+
this.onAskChanged?.(undefined);
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
if (state.status !== "ready") return;
|
|
158
|
+
if (!this.askScope || this.askAnchorIndex == null) return;
|
|
159
|
+
const anchorLine = this.lines[this.askAnchorIndex];
|
|
160
|
+
if (!anchorLine) return;
|
|
161
|
+
this.onAskChanged?.({
|
|
162
|
+
scopeKey: this.askScope.key,
|
|
163
|
+
anchorLineId: anchorLine.id,
|
|
164
|
+
text: state.text,
|
|
165
|
+
});
|
|
166
|
+
},
|
|
132
167
|
);
|
|
133
168
|
|
|
134
169
|
this.editor = new Editor(tui as never, {
|
|
@@ -151,6 +186,7 @@ export class ReviewComponent {
|
|
|
151
186
|
this.explanationController.ask(this.askScope, question);
|
|
152
187
|
} else {
|
|
153
188
|
this.askScope = undefined;
|
|
189
|
+
this.askAnchorIndex = undefined;
|
|
154
190
|
}
|
|
155
191
|
this.invalidateAnnotatedRows();
|
|
156
192
|
this.tui.requestRender(true);
|
|
@@ -259,7 +295,11 @@ export class ReviewComponent {
|
|
|
259
295
|
return;
|
|
260
296
|
}
|
|
261
297
|
if (data === "q") {
|
|
262
|
-
this.
|
|
298
|
+
if (this.hasSelection()) {
|
|
299
|
+
this.clearSelection();
|
|
300
|
+
} else {
|
|
301
|
+
this.done({ action: "cancel" });
|
|
302
|
+
}
|
|
263
303
|
return;
|
|
264
304
|
}
|
|
265
305
|
if (data === "h") {
|
|
@@ -286,11 +326,11 @@ export class ReviewComponent {
|
|
|
286
326
|
this.startSearchMode();
|
|
287
327
|
return;
|
|
288
328
|
}
|
|
289
|
-
if (matchesKey(data, "ctrl+d")) {
|
|
329
|
+
if (matchesKey(data, "pageDown") || matchesKey(data, "ctrl+d")) {
|
|
290
330
|
this.move(this.getPageMoveAmount());
|
|
291
331
|
return;
|
|
292
332
|
}
|
|
293
|
-
if (matchesKey(data, "ctrl+u")) {
|
|
333
|
+
if (matchesKey(data, "pageUp") || matchesKey(data, "ctrl+u")) {
|
|
294
334
|
this.move(-this.getPageMoveAmount());
|
|
295
335
|
return;
|
|
296
336
|
}
|
|
@@ -358,11 +398,12 @@ export class ReviewComponent {
|
|
|
358
398
|
render(width: number): string[] {
|
|
359
399
|
const viewportHeight = this.getContentHeight();
|
|
360
400
|
const selectedLine = this.lines[this.selected];
|
|
401
|
+
const workspaceSummary = this.getWorkspaceCommentSummary?.(this.comments);
|
|
361
402
|
const output: string[] = [];
|
|
362
403
|
|
|
363
404
|
output.push(
|
|
364
405
|
this.renderStatusLine(
|
|
365
|
-
this.getHeaderText(selectedLine),
|
|
406
|
+
this.getHeaderText(selectedLine, workspaceSummary),
|
|
366
407
|
"Press h for help",
|
|
367
408
|
width,
|
|
368
409
|
),
|
|
@@ -373,15 +414,21 @@ export class ReviewComponent {
|
|
|
373
414
|
|
|
374
415
|
output.push(
|
|
375
416
|
truncateToWidth(
|
|
376
|
-
this.theme.fg(
|
|
417
|
+
this.theme.fg(
|
|
418
|
+
"muted",
|
|
419
|
+
this.getFooterText(selectedLine, workspaceSummary),
|
|
420
|
+
),
|
|
377
421
|
width,
|
|
378
422
|
),
|
|
379
423
|
);
|
|
380
424
|
return this.helpVisible ? this.renderHelpModal(output, width) : output;
|
|
381
425
|
}
|
|
382
426
|
|
|
383
|
-
private getHeaderText(
|
|
384
|
-
|
|
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)}`;
|
|
385
432
|
|
|
386
433
|
if (this.editMode) {
|
|
387
434
|
return `${base} • editing ${this.editingCommentKey === GLOBAL_COMMENT_KEY ? "overall comment" : "inline comment"}`;
|
|
@@ -700,10 +747,10 @@ export class ReviewComponent {
|
|
|
700
747
|
const scope = getCurrentHunkScope(this.lines, lineIndex);
|
|
701
748
|
if (!scope) return;
|
|
702
749
|
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
750
|
+
if (
|
|
751
|
+
this.visibleExplanationKeys.has(scope.key) &&
|
|
752
|
+
this.getExplanationAnchorIndex(lineIndex, scope.key) === lineIndex
|
|
753
|
+
) {
|
|
707
754
|
const explanation = this.explanationController.getState(scope);
|
|
708
755
|
if (!this.explanationController.isAvailable) {
|
|
709
756
|
this.pushExplanationBlock(rows, "Explanation unavailable.", width);
|
|
@@ -726,11 +773,19 @@ export class ReviewComponent {
|
|
|
726
773
|
}
|
|
727
774
|
}
|
|
728
775
|
|
|
729
|
-
if (
|
|
776
|
+
if (
|
|
777
|
+
this.askInputMode &&
|
|
778
|
+
this.askScope?.key === scope.key &&
|
|
779
|
+
this.askAnchorIndex === lineIndex
|
|
780
|
+
) {
|
|
730
781
|
this.pushInlineEditorBlock(rows, "Ask about this hunk", width);
|
|
731
782
|
}
|
|
732
783
|
|
|
733
|
-
if (
|
|
784
|
+
if (
|
|
785
|
+
!this.askInputMode &&
|
|
786
|
+
this.askScope?.key === scope.key &&
|
|
787
|
+
this.askAnchorIndex === lineIndex
|
|
788
|
+
) {
|
|
734
789
|
const askState = this.explanationController.getAskState();
|
|
735
790
|
if (askState) this.pushAskBlock(rows, askState, width);
|
|
736
791
|
}
|
|
@@ -908,6 +963,27 @@ export class ReviewComponent {
|
|
|
908
963
|
return comments.sort((a, b) => a.id.localeCompare(b.id));
|
|
909
964
|
}
|
|
910
965
|
|
|
966
|
+
private restorePersistedAsk(
|
|
967
|
+
cachedAsk: PersistedAsk,
|
|
968
|
+
): { scope: ExplanationScope; anchorIndex: number } | undefined {
|
|
969
|
+
const anchorIndex = this.lineIndexById.get(cachedAsk.anchorLineId);
|
|
970
|
+
if (anchorIndex != null) {
|
|
971
|
+
const scope = getCurrentHunkScope(this.lines, anchorIndex);
|
|
972
|
+
if (scope?.key === cachedAsk.scopeKey) {
|
|
973
|
+
return { scope, anchorIndex };
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
for (let index = 0; index < this.lines.length; index++) {
|
|
978
|
+
const scope = getCurrentHunkScope(this.lines, index);
|
|
979
|
+
if (scope?.key === cachedAsk.scopeKey) {
|
|
980
|
+
return { scope, anchorIndex: index };
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
return undefined;
|
|
985
|
+
}
|
|
986
|
+
|
|
911
987
|
private getHunkEndIndex(selected: number): number | undefined {
|
|
912
988
|
const selectedLine = this.lines[selected];
|
|
913
989
|
if (!selectedLine?.filePath || !selectedLine.hunkLabel) return undefined;
|
|
@@ -923,6 +999,16 @@ export class ReviewComponent {
|
|
|
923
999
|
return end;
|
|
924
1000
|
}
|
|
925
1001
|
|
|
1002
|
+
private getExplanationAnchorIndex(
|
|
1003
|
+
lineIndex: number,
|
|
1004
|
+
scopeKey: string,
|
|
1005
|
+
): number | undefined {
|
|
1006
|
+
return (
|
|
1007
|
+
this.explanationAnchorByScope.get(scopeKey) ??
|
|
1008
|
+
this.getHunkEndIndex(lineIndex)
|
|
1009
|
+
);
|
|
1010
|
+
}
|
|
1011
|
+
|
|
926
1012
|
private renderSplitDiffRowAt(splitRowIndex: number, width: number): string {
|
|
927
1013
|
const splitRow = this.getSplitDiffRows()[splitRowIndex];
|
|
928
1014
|
if (!splitRow) return " ".repeat(width);
|
|
@@ -974,6 +1060,7 @@ export class ReviewComponent {
|
|
|
974
1060
|
if (!scope) return;
|
|
975
1061
|
this.clearAsk();
|
|
976
1062
|
this.askScope = scope;
|
|
1063
|
+
this.askAnchorIndex = this.selected;
|
|
977
1064
|
this.askInputMode = true;
|
|
978
1065
|
this.inlineAnnotationsVisible = true;
|
|
979
1066
|
this.editor.setText("");
|
|
@@ -984,7 +1071,10 @@ export class ReviewComponent {
|
|
|
984
1071
|
private exitAskInputMode(): void {
|
|
985
1072
|
this.askInputMode = false;
|
|
986
1073
|
this.editor.setText("");
|
|
987
|
-
if (!this.explanationController.getAskState())
|
|
1074
|
+
if (!this.explanationController.getAskState()) {
|
|
1075
|
+
this.askScope = undefined;
|
|
1076
|
+
this.askAnchorIndex = undefined;
|
|
1077
|
+
}
|
|
988
1078
|
this.invalidateAnnotatedRows();
|
|
989
1079
|
this.tui.requestRender(true);
|
|
990
1080
|
}
|
|
@@ -992,6 +1082,7 @@ export class ReviewComponent {
|
|
|
992
1082
|
private clearAsk(): void {
|
|
993
1083
|
this.askInputMode = false;
|
|
994
1084
|
this.askScope = undefined;
|
|
1085
|
+
this.askAnchorIndex = undefined;
|
|
995
1086
|
this.explanationController.clearAsk();
|
|
996
1087
|
this.editor.setText("");
|
|
997
1088
|
this.invalidateAnnotatedRows();
|
|
@@ -1027,6 +1118,7 @@ export class ReviewComponent {
|
|
|
1027
1118
|
|
|
1028
1119
|
private hideInlineExplanation(): void {
|
|
1029
1120
|
this.visibleExplanationKeys.clear();
|
|
1121
|
+
this.explanationAnchorByScope.clear();
|
|
1030
1122
|
}
|
|
1031
1123
|
|
|
1032
1124
|
private toggleExplanationPane(): void {
|
|
@@ -1035,8 +1127,10 @@ export class ReviewComponent {
|
|
|
1035
1127
|
|
|
1036
1128
|
if (this.visibleExplanationKeys.has(scope.key)) {
|
|
1037
1129
|
this.visibleExplanationKeys.delete(scope.key);
|
|
1130
|
+
this.explanationAnchorByScope.delete(scope.key);
|
|
1038
1131
|
} else {
|
|
1039
1132
|
this.visibleExplanationKeys.add(scope.key);
|
|
1133
|
+
this.explanationAnchorByScope.set(scope.key, this.selected);
|
|
1040
1134
|
this.ensureCurrentExplanation();
|
|
1041
1135
|
}
|
|
1042
1136
|
|
|
@@ -1126,7 +1220,10 @@ export class ReviewComponent {
|
|
|
1126
1220
|
: position;
|
|
1127
1221
|
}
|
|
1128
1222
|
|
|
1129
|
-
private getFooterText(
|
|
1223
|
+
private getFooterText(
|
|
1224
|
+
selectedLine?: ReviewLine,
|
|
1225
|
+
workspaceSummary?: WorkspaceCommentSummary,
|
|
1226
|
+
): string {
|
|
1130
1227
|
if (this.search.mode) {
|
|
1131
1228
|
return `Search: /${this.search.draftQuery} • Enter jump • Esc cancel`;
|
|
1132
1229
|
}
|
|
@@ -1141,7 +1238,28 @@ export class ReviewComponent {
|
|
|
1141
1238
|
const endLine = this.lines[selection.end]!;
|
|
1142
1239
|
return `Selected ${count} lines: ${formatLocation(startLine)} -> ${formatLocation(endLine)}`;
|
|
1143
1240
|
}
|
|
1144
|
-
|
|
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(" • ")}` : "";
|
|
1145
1263
|
}
|
|
1146
1264
|
|
|
1147
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
|
@@ -42,6 +42,20 @@ export type DiffSource = {
|
|
|
42
42
|
args: string[];
|
|
43
43
|
};
|
|
44
44
|
|
|
45
|
+
export type PersistedAsk = {
|
|
46
|
+
scopeKey: string;
|
|
47
|
+
anchorLineId: string;
|
|
48
|
+
text: string;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export type WorkspaceCommentSummary = {
|
|
52
|
+
visible: number;
|
|
53
|
+
hiddenInCurrentFiles: number;
|
|
54
|
+
elsewhere: number;
|
|
55
|
+
stale: number;
|
|
56
|
+
orphaned: number;
|
|
57
|
+
};
|
|
58
|
+
|
|
45
59
|
export type DiffRenderMode = "unified" | "split";
|
|
46
60
|
|
|
47
61
|
export type SplitDiffCell = {
|