@oh-my-pi/pi-tui 18.2.10 → 18.3.0
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 +32 -0
- package/THIRD-PARTY-NOTICES.txt +1 -1
- package/dist/types/apps/ps-data.d.ts +1 -1
- package/dist/types/apps/ps-top.d.ts +1 -1
- package/dist/types/chat/chat-transcript-builder.d.ts +1 -1
- package/dist/types/overlays/annotation-overlay.d.ts +30 -0
- package/dist/types/overlays/annotation-types.d.ts +84 -0
- package/dist/types/overlays/copy-selector.d.ts +30 -2
- package/dist/types/overlays/copy-targets.d.ts +2 -0
- package/dist/types/overlays/model-hub.d.ts +2 -2
- package/dist/types/overlays/oauth-selector.d.ts +3 -6
- package/dist/types/overlays/usage-dashboard.d.ts +2 -0
- package/dist/types/prompt/image-source.d.ts +9 -8
- package/dist/types/render/render-utils.d.ts +2 -0
- package/dist/types/status-line/context-usage.d.ts +3 -1
- package/dist/types/theme/mermaid-cache.d.ts +4 -6
- package/dist/types/tools/bash.d.ts +7 -0
- package/dist/types/tools/daemon.d.ts +46 -0
- package/dist/types/tools/irc.d.ts +30 -0
- package/dist/types/tools/line-ranges.d.ts +1 -1
- package/dist/types/tools/proc-render.d.ts +34 -0
- package/dist/types/tools/read.d.ts +4 -1
- package/dist/types/tools/task.d.ts +4 -0
- package/dist/types/tools/wait.d.ts +131 -0
- package/dist/types/tools/write.d.ts +4 -0
- package/dist/types/tools/xd-url.d.ts +8 -0
- package/dist/types/windows-altgr.d.ts +54 -0
- package/package.json +13 -10
- package/src/apps/debug/protocol-probe.ts +1 -1
- package/src/apps/ps-data.ts +1 -1
- package/src/apps/ps-top.ts +1 -1
- package/src/chat/chat-transcript-builder.ts +4 -4
- package/src/chat/tool-execution.ts +8 -8
- package/src/chat/transcript-render-helpers.ts +3 -3
- package/src/components/markdown.ts +1 -1
- package/src/desktop-notify.ts +1 -1
- package/src/overlays/annotation-overlay.ts +1145 -0
- package/src/overlays/annotation-types.ts +78 -0
- package/src/overlays/copy-selector.ts +45 -19
- package/src/overlays/copy-targets.ts +3 -3
- package/src/overlays/model-hub.ts +5 -4
- package/src/overlays/oauth-selector.ts +8 -12
- package/src/overlays/plan-review-overlay.ts +213 -40
- package/src/overlays/settings-defs.ts +1 -0
- package/src/overlays/usage-dashboard.ts +120 -25
- package/src/prompt/image-source.ts +9 -8
- package/src/prompt/welcome.ts +4 -1
- package/src/render/render-utils.ts +1 -1
- package/src/setup/scenes/sign-in.ts +1 -1
- package/src/status-line/component.ts +15 -4
- package/src/status-line/context-usage.ts +15 -3
- package/src/status-line/segments.ts +3 -3
- package/src/terminal-capabilities.ts +16 -7
- package/src/terminal.ts +8 -1
- package/src/theme/mermaid-cache.ts +83 -25
- package/src/tools/bash.ts +14 -1
- package/src/tools/daemon.ts +51 -0
- package/src/tools/index.ts +2 -2
- package/src/tools/irc.ts +33 -0
- package/src/tools/line-ranges.ts +13 -3
- package/src/tools/proc-render.ts +294 -0
- package/src/tools/read.ts +22 -1
- package/src/tools/task.ts +18 -0
- package/src/tools/wait.ts +595 -0
- package/src/tools/write.ts +76 -0
- package/src/tools/xd-url.ts +9 -0
- package/src/windows-altgr.ts +227 -0
- package/dist/types/tools/hub.d.ts +0 -342
- package/src/tools/hub.ts +0 -1492
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
import {
|
|
20
20
|
type Component,
|
|
21
21
|
Ellipsis,
|
|
22
|
-
|
|
22
|
+
Editor,
|
|
23
23
|
Markdown,
|
|
24
24
|
type MarkdownTheme,
|
|
25
25
|
matchesKey,
|
|
@@ -31,7 +31,7 @@ import {
|
|
|
31
31
|
} from "../index";
|
|
32
32
|
import { sanitizeText } from "@oh-my-pi/pi-utils";
|
|
33
33
|
import { sanitizeStatusText } from "../chrome/shared";
|
|
34
|
-
import { getMarkdownTheme, theme } from "../theme/theme";
|
|
34
|
+
import { getEditorTheme, getMarkdownTheme, theme } from "../theme/theme";
|
|
35
35
|
import {
|
|
36
36
|
matchesAppExternalEditor,
|
|
37
37
|
matchesSelectCancel,
|
|
@@ -57,6 +57,8 @@ import { renderSegmentTrack } from "../chrome/segment-track";
|
|
|
57
57
|
const OVERLAY_TITLE = "Plan Review";
|
|
58
58
|
/** Minimum plan-body rows kept visible even on short terminals. */
|
|
59
59
|
const MIN_BODY_ROWS = 3;
|
|
60
|
+
/** Visible rows for the in-overlay annotation editor before it scrolls. */
|
|
61
|
+
const MAX_ANNOTATION_EDITOR_ROWS = 6;
|
|
60
62
|
/** Sidebar gates: enough headings, a wide terminal, and a usable body column. */
|
|
61
63
|
const SIDEBAR_MIN_HEADINGS = 2;
|
|
62
64
|
const SIDEBAR_MIN_TOTAL_WIDTH = 64;
|
|
@@ -93,6 +95,16 @@ interface BodyRowAnchor {
|
|
|
93
95
|
contextTruncated: boolean;
|
|
94
96
|
}
|
|
95
97
|
|
|
98
|
+
interface AnnotationSelection {
|
|
99
|
+
sectionIndex: number;
|
|
100
|
+
annotationIndex: number;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
interface AnnotationChooser {
|
|
104
|
+
entries: AnnotationSelection[];
|
|
105
|
+
selected: number;
|
|
106
|
+
}
|
|
107
|
+
|
|
96
108
|
/** Serializable annotations retained by the plan-review owner between overlays. */
|
|
97
109
|
export interface PlanReviewAnnotationState {
|
|
98
110
|
annotations: Array<{
|
|
@@ -207,8 +219,10 @@ export class PlanReviewOverlay implements Component {
|
|
|
207
219
|
/** Label of the committed choice, shown while the async approval settles. */
|
|
208
220
|
#committedLabel: string | undefined;
|
|
209
221
|
#annotating = false;
|
|
210
|
-
#
|
|
222
|
+
#editor: Editor;
|
|
211
223
|
#annotationTarget: BodyRowAnchor | { sectionIndex: number; row: null; context: null } | undefined;
|
|
224
|
+
#editingAnnotation: AnnotationSelection | undefined;
|
|
225
|
+
#annotationChooser: AnnotationChooser | undefined;
|
|
212
226
|
|
|
213
227
|
constructor(
|
|
214
228
|
planContent: string,
|
|
@@ -236,10 +250,11 @@ export class PlanReviewOverlay implements Component {
|
|
|
236
250
|
} else {
|
|
237
251
|
this.#sliderIndex = 0;
|
|
238
252
|
}
|
|
239
|
-
this.#
|
|
240
|
-
this.#
|
|
241
|
-
this.#
|
|
242
|
-
this.#
|
|
253
|
+
this.#editor = new Editor(getEditorTheme());
|
|
254
|
+
this.#editor.setBorderVisible(false);
|
|
255
|
+
this.#editor.setPromptGutter("> ");
|
|
256
|
+
this.#editor.setUseTerminalCursor(false);
|
|
257
|
+
this.#editor.onSubmit = value => this.#submitAnnotation(value);
|
|
243
258
|
this.#setSections(planContent);
|
|
244
259
|
this.#restoreAnnotationState(options.annotationState);
|
|
245
260
|
if (Array.isArray(options.annotationState?.annotations) && options.annotationState.annotations.length > 0) {
|
|
@@ -490,14 +505,22 @@ export class PlanReviewOverlay implements Component {
|
|
|
490
505
|
handleInput(keyData: string): void {
|
|
491
506
|
if (this.#committed) return;
|
|
492
507
|
if (keyData.startsWith("\x1b[<") && this.#handleMouse(keyData)) return;
|
|
508
|
+
if (this.#annotationChooser) {
|
|
509
|
+
this.#handleAnnotationChooser(keyData);
|
|
510
|
+
return;
|
|
511
|
+
}
|
|
493
512
|
if (this.#annotating) {
|
|
494
513
|
if (this.callbacks.onAnnotationExternalEditor && matchesAppExternalEditor(keyData)) {
|
|
495
|
-
this.callbacks.onAnnotationExternalEditor(this.#
|
|
496
|
-
if (text !== null) this.#
|
|
514
|
+
this.callbacks.onAnnotationExternalEditor(this.#editor.getExpandedText(), text => {
|
|
515
|
+
if (text !== null && this.#annotating) this.#editor.setText(text);
|
|
497
516
|
});
|
|
498
517
|
return;
|
|
499
518
|
}
|
|
500
|
-
|
|
519
|
+
if (matchesSelectCancel(keyData)) {
|
|
520
|
+
this.#exitAnnotate();
|
|
521
|
+
return;
|
|
522
|
+
}
|
|
523
|
+
this.#editor.handleInput(keyData);
|
|
501
524
|
return;
|
|
502
525
|
}
|
|
503
526
|
if (matchesSelectCancel(keyData)) {
|
|
@@ -505,6 +528,14 @@ export class PlanReviewOverlay implements Component {
|
|
|
505
528
|
this.callbacks.onCancel();
|
|
506
529
|
return;
|
|
507
530
|
}
|
|
531
|
+
if (this.#focus !== "actions" && keyData === "u") {
|
|
532
|
+
this.#undoLast();
|
|
533
|
+
return;
|
|
534
|
+
}
|
|
535
|
+
if (this.#focus !== "actions" && keyData === "e") {
|
|
536
|
+
this.#editCurrentAnnotation();
|
|
537
|
+
return;
|
|
538
|
+
}
|
|
508
539
|
if (this.callbacks.onExternalEditor && matchesAppExternalEditor(keyData)) {
|
|
509
540
|
this.callbacks.onExternalEditor();
|
|
510
541
|
return;
|
|
@@ -726,10 +757,6 @@ export class PlanReviewOverlay implements Component {
|
|
|
726
757
|
this.#startSectionAnnotate();
|
|
727
758
|
return;
|
|
728
759
|
}
|
|
729
|
-
if (data === "u") {
|
|
730
|
-
this.#undoLast();
|
|
731
|
-
return;
|
|
732
|
-
}
|
|
733
760
|
}
|
|
734
761
|
|
|
735
762
|
#moveTocCursor(delta: number): void {
|
|
@@ -828,39 +855,145 @@ export class PlanReviewOverlay implements Component {
|
|
|
828
855
|
|
|
829
856
|
#startAnnotate(target: BodyRowAnchor | { sectionIndex: number; row: null; context: null }): void {
|
|
830
857
|
this.#annotationTarget = target;
|
|
858
|
+
this.#editingAnnotation = undefined;
|
|
859
|
+
this.#annotating = true;
|
|
860
|
+
this.#editor.setText("");
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
#startExistingAnnotation(selection: AnnotationSelection): void {
|
|
864
|
+
const section = this.#sections[selection.sectionIndex];
|
|
865
|
+
const annotation = section?.annotations[selection.annotationIndex];
|
|
866
|
+
if (!section || !annotation) return;
|
|
867
|
+
this.#annotationChooser = undefined;
|
|
868
|
+
this.#editingAnnotation = selection;
|
|
869
|
+
this.#annotationTarget =
|
|
870
|
+
annotation.target.kind === "section"
|
|
871
|
+
? { sectionIndex: selection.sectionIndex, row: null, context: null }
|
|
872
|
+
: {
|
|
873
|
+
sectionIndex: selection.sectionIndex,
|
|
874
|
+
row: annotation.target.row,
|
|
875
|
+
context: annotation.target.context,
|
|
876
|
+
contextTruncated: annotation.target.contextTruncated,
|
|
877
|
+
};
|
|
831
878
|
this.#annotating = true;
|
|
832
|
-
this.#
|
|
879
|
+
this.#editor.setText(annotation.note);
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
#annotationMatchesBodyAnchor(annotation: OverlayAnnotation, anchor: BodyRowAnchor): boolean {
|
|
883
|
+
if (annotation.target.kind === "section") return true;
|
|
884
|
+
if (annotation.target.row === anchor.row && annotation.target.context === anchor.context) return true;
|
|
885
|
+
const seenRows = new Set<number>();
|
|
886
|
+
const sectionAnchors: LineAnchorContext[] = [];
|
|
887
|
+
for (const candidate of this.#bodyRowAnchors) {
|
|
888
|
+
if (candidate.sectionIndex !== anchor.sectionIndex || seenRows.has(candidate.row)) continue;
|
|
889
|
+
seenRows.add(candidate.row);
|
|
890
|
+
sectionAnchors.push({ text: candidate.context, truncated: candidate.contextTruncated });
|
|
891
|
+
}
|
|
892
|
+
const resolvedRow = this.#resolveLineRow(
|
|
893
|
+
annotation.target.row,
|
|
894
|
+
{ text: annotation.target.context, truncated: annotation.target.contextTruncated },
|
|
895
|
+
sectionAnchors,
|
|
896
|
+
);
|
|
897
|
+
return resolvedRow === anchor.row;
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
#annotationCandidates(): AnnotationSelection[] {
|
|
901
|
+
if (this.#focus === "toc") {
|
|
902
|
+
const sectionIndex = this.#toc[this.#tocCursor];
|
|
903
|
+
const section = sectionIndex === undefined ? undefined : this.#sections[sectionIndex];
|
|
904
|
+
if (!section || section.annotations.length === 0) return [];
|
|
905
|
+
return section.annotations.map((_, annotationIndex) => ({ sectionIndex, annotationIndex }));
|
|
906
|
+
}
|
|
907
|
+
if (this.#focus !== "body") return [];
|
|
908
|
+
const maxRow = this.#bodyRowAnchors.length - 1;
|
|
909
|
+
if (maxRow < 0) return [];
|
|
910
|
+
const topRow = Math.max(0, Math.min(maxRow, Math.floor(this.#scrollView.getScrollOffset())));
|
|
911
|
+
const anchor = this.#bodyRowAnchors[topRow];
|
|
912
|
+
const section = anchor ? this.#sections[anchor.sectionIndex] : undefined;
|
|
913
|
+
if (!anchor || !section) return [];
|
|
914
|
+
const entries: AnnotationSelection[] = [];
|
|
915
|
+
for (const [annotationIndex, annotation] of section.annotations.entries()) {
|
|
916
|
+
if (this.#annotationMatchesBodyAnchor(annotation, anchor)) {
|
|
917
|
+
entries.push({ sectionIndex: anchor.sectionIndex, annotationIndex });
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
return entries;
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
#editCurrentAnnotation(): void {
|
|
924
|
+
const entries = this.#annotationCandidates();
|
|
925
|
+
if (entries.length === 0) return;
|
|
926
|
+
if (entries.length === 1) {
|
|
927
|
+
this.#startExistingAnnotation(entries[0]!);
|
|
928
|
+
return;
|
|
929
|
+
}
|
|
930
|
+
this.#annotationChooser = { entries, selected: 0 };
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
#handleAnnotationChooser(data: string): void {
|
|
934
|
+
const chooser = this.#annotationChooser;
|
|
935
|
+
if (!chooser) return;
|
|
936
|
+
if (matchesSelectCancel(data)) {
|
|
937
|
+
this.#annotationChooser = undefined;
|
|
938
|
+
return;
|
|
939
|
+
}
|
|
940
|
+
if (matchesSelectUp(data) || matchesKey(data, "k")) {
|
|
941
|
+
chooser.selected = (chooser.selected - 1 + chooser.entries.length) % chooser.entries.length;
|
|
942
|
+
return;
|
|
943
|
+
}
|
|
944
|
+
if (matchesSelectDown(data) || matchesKey(data, "j")) {
|
|
945
|
+
chooser.selected = (chooser.selected + 1) % chooser.entries.length;
|
|
946
|
+
return;
|
|
947
|
+
}
|
|
948
|
+
if (matchesKey(data, "enter") || matchesKey(data, "return") || data === "\n") {
|
|
949
|
+
const selection = chooser.entries[chooser.selected];
|
|
950
|
+
this.#annotationChooser = undefined;
|
|
951
|
+
if (selection) this.#startExistingAnnotation(selection);
|
|
952
|
+
}
|
|
833
953
|
}
|
|
834
954
|
|
|
835
955
|
#submitAnnotation(value: string): void {
|
|
836
|
-
this.#annotating = false;
|
|
837
956
|
const note = value.trim();
|
|
838
957
|
const target = this.#annotationTarget;
|
|
958
|
+
const editing = this.#editingAnnotation;
|
|
959
|
+
this.#annotating = false;
|
|
839
960
|
this.#annotationTarget = undefined;
|
|
840
|
-
|
|
841
|
-
|
|
961
|
+
this.#editingAnnotation = undefined;
|
|
962
|
+
this.#editor.setText("");
|
|
963
|
+
if (editing) {
|
|
964
|
+
const section = this.#sections[editing.sectionIndex];
|
|
965
|
+
const annotation = section?.annotations[editing.annotationIndex];
|
|
966
|
+
if (!section || !annotation) return;
|
|
842
967
|
this.#pushUndo();
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
target:
|
|
846
|
-
target.row === null
|
|
847
|
-
? { kind: "section" }
|
|
848
|
-
: {
|
|
849
|
-
kind: "line",
|
|
850
|
-
row: target.row,
|
|
851
|
-
context: target.context,
|
|
852
|
-
contextTruncated: target.contextTruncated,
|
|
853
|
-
},
|
|
854
|
-
});
|
|
968
|
+
if (note) annotation.note = note;
|
|
969
|
+
else section.annotations.splice(editing.annotationIndex, 1);
|
|
855
970
|
this.#recomputeFeedback();
|
|
971
|
+
return;
|
|
856
972
|
}
|
|
857
|
-
|
|
973
|
+
if (!note || !target) return;
|
|
974
|
+
const section = this.#sections[target.sectionIndex];
|
|
975
|
+
if (!section) return;
|
|
976
|
+
this.#pushUndo();
|
|
977
|
+
section.annotations.push({
|
|
978
|
+
note,
|
|
979
|
+
target:
|
|
980
|
+
target.row === null
|
|
981
|
+
? { kind: "section" }
|
|
982
|
+
: {
|
|
983
|
+
kind: "line",
|
|
984
|
+
row: target.row,
|
|
985
|
+
context: target.context,
|
|
986
|
+
contextTruncated: target.contextTruncated,
|
|
987
|
+
},
|
|
988
|
+
});
|
|
989
|
+
this.#recomputeFeedback();
|
|
858
990
|
}
|
|
859
991
|
|
|
860
992
|
#exitAnnotate(): void {
|
|
861
993
|
this.#annotating = false;
|
|
862
994
|
this.#annotationTarget = undefined;
|
|
863
|
-
this.#
|
|
995
|
+
this.#editingAnnotation = undefined;
|
|
996
|
+
this.#editor.setText("");
|
|
864
997
|
}
|
|
865
998
|
|
|
866
999
|
#recomputeFeedback(): void {
|
|
@@ -942,10 +1075,10 @@ export class PlanReviewOverlay implements Component {
|
|
|
942
1075
|
if (this.#slider) parts.push("◂▸ model");
|
|
943
1076
|
break;
|
|
944
1077
|
case "toc":
|
|
945
|
-
parts.push("↑↓ section", "⏎ open", "a annotate", "d delete", "u undo");
|
|
1078
|
+
parts.push("↑↓ section", "⏎ open", "a annotate", "e edit", "d delete", "u undo");
|
|
946
1079
|
break;
|
|
947
1080
|
case "body":
|
|
948
|
-
parts.push("↑↓ scroll", "⇧ faster", "pgup/pgdn", "g/G ends", "a annotate");
|
|
1081
|
+
parts.push("↑↓ scroll", "⇧ faster", "pgup/pgdn", "g/G ends", "a annotate", "e edit", "u undo");
|
|
949
1082
|
break;
|
|
950
1083
|
}
|
|
951
1084
|
if (this.callbacks.onCopyPlan) parts.push("c copy");
|
|
@@ -1136,8 +1269,39 @@ export class PlanReviewOverlay implements Component {
|
|
|
1136
1269
|
if (glow) return theme.fg("accent", line);
|
|
1137
1270
|
return theme.fg("muted", line);
|
|
1138
1271
|
}
|
|
1272
|
+
#renderAnnotationChooserLines(innerWidth: number, maxRows = Number.POSITIVE_INFINITY): string[] {
|
|
1273
|
+
const chooser = this.#annotationChooser;
|
|
1274
|
+
if (!chooser || maxRows <= 0) return [];
|
|
1275
|
+
const optionLimit = Number.isFinite(maxRows) ? Math.max(0, Math.floor(maxRows) - 2) : chooser.entries.length;
|
|
1276
|
+
const start =
|
|
1277
|
+
optionLimit >= chooser.entries.length
|
|
1278
|
+
? 0
|
|
1279
|
+
: Math.max(
|
|
1280
|
+
0,
|
|
1281
|
+
Math.min(chooser.selected - Math.floor(optionLimit / 2), chooser.entries.length - optionLimit),
|
|
1282
|
+
);
|
|
1283
|
+
const lines = [theme.bold(theme.fg("accent", "Edit annotation"))];
|
|
1284
|
+
for (let windowIndex = 0; windowIndex < optionLimit; windowIndex++) {
|
|
1285
|
+
const optionIndex = start + windowIndex;
|
|
1286
|
+
const selection = chooser.entries[optionIndex];
|
|
1287
|
+
if (!selection) continue;
|
|
1288
|
+
const section = this.#sections[selection.sectionIndex];
|
|
1289
|
+
const annotation = section?.annotations[selection.annotationIndex];
|
|
1290
|
+
if (!section || !annotation) continue;
|
|
1291
|
+
const target = annotation.target.kind === "section" ? "section" : `line ${annotation.target.context}`;
|
|
1292
|
+
const note = sanitizeStatusText(annotation.note);
|
|
1293
|
+
const marker = optionIndex === chooser.selected ? theme.fg("accent", "› ") : " ";
|
|
1294
|
+
const label = `${marker}${sanitizeStatusText(section.title || "Plan preamble")} · ${target} · ${note}`;
|
|
1295
|
+
lines.push(truncateToWidth(label, innerWidth, Ellipsis.Unicode));
|
|
1296
|
+
}
|
|
1297
|
+
if (!Number.isFinite(maxRows) || maxRows >= 2) {
|
|
1298
|
+
lines.push(theme.fg("dim", "↑↓ choose · enter edit · esc cancel"));
|
|
1299
|
+
}
|
|
1300
|
+
return lines.slice(0, Math.max(0, Math.floor(maxRows)));
|
|
1301
|
+
}
|
|
1139
1302
|
|
|
1140
|
-
#renderFooterLines(innerWidth: number): string[] {
|
|
1303
|
+
#renderFooterLines(innerWidth: number, maxChooserRows = Number.POSITIVE_INFINITY): string[] {
|
|
1304
|
+
if (this.#annotationChooser) return this.#renderAnnotationChooserLines(innerWidth, maxChooserRows);
|
|
1141
1305
|
if (this.#annotating) {
|
|
1142
1306
|
const target = this.#annotationTarget;
|
|
1143
1307
|
const section = target ? this.#sections[target.sectionIndex] : undefined;
|
|
@@ -1151,9 +1315,12 @@ export class PlanReviewOverlay implements Component {
|
|
|
1151
1315
|
innerWidth,
|
|
1152
1316
|
Ellipsis.Unicode,
|
|
1153
1317
|
);
|
|
1154
|
-
const hintParts = ["enter save", "esc cancel"];
|
|
1318
|
+
const hintParts = ["enter save", "shift+enter newline", "esc cancel"];
|
|
1319
|
+
if (this.#editingAnnotation) hintParts.push("empty deletes");
|
|
1155
1320
|
if (this.#externalEditorLabel) hintParts.push(`${this.#externalEditorLabel} editor`);
|
|
1156
|
-
|
|
1321
|
+
this.#editor.setMaxHeight(Math.max(1, Math.min(MAX_ANNOTATION_EDITOR_ROWS, (process.stdout.rows || 40) - 12)));
|
|
1322
|
+
this.#editor.focused = true;
|
|
1323
|
+
return [caption, ...this.#editor.render(innerWidth), theme.fg("dim", hintParts.join(" · "))];
|
|
1157
1324
|
}
|
|
1158
1325
|
return [theme.fg("dim", this.#buildHelp())];
|
|
1159
1326
|
}
|
|
@@ -1171,14 +1338,20 @@ export class PlanReviewOverlay implements Component {
|
|
|
1171
1338
|
const submittingLabel = this.#committedLabel ? `${this.#committedLabel} — submitting…` : "Submitting…";
|
|
1172
1339
|
const optionLines = committed ? [theme.bold(theme.fg("accent", submittingLabel))] : this.#renderOptionLines();
|
|
1173
1340
|
const promptLines = this.#promptTitle ? [theme.bold(theme.fg("accent", this.#promptTitle))] : [];
|
|
1341
|
+
const baseChrome = 4 + promptLines.length + sliderLines.length + optionLines.length;
|
|
1342
|
+
const chooserFooterRows = this.#annotationChooser
|
|
1343
|
+
? Math.max(0, termHeight - baseChrome - MIN_BODY_ROWS)
|
|
1344
|
+
: Number.POSITIVE_INFINITY;
|
|
1174
1345
|
const footerLines = committed
|
|
1175
1346
|
? [theme.fg("dim", "Applying your selection — this can take a moment while context is prepared.")]
|
|
1176
|
-
: this.#renderFooterLines(innerWidth);
|
|
1347
|
+
: this.#renderFooterLines(innerWidth, chooserFooterRows);
|
|
1177
1348
|
|
|
1178
1349
|
// Chrome rows: top border, two dividers, bottom border, plus the
|
|
1179
1350
|
// prompt/slider/option/footer rows between them.
|
|
1180
|
-
const chrome =
|
|
1181
|
-
const regionRows =
|
|
1351
|
+
const chrome = baseChrome + footerLines.length;
|
|
1352
|
+
const regionRows = this.#annotationChooser
|
|
1353
|
+
? Math.max(0, termHeight - chrome)
|
|
1354
|
+
: Math.max(MIN_BODY_ROWS, termHeight - chrome);
|
|
1182
1355
|
|
|
1183
1356
|
const bodyLines = this.#buildBody(bodyContentWidth);
|
|
1184
1357
|
this.#layoutBody(bodyLines, regionRows);
|
|
@@ -64,6 +64,7 @@ export const TAB_GROUPS: Record<SettingTab, readonly string[]> = {
|
|
|
64
64
|
"Power",
|
|
65
65
|
"Agent",
|
|
66
66
|
"Git",
|
|
67
|
+
"Skills",
|
|
67
68
|
],
|
|
68
69
|
context: ["General", "Compaction", "Rules (TTSR)", "Experimental"],
|
|
69
70
|
memory: ["General", "Auto-Learn", "Mnemopi", "Hindsight", "Sharpshooter"],
|
|
@@ -7,7 +7,16 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import * as os from "node:os";
|
|
9
9
|
import { resolveUsedFraction, type UsageLimit, type UsageReport } from "@oh-my-pi/pi-ai";
|
|
10
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
type Component,
|
|
12
|
+
matchesKey,
|
|
13
|
+
replaceTabs,
|
|
14
|
+
routeSgrMouseInput,
|
|
15
|
+
sliceWithWidth,
|
|
16
|
+
truncateToWidth,
|
|
17
|
+
visibleWidth,
|
|
18
|
+
wrapTextWithAnsi,
|
|
19
|
+
} from "../index";
|
|
11
20
|
import { colorLuma, formatDuration, hexToRgb, rgbToHex, sanitizeText } from "@oh-my-pi/pi-utils";
|
|
12
21
|
import { formatProviderName } from "../chrome/format";
|
|
13
22
|
import {
|
|
@@ -19,6 +28,8 @@ import {
|
|
|
19
28
|
import { colorToAnsi } from "../theme/color";
|
|
20
29
|
import { ensureThemeSync, theme } from "../theme/theme";
|
|
21
30
|
import { formatAbsoluteOnlyAmount } from "../prompt/usage-amounts";
|
|
31
|
+
import { truncateMiddleToWidth } from "../render/render-utils";
|
|
32
|
+
import { sanitizeDisplayLine } from "./extensions/display-text";
|
|
22
33
|
import {
|
|
23
34
|
matchesSelectCancel,
|
|
24
35
|
matchesSelectDown,
|
|
@@ -72,6 +83,8 @@ export interface ProviderCard {
|
|
|
72
83
|
soonestExpiryMs?: number;
|
|
73
84
|
unavailableReasons: string[];
|
|
74
85
|
};
|
|
86
|
+
/** Labels of accounts with verified Daybreak access. */
|
|
87
|
+
daybreakAccounts?: string[];
|
|
75
88
|
}
|
|
76
89
|
|
|
77
90
|
/**
|
|
@@ -191,6 +204,17 @@ export function buildProviderCards(reports: UsageReport[], nowMs: number): Provi
|
|
|
191
204
|
unavailableReasons,
|
|
192
205
|
}
|
|
193
206
|
: undefined;
|
|
207
|
+
const daybreakAccounts = providerReports.flatMap((report, index) =>
|
|
208
|
+
report.metadata?.daybreak === true
|
|
209
|
+
? [
|
|
210
|
+
typeof report.metadata.email === "string" && report.metadata.email
|
|
211
|
+
? report.metadata.email
|
|
212
|
+
: typeof report.metadata.accountId === "string" && report.metadata.accountId
|
|
213
|
+
? report.metadata.accountId
|
|
214
|
+
: `account ${index + 1}`,
|
|
215
|
+
]
|
|
216
|
+
: [],
|
|
217
|
+
);
|
|
194
218
|
cards.push({
|
|
195
219
|
provider,
|
|
196
220
|
name: formatProviderName(provider),
|
|
@@ -198,8 +222,11 @@ export function buildProviderCards(reports: UsageReport[], nowMs: number): Provi
|
|
|
198
222
|
windows,
|
|
199
223
|
unlimited: windows.length === 0,
|
|
200
224
|
idle:
|
|
201
|
-
!resetCredits &&
|
|
225
|
+
!resetCredits &&
|
|
226
|
+
daybreakAccounts.length === 0 &&
|
|
227
|
+
windows.every(window => window.fraction !== undefined && window.fraction < IDLE_FRACTION),
|
|
202
228
|
resetCredits,
|
|
229
|
+
...(daybreakAccounts.length > 0 ? { daybreakAccounts } : {}),
|
|
203
230
|
});
|
|
204
231
|
}
|
|
205
232
|
|
|
@@ -333,6 +360,16 @@ export function formatActivityErrorDetail(error: string, homeDir = os.homedir())
|
|
|
333
360
|
const CARD_MIN_WIDTH = 32;
|
|
334
361
|
const CARD_GUTTER = 3;
|
|
335
362
|
const CARD_MAX_WINDOWS = 4;
|
|
363
|
+
const CARD_MIN_BAR_WIDTH = 12;
|
|
364
|
+
const CARD_MAX_LABEL_LINES = 2;
|
|
365
|
+
|
|
366
|
+
interface CardRowLayout {
|
|
367
|
+
labelWidth: number;
|
|
368
|
+
resetWidth: number;
|
|
369
|
+
barWidth: number;
|
|
370
|
+
stacked: boolean;
|
|
371
|
+
labelHeights: number[];
|
|
372
|
+
}
|
|
336
373
|
|
|
337
374
|
export class UsageDashboardComponent implements Component {
|
|
338
375
|
#options: UsageDashboardOptions;
|
|
@@ -423,7 +460,7 @@ export class UsageDashboardComponent implements Component {
|
|
|
423
460
|
return `${theme.fg(this.#statusColor(status), bar)}${theme.fg("dim", empty)}`;
|
|
424
461
|
}
|
|
425
462
|
|
|
426
|
-
#renderCardLines(card: ProviderCard, width: number): string[] {
|
|
463
|
+
#renderCardLines(card: ProviderCard, width: number, labels: string[][], layout: CardRowLayout): string[] {
|
|
427
464
|
const lines: string[] = [];
|
|
428
465
|
const cardStatus = card.unlimited ? "ok" : aggregateStatus(card.windows);
|
|
429
466
|
const accountsText = card.accounts > 1 ? theme.fg("dim", `${card.accounts} accts`) : "";
|
|
@@ -432,6 +469,11 @@ export class UsageDashboardComponent implements Component {
|
|
|
432
469
|
const titlePad = Math.max(0, width - 2 - visibleWidth(title) - visibleWidth(accountsText));
|
|
433
470
|
lines.push(`${this.#statusIcon(cardStatus)} ${title}${" ".repeat(titlePad)}${accountsText}`);
|
|
434
471
|
|
|
472
|
+
for (const account of card.daybreakAccounts ?? []) {
|
|
473
|
+
const label = sanitizeText(account.replace(/[\r\n\t]+/g, " "));
|
|
474
|
+
lines.push(` ${theme.fg("success", truncateToWidth(`daybreak · ${label}`, width - 2))}`);
|
|
475
|
+
}
|
|
476
|
+
|
|
435
477
|
if (card.resetCredits) {
|
|
436
478
|
const resets = card.resetCredits;
|
|
437
479
|
let resetText = `✦ ${resets.bankedCount} reset${resets.bankedCount === 1 ? "" : "s"}`;
|
|
@@ -457,35 +499,33 @@ export class UsageDashboardComponent implements Component {
|
|
|
457
499
|
}
|
|
458
500
|
|
|
459
501
|
const hidden = card.windows.length - CARD_MAX_WINDOWS;
|
|
460
|
-
const
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
? truncateToWidth(window.windowTag, Math.max(2, Math.floor(labelWidth / 2) - 1))
|
|
473
|
-
: "";
|
|
474
|
-
const baseWidth = tagPlain ? labelWidth - visibleWidth(tagPlain) - 1 : labelWidth;
|
|
475
|
-
const basePlain = truncateToWidth(window.label, baseWidth).padEnd(baseWidth);
|
|
476
|
-
const label = tagPlain
|
|
477
|
-
? `${theme.fg("muted", basePlain)} ${theme.fg("dim", tagPlain)}`
|
|
478
|
-
: theme.fg("muted", basePlain);
|
|
502
|
+
const { labelWidth, resetWidth, barWidth, stacked, labelHeights } = layout;
|
|
503
|
+
const contentWidth = Math.max(1, width - 2);
|
|
504
|
+
for (let index = 0; index < Math.min(card.windows.length, CARD_MAX_WINDOWS); index++) {
|
|
505
|
+
const window = card.windows[index]!;
|
|
506
|
+
const labelLines = labels[index]!;
|
|
507
|
+
const label = labelLines[0] ?? "";
|
|
508
|
+
const prefix = stacked ? "" : `${label}${" ".repeat(labelWidth - visibleWidth(label))} `;
|
|
509
|
+
if (stacked) {
|
|
510
|
+
for (let line = 0; line < labelHeights[index]; line++) {
|
|
511
|
+
lines.push(` ${labelLines[line] ?? ""}`);
|
|
512
|
+
}
|
|
513
|
+
}
|
|
479
514
|
if (window.fraction === undefined) {
|
|
480
515
|
const text = theme.fg("dim", window.usedText ?? "no data");
|
|
481
|
-
|
|
516
|
+
for (const line of wrapTextWithAnsi(`${prefix}${text}`, contentWidth)) lines.push(` ${line}`);
|
|
482
517
|
continue;
|
|
483
518
|
}
|
|
484
519
|
const freePct = Math.max(0, Math.round((1 - window.fraction) * 100));
|
|
485
520
|
const pctText = theme.fg(this.#statusColor(window.status), `${freePct}%`.padStart(5));
|
|
486
521
|
const resetPlain = window.resetMs !== undefined ? formatDuration(window.resetMs) : "";
|
|
487
522
|
const resetText = resetWidth > 0 ? ` ${theme.fg("dim", resetPlain.padStart(resetWidth))}` : "";
|
|
488
|
-
|
|
523
|
+
for (const line of wrapTextWithAnsi(
|
|
524
|
+
`${prefix}${this.#miniBar(window.fraction, window.status, barWidth)}${pctText}${resetText}`,
|
|
525
|
+
contentWidth,
|
|
526
|
+
)) {
|
|
527
|
+
lines.push(` ${line}`);
|
|
528
|
+
}
|
|
489
529
|
}
|
|
490
530
|
if (hidden > 0) lines.push(` ${theme.fg("dim", `+${hidden} more`)}`);
|
|
491
531
|
return lines;
|
|
@@ -499,7 +539,62 @@ export class UsageDashboardComponent implements Component {
|
|
|
499
539
|
const cardWidth = Math.floor((innerWidth - (columns - 1) * CARD_GUTTER) / columns);
|
|
500
540
|
const lines: string[] = [];
|
|
501
541
|
for (let start = 0; start < active.length; start += columns) {
|
|
502
|
-
const
|
|
542
|
+
const cards = active.slice(start, start + columns);
|
|
543
|
+
const windows = cards.map(card => card.windows.slice(0, CARD_MAX_WINDOWS));
|
|
544
|
+
const labels = windows.map(rows =>
|
|
545
|
+
rows.map(window => {
|
|
546
|
+
const label = theme.fg("muted", sanitizeDisplayLine(window.label));
|
|
547
|
+
const tag = window.windowTag ? sanitizeDisplayLine(window.windowTag) : "";
|
|
548
|
+
return tag ? `${label} ${theme.fg("dim", tag)}` : label;
|
|
549
|
+
}),
|
|
550
|
+
);
|
|
551
|
+
// One geometry per grid row: labels, bars, and resets share columns,
|
|
552
|
+
// and every card stacks together when inline bars would be too short.
|
|
553
|
+
const labelWidth = labels.reduce(
|
|
554
|
+
(max, rows) => rows.reduce((width, label) => Math.max(width, visibleWidth(label)), max),
|
|
555
|
+
0,
|
|
556
|
+
);
|
|
557
|
+
const resetWidth = windows.reduce(
|
|
558
|
+
(max, rows) =>
|
|
559
|
+
rows.reduce(
|
|
560
|
+
(width, window) =>
|
|
561
|
+
Math.max(width, window.resetMs !== undefined ? formatDuration(window.resetMs).length : 0),
|
|
562
|
+
max,
|
|
563
|
+
),
|
|
564
|
+
0,
|
|
565
|
+
);
|
|
566
|
+
const contentWidth = Math.max(1, cardWidth - 2);
|
|
567
|
+
const suffixWidth = 5 + (resetWidth > 0 ? resetWidth + 1 : 0);
|
|
568
|
+
const inlineBarWidth = contentWidth - labelWidth - 1 - suffixWidth;
|
|
569
|
+
const stacked = inlineBarWidth < CARD_MIN_BAR_WIDTH;
|
|
570
|
+
const labelLines = labels.map(rows =>
|
|
571
|
+
rows.map(label => {
|
|
572
|
+
if (!stacked) return [label];
|
|
573
|
+
// Bound wrapping work as well as height, keeping the suffix that
|
|
574
|
+
// distinguishes model-specific quota buckets.
|
|
575
|
+
const bounded = truncateMiddleToWidth(label, contentWidth * CARD_MAX_LABEL_LINES);
|
|
576
|
+
const lines = wrapTextWithAnsi(bounded, contentWidth);
|
|
577
|
+
if (lines.length <= CARD_MAX_LABEL_LINES) return lines;
|
|
578
|
+
// Word wrapping can waste enough cells to create a third line even
|
|
579
|
+
// when the label fits. Use a grapheme-safe column split in that case.
|
|
580
|
+
const first = sliceWithWidth(bounded, 0, contentWidth, true);
|
|
581
|
+
const rest = sliceWithWidth(bounded, first.width, visibleWidth(bounded) - first.width, true);
|
|
582
|
+
return [first.text, truncateMiddleToWidth(rest.text, contentWidth)];
|
|
583
|
+
}),
|
|
584
|
+
);
|
|
585
|
+
const labelHeights = Array.from({ length: CARD_MAX_WINDOWS }, (_, index) =>
|
|
586
|
+
Math.max(0, ...labelLines.map(rows => rows[index]?.length ?? 0)),
|
|
587
|
+
);
|
|
588
|
+
const layout: CardRowLayout = {
|
|
589
|
+
labelWidth,
|
|
590
|
+
resetWidth,
|
|
591
|
+
barWidth: Math.max(1, stacked ? contentWidth - suffixWidth : inlineBarWidth),
|
|
592
|
+
stacked,
|
|
593
|
+
labelHeights,
|
|
594
|
+
};
|
|
595
|
+
const rowCards = cards.map((card, index) =>
|
|
596
|
+
this.#renderCardLines(card, cardWidth, labelLines[index]!, layout),
|
|
597
|
+
);
|
|
503
598
|
const height = Math.max(...rowCards.map(card => card.length));
|
|
504
599
|
for (let lineIdx = 0; lineIdx < height; lineIdx++) {
|
|
505
600
|
const segments = rowCards.map(card => {
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* the
|
|
7
|
-
* materialization prefers it over a blob copy).
|
|
2
|
+
* File source carried on image attachments backed by a file on disk:
|
|
3
|
+
* path-pasted/drag-and-dropped images, clipboard images the coding agent commits
|
|
4
|
+
* to the session's `local://` root, and generated video contact-sheet previews.
|
|
5
|
+
* Symbol metadata stays out of serialized/model-bound image data while traveling
|
|
6
|
+
* with the draft object, until AgentSession creates the hidden companion message
|
|
7
|
+
* that tells the model the path (and link materialization prefers it over a blob copy).
|
|
8
8
|
*/
|
|
9
9
|
import type { ImageContent } from "@oh-my-pi/pi-ai";
|
|
10
10
|
import { isRecord } from "@oh-my-pi/pi-utils";
|
|
@@ -12,8 +12,9 @@ import { isRecord } from "@oh-my-pi/pi-utils";
|
|
|
12
12
|
/** How an image attachment's backing file entered the session. */
|
|
13
13
|
export type ImageAttachmentSourceKind = "image" | "video";
|
|
14
14
|
|
|
15
|
-
/**
|
|
15
|
+
/** Local file backing an image attachment. */
|
|
16
16
|
export interface ImageAttachmentSource {
|
|
17
|
+
/** Absolute filesystem path, or a session-relative `local://` URL for images committed to the session. */
|
|
17
18
|
readonly path: string;
|
|
18
19
|
readonly kind: ImageAttachmentSourceKind;
|
|
19
20
|
}
|
|
@@ -43,7 +44,7 @@ function isImageAttachmentSource(value: unknown): value is ImageAttachmentSource
|
|
|
43
44
|
return isRecord(value) && typeof value.path === "string" && (value.kind === "image" || value.kind === "video");
|
|
44
45
|
}
|
|
45
46
|
|
|
46
|
-
/** Return the
|
|
47
|
+
/** Return the local file backing an image attachment, or undefined for payloads with no file on disk. */
|
|
47
48
|
export function imageAttachmentSource(image: ImageContent): ImageAttachmentSource | undefined {
|
|
48
49
|
if (!(kImageAttachmentSource in image)) return undefined;
|
|
49
50
|
const source = image[kImageAttachmentSource];
|
package/src/prompt/welcome.ts
CHANGED
|
@@ -185,10 +185,13 @@ export class WelcomeComponent implements Component {
|
|
|
185
185
|
this.#requestRender();
|
|
186
186
|
this.#animTimer = setInterval(() => {
|
|
187
187
|
const elapsed = performance.now() - (this.#animStart ?? 0);
|
|
188
|
+
const requestRender = this.#requestRender;
|
|
188
189
|
if (elapsed >= INTRO_MS) {
|
|
189
190
|
this.#stopAnimation();
|
|
190
191
|
}
|
|
191
|
-
|
|
192
|
+
// Stopping clears the callback, but the settled frame must still paint
|
|
193
|
+
// so an oversized startup header can retire into native scrollback.
|
|
194
|
+
requestRender?.();
|
|
192
195
|
}, INTRO_TICK_MS);
|
|
193
196
|
}
|
|
194
197
|
|
|
@@ -211,7 +211,7 @@ export function expandKeyHint(): string {
|
|
|
211
211
|
// Text Truncation Utilities
|
|
212
212
|
// =============================================================================
|
|
213
213
|
/** Keep both ends of a single-line label without splitting wide characters. */
|
|
214
|
-
function truncateMiddleToWidth(text: string, maxWidth: number): string {
|
|
214
|
+
export function truncateMiddleToWidth(text: string, maxWidth: number): string {
|
|
215
215
|
const width = visibleWidth(text);
|
|
216
216
|
if (width <= maxWidth) return text;
|
|
217
217
|
if (maxWidth <= 1) return maxWidth === 1 ? "…" : "";
|