pi-diff-review 0.1.16 → 0.1.18
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 +1 -0
- package/package.json +1 -1
- package/src/explanation/controller.ts +62 -4
- package/src/explanation/explainer.ts +12 -1
- package/src/review/component.ts +304 -34
package/README.md
CHANGED
|
@@ -62,6 +62,7 @@ Review a branch or commit range by passing any `git diff` arguments after `/diff
|
|
|
62
62
|
- `/diff --cached` reviews staged changes
|
|
63
63
|
- `/diff main...HEAD` reviews changes on the current branch relative to `main`
|
|
64
64
|
- `/diff <git-diff-args>` passes arguments through to `git diff`
|
|
65
|
+
- `h` toggles the command help modal
|
|
65
66
|
- `j/k` or arrow keys to move
|
|
66
67
|
- `g/G` to jump to the top or bottom of the diff
|
|
67
68
|
- `ctrl-u` / `ctrl-d` to move up/down by half a page
|
package/package.json
CHANGED
|
@@ -49,6 +49,9 @@ export class ExplanationController {
|
|
|
49
49
|
readonly explanations = new Map<string, ExplanationState>();
|
|
50
50
|
private abortController?: AbortController;
|
|
51
51
|
private requestId = 0;
|
|
52
|
+
private askState: ExplanationState | undefined;
|
|
53
|
+
private askAbortController?: AbortController;
|
|
54
|
+
private askRequestId = 0;
|
|
52
55
|
private loadingFrame = 0;
|
|
53
56
|
private loadingTimer?: ReturnType<typeof setInterval>;
|
|
54
57
|
|
|
@@ -79,6 +82,60 @@ export class ExplanationController {
|
|
|
79
82
|
return LOADING_FRAMES[this.loadingFrame % LOADING_FRAMES.length] ?? "⠋";
|
|
80
83
|
}
|
|
81
84
|
|
|
85
|
+
ask(scope: ExplanationScope, question: string): void {
|
|
86
|
+
if (!this.explainer) return;
|
|
87
|
+
this.askAbortController?.abort();
|
|
88
|
+
const controller = new AbortController();
|
|
89
|
+
this.askAbortController = controller;
|
|
90
|
+
const requestId = ++this.askRequestId;
|
|
91
|
+
let text = "";
|
|
92
|
+
this.askState = { status: "loading", text };
|
|
93
|
+
this.startLoadingTimer();
|
|
94
|
+
|
|
95
|
+
void this.explainer
|
|
96
|
+
.explain(scope, question, {
|
|
97
|
+
signal: controller.signal,
|
|
98
|
+
onDelta: (delta) => {
|
|
99
|
+
if (requestId !== this.askRequestId) return;
|
|
100
|
+
text += delta;
|
|
101
|
+
this.askState = { status: "loading", text };
|
|
102
|
+
this.tui.requestRender();
|
|
103
|
+
},
|
|
104
|
+
})
|
|
105
|
+
.then((finalText) => {
|
|
106
|
+
if (requestId !== this.askRequestId) return;
|
|
107
|
+
this.askState = {
|
|
108
|
+
status: "ready",
|
|
109
|
+
text: finalText.trim() || text.trim() || "No answer returned.",
|
|
110
|
+
};
|
|
111
|
+
})
|
|
112
|
+
.catch((error) => {
|
|
113
|
+
if (requestId !== this.askRequestId) return;
|
|
114
|
+
if (controller.signal.aborted) return;
|
|
115
|
+
this.askState = {
|
|
116
|
+
status: "error",
|
|
117
|
+
message: error instanceof Error ? error.message : String(error),
|
|
118
|
+
};
|
|
119
|
+
})
|
|
120
|
+
.finally(() => {
|
|
121
|
+
if (requestId !== this.askRequestId) return;
|
|
122
|
+
this.stopLoadingTimerIfIdle();
|
|
123
|
+
this.tui.requestRender();
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
this.tui.requestRender();
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
getAskState(): ExplanationState | undefined {
|
|
130
|
+
return this.askState;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
clearAsk(): void {
|
|
134
|
+
this.askAbortController?.abort();
|
|
135
|
+
this.askAbortController = undefined;
|
|
136
|
+
this.askState = undefined;
|
|
137
|
+
}
|
|
138
|
+
|
|
82
139
|
ensure(scope: ExplanationScope | undefined): void {
|
|
83
140
|
if (!scope || !this.explainer) return;
|
|
84
141
|
if (this.explanations.has(scope.key)) return;
|
|
@@ -93,7 +150,7 @@ export class ExplanationController {
|
|
|
93
150
|
this.startLoadingTimer();
|
|
94
151
|
|
|
95
152
|
void this.explainer
|
|
96
|
-
.explain(scope, {
|
|
153
|
+
.explain(scope, undefined, {
|
|
97
154
|
signal: controller.signal,
|
|
98
155
|
onDelta: (delta) => {
|
|
99
156
|
if (requestId !== this.requestId) return;
|
|
@@ -129,6 +186,7 @@ export class ExplanationController {
|
|
|
129
186
|
|
|
130
187
|
dispose(): void {
|
|
131
188
|
this.abortController?.abort();
|
|
189
|
+
this.askAbortController?.abort();
|
|
132
190
|
this.stopLoadingTimer();
|
|
133
191
|
}
|
|
134
192
|
|
|
@@ -153,9 +211,9 @@ export class ExplanationController {
|
|
|
153
211
|
}
|
|
154
212
|
|
|
155
213
|
private stopLoadingTimerIfIdle(): void {
|
|
156
|
-
const hasLoading =
|
|
157
|
-
(
|
|
158
|
-
|
|
214
|
+
const hasLoading =
|
|
215
|
+
[...this.explanations.values()].some((e) => e.status === "loading") ||
|
|
216
|
+
this.askState?.status === "loading";
|
|
159
217
|
if (!hasLoading) this.stopLoadingTimer();
|
|
160
218
|
}
|
|
161
219
|
|
|
@@ -18,6 +18,7 @@ export type ExplanationState =
|
|
|
18
18
|
export type DiffExplainer = {
|
|
19
19
|
explain(
|
|
20
20
|
scope: ExplanationScope,
|
|
21
|
+
question?: string,
|
|
21
22
|
options?: {
|
|
22
23
|
signal?: AbortSignal;
|
|
23
24
|
onDelta?: (delta: string) => void;
|
|
@@ -25,6 +26,13 @@ export type DiffExplainer = {
|
|
|
25
26
|
): Promise<string>;
|
|
26
27
|
};
|
|
27
28
|
|
|
29
|
+
export function buildAskPrompt(
|
|
30
|
+
scope: ExplanationScope,
|
|
31
|
+
question: string,
|
|
32
|
+
): string {
|
|
33
|
+
return `Given this git diff hunk:\n\`\`\`diff\n${scope.diffText}\n\`\`\`\n\n${question}`;
|
|
34
|
+
}
|
|
35
|
+
|
|
28
36
|
export function buildExplanationPrompt(scope: ExplanationScope): string {
|
|
29
37
|
return `Explain this git diff hunk for a code reviewer.
|
|
30
38
|
|
|
@@ -46,6 +54,7 @@ export class PiModelDiffExplainer implements DiffExplainer {
|
|
|
46
54
|
|
|
47
55
|
async explain(
|
|
48
56
|
scope: ExplanationScope,
|
|
57
|
+
question?: string,
|
|
49
58
|
options: {
|
|
50
59
|
signal?: AbortSignal;
|
|
51
60
|
onDelta?: (delta: string) => void;
|
|
@@ -67,7 +76,9 @@ export class PiModelDiffExplainer implements DiffExplainer {
|
|
|
67
76
|
messages: [
|
|
68
77
|
{
|
|
69
78
|
role: "user",
|
|
70
|
-
content:
|
|
79
|
+
content: question
|
|
80
|
+
? buildAskPrompt(scope, question)
|
|
81
|
+
: buildExplanationPrompt(scope),
|
|
71
82
|
timestamp: Date.now(),
|
|
72
83
|
},
|
|
73
84
|
],
|
package/src/review/component.ts
CHANGED
|
@@ -9,7 +9,11 @@ import {
|
|
|
9
9
|
visibleWidth,
|
|
10
10
|
wrapTextWithAnsi,
|
|
11
11
|
} from "@earendil-works/pi-tui";
|
|
12
|
-
import type {
|
|
12
|
+
import type {
|
|
13
|
+
DiffExplainer,
|
|
14
|
+
ExplanationScope,
|
|
15
|
+
ExplanationState,
|
|
16
|
+
} from "../explanation/explainer.ts";
|
|
13
17
|
import {
|
|
14
18
|
GLOBAL_COMMENT_KEY,
|
|
15
19
|
buildCommentFromSelection,
|
|
@@ -51,14 +55,38 @@ type AnnotatedDiffRow =
|
|
|
51
55
|
| { kind: "split"; splitRowIndex: number }
|
|
52
56
|
| InlineBoxRow;
|
|
53
57
|
|
|
58
|
+
const HELP_COMMANDS = [
|
|
59
|
+
["h", "show or hide this help"],
|
|
60
|
+
["j/k or arrows", "move selection"],
|
|
61
|
+
["ctrl-u / ctrl-d", "move up or down half a page"],
|
|
62
|
+
["g / G", "jump to top or bottom"],
|
|
63
|
+
["n / p", "jump to next or previous hunk"],
|
|
64
|
+
["/", "search diff lines"],
|
|
65
|
+
["n / N", "jump between search matches"],
|
|
66
|
+
["J / K", "extend highlighted selection"],
|
|
67
|
+
["c", "add or edit a line or range comment"],
|
|
68
|
+
["C", "add or edit an overall diff comment"],
|
|
69
|
+
["x", "delete the current line or range comment"],
|
|
70
|
+
["t", "toggle inline comments and explanations"],
|
|
71
|
+
["v", "toggle unified or split rendering"],
|
|
72
|
+
["?", "toggle AI explanation for current hunk"],
|
|
73
|
+
["a", "ask a question about the current hunk"],
|
|
74
|
+
["Enter", "submit comments, save edits, or jump to search result"],
|
|
75
|
+
["Esc", "close help, cancel search/edit, clear selection, or exit"],
|
|
76
|
+
["q", "exit review"],
|
|
77
|
+
] as const;
|
|
78
|
+
|
|
54
79
|
export class ReviewComponent {
|
|
55
80
|
private navigation: ReviewNavigationState;
|
|
56
81
|
private editMode = false;
|
|
57
82
|
private editingCommentKey?: string;
|
|
58
83
|
private search: ReviewSearchState;
|
|
84
|
+
private helpVisible = false;
|
|
59
85
|
|
|
60
86
|
private inlineAnnotationsVisible = true;
|
|
61
87
|
private visibleExplanationKeys = new Set<string>();
|
|
88
|
+
private askInputMode = false;
|
|
89
|
+
private askScope?: ExplanationScope;
|
|
62
90
|
private explanationController: ExplanationController;
|
|
63
91
|
private editor: Editor;
|
|
64
92
|
private splitRows?: SplitDiffRow[];
|
|
@@ -115,6 +143,19 @@ export class ReviewComponent {
|
|
|
115
143
|
});
|
|
116
144
|
|
|
117
145
|
this.editor.onSubmit = (value) => {
|
|
146
|
+
if (this.askInputMode) {
|
|
147
|
+
this.askInputMode = false;
|
|
148
|
+
this.editor.setText("");
|
|
149
|
+
const question = value.trim();
|
|
150
|
+
if (question && this.askScope) {
|
|
151
|
+
this.explanationController.ask(this.askScope, question);
|
|
152
|
+
} else {
|
|
153
|
+
this.askScope = undefined;
|
|
154
|
+
}
|
|
155
|
+
this.invalidateAnnotatedRows();
|
|
156
|
+
this.tui.requestRender(true);
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
118
159
|
const trimmed = value.trim();
|
|
119
160
|
if (this.editingCommentKey === GLOBAL_COMMENT_KEY) {
|
|
120
161
|
if (!trimmed) {
|
|
@@ -171,6 +212,13 @@ export class ReviewComponent {
|
|
|
171
212
|
}
|
|
172
213
|
|
|
173
214
|
handleInput(data: string): void {
|
|
215
|
+
if (this.helpVisible) {
|
|
216
|
+
if (data === "h" || matchesKey(data, "escape")) {
|
|
217
|
+
this.toggleHelp();
|
|
218
|
+
}
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
|
|
174
222
|
if (this.editMode) {
|
|
175
223
|
if (matchesKey(data, "escape") || matchesKey(data, "ctrl+c")) {
|
|
176
224
|
this.exitEditMode();
|
|
@@ -182,13 +230,26 @@ export class ReviewComponent {
|
|
|
182
230
|
return;
|
|
183
231
|
}
|
|
184
232
|
|
|
233
|
+
if (this.askInputMode) {
|
|
234
|
+
if (matchesKey(data, "escape") || matchesKey(data, "ctrl+c")) {
|
|
235
|
+
this.exitAskInputMode();
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
this.editor.handleInput(data);
|
|
239
|
+
this.invalidateAnnotatedRows();
|
|
240
|
+
this.tui.requestRender();
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
|
|
185
244
|
if (this.search.mode) {
|
|
186
245
|
this.handleSearchInput(data);
|
|
187
246
|
return;
|
|
188
247
|
}
|
|
189
248
|
|
|
190
249
|
if (matchesKey(data, "escape")) {
|
|
191
|
-
if (this.
|
|
250
|
+
if (this.askScope) {
|
|
251
|
+
this.clearAsk();
|
|
252
|
+
} else if (this.search.query) {
|
|
192
253
|
this.clearSearch();
|
|
193
254
|
} else if (this.hasSelection()) {
|
|
194
255
|
this.clearSelection();
|
|
@@ -201,6 +262,10 @@ export class ReviewComponent {
|
|
|
201
262
|
this.done({ action: "cancel" });
|
|
202
263
|
return;
|
|
203
264
|
}
|
|
265
|
+
if (data === "h") {
|
|
266
|
+
this.toggleHelp();
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
204
269
|
if (data === "t") {
|
|
205
270
|
this.toggleInlineAnnotations();
|
|
206
271
|
return;
|
|
@@ -213,6 +278,10 @@ export class ReviewComponent {
|
|
|
213
278
|
this.toggleExplanationPane();
|
|
214
279
|
return;
|
|
215
280
|
}
|
|
281
|
+
if (data === "a") {
|
|
282
|
+
this.startAskMode();
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
216
285
|
if (data === "/") {
|
|
217
286
|
this.startSearchMode();
|
|
218
287
|
return;
|
|
@@ -292,15 +361,9 @@ export class ReviewComponent {
|
|
|
292
361
|
const output: string[] = [];
|
|
293
362
|
|
|
294
363
|
output.push(
|
|
295
|
-
|
|
296
|
-
this.
|
|
297
|
-
|
|
298
|
-
this.editMode
|
|
299
|
-
? `${this.title} • ${this.lines.length} lines • ${this.comments.size} comments • editing inline comment • Enter save • Esc/Ctrl+C cancel`
|
|
300
|
-
: this.hasSelection()
|
|
301
|
-
? `${this.title} • ${this.lines.length} lines • ${this.comments.size} comments • J/K extend • Esc clear selection • c comment range • C overall comment • Enter submit`
|
|
302
|
-
: `${this.title} • ${this.lines.length} lines • ${this.comments.size} comments • ${this.getPositionText(selectedLine)} • inline ${this.inlineAnnotationsVisible ? "shown" : "hidden"} • j/k move • g/G top/bottom • ctrl-u/d page • / search • t annotations • v unified/split • ? explain • J/K extend • c comment • C overall • x delete • ${this.search.query ? "n/N search" : "n/p hunk"} • Enter submit • q quit`,
|
|
303
|
-
),
|
|
364
|
+
this.renderStatusLine(
|
|
365
|
+
this.getHeaderText(selectedLine),
|
|
366
|
+
"Press h for help",
|
|
304
367
|
width,
|
|
305
368
|
),
|
|
306
369
|
);
|
|
@@ -314,9 +377,132 @@ export class ReviewComponent {
|
|
|
314
377
|
width,
|
|
315
378
|
),
|
|
316
379
|
);
|
|
380
|
+
return this.helpVisible ? this.renderHelpModal(output, width) : output;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
private getHeaderText(selectedLine?: ReviewLine): string {
|
|
384
|
+
const base = `${this.title} • ${this.lines.length} lines • ${this.comments.size} comments`;
|
|
385
|
+
|
|
386
|
+
if (this.editMode) {
|
|
387
|
+
return `${base} • editing ${this.editingCommentKey === GLOBAL_COMMENT_KEY ? "overall comment" : "inline comment"}`;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
if (this.hasSelection()) {
|
|
391
|
+
return `${base} • selection active`;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
return `${base} • ${this.getPositionText(selectedLine)} • inline ${this.inlineAnnotationsVisible ? "shown" : "hidden"} • ${this.diffRenderMode}`;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
private renderStatusLine(left: string, right: string, width: number): string {
|
|
398
|
+
const styledLeft = this.theme.fg("dim", left);
|
|
399
|
+
const styledRight = this.theme.fg("muted", right);
|
|
400
|
+
const rightWidth = visibleWidth(styledRight);
|
|
401
|
+
const leftWidth = Math.max(0, width - rightWidth - 1);
|
|
402
|
+
const truncatedLeft = truncateToWidth(styledLeft, leftWidth);
|
|
403
|
+
const spacer = Math.max(
|
|
404
|
+
1,
|
|
405
|
+
width - visibleWidth(truncatedLeft) - rightWidth,
|
|
406
|
+
);
|
|
407
|
+
return truncateToWidth(
|
|
408
|
+
`${truncatedLeft}${" ".repeat(spacer)}${styledRight}`,
|
|
409
|
+
width,
|
|
410
|
+
);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
private renderHelpModal(rows: string[], width: number): string[] {
|
|
414
|
+
if (rows.length === 0 || width < 8) return rows;
|
|
415
|
+
|
|
416
|
+
const modalWidth = Math.max(8, Math.min(72, width - 2));
|
|
417
|
+
const contentWidth = Math.max(4, modalWidth - 4);
|
|
418
|
+
const keyWidth = Math.min(16, Math.max(8, Math.floor(contentWidth * 0.35)));
|
|
419
|
+
const modalRows = this.buildHelpModalRows(
|
|
420
|
+
modalWidth,
|
|
421
|
+
contentWidth,
|
|
422
|
+
keyWidth,
|
|
423
|
+
);
|
|
424
|
+
const maxRows = Math.max(1, rows.length - 2);
|
|
425
|
+
const visibleModalRows =
|
|
426
|
+
modalRows.length > maxRows
|
|
427
|
+
? [
|
|
428
|
+
...modalRows.slice(0, maxRows - 1),
|
|
429
|
+
this.renderModalRow(
|
|
430
|
+
this.theme.fg("dim", "More commands available on taller screens"),
|
|
431
|
+
contentWidth,
|
|
432
|
+
),
|
|
433
|
+
]
|
|
434
|
+
: modalRows;
|
|
435
|
+
const top = Math.max(
|
|
436
|
+
1,
|
|
437
|
+
Math.floor((rows.length - visibleModalRows.length) / 2),
|
|
438
|
+
);
|
|
439
|
+
const left = Math.max(0, Math.floor((width - modalWidth) / 2));
|
|
440
|
+
|
|
441
|
+
const output = [...rows];
|
|
442
|
+
for (let index = 0; index < visibleModalRows.length; index++) {
|
|
443
|
+
const row = visibleModalRows[index]!;
|
|
444
|
+
output[top + index] = padToWidth(
|
|
445
|
+
truncateToWidth(`${" ".repeat(left)}${row}`, width),
|
|
446
|
+
width,
|
|
447
|
+
);
|
|
448
|
+
}
|
|
317
449
|
return output;
|
|
318
450
|
}
|
|
319
451
|
|
|
452
|
+
private buildHelpModalRows(
|
|
453
|
+
modalWidth: number,
|
|
454
|
+
contentWidth: number,
|
|
455
|
+
keyWidth: number,
|
|
456
|
+
): string[] {
|
|
457
|
+
const rows = [
|
|
458
|
+
this.renderModalHorizontal(" Help ", modalWidth, "top"),
|
|
459
|
+
this.renderModalRow(
|
|
460
|
+
this.theme.fg("dim", "Press h or Esc to close."),
|
|
461
|
+
contentWidth,
|
|
462
|
+
),
|
|
463
|
+
this.renderModalRow("", contentWidth),
|
|
464
|
+
];
|
|
465
|
+
|
|
466
|
+
for (const [keys, description] of HELP_COMMANDS) {
|
|
467
|
+
const keyCell = padToWidth(
|
|
468
|
+
truncateToWidth(this.theme.fg("accent", keys), keyWidth),
|
|
469
|
+
keyWidth,
|
|
470
|
+
);
|
|
471
|
+
rows.push(
|
|
472
|
+
this.renderModalRow(
|
|
473
|
+
`${keyCell} ${this.theme.fg("text", description)}`,
|
|
474
|
+
contentWidth,
|
|
475
|
+
),
|
|
476
|
+
);
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
rows.push(this.renderModalHorizontal("", modalWidth, "bottom"));
|
|
480
|
+
return rows;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
private renderModalRow(text: string, contentWidth: number): string {
|
|
484
|
+
return `${this.theme.fg("borderMuted", "│")} ${padToWidth(
|
|
485
|
+
truncateToWidth(text, contentWidth),
|
|
486
|
+
contentWidth,
|
|
487
|
+
)} ${this.theme.fg("borderMuted", "│")}`;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
private renderModalHorizontal(
|
|
491
|
+
title: string,
|
|
492
|
+
width: number,
|
|
493
|
+
part: "top" | "bottom",
|
|
494
|
+
): string {
|
|
495
|
+
const contentWidth = Math.max(0, width - 2);
|
|
496
|
+
const visibleTitle = truncateToWidth(title, contentWidth);
|
|
497
|
+
const remaining = Math.max(0, contentWidth - visibleWidth(visibleTitle));
|
|
498
|
+
const left = part === "top" ? "╭" : "╰";
|
|
499
|
+
const right = part === "top" ? "╮" : "╯";
|
|
500
|
+
return `${this.theme.fg("borderMuted", left)}${visibleTitle}${this.theme.fg(
|
|
501
|
+
"borderMuted",
|
|
502
|
+
"─".repeat(remaining),
|
|
503
|
+
)}${this.theme.fg("borderMuted", right)}`;
|
|
504
|
+
}
|
|
505
|
+
|
|
320
506
|
private renderAnnotatedDiffRows(width: number, height: number): string[] {
|
|
321
507
|
const rows = this.getAnnotatedRows(width);
|
|
322
508
|
const output: string[] = [];
|
|
@@ -366,7 +552,9 @@ export class ReviewComponent {
|
|
|
366
552
|
this.inlineAnnotationsVisible &&
|
|
367
553
|
this.annotatedRowsVisibleExplanationCount ===
|
|
368
554
|
this.visibleExplanationKeys.size &&
|
|
369
|
-
this.visibleExplanationKeys.size === 0
|
|
555
|
+
this.visibleExplanationKeys.size === 0 &&
|
|
556
|
+
!this.askInputMode &&
|
|
557
|
+
!this.askScope
|
|
370
558
|
) {
|
|
371
559
|
return this.annotatedRows;
|
|
372
560
|
}
|
|
@@ -510,30 +698,41 @@ export class ReviewComponent {
|
|
|
510
698
|
width: number,
|
|
511
699
|
): void {
|
|
512
700
|
const scope = getCurrentHunkScope(this.lines, lineIndex);
|
|
513
|
-
if (!scope
|
|
701
|
+
if (!scope) return;
|
|
514
702
|
|
|
515
703
|
const end = this.getHunkEndIndex(lineIndex);
|
|
516
704
|
if (end !== lineIndex) return;
|
|
517
705
|
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
this.
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
706
|
+
if (this.visibleExplanationKeys.has(scope.key)) {
|
|
707
|
+
const explanation = this.explanationController.getState(scope);
|
|
708
|
+
if (!this.explanationController.isAvailable) {
|
|
709
|
+
this.pushExplanationBlock(rows, "Explanation unavailable.", width);
|
|
710
|
+
} else if (!explanation) {
|
|
711
|
+
this.pushExplanationBlock(rows, "No explanation generated yet.", width);
|
|
712
|
+
} else if (explanation.status === "loading") {
|
|
713
|
+
this.pushExplanationBlock(
|
|
714
|
+
rows,
|
|
715
|
+
`${this.explanationController.getLoadingFrame()} ${explanation.text || "Generating explanation..."}`,
|
|
716
|
+
width,
|
|
717
|
+
);
|
|
718
|
+
} else if (explanation.status === "error") {
|
|
719
|
+
this.pushExplanationBlock(
|
|
720
|
+
rows,
|
|
721
|
+
`Explanation failed: ${explanation.message}`,
|
|
722
|
+
width,
|
|
723
|
+
);
|
|
724
|
+
} else {
|
|
725
|
+
this.pushExplanationBlock(rows, explanation.text, width);
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
if (this.askInputMode && this.askScope?.key === scope.key) {
|
|
730
|
+
this.pushInlineEditorBlock(rows, "Ask about this hunk", width);
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
if (!this.askInputMode && this.askScope?.key === scope.key) {
|
|
734
|
+
const askState = this.explanationController.getAskState();
|
|
735
|
+
if (askState) this.pushAskBlock(rows, askState, width);
|
|
537
736
|
}
|
|
538
737
|
}
|
|
539
738
|
|
|
@@ -568,14 +767,39 @@ export class ReviewComponent {
|
|
|
568
767
|
rows.push({ kind: "comment", text: "", part: "bottom" });
|
|
569
768
|
}
|
|
570
769
|
|
|
770
|
+
private pushAskBlock(
|
|
771
|
+
rows: AnnotatedDiffRow[],
|
|
772
|
+
state: ExplanationState,
|
|
773
|
+
width: number,
|
|
774
|
+
): void {
|
|
775
|
+
if (state.status === "loading") {
|
|
776
|
+
this.pushExplanationBlock(
|
|
777
|
+
rows,
|
|
778
|
+
`${this.explanationController.getLoadingFrame()} ${state.text || "Generating answer..."}`,
|
|
779
|
+
width,
|
|
780
|
+
" 💬 Answer ",
|
|
781
|
+
);
|
|
782
|
+
} else if (state.status === "error") {
|
|
783
|
+
this.pushExplanationBlock(
|
|
784
|
+
rows,
|
|
785
|
+
`Failed: ${state.message}`,
|
|
786
|
+
width,
|
|
787
|
+
" 💬 Answer ",
|
|
788
|
+
);
|
|
789
|
+
} else {
|
|
790
|
+
this.pushExplanationBlock(rows, state.text, width, " 💬 Answer ");
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
|
|
571
794
|
private pushExplanationBlock(
|
|
572
795
|
rows: AnnotatedDiffRow[],
|
|
573
796
|
text: string,
|
|
574
797
|
width: number,
|
|
798
|
+
title = " ✨ Explanation ",
|
|
575
799
|
): void {
|
|
576
800
|
rows.push({
|
|
577
801
|
kind: "explanation",
|
|
578
|
-
text: this.theme.fg("accent",
|
|
802
|
+
text: this.theme.fg("accent", title),
|
|
579
803
|
part: "top",
|
|
580
804
|
});
|
|
581
805
|
const wrapped = wrapTextWithAnsi(
|
|
@@ -742,6 +966,36 @@ export class ReviewComponent {
|
|
|
742
966
|
|
|
743
967
|
dispose(): void {
|
|
744
968
|
this.explanationController.dispose();
|
|
969
|
+
this.clearAsk();
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
private startAskMode(): void {
|
|
973
|
+
const scope = this.getCurrentHunkScope();
|
|
974
|
+
if (!scope) return;
|
|
975
|
+
this.clearAsk();
|
|
976
|
+
this.askScope = scope;
|
|
977
|
+
this.askInputMode = true;
|
|
978
|
+
this.inlineAnnotationsVisible = true;
|
|
979
|
+
this.editor.setText("");
|
|
980
|
+
this.invalidateAnnotatedRows();
|
|
981
|
+
this.tui.requestRender(true);
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
private exitAskInputMode(): void {
|
|
985
|
+
this.askInputMode = false;
|
|
986
|
+
this.editor.setText("");
|
|
987
|
+
if (!this.explanationController.getAskState()) this.askScope = undefined;
|
|
988
|
+
this.invalidateAnnotatedRows();
|
|
989
|
+
this.tui.requestRender(true);
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
private clearAsk(): void {
|
|
993
|
+
this.askInputMode = false;
|
|
994
|
+
this.askScope = undefined;
|
|
995
|
+
this.explanationController.clearAsk();
|
|
996
|
+
this.editor.setText("");
|
|
997
|
+
this.invalidateAnnotatedRows();
|
|
998
|
+
this.tui.requestRender(true);
|
|
745
999
|
}
|
|
746
1000
|
|
|
747
1001
|
private move(delta: number): void {
|
|
@@ -766,6 +1020,11 @@ export class ReviewComponent {
|
|
|
766
1020
|
this.tui.requestRender(true);
|
|
767
1021
|
}
|
|
768
1022
|
|
|
1023
|
+
private toggleHelp(): void {
|
|
1024
|
+
this.helpVisible = !this.helpVisible;
|
|
1025
|
+
this.tui.requestRender(true);
|
|
1026
|
+
}
|
|
1027
|
+
|
|
769
1028
|
private hideInlineExplanation(): void {
|
|
770
1029
|
this.visibleExplanationKeys.clear();
|
|
771
1030
|
}
|
|
@@ -1013,11 +1272,22 @@ export class ReviewComponent {
|
|
|
1013
1272
|
private ensureScroll(viewportHeight: number, width: number): void {
|
|
1014
1273
|
this.navigation.ensureScroll(
|
|
1015
1274
|
viewportHeight,
|
|
1016
|
-
this.
|
|
1275
|
+
this.getScrollTargetDisplayRow(width),
|
|
1017
1276
|
this.getDisplayRowCount(width),
|
|
1018
1277
|
);
|
|
1019
1278
|
}
|
|
1020
1279
|
|
|
1280
|
+
private getScrollTargetDisplayRow(width: number): number {
|
|
1281
|
+
if (this.editMode || this.askInputMode) {
|
|
1282
|
+
const editorRow = this.getAnnotatedRows(width).findIndex(
|
|
1283
|
+
(row) => row.kind === "editor",
|
|
1284
|
+
);
|
|
1285
|
+
if (editorRow >= 0) return editorRow;
|
|
1286
|
+
}
|
|
1287
|
+
|
|
1288
|
+
return this.getSelectedDisplayRow(width);
|
|
1289
|
+
}
|
|
1290
|
+
|
|
1021
1291
|
private getDisplayText(line: ReviewLine): string {
|
|
1022
1292
|
const raw =
|
|
1023
1293
|
line.kind === "add" || line.kind === "remove" || line.kind === "context"
|