pi-diff-review 0.1.18 → 0.1.19
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/package.json +1 -1
- package/src/explanation/controller.ts +12 -0
- package/src/index.ts +66 -0
- package/src/review/component.ts +93 -10
- package/src/review/types.ts +6 -0
package/package.json
CHANGED
|
@@ -62,12 +62,19 @@ export class ExplanationController {
|
|
|
62
62
|
private readonly onExplanationsChanged?: (
|
|
63
63
|
explanations: Map<string, string>,
|
|
64
64
|
) => void,
|
|
65
|
+
cachedAskText?: string,
|
|
66
|
+
private readonly onAskChanged?: (state?: ExplanationState) => void,
|
|
65
67
|
) {
|
|
66
68
|
for (const [key, text] of cachedExplanations) {
|
|
67
69
|
const trimmed = text.trim();
|
|
68
70
|
if (trimmed)
|
|
69
71
|
this.explanations.set(key, { status: "ready", text: trimmed });
|
|
70
72
|
}
|
|
73
|
+
|
|
74
|
+
const trimmedAskText = cachedAskText?.trim();
|
|
75
|
+
if (trimmedAskText) {
|
|
76
|
+
this.askState = { status: "ready", text: trimmedAskText };
|
|
77
|
+
}
|
|
71
78
|
}
|
|
72
79
|
|
|
73
80
|
get isAvailable(): boolean {
|
|
@@ -90,6 +97,7 @@ export class ExplanationController {
|
|
|
90
97
|
const requestId = ++this.askRequestId;
|
|
91
98
|
let text = "";
|
|
92
99
|
this.askState = { status: "loading", text };
|
|
100
|
+
this.onAskChanged?.(this.askState);
|
|
93
101
|
this.startLoadingTimer();
|
|
94
102
|
|
|
95
103
|
void this.explainer
|
|
@@ -99,6 +107,7 @@ export class ExplanationController {
|
|
|
99
107
|
if (requestId !== this.askRequestId) return;
|
|
100
108
|
text += delta;
|
|
101
109
|
this.askState = { status: "loading", text };
|
|
110
|
+
this.onAskChanged?.(this.askState);
|
|
102
111
|
this.tui.requestRender();
|
|
103
112
|
},
|
|
104
113
|
})
|
|
@@ -108,6 +117,7 @@ export class ExplanationController {
|
|
|
108
117
|
status: "ready",
|
|
109
118
|
text: finalText.trim() || text.trim() || "No answer returned.",
|
|
110
119
|
};
|
|
120
|
+
this.onAskChanged?.(this.askState);
|
|
111
121
|
})
|
|
112
122
|
.catch((error) => {
|
|
113
123
|
if (requestId !== this.askRequestId) return;
|
|
@@ -116,6 +126,7 @@ export class ExplanationController {
|
|
|
116
126
|
status: "error",
|
|
117
127
|
message: error instanceof Error ? error.message : String(error),
|
|
118
128
|
};
|
|
129
|
+
this.onAskChanged?.(this.askState);
|
|
119
130
|
})
|
|
120
131
|
.finally(() => {
|
|
121
132
|
if (requestId !== this.askRequestId) return;
|
|
@@ -134,6 +145,7 @@ export class ExplanationController {
|
|
|
134
145
|
this.askAbortController?.abort();
|
|
135
146
|
this.askAbortController = undefined;
|
|
136
147
|
this.askState = undefined;
|
|
148
|
+
this.onAskChanged?.(undefined);
|
|
137
149
|
}
|
|
138
150
|
|
|
139
151
|
ensure(scope: ExplanationScope | undefined): void {
|
package/src/index.ts
CHANGED
|
@@ -10,12 +10,14 @@ import { buildReviewPrompt } from "./review/prompt.ts";
|
|
|
10
10
|
import { ReviewComponent } from "./review/component.ts";
|
|
11
11
|
import type {
|
|
12
12
|
DiffSource,
|
|
13
|
+
PersistedAsk,
|
|
13
14
|
ReviewComment,
|
|
14
15
|
ReviewResult,
|
|
15
16
|
} from "./review/types.ts";
|
|
16
17
|
|
|
17
18
|
const DIFF_REVIEW_CACHE_ENTRY = "pi-diff-review-cache";
|
|
18
19
|
const DIFF_REVIEW_EXPLANATION_CACHE_ENTRY = "pi-diff-review-explanation-cache";
|
|
20
|
+
const DIFF_REVIEW_ASK_CACHE_ENTRY = "pi-diff-review-ask-cache";
|
|
19
21
|
|
|
20
22
|
type DiffReviewCacheEntry = {
|
|
21
23
|
cacheKey: string;
|
|
@@ -29,6 +31,12 @@ type DiffExplanationCacheEntry = {
|
|
|
29
31
|
updatedAt: number;
|
|
30
32
|
};
|
|
31
33
|
|
|
34
|
+
type DiffAskCacheEntry = {
|
|
35
|
+
cacheKey: string;
|
|
36
|
+
ask?: PersistedAsk;
|
|
37
|
+
updatedAt: number;
|
|
38
|
+
};
|
|
39
|
+
|
|
32
40
|
function getDiffCacheKey(
|
|
33
41
|
cwd: string,
|
|
34
42
|
source: DiffSource,
|
|
@@ -134,6 +142,56 @@ function persistCachedExplanations(
|
|
|
134
142
|
} satisfies DiffExplanationCacheEntry);
|
|
135
143
|
}
|
|
136
144
|
|
|
145
|
+
function getCachedAsk(
|
|
146
|
+
ctx: ExtensionCommandContext,
|
|
147
|
+
cacheKey: string,
|
|
148
|
+
): PersistedAsk | undefined {
|
|
149
|
+
let latest: DiffAskCacheEntry | undefined;
|
|
150
|
+
for (const entry of ctx.sessionManager.getEntries()) {
|
|
151
|
+
if (
|
|
152
|
+
entry.type !== "custom" ||
|
|
153
|
+
entry.customType !== DIFF_REVIEW_ASK_CACHE_ENTRY
|
|
154
|
+
) {
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const data = entry.data as Partial<DiffAskCacheEntry> | undefined;
|
|
159
|
+
if (data?.cacheKey !== cacheKey) continue;
|
|
160
|
+
|
|
161
|
+
const ask = data.ask;
|
|
162
|
+
const validAsk =
|
|
163
|
+
ask &&
|
|
164
|
+
typeof ask === "object" &&
|
|
165
|
+
typeof ask.scopeKey === "string" &&
|
|
166
|
+
typeof ask.anchorLineId === "string" &&
|
|
167
|
+
typeof ask.text === "string"
|
|
168
|
+
? ask
|
|
169
|
+
: undefined;
|
|
170
|
+
|
|
171
|
+
if (!latest || (data.updatedAt ?? 0) >= latest.updatedAt) {
|
|
172
|
+
latest = {
|
|
173
|
+
cacheKey: data.cacheKey,
|
|
174
|
+
ask: validAsk,
|
|
175
|
+
updatedAt: data.updatedAt ?? 0,
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return latest?.ask;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function persistCachedAsk(
|
|
184
|
+
pi: ExtensionAPI,
|
|
185
|
+
cacheKey: string,
|
|
186
|
+
ask?: PersistedAsk,
|
|
187
|
+
): void {
|
|
188
|
+
pi.appendEntry(DIFF_REVIEW_ASK_CACHE_ENTRY, {
|
|
189
|
+
cacheKey,
|
|
190
|
+
ask,
|
|
191
|
+
updatedAt: Date.now(),
|
|
192
|
+
} satisfies DiffAskCacheEntry);
|
|
193
|
+
}
|
|
194
|
+
|
|
137
195
|
export function registerDiffReviewCommand(pi: ExtensionAPI): void {
|
|
138
196
|
pi.registerCommand("diff", {
|
|
139
197
|
description: "Review a git diff in a custom TUI (/diff [git diff args])",
|
|
@@ -158,6 +216,7 @@ export function registerDiffReviewCommand(pi: ExtensionAPI): void {
|
|
|
158
216
|
const cacheKey = getDiffCacheKey(ctx.cwd, source, diffText);
|
|
159
217
|
const comments = getCachedComments(ctx, cacheKey);
|
|
160
218
|
const explanations = getCachedExplanations(ctx, cacheKey);
|
|
219
|
+
const ask = getCachedAsk(ctx, cacheKey);
|
|
161
220
|
if (comments.size > 0) {
|
|
162
221
|
ctx.ui.notify(
|
|
163
222
|
`Restored ${comments.size} cached diff comment${comments.size === 1 ? "" : "s"}.`,
|
|
@@ -170,6 +229,9 @@ export function registerDiffReviewCommand(pi: ExtensionAPI): void {
|
|
|
170
229
|
"info",
|
|
171
230
|
);
|
|
172
231
|
}
|
|
232
|
+
if (ask) {
|
|
233
|
+
ctx.ui.notify("Restored cached ask answer.", "info");
|
|
234
|
+
}
|
|
173
235
|
|
|
174
236
|
const result = await ctx.ui.custom<ReviewResult>(
|
|
175
237
|
(tui, theme, _keybindings, done) => {
|
|
@@ -188,6 +250,10 @@ export function registerDiffReviewCommand(pi: ExtensionAPI): void {
|
|
|
188
250
|
(updatedExplanations) => {
|
|
189
251
|
persistCachedExplanations(pi, cacheKey, updatedExplanations);
|
|
190
252
|
},
|
|
253
|
+
ask,
|
|
254
|
+
(updatedAsk) => {
|
|
255
|
+
persistCachedAsk(pi, cacheKey, updatedAsk);
|
|
256
|
+
},
|
|
191
257
|
);
|
|
192
258
|
},
|
|
193
259
|
);
|
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,
|
|
@@ -58,6 +59,7 @@ type AnnotatedDiffRow =
|
|
|
58
59
|
const HELP_COMMANDS = [
|
|
59
60
|
["h", "show or hide this help"],
|
|
60
61
|
["j/k or arrows", "move selection"],
|
|
62
|
+
["PgUp / PgDown", "move up or down half a page"],
|
|
61
63
|
["ctrl-u / ctrl-d", "move up or down half a page"],
|
|
62
64
|
["g / G", "jump to top or bottom"],
|
|
63
65
|
["n / p", "jump to next or previous hunk"],
|
|
@@ -85,8 +87,10 @@ export class ReviewComponent {
|
|
|
85
87
|
|
|
86
88
|
private inlineAnnotationsVisible = true;
|
|
87
89
|
private visibleExplanationKeys = new Set<string>();
|
|
90
|
+
private explanationAnchorByScope = new Map<string, number>();
|
|
88
91
|
private askInputMode = false;
|
|
89
92
|
private askScope?: ExplanationScope;
|
|
93
|
+
private askAnchorIndex?: number;
|
|
90
94
|
private explanationController: ExplanationController;
|
|
91
95
|
private editor: Editor;
|
|
92
96
|
private splitRows?: SplitDiffRow[];
|
|
@@ -116,6 +120,8 @@ export class ReviewComponent {
|
|
|
116
120
|
private onCommentsChanged?: (comments: Map<string, ReviewComment>) => void,
|
|
117
121
|
cachedExplanations?: Map<string, string>,
|
|
118
122
|
private onExplanationsChanged?: (explanations: Map<string, string>) => void,
|
|
123
|
+
cachedAsk?: PersistedAsk,
|
|
124
|
+
private onAskChanged?: (ask?: PersistedAsk) => void,
|
|
119
125
|
) {
|
|
120
126
|
const firstCommentable = this.lines.findIndex((line) => line.commentable);
|
|
121
127
|
this.navigation = new ReviewNavigationState(
|
|
@@ -124,11 +130,36 @@ export class ReviewComponent {
|
|
|
124
130
|
);
|
|
125
131
|
this.lines.forEach((line, index) => this.lineIndexById.set(line.id, index));
|
|
126
132
|
this.search = new ReviewSearchState(this.lines);
|
|
133
|
+
|
|
134
|
+
const restoredAsk = cachedAsk
|
|
135
|
+
? this.restorePersistedAsk(cachedAsk)
|
|
136
|
+
: undefined;
|
|
137
|
+
if (restoredAsk) {
|
|
138
|
+
this.askScope = restoredAsk.scope;
|
|
139
|
+
this.askAnchorIndex = restoredAsk.anchorIndex;
|
|
140
|
+
}
|
|
141
|
+
|
|
127
142
|
this.explanationController = new ExplanationController(
|
|
128
143
|
tui,
|
|
129
144
|
explainer,
|
|
130
145
|
cachedExplanations,
|
|
131
146
|
onExplanationsChanged,
|
|
147
|
+
restoredAsk ? cachedAsk?.text : undefined,
|
|
148
|
+
(state) => {
|
|
149
|
+
if (!state) {
|
|
150
|
+
this.onAskChanged?.(undefined);
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
if (state.status !== "ready") return;
|
|
154
|
+
if (!this.askScope || this.askAnchorIndex == null) return;
|
|
155
|
+
const anchorLine = this.lines[this.askAnchorIndex];
|
|
156
|
+
if (!anchorLine) return;
|
|
157
|
+
this.onAskChanged?.({
|
|
158
|
+
scopeKey: this.askScope.key,
|
|
159
|
+
anchorLineId: anchorLine.id,
|
|
160
|
+
text: state.text,
|
|
161
|
+
});
|
|
162
|
+
},
|
|
132
163
|
);
|
|
133
164
|
|
|
134
165
|
this.editor = new Editor(tui as never, {
|
|
@@ -151,6 +182,7 @@ export class ReviewComponent {
|
|
|
151
182
|
this.explanationController.ask(this.askScope, question);
|
|
152
183
|
} else {
|
|
153
184
|
this.askScope = undefined;
|
|
185
|
+
this.askAnchorIndex = undefined;
|
|
154
186
|
}
|
|
155
187
|
this.invalidateAnnotatedRows();
|
|
156
188
|
this.tui.requestRender(true);
|
|
@@ -259,7 +291,11 @@ export class ReviewComponent {
|
|
|
259
291
|
return;
|
|
260
292
|
}
|
|
261
293
|
if (data === "q") {
|
|
262
|
-
this.
|
|
294
|
+
if (this.hasSelection()) {
|
|
295
|
+
this.clearSelection();
|
|
296
|
+
} else {
|
|
297
|
+
this.done({ action: "cancel" });
|
|
298
|
+
}
|
|
263
299
|
return;
|
|
264
300
|
}
|
|
265
301
|
if (data === "h") {
|
|
@@ -286,11 +322,11 @@ export class ReviewComponent {
|
|
|
286
322
|
this.startSearchMode();
|
|
287
323
|
return;
|
|
288
324
|
}
|
|
289
|
-
if (matchesKey(data, "ctrl+d")) {
|
|
325
|
+
if (matchesKey(data, "pageDown") || matchesKey(data, "ctrl+d")) {
|
|
290
326
|
this.move(this.getPageMoveAmount());
|
|
291
327
|
return;
|
|
292
328
|
}
|
|
293
|
-
if (matchesKey(data, "ctrl+u")) {
|
|
329
|
+
if (matchesKey(data, "pageUp") || matchesKey(data, "ctrl+u")) {
|
|
294
330
|
this.move(-this.getPageMoveAmount());
|
|
295
331
|
return;
|
|
296
332
|
}
|
|
@@ -700,10 +736,10 @@ export class ReviewComponent {
|
|
|
700
736
|
const scope = getCurrentHunkScope(this.lines, lineIndex);
|
|
701
737
|
if (!scope) return;
|
|
702
738
|
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
739
|
+
if (
|
|
740
|
+
this.visibleExplanationKeys.has(scope.key) &&
|
|
741
|
+
this.getExplanationAnchorIndex(lineIndex, scope.key) === lineIndex
|
|
742
|
+
) {
|
|
707
743
|
const explanation = this.explanationController.getState(scope);
|
|
708
744
|
if (!this.explanationController.isAvailable) {
|
|
709
745
|
this.pushExplanationBlock(rows, "Explanation unavailable.", width);
|
|
@@ -726,11 +762,19 @@ export class ReviewComponent {
|
|
|
726
762
|
}
|
|
727
763
|
}
|
|
728
764
|
|
|
729
|
-
if (
|
|
765
|
+
if (
|
|
766
|
+
this.askInputMode &&
|
|
767
|
+
this.askScope?.key === scope.key &&
|
|
768
|
+
this.askAnchorIndex === lineIndex
|
|
769
|
+
) {
|
|
730
770
|
this.pushInlineEditorBlock(rows, "Ask about this hunk", width);
|
|
731
771
|
}
|
|
732
772
|
|
|
733
|
-
if (
|
|
773
|
+
if (
|
|
774
|
+
!this.askInputMode &&
|
|
775
|
+
this.askScope?.key === scope.key &&
|
|
776
|
+
this.askAnchorIndex === lineIndex
|
|
777
|
+
) {
|
|
734
778
|
const askState = this.explanationController.getAskState();
|
|
735
779
|
if (askState) this.pushAskBlock(rows, askState, width);
|
|
736
780
|
}
|
|
@@ -908,6 +952,27 @@ export class ReviewComponent {
|
|
|
908
952
|
return comments.sort((a, b) => a.id.localeCompare(b.id));
|
|
909
953
|
}
|
|
910
954
|
|
|
955
|
+
private restorePersistedAsk(
|
|
956
|
+
cachedAsk: PersistedAsk,
|
|
957
|
+
): { scope: ExplanationScope; anchorIndex: number } | undefined {
|
|
958
|
+
const anchorIndex = this.lineIndexById.get(cachedAsk.anchorLineId);
|
|
959
|
+
if (anchorIndex != null) {
|
|
960
|
+
const scope = getCurrentHunkScope(this.lines, anchorIndex);
|
|
961
|
+
if (scope?.key === cachedAsk.scopeKey) {
|
|
962
|
+
return { scope, anchorIndex };
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
for (let index = 0; index < this.lines.length; index++) {
|
|
967
|
+
const scope = getCurrentHunkScope(this.lines, index);
|
|
968
|
+
if (scope?.key === cachedAsk.scopeKey) {
|
|
969
|
+
return { scope, anchorIndex: index };
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
return undefined;
|
|
974
|
+
}
|
|
975
|
+
|
|
911
976
|
private getHunkEndIndex(selected: number): number | undefined {
|
|
912
977
|
const selectedLine = this.lines[selected];
|
|
913
978
|
if (!selectedLine?.filePath || !selectedLine.hunkLabel) return undefined;
|
|
@@ -923,6 +988,16 @@ export class ReviewComponent {
|
|
|
923
988
|
return end;
|
|
924
989
|
}
|
|
925
990
|
|
|
991
|
+
private getExplanationAnchorIndex(
|
|
992
|
+
lineIndex: number,
|
|
993
|
+
scopeKey: string,
|
|
994
|
+
): number | undefined {
|
|
995
|
+
return (
|
|
996
|
+
this.explanationAnchorByScope.get(scopeKey) ??
|
|
997
|
+
this.getHunkEndIndex(lineIndex)
|
|
998
|
+
);
|
|
999
|
+
}
|
|
1000
|
+
|
|
926
1001
|
private renderSplitDiffRowAt(splitRowIndex: number, width: number): string {
|
|
927
1002
|
const splitRow = this.getSplitDiffRows()[splitRowIndex];
|
|
928
1003
|
if (!splitRow) return " ".repeat(width);
|
|
@@ -974,6 +1049,7 @@ export class ReviewComponent {
|
|
|
974
1049
|
if (!scope) return;
|
|
975
1050
|
this.clearAsk();
|
|
976
1051
|
this.askScope = scope;
|
|
1052
|
+
this.askAnchorIndex = this.selected;
|
|
977
1053
|
this.askInputMode = true;
|
|
978
1054
|
this.inlineAnnotationsVisible = true;
|
|
979
1055
|
this.editor.setText("");
|
|
@@ -984,7 +1060,10 @@ export class ReviewComponent {
|
|
|
984
1060
|
private exitAskInputMode(): void {
|
|
985
1061
|
this.askInputMode = false;
|
|
986
1062
|
this.editor.setText("");
|
|
987
|
-
if (!this.explanationController.getAskState())
|
|
1063
|
+
if (!this.explanationController.getAskState()) {
|
|
1064
|
+
this.askScope = undefined;
|
|
1065
|
+
this.askAnchorIndex = undefined;
|
|
1066
|
+
}
|
|
988
1067
|
this.invalidateAnnotatedRows();
|
|
989
1068
|
this.tui.requestRender(true);
|
|
990
1069
|
}
|
|
@@ -992,6 +1071,7 @@ export class ReviewComponent {
|
|
|
992
1071
|
private clearAsk(): void {
|
|
993
1072
|
this.askInputMode = false;
|
|
994
1073
|
this.askScope = undefined;
|
|
1074
|
+
this.askAnchorIndex = undefined;
|
|
995
1075
|
this.explanationController.clearAsk();
|
|
996
1076
|
this.editor.setText("");
|
|
997
1077
|
this.invalidateAnnotatedRows();
|
|
@@ -1027,6 +1107,7 @@ export class ReviewComponent {
|
|
|
1027
1107
|
|
|
1028
1108
|
private hideInlineExplanation(): void {
|
|
1029
1109
|
this.visibleExplanationKeys.clear();
|
|
1110
|
+
this.explanationAnchorByScope.clear();
|
|
1030
1111
|
}
|
|
1031
1112
|
|
|
1032
1113
|
private toggleExplanationPane(): void {
|
|
@@ -1035,8 +1116,10 @@ export class ReviewComponent {
|
|
|
1035
1116
|
|
|
1036
1117
|
if (this.visibleExplanationKeys.has(scope.key)) {
|
|
1037
1118
|
this.visibleExplanationKeys.delete(scope.key);
|
|
1119
|
+
this.explanationAnchorByScope.delete(scope.key);
|
|
1038
1120
|
} else {
|
|
1039
1121
|
this.visibleExplanationKeys.add(scope.key);
|
|
1122
|
+
this.explanationAnchorByScope.set(scope.key, this.selected);
|
|
1040
1123
|
this.ensureCurrentExplanation();
|
|
1041
1124
|
}
|
|
1042
1125
|
|
package/src/review/types.ts
CHANGED