@oh-my-pi/pi-tui 18.13.2 → 18.13.3
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/CHANGELOG.md +24 -14
- package/dist/types/apps/session-picker.d.ts +4 -0
- package/dist/types/chat/assistant-message.d.ts +1 -1
- package/dist/types/chat/bash-execution.d.ts +2 -0
- package/dist/types/chat/display-preferences.d.ts +2 -0
- package/dist/types/chat/tool-execution.d.ts +1 -1
- package/dist/types/components/editor.d.ts +2 -0
- package/dist/types/overlays/annotation-overlay.d.ts +7 -1
- package/dist/types/overlays/annotation-types.d.ts +28 -0
- package/dist/types/overlays/plan-review-overlay.d.ts +2 -0
- package/dist/types/overlays/session-selector.d.ts +13 -2
- package/dist/types/theme/theme.d.ts +0 -23
- package/dist/types/tools/bash-interactive.d.ts +6 -0
- package/dist/types/tui.d.ts +4 -1
- package/package.json +10 -10
- package/src/apps/session-picker.ts +5 -0
- package/src/chat/advisor-message.ts +2 -0
- package/src/chat/assistant-message.ts +3 -1
- package/src/chat/bash-execution.ts +6 -0
- package/src/chat/display-preferences.ts +3 -0
- package/src/chat/tool-execution.ts +7 -3
- package/src/components/editor.ts +3 -1
- package/src/native/backend.ts +2 -64
- package/src/overlays/annotation-overlay.ts +125 -37
- package/src/overlays/annotation-types.ts +24 -0
- package/src/overlays/hook-selector.ts +2 -2
- package/src/overlays/plan-review-overlay.ts +13 -2
- package/src/overlays/session-selector.ts +195 -26
- package/src/overlays/usage-dashboard.ts +5 -12
- package/src/prompt/custom-editor.ts +6 -1
- package/src/terminal.ts +24 -2
- package/src/theme/theme.ts +0 -65
- package/src/tools/bash-interactive.ts +6 -1
- package/src/tools/job-widget.ts +13 -1
- package/src/tools/json-tree.ts +1 -1
- package/src/tui.ts +7 -2
|
@@ -19,6 +19,7 @@ import type { Keybinding } from "../keybindings";
|
|
|
19
19
|
import { sanitizeText } from "@oh-my-pi/pi-utils";
|
|
20
20
|
import { type Theme } from "../theme/theme";
|
|
21
21
|
import type {
|
|
22
|
+
AnnotationOverlayDraft,
|
|
22
23
|
CodeReviewAnnotation,
|
|
23
24
|
CodeReviewOverlayResult,
|
|
24
25
|
ReviewDiffFile,
|
|
@@ -125,8 +126,14 @@ const MIN_BODY_ROWS = 3;
|
|
|
125
126
|
const SIDEBAR_MIN_TOTAL_WIDTH = 64;
|
|
126
127
|
const SIDEBAR_MIN_BODY_WIDTH = 40;
|
|
127
128
|
const MAX_ANNOTATION_EDITOR_ROWS = 6;
|
|
128
|
-
|
|
129
|
-
const
|
|
129
|
+
/** Clears every note and closes; the host then has nothing to keep. Needs a second confirm. */
|
|
130
|
+
export const DISCARD_ANNOTATIONS_ACTION = "Discard notes and close";
|
|
131
|
+
const CODE_REVIEW_ACTIONS = [
|
|
132
|
+
CONTINUE_CODE_REVIEW_ACTION,
|
|
133
|
+
PASTE_CODE_REVIEW_ACTION,
|
|
134
|
+
DISCARD_ANNOTATIONS_ACTION,
|
|
135
|
+
] as const;
|
|
136
|
+
const TEXT_REVIEW_ACTIONS = [PASTE_CODE_REVIEW_ACTION, DISCARD_ANNOTATIONS_ACTION] as const;
|
|
130
137
|
|
|
131
138
|
function isSourceRow(row: ReviewDiffRow): row is ReviewSourceRow {
|
|
132
139
|
return row.kind === "context" || row.kind === "added" || row.kind === "removed";
|
|
@@ -229,6 +236,8 @@ export class AnnotationOverlay implements Focusable {
|
|
|
229
236
|
/** Set once {@link replaceTextSource} runs; the paste result then carries the edited text. */
|
|
230
237
|
#textSourceEdited = false;
|
|
231
238
|
#finished = false;
|
|
239
|
+
/** Discard was chosen once; choosing it again discards, any other input disarms it. */
|
|
240
|
+
#discardArmed = false;
|
|
232
241
|
#annotations: CommittedAnnotation[] = [];
|
|
233
242
|
#textAnnotations: CommittedTextAnnotation[] = [];
|
|
234
243
|
/** Annotation lists before each create/edit/delete, restored by `u`. */
|
|
@@ -295,6 +304,8 @@ export class AnnotationOverlay implements Focusable {
|
|
|
295
304
|
this.#files = filesOrSource;
|
|
296
305
|
this.#mode = modeOrCallbacks as string;
|
|
297
306
|
this.#callbacks = maybeCallbacks!;
|
|
307
|
+
// Start on Paste, not the LLM review: a stray Enter must not submit (it does nothing until a note exists).
|
|
308
|
+
this.#actionIndex = 1;
|
|
298
309
|
}
|
|
299
310
|
const symbols = {
|
|
300
311
|
cursor: theme.nav.cursor,
|
|
@@ -357,6 +368,60 @@ export class AnnotationOverlay implements Focusable {
|
|
|
357
368
|
this.#finished = true;
|
|
358
369
|
}
|
|
359
370
|
|
|
371
|
+
/** Notes, the unfinished note, and any source edit, in the form {@link restoreDraft} replays. */
|
|
372
|
+
getDraft(): AnnotationOverlayDraft {
|
|
373
|
+
const editing =
|
|
374
|
+
this.#editingAnnotationIndex === undefined ? {} : { editingAnnotationIndex: this.#editingAnnotationIndex };
|
|
375
|
+
const text = this.#editor.getExpandedText();
|
|
376
|
+
if (this.#textSource) {
|
|
377
|
+
const scope = this.#annotationScope === "text" ? "text" : "line";
|
|
378
|
+
const sourceIndex = scope === "text" ? -1 : this.#sourceIndex;
|
|
379
|
+
return {
|
|
380
|
+
kind: "text",
|
|
381
|
+
annotations: this.#textAnnotations.map(entry => ({ ...entry, annotation: { ...entry.annotation } })),
|
|
382
|
+
...(this.#textSourceEdited ? { editedText: this.#textSource.text } : {}),
|
|
383
|
+
...(this.#annotating ? { editor: { sourceIndex, scope, ...editing, text } } : {}),
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
const scope = this.#annotationScope === "file" ? "file" : "line";
|
|
387
|
+
const editor = { fileIndex: this.#fileIndex, sourceIndex: this.#sourceIndex, scope, ...editing, text } as const;
|
|
388
|
+
return {
|
|
389
|
+
kind: "diff",
|
|
390
|
+
annotations: this.#annotations.map(entry => ({ ...entry, annotation: { ...entry.annotation } })),
|
|
391
|
+
...(this.#annotating ? { editor } : {}),
|
|
392
|
+
};
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/** Replay a draft taken from this same source; anything anchored outside it is dropped. */
|
|
396
|
+
restoreDraft(draft: AnnotationOverlayDraft): void {
|
|
397
|
+
const rows = (fileIndex: number) =>
|
|
398
|
+
this.#textSource ? this.#textLines.length : (this.#files[fileIndex]?.rows.filter(isSourceRow).length ?? -1);
|
|
399
|
+
const fits = (fileIndex: number, sourceIndex: number, whole: boolean) =>
|
|
400
|
+
rows(fileIndex) >= 0 && (whole ? sourceIndex === -1 : sourceIndex >= 0 && sourceIndex < rows(fileIndex));
|
|
401
|
+
if (draft.kind === "text" && this.#textSource) {
|
|
402
|
+
if (draft.editedText !== undefined) this.replaceTextSource(draft.editedText);
|
|
403
|
+
this.#textAnnotations = draft.annotations.filter(e => fits(0, e.sourceIndex, e.annotation.scope === "text"));
|
|
404
|
+
} else if (draft.kind === "diff" && !this.#textSource) {
|
|
405
|
+
this.#annotations = draft.annotations.filter(e =>
|
|
406
|
+
fits(e.fileIndex, e.sourceIndex, e.annotation.scope === "file"),
|
|
407
|
+
);
|
|
408
|
+
} else return;
|
|
409
|
+
const editor = draft.editor;
|
|
410
|
+
const fileIndex = editor && "fileIndex" in editor ? editor.fileIndex : 0;
|
|
411
|
+
if (editor && fits(fileIndex, editor.sourceIndex, editor.scope === "text" || editor.scope === "file")) {
|
|
412
|
+
this.#fileIndex = fileIndex;
|
|
413
|
+
if (editor.sourceIndex >= 0) this.#sourceIndex = editor.sourceIndex;
|
|
414
|
+
this.#annotationScope = editor.scope;
|
|
415
|
+
const count = this.#textSource ? this.#textAnnotations.length : this.#annotations.length;
|
|
416
|
+
this.#editingAnnotationIndex =
|
|
417
|
+
(editor.editingAnnotationIndex ?? count) < count ? editor.editingAnnotationIndex : undefined;
|
|
418
|
+
this.#annotating = true;
|
|
419
|
+
this.#editor.setText(editor.text);
|
|
420
|
+
}
|
|
421
|
+
this.#annotationRev++;
|
|
422
|
+
this.#nativeBody = undefined;
|
|
423
|
+
}
|
|
424
|
+
|
|
360
425
|
getAnnotations(): CodeReviewAnnotation[] {
|
|
361
426
|
return this.#annotations.map(entry => ({ ...entry.annotation }));
|
|
362
427
|
}
|
|
@@ -422,6 +487,8 @@ export class AnnotationOverlay implements Focusable {
|
|
|
422
487
|
|
|
423
488
|
handleInput(data: string): void {
|
|
424
489
|
if (this.#finished || this.#externalOperation) return;
|
|
490
|
+
const discardArmed = this.#discardArmed;
|
|
491
|
+
this.#discardArmed = false;
|
|
425
492
|
if (this.#annotationChooser) {
|
|
426
493
|
this.#handleAnnotationChooser(data);
|
|
427
494
|
return;
|
|
@@ -431,8 +498,9 @@ export class AnnotationOverlay implements Focusable {
|
|
|
431
498
|
void this.#openAnnotationEditor();
|
|
432
499
|
return;
|
|
433
500
|
}
|
|
501
|
+
// Close, keeping the unfinished note in the draft so it reopens in the editor.
|
|
434
502
|
if (this.#keybindings.matches(data, "tui.select.cancel")) {
|
|
435
|
-
this.#
|
|
503
|
+
this.#finish(undefined);
|
|
436
504
|
return;
|
|
437
505
|
}
|
|
438
506
|
this.#editor.handleInput(data);
|
|
@@ -476,7 +544,7 @@ export class AnnotationOverlay implements Focusable {
|
|
|
476
544
|
}
|
|
477
545
|
if (this.#focus === "files") this.#handleFiles(data);
|
|
478
546
|
else if (this.#focus === "diff") this.#handleDiff(data);
|
|
479
|
-
else this.#handleActions(data);
|
|
547
|
+
else this.#handleActions(data, discardArmed);
|
|
480
548
|
}
|
|
481
549
|
|
|
482
550
|
/** Primary key bound to `action` in this overlay's keybindings, formatted for footer hints. */
|
|
@@ -495,10 +563,9 @@ export class AnnotationOverlay implements Focusable {
|
|
|
495
563
|
}
|
|
496
564
|
}
|
|
497
565
|
|
|
498
|
-
/** Esc, by state: leave the note chooser,
|
|
566
|
+
/** Esc, by state: leave the note chooser, else close the review; the host keeps the draft. */
|
|
499
567
|
#escape(): void {
|
|
500
568
|
if (this.#annotationChooser) this.#annotationChooser = undefined;
|
|
501
|
-
else if (this.#annotating) this.#cancelAnnotation();
|
|
502
569
|
else this.#finish(undefined);
|
|
503
570
|
}
|
|
504
571
|
|
|
@@ -602,33 +669,41 @@ export class AnnotationOverlay implements Focusable {
|
|
|
602
669
|
}
|
|
603
670
|
}
|
|
604
671
|
|
|
605
|
-
#handleActions(data: string): void {
|
|
606
|
-
const
|
|
607
|
-
if (
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
if (this.#keybindings.matches(data, "tui.select.down") || matchesKey(data, "j")) {
|
|
614
|
-
if (this.#textSource) {
|
|
615
|
-
this.#actionIndex = (this.#actionIndex + 1) % actionCount;
|
|
616
|
-
} else if (hasAnnotations) {
|
|
617
|
-
this.#actionIndex = Math.min(actionCount - 1, this.#actionIndex + 1);
|
|
672
|
+
#handleActions(data: string, discardArmed: boolean): void {
|
|
673
|
+
const up = this.#keybindings.matches(data, "tui.select.up") || matchesKey(data, "k");
|
|
674
|
+
if (up || this.#keybindings.matches(data, "tui.select.down") || matchesKey(data, "j")) {
|
|
675
|
+
const step = up ? -1 : 1;
|
|
676
|
+
for (let index = this.#actionIndex + step; index >= 0 && index < this.#actions.length; index += step) {
|
|
677
|
+
if (this.#actionDisabled(index)) continue;
|
|
678
|
+
this.#actionIndex = index;
|
|
679
|
+
break;
|
|
618
680
|
}
|
|
619
681
|
return;
|
|
620
682
|
}
|
|
621
|
-
if (this.#keybindings.matches(data, "tui.select.confirm")) this.#confirmAction();
|
|
683
|
+
if (this.#keybindings.matches(data, "tui.select.confirm")) this.#confirmAction(discardArmed);
|
|
622
684
|
}
|
|
623
685
|
|
|
624
686
|
#actionDisabled(index: number): boolean {
|
|
687
|
+
if (this.#actions[index] === DISCARD_ANNOTATIONS_ACTION) return false;
|
|
625
688
|
const hasAnnotations = this.#textSource ? this.#textAnnotations.length > 0 : this.#annotations.length > 0;
|
|
626
689
|
return this.#textSource ? !hasAnnotations : index === 1 && !hasAnnotations;
|
|
627
690
|
}
|
|
628
691
|
|
|
629
|
-
#confirmAction(): void {
|
|
692
|
+
#confirmAction(discardArmed = false): void {
|
|
630
693
|
if (this.#actionDisabled(this.#actionIndex)) return;
|
|
631
|
-
if (this.#
|
|
694
|
+
if (this.#actions[this.#actionIndex] === DISCARD_ANNOTATIONS_ACTION) {
|
|
695
|
+
if (!discardArmed) {
|
|
696
|
+
this.#discardArmed = true;
|
|
697
|
+
return;
|
|
698
|
+
}
|
|
699
|
+
this.#annotations = [];
|
|
700
|
+
this.#textAnnotations = [];
|
|
701
|
+
this.#textSourceEdited = false;
|
|
702
|
+
this.#annotating = false;
|
|
703
|
+
this.#editingAnnotationIndex = undefined;
|
|
704
|
+
this.#editor.setText("");
|
|
705
|
+
this.#finish(undefined);
|
|
706
|
+
} else if (this.#textSource) {
|
|
632
707
|
this.#finish({
|
|
633
708
|
action: "paste",
|
|
634
709
|
annotations: this.getTextAnnotations(),
|
|
@@ -721,12 +796,6 @@ export class AnnotationOverlay implements Focusable {
|
|
|
721
796
|
this.#editor.setText(existing?.annotation.note ?? "");
|
|
722
797
|
}
|
|
723
798
|
|
|
724
|
-
#cancelAnnotation(): void {
|
|
725
|
-
this.#annotating = false;
|
|
726
|
-
this.#editingAnnotationIndex = undefined;
|
|
727
|
-
this.#editor.setText("");
|
|
728
|
-
}
|
|
729
|
-
|
|
730
799
|
#commitAnnotation(value: string): void {
|
|
731
800
|
if (this.#textSource) {
|
|
732
801
|
this.#commitTextAnnotation(value);
|
|
@@ -925,7 +994,6 @@ export class AnnotationOverlay implements Focusable {
|
|
|
925
994
|
this.#annotationRev++;
|
|
926
995
|
this.#annotations = snapshot.annotations;
|
|
927
996
|
this.#textAnnotations = snapshot.textAnnotations;
|
|
928
|
-
if (this.#annotations.length === 0 && this.#actionIndex === 1) this.#actionIndex = 0;
|
|
929
997
|
}
|
|
930
998
|
|
|
931
999
|
async #openAnnotationEditor(): Promise<void> {
|
|
@@ -1146,9 +1214,10 @@ export class AnnotationOverlay implements Focusable {
|
|
|
1146
1214
|
}
|
|
1147
1215
|
|
|
1148
1216
|
#renderActions(): string[] {
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1217
|
+
return this.#actions.map((action, index) => {
|
|
1218
|
+
const label =
|
|
1219
|
+
action === DISCARD_ANNOTATIONS_ACTION && this.#discardArmed ? "Confirm: discard all notes" : action;
|
|
1220
|
+
const disabled = this.#actionDisabled(index);
|
|
1152
1221
|
const selected = index === this.#actionIndex;
|
|
1153
1222
|
const cursor = selected ? `${this.#theme.nav.cursor} ` : " ";
|
|
1154
1223
|
const text = disabled
|
|
@@ -1224,7 +1293,7 @@ export class AnnotationOverlay implements Focusable {
|
|
|
1224
1293
|
const hints = [
|
|
1225
1294
|
`${editorKey("tui.input.submit")} save`,
|
|
1226
1295
|
`${editorKey("tui.input.newLine")} newline`,
|
|
1227
|
-
`${this.#key("tui.select.cancel")}
|
|
1296
|
+
`${this.#key("tui.select.cancel")} close, keep note`,
|
|
1228
1297
|
];
|
|
1229
1298
|
hints.push(`${formatKeyHint(appExternalEditorKey())} editor`);
|
|
1230
1299
|
return [caption, ...this.#editor.render(width), this.#theme.fg("dim", hints.join(" · "))];
|
|
@@ -1428,6 +1497,7 @@ export class AnnotationOverlay implements Focusable {
|
|
|
1428
1497
|
this.#bodyHeight,
|
|
1429
1498
|
this.#annotating ? `annotating:${this.#annotationScope}:${this.#editingAnnotationIndex}` : "",
|
|
1430
1499
|
chooser ? `chooser:${chooser.selected}:${chooser.entries.join()}` : "",
|
|
1500
|
+
this.#discardArmed ? "discard-armed" : "",
|
|
1431
1501
|
].join("|");
|
|
1432
1502
|
if (this.#nativeRoot && sig === this.#nativeSig && body === this.#nativeRootBody) return this.#nativeRoot;
|
|
1433
1503
|
|
|
@@ -1462,6 +1532,8 @@ export class AnnotationOverlay implements Focusable {
|
|
|
1462
1532
|
|
|
1463
1533
|
handleNativeEvent(event: NativeUiEvent): void {
|
|
1464
1534
|
if (this.#finished || this.#externalOperation) return;
|
|
1535
|
+
const discardArmed = this.#discardArmed;
|
|
1536
|
+
this.#discardArmed = false;
|
|
1465
1537
|
if (event.type === "action") {
|
|
1466
1538
|
if (event.act === "close") {
|
|
1467
1539
|
this.#escape();
|
|
@@ -1470,10 +1542,18 @@ export class AnnotationOverlay implements Focusable {
|
|
|
1470
1542
|
// A draft or the note chooser is open: finish or leave it first.
|
|
1471
1543
|
if (this.#annotating || this.#annotationChooser) return;
|
|
1472
1544
|
const index =
|
|
1473
|
-
event.act === "
|
|
1545
|
+
event.act === "discard"
|
|
1546
|
+
? this.#actions.indexOf(DISCARD_ANNOTATIONS_ACTION)
|
|
1547
|
+
: event.act === "review" && !this.#textSource
|
|
1548
|
+
? 0
|
|
1549
|
+
: event.act === "paste"
|
|
1550
|
+
? this.#textSource
|
|
1551
|
+
? 0
|
|
1552
|
+
: 1
|
|
1553
|
+
: -1;
|
|
1474
1554
|
if (index < 0 || this.#actionDisabled(index)) return;
|
|
1475
1555
|
this.#actionIndex = index;
|
|
1476
|
-
this.#confirmAction();
|
|
1556
|
+
this.#confirmAction(discardArmed);
|
|
1477
1557
|
return;
|
|
1478
1558
|
}
|
|
1479
1559
|
if (event.type !== "select" && event.type !== "activate") return;
|
|
@@ -1824,7 +1904,15 @@ export class AnnotationOverlay implements Focusable {
|
|
|
1824
1904
|
mark ? ({ ...button, p: { ...button.p, mark } } as NativeNode) : button;
|
|
1825
1905
|
const hasNotes = this.#textSource ? this.#textAnnotations.length > 0 : this.#annotations.length > 0;
|
|
1826
1906
|
const pasteTitle = hasNotes ? PASTE_CODE_REVIEW_ACTION : "Add a note to paste it into the prompt";
|
|
1827
|
-
const buttons: NativeNode[] = [
|
|
1907
|
+
const buttons: NativeNode[] = [
|
|
1908
|
+
actionButton("Cancel", "close", { keys: "escape" }),
|
|
1909
|
+
marked(
|
|
1910
|
+
actionButton(this.#discardArmed ? "Confirm discard" : "Discard", "discard", {
|
|
1911
|
+
title: DISCARD_ANNOTATIONS_ACTION,
|
|
1912
|
+
}),
|
|
1913
|
+
pick(this.#actions.indexOf(DISCARD_ANNOTATIONS_ACTION)),
|
|
1914
|
+
),
|
|
1915
|
+
];
|
|
1828
1916
|
if (this.#textSource) {
|
|
1829
1917
|
buttons.push(
|
|
1830
1918
|
marked(actionButton("Paste into prompt", "paste", { tone: "accent", title: pasteTitle }), pick(0)),
|
|
@@ -1860,7 +1948,7 @@ export class AnnotationOverlay implements Focusable {
|
|
|
1860
1948
|
return [
|
|
1861
1949
|
this.#hint("tui.input.submit", "save"),
|
|
1862
1950
|
this.#hint("tui.input.newLine", "new line"),
|
|
1863
|
-
this.#hint("tui.select.cancel", "
|
|
1951
|
+
this.#hint("tui.select.cancel", "close, keep note"),
|
|
1864
1952
|
this.#callbacks.onAnnotationExternalEditor ? key(appExternalEditorKey(), "editor") : undefined,
|
|
1865
1953
|
];
|
|
1866
1954
|
}
|
|
@@ -78,3 +78,27 @@ export interface TextReviewOverlayResult {
|
|
|
78
78
|
/** Source text after an external-editor edit; absent when the source was not edited. */
|
|
79
79
|
editedText?: string;
|
|
80
80
|
}
|
|
81
|
+
|
|
82
|
+
export type AnnotationOverlayDraft =
|
|
83
|
+
| {
|
|
84
|
+
kind: "diff";
|
|
85
|
+
annotations: { fileIndex: number; sourceIndex: number; annotation: CodeReviewAnnotation }[];
|
|
86
|
+
editor?: {
|
|
87
|
+
fileIndex: number;
|
|
88
|
+
sourceIndex: number;
|
|
89
|
+
scope: "file" | "line";
|
|
90
|
+
editingAnnotationIndex?: number;
|
|
91
|
+
text: string;
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
| {
|
|
95
|
+
kind: "text";
|
|
96
|
+
annotations: { sourceIndex: number; annotation: TextReviewAnnotation }[];
|
|
97
|
+
editedText?: string;
|
|
98
|
+
editor?: {
|
|
99
|
+
sourceIndex: number;
|
|
100
|
+
scope: "text" | "line";
|
|
101
|
+
editingAnnotationIndex?: number;
|
|
102
|
+
text: string;
|
|
103
|
+
};
|
|
104
|
+
};
|
|
@@ -647,9 +647,9 @@ export class HookSelectorComponent extends OverlayPanel {
|
|
|
647
647
|
return;
|
|
648
648
|
}
|
|
649
649
|
|
|
650
|
-
if (matchesSelectUp(keyData)
|
|
650
|
+
if (matchesSelectUp(keyData)) {
|
|
651
651
|
this.#moveSelection(-1);
|
|
652
|
-
} else if (matchesSelectDown(keyData)
|
|
652
|
+
} else if (matchesSelectDown(keyData)) {
|
|
653
653
|
this.#moveSelection(1);
|
|
654
654
|
} else if (matchesKey(keyData, "enter") || matchesKey(keyData, "return") || keyData === "\n") {
|
|
655
655
|
const selected = this.#menu.selectedItem;
|
|
@@ -256,6 +256,8 @@ export interface PlanReviewOverlayOptions {
|
|
|
256
256
|
slider?: HookSelectorSlider;
|
|
257
257
|
/** Display label for the external-editor key, surfaced in the footer help. */
|
|
258
258
|
externalEditorLabel?: string;
|
|
259
|
+
/** The external editor's name ("Notepad", "nvim") for the native Edit button; `$EDITOR` when unset. */
|
|
260
|
+
externalEditorName?: string;
|
|
259
261
|
/** Serializable annotations restored into this overlay instance. */
|
|
260
262
|
annotationState?: PlanReviewAnnotationState;
|
|
261
263
|
}
|
|
@@ -286,6 +288,7 @@ export class PlanReviewOverlay implements Component {
|
|
|
286
288
|
#disabled: Set<number>;
|
|
287
289
|
#helpSuffix: string | undefined;
|
|
288
290
|
#externalEditorLabel: string | undefined;
|
|
291
|
+
#externalEditorName: string | undefined;
|
|
289
292
|
#promptTitle: string | undefined;
|
|
290
293
|
#selectedIndex: number;
|
|
291
294
|
#slider: HookSelectorSlider | undefined;
|
|
@@ -354,6 +357,7 @@ export class PlanReviewOverlay implements Component {
|
|
|
354
357
|
);
|
|
355
358
|
this.#helpSuffix = options.helpText;
|
|
356
359
|
this.#externalEditorLabel = options.externalEditorLabel;
|
|
360
|
+
this.#externalEditorName = options.externalEditorName;
|
|
357
361
|
this.#promptTitle = options.promptTitle;
|
|
358
362
|
this.#selectedIndex = this.#coerceIndex(options.initialIndex ?? 0);
|
|
359
363
|
if (options.slider && options.slider.segments.length > 0) {
|
|
@@ -1632,7 +1636,13 @@ export class PlanReviewOverlay implements Component {
|
|
|
1632
1636
|
// Unbound: the button still mirrors the callback, without a keycap.
|
|
1633
1637
|
const editorKeyId = getKeybindings().getKeys("app.editor.external")[0];
|
|
1634
1638
|
if (this.callbacks.onExternalEditor) {
|
|
1635
|
-
buttons.push(
|
|
1639
|
+
buttons.push(
|
|
1640
|
+
actionButton(
|
|
1641
|
+
`Edit in ${this.#externalEditorName ?? "$EDITOR"}`,
|
|
1642
|
+
"externalEditor",
|
|
1643
|
+
editorKeyId ? { keys: editorKeyId } : {},
|
|
1644
|
+
),
|
|
1645
|
+
);
|
|
1636
1646
|
}
|
|
1637
1647
|
// Cancel the review the way the cancel key does; while annotating that key
|
|
1638
1648
|
// only leaves the draft/chooser, so the button is hidden there.
|
|
@@ -1754,7 +1764,8 @@ export class PlanReviewOverlay implements Component {
|
|
|
1754
1764
|
"tabs",
|
|
1755
1765
|
);
|
|
1756
1766
|
const track: NativeChild[] = slider.caption ? [text([span(slider.caption, "dim")]), tabs] : [tabs];
|
|
1757
|
-
|
|
1767
|
+
// A role of its own: Tern's sheet lets only the role-less split row (Contents + body) shrink and scroll.
|
|
1768
|
+
const out = [keyed(rowNode(track, { role: "omp.plan.slider", gap: "sm", align: "center" }), "slider")];
|
|
1758
1769
|
const detail = slider.segments[this.#sliderIndex]?.detail;
|
|
1759
1770
|
if (detail) out.push(keyed(text([span("↳ ", "dim"), span(detail, "muted")], { wrap: "word" }), "sliderDetail"));
|
|
1760
1771
|
return out;
|