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 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-diff-review",
3
- "version": "0.1.16",
3
+ "version": "0.1.18",
4
4
  "description": "Local diff review TUI extension for pi",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -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 = [...this.explanations.values()].some(
157
- (explanation) => explanation.status === "loading",
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: buildExplanationPrompt(scope),
79
+ content: question
80
+ ? buildAskPrompt(scope, question)
81
+ : buildExplanationPrompt(scope),
71
82
  timestamp: Date.now(),
72
83
  },
73
84
  ],
@@ -9,7 +9,11 @@ import {
9
9
  visibleWidth,
10
10
  wrapTextWithAnsi,
11
11
  } from "@earendil-works/pi-tui";
12
- import type { DiffExplainer } from "../explanation/explainer.ts";
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.search.query) {
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
- truncateToWidth(
296
- this.theme.fg(
297
- "dim",
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 || !this.visibleExplanationKeys.has(scope.key)) return;
701
+ if (!scope) return;
514
702
 
515
703
  const end = this.getHunkEndIndex(lineIndex);
516
704
  if (end !== lineIndex) return;
517
705
 
518
- const explanation = this.explanationController.getState(scope);
519
- if (!this.explanationController.isAvailable) {
520
- this.pushExplanationBlock(rows, "Explanation unavailable.", width);
521
- } else if (!explanation) {
522
- this.pushExplanationBlock(rows, "No explanation generated yet.", width);
523
- } else if (explanation.status === "loading") {
524
- this.pushExplanationBlock(
525
- rows,
526
- `${this.explanationController.getLoadingFrame()} ${explanation.text || "Generating explanation..."}`,
527
- width,
528
- );
529
- } else if (explanation.status === "error") {
530
- this.pushExplanationBlock(
531
- rows,
532
- `Explanation failed: ${explanation.message}`,
533
- width,
534
- );
535
- } else {
536
- this.pushExplanationBlock(rows, explanation.text, width);
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", " ✨ Explanation "),
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.getSelectedDisplayRow(width),
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"