@oh-my-pi/pi-tui 18.2.11 → 18.3.1

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.
Files changed (101) hide show
  1. package/CHANGELOG.md +44 -0
  2. package/THIRD-PARTY-NOTICES.txt +3 -3
  3. package/dist/types/apps/ps-data.d.ts +1 -1
  4. package/dist/types/apps/ps-top.d.ts +1 -1
  5. package/dist/types/chat/chat-transcript-builder.d.ts +1 -1
  6. package/dist/types/chat/read-tool-group.d.ts +3 -3
  7. package/dist/types/chat/user-message.d.ts +4 -1
  8. package/dist/types/components/editor.d.ts +0 -2
  9. package/dist/types/components/input.d.ts +16 -0
  10. package/dist/types/overlays/annotation-overlay.d.ts +30 -0
  11. package/dist/types/overlays/annotation-types.d.ts +84 -0
  12. package/dist/types/overlays/btw-history-panel.d.ts +4 -1
  13. package/dist/types/overlays/copy-selector.d.ts +30 -2
  14. package/dist/types/overlays/copy-targets.d.ts +2 -0
  15. package/dist/types/overlays/model-hub.d.ts +2 -2
  16. package/dist/types/overlays/oauth-selector.d.ts +3 -6
  17. package/dist/types/overlays/settings-defs.d.ts +5 -0
  18. package/dist/types/overlays/usage-dashboard.d.ts +2 -0
  19. package/dist/types/prompt/composer.d.ts +12 -1
  20. package/dist/types/prompt/custom-editor.d.ts +5 -26
  21. package/dist/types/prompt/image-source.d.ts +9 -8
  22. package/dist/types/space-hold.d.ts +45 -0
  23. package/dist/types/status-line/context-usage.d.ts +3 -1
  24. package/dist/types/status-line/host.d.ts +2 -0
  25. package/dist/types/theme/mermaid-cache.d.ts +4 -6
  26. package/dist/types/tools/bash.d.ts +7 -0
  27. package/dist/types/tools/cfg-render.d.ts +37 -0
  28. package/dist/types/tools/cfg-url.d.ts +16 -0
  29. package/dist/types/tools/daemon.d.ts +46 -0
  30. package/dist/types/tools/find.d.ts +2 -2
  31. package/dist/types/tools/irc.d.ts +30 -0
  32. package/dist/types/tools/line-ranges.d.ts +1 -1
  33. package/dist/types/tools/output-meta.d.ts +2 -0
  34. package/dist/types/tools/proc-render.d.ts +27 -0
  35. package/dist/types/tools/read.d.ts +18 -4
  36. package/dist/types/tools/result-card.d.ts +20 -0
  37. package/dist/types/tools/url-scheme-host.d.ts +29 -0
  38. package/dist/types/tools/wait.d.ts +131 -0
  39. package/dist/types/tools/write.d.ts +6 -0
  40. package/dist/types/tools/xd-url.d.ts +8 -0
  41. package/dist/types/tools/xdev.d.ts +9 -1
  42. package/dist/types/windows-altgr.d.ts +54 -0
  43. package/package.json +13 -10
  44. package/src/apps/debug/protocol-probe.ts +1 -1
  45. package/src/apps/ps-data.ts +1 -1
  46. package/src/apps/ps-top.ts +1 -1
  47. package/src/chat/chat-transcript-builder.ts +9 -5
  48. package/src/chat/read-tool-group.ts +8 -9
  49. package/src/chat/tool-execution.ts +15 -13
  50. package/src/chat/transcript-render-helpers.ts +3 -3
  51. package/src/chat/user-message.ts +16 -6
  52. package/src/components/editor.ts +2 -4
  53. package/src/components/input.ts +103 -30
  54. package/src/components/markdown.ts +19 -7
  55. package/src/desktop-notify.ts +1 -1
  56. package/src/overlays/annotation-overlay.ts +1144 -0
  57. package/src/overlays/annotation-types.ts +78 -0
  58. package/src/overlays/btw-history-panel.ts +4 -0
  59. package/src/overlays/copy-selector.ts +45 -19
  60. package/src/overlays/copy-targets.ts +3 -3
  61. package/src/overlays/model-hub.ts +5 -4
  62. package/src/overlays/oauth-selector.ts +8 -12
  63. package/src/overlays/plan-review-overlay.ts +213 -40
  64. package/src/overlays/settings-defs.ts +6 -0
  65. package/src/overlays/settings-selector.ts +4 -3
  66. package/src/overlays/usage-dashboard.ts +22 -1
  67. package/src/prompt/composer.ts +83 -56
  68. package/src/prompt/custom-editor.ts +15 -137
  69. package/src/prompt/image-source.ts +9 -8
  70. package/src/prompt/tips.txt +1 -0
  71. package/src/setup/scenes/sign-in.ts +1 -1
  72. package/src/space-hold.ts +143 -0
  73. package/src/status-line/component.ts +6 -0
  74. package/src/status-line/context-usage.ts +15 -3
  75. package/src/status-line/host.ts +2 -0
  76. package/src/status-line/segments.ts +4 -0
  77. package/src/terminal-capabilities.ts +2 -2
  78. package/src/terminal.ts +8 -1
  79. package/src/theme/mermaid-cache.ts +83 -25
  80. package/src/tools/bash.ts +14 -1
  81. package/src/tools/cfg-render.ts +117 -0
  82. package/src/tools/cfg-url.ts +30 -0
  83. package/src/tools/daemon.ts +51 -0
  84. package/src/tools/find.ts +7 -6
  85. package/src/tools/index.ts +2 -2
  86. package/src/tools/irc.ts +33 -0
  87. package/src/tools/line-ranges.ts +13 -3
  88. package/src/tools/output-meta.ts +2 -0
  89. package/src/tools/proc-render.ts +270 -0
  90. package/src/tools/read.ts +118 -56
  91. package/src/tools/result-card.ts +36 -0
  92. package/src/tools/url-scheme-host.ts +42 -0
  93. package/src/tools/wait.ts +595 -0
  94. package/src/tools/write.ts +120 -1
  95. package/src/tools/xd-url.ts +9 -0
  96. package/src/tools/xdev.ts +9 -1
  97. package/src/windows-altgr.ts +227 -0
  98. package/dist/types/chat/read-target.d.ts +0 -4
  99. package/dist/types/tools/hub.d.ts +0 -342
  100. package/src/chat/read-target.ts +0 -7
  101. package/src/tools/hub.ts +0 -1492
@@ -19,7 +19,7 @@
19
19
  import {
20
20
  type Component,
21
21
  Ellipsis,
22
- Input,
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
- #input: Input;
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.#input = new Input();
240
- this.#input.setUseTerminalCursor(false);
241
- this.#input.onSubmit = value => this.#submitAnnotation(value);
242
- this.#input.onEscape = () => this.#exitAnnotate();
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.#input.getValue(), text => {
496
- if (text !== null) this.#submitAnnotation(text);
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
- this.#input.handleInput(keyData);
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.#input.setValue("");
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
- const section = target ? this.#sections[target.sectionIndex] : undefined;
841
- if (note && section && target) {
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
- section.annotations.push({
844
- note,
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
- this.#input.setValue("");
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.#input.setValue("");
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
- return [caption, this.#input.render(innerWidth)[0] ?? "", theme.fg("dim", hintParts.join(" · "))];
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 = 4 + promptLines.length + sliderLines.length + optionLines.length + footerLines.length;
1181
- const regionRows = Math.max(MIN_BODY_ROWS, termHeight - chrome);
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);
@@ -75,6 +75,7 @@ export const TAB_GROUPS: Record<SettingTab, readonly string[]> = {
75
75
  "Todos",
76
76
  "Grep & Browser",
77
77
  "Computer",
78
+ "IDA Pro",
78
79
  "GitHub",
79
80
  "Output Limits",
80
81
  "Execution",
@@ -131,6 +132,11 @@ export interface SettingsHost {
131
132
  entries: readonly SettingsDisplayEntry[];
132
133
  get(path: string): unknown;
133
134
  set(path: string, value: unknown): void;
135
+ /**
136
+ * Removes the value from the global config: a project or other layer, or an environment
137
+ * variable, that configures the setting still applies; otherwise the default does.
138
+ */
139
+ unset(path: string): void;
134
140
  normalizeProviderLimits(value: unknown): Record<string, number>;
135
141
  validateProviderLimits(value: unknown): Record<string, number>;
136
142
  }
@@ -1100,9 +1100,10 @@ export class SettingsSelectorComponent implements Component {
1100
1100
  this.#formatTextInputEditValue(def.path, this.#context.settings.get(def.path)),
1101
1101
  def.secret,
1102
1102
  value => {
1103
- // Empty string clears the setting; undefined-typed string settings
1104
- // store "" which the browser.ts expandPath ignores (no-op fallback).
1105
- this.#setSettingValue(def.path, value);
1103
+ // An empty field removes the persisted value, so the default (or an
1104
+ // environment fallback) applies again instead of a pinned "".
1105
+ if (value === "") this.#context.settings.unset(def.path);
1106
+ else this.#setSettingValue(def.path, value);
1106
1107
  this.#callbacks.onChange(def.path, this.#context.settings.get(def.path));
1107
1108
  wrappedDone(this.#formatTextInputValue(def, this.#context.settings.get(def.path)));
1108
1109
  },
@@ -83,6 +83,8 @@ export interface ProviderCard {
83
83
  soonestExpiryMs?: number;
84
84
  unavailableReasons: string[];
85
85
  };
86
+ /** Labels of accounts with verified Daybreak access. */
87
+ daybreakAccounts?: string[];
86
88
  }
87
89
 
88
90
  /**
@@ -202,6 +204,17 @@ export function buildProviderCards(reports: UsageReport[], nowMs: number): Provi
202
204
  unavailableReasons,
203
205
  }
204
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
+ );
205
218
  cards.push({
206
219
  provider,
207
220
  name: formatProviderName(provider),
@@ -209,8 +222,11 @@ export function buildProviderCards(reports: UsageReport[], nowMs: number): Provi
209
222
  windows,
210
223
  unlimited: windows.length === 0,
211
224
  idle:
212
- !resetCredits && windows.every(window => window.fraction !== undefined && window.fraction < IDLE_FRACTION),
225
+ !resetCredits &&
226
+ daybreakAccounts.length === 0 &&
227
+ windows.every(window => window.fraction !== undefined && window.fraction < IDLE_FRACTION),
213
228
  resetCredits,
229
+ ...(daybreakAccounts.length > 0 ? { daybreakAccounts } : {}),
214
230
  });
215
231
  }
216
232
 
@@ -453,6 +469,11 @@ export class UsageDashboardComponent implements Component {
453
469
  const titlePad = Math.max(0, width - 2 - visibleWidth(title) - visibleWidth(accountsText));
454
470
  lines.push(`${this.#statusIcon(cardStatus)} ${title}${" ".repeat(titlePad)}${accountsText}`);
455
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
+
456
477
  if (card.resetCredits) {
457
478
  const resets = card.resetCredits;
458
479
  let resetText = `✦ ${resets.bankedCount} reset${resets.bankedCount === 1 ? "" : "s"}`;
@@ -91,6 +91,18 @@ export interface ComposerOptions {
91
91
  readonly now?: () => number;
92
92
  }
93
93
 
94
+ /** How {@link Composer.setRuntimeChildren} bills below-transcript roots against transcript retirement. */
95
+ export interface RuntimeChildrenOptions {
96
+ /**
97
+ * Below-transcript roots whose growth is momentary (inline dialogs, a tall
98
+ * multi-line editor). They are billed at their smallest height so expansion
99
+ * clips the live tail instead of retiring rows a later shrink could not
100
+ * reclaim (#11007). Every other root is billed at its current height, so
101
+ * settled rows it displaces retire to native scrollback.
102
+ */
103
+ readonly transient?: readonly Component[];
104
+ }
105
+
94
106
  /** Controls the first terminal paint for a composer that does not already own the terminal. */
95
107
  export interface ComposerStartOptions {
96
108
  readonly clearScrollback?: boolean;
@@ -253,15 +265,18 @@ export class Composer implements TerminalFrameProvider {
253
265
  #retiredHeaderStart = 0;
254
266
  #resizeRetiredHeaderStart: number | undefined;
255
267
  #lastNormalRows = 0;
256
- // Smallest below-transcript chrome height (editor + status + any transient
257
- // inline dialog) seen since mount. Retirement is billed against this
258
- // persistent baseline, never the transient peak, so a dialog or tall editor
259
- // that later shrinks never leaves committed transcript rows the live viewport
260
- // cannot reclaim (#11007). The baseline is terminal-height independent — the
261
- // editor and status floors do not scale with rows — so it is retained across
262
- // resizes rather than rediscovered from whatever chrome is expanded at the
263
- // moment the height changes.
264
- #retirementBelowFloor: number | undefined;
268
+ // Roots from `RuntimeChildrenOptions.transient`, and the smallest height
269
+ // they have rendered at since mount. Retirement bills transient roots at
270
+ // this floor, never their peak, so a dialog or tall editor that later
271
+ // shrinks never leaves committed transcript rows the live viewport cannot
272
+ // reclaim (#11007). Persistent roots (loader, todo/subagent HUDs) bill at
273
+ // their current height: they stay up for a whole turn, and billing them at
274
+ // an idle floor hid the settled rows they displaced until the turn ended.
275
+ // The floor is terminal-height independent — editor and dialog heights do
276
+ // not scale with rows — so it is retained across resizes rather than
277
+ // rediscovered from whatever chrome is expanded when the height changes.
278
+ #transientChrome: ReadonlySet<Component> = new Set();
279
+ #transientChromeFloor: number | undefined;
265
280
  #lastInterruptAt = 0;
266
281
  #started = false;
267
282
  #stopped = false;
@@ -346,60 +361,24 @@ export class Composer implements TerminalFrameProvider {
346
361
  const afterRoots = roots.slice(transcriptIndex + 1);
347
362
  const after: string[] = [];
348
363
  const afterSpans: ViewportClickSpan[] = [];
364
+ let transientRows = 0;
349
365
  for (const root of afterRoots) {
350
366
  const start = after.length;
351
- // Row targets usually nest one level down: chrome roots are plain
352
- // containers (the HUD lives inside `subagentContainer`), and
353
- // `Container.render` is a pure concatenation, so child spans tile
354
- // the root span exactly. Render those children once and share the
355
- // rows for composition and measurement — a second render per frame
356
- // would duplicate render-time side effects (image placement
357
- // registration). Roots with a custom render keep the composed
358
- // output as the source of truth and measure up to the last target.
359
- const plainContainer = root instanceof Container && root.render === Container.prototype.render;
360
- const targets = root instanceof Container ? root.children : [root];
361
- const resolves = targets.map(rowTargetCandidates);
362
- const lastTarget = resolves.findLastIndex(resolve => resolve !== undefined);
363
- if (plainContainer) {
364
- let offset = start;
365
- for (let index = 0; index < targets.length; index++) {
366
- const childLines = targets[index]!.render(width);
367
- after.push(...childLines);
368
- if (index > lastTarget) continue;
369
- const resolve = resolves[index];
370
- if (resolve !== undefined && childLines.length > 0) {
371
- afterSpans.push({ start: offset, end: offset + childLines.length, candidates: resolve });
372
- }
373
- offset += childLines.length;
374
- }
375
- continue;
376
- }
377
- after.push(...root.render(width));
378
- if (lastTarget === -1) continue;
379
- let offset = start;
380
- for (let index = 0; index <= lastTarget; index++) {
381
- const childLines = targets[index] === root ? after.length - start : targets[index]!.render(width).length;
382
- const resolve = resolves[index];
383
- if (resolve !== undefined && childLines > 0) {
384
- afterSpans.push({ start: offset, end: offset + childLines, candidates: resolve });
385
- }
386
- offset += childLines;
387
- }
367
+ this.#renderBelowRoot(root, width, after, afterSpans);
368
+ if (this.#transientChrome.has(root)) transientRows += after.length - start;
388
369
  }
389
370
  // Offer history under capacity pressure only: blocks stay live (and keep
390
371
  // reflowing to the current width) while the screen has room. A batch
391
372
  // leaves the mutable viewport in the same frame it is appended, so its
392
373
  // rows are never painted twice.
393
374
  //
394
- // Retirement is billed against the persistent below-transcript chrome
395
- // baseline, not the transient peak: a confirmation dialog or a tall
396
- // multi-line editor swapped in below the transcript clips the live tail
397
- // for its lifetime, but must not permanently commit transcript rows to
398
- // native history — otherwise a later shrink cannot refill the freed rows
399
- // and the editor drifts up above a band of blank rows (#11007).
400
- this.#retirementBelowFloor =
401
- this.#retirementBelowFloor === undefined ? after.length : Math.min(this.#retirementBelowFloor, after.length);
402
- const belowFloor = this.#retirementBelowFloor;
375
+ // Retirement bills transient roots at their floor, not their peak: a
376
+ // confirmation dialog or a tall multi-line editor clips the live tail for
377
+ // its lifetime, but must not permanently commit transcript rows to native
378
+ // history — otherwise a later shrink cannot refill the freed rows and the
379
+ // editor drifts up above a band of blank rows (#11007).
380
+ this.#transientChromeFloor = Math.min(this.#transientChromeFloor ?? transientRows, transientRows);
381
+ const belowFloor = after.length - transientRows + this.#transientChromeFloor;
403
382
  const history = this.#offerHistory(transcript, width, rows, preRoots.length + belowFloor);
404
383
  const headerVisible = !this.#headerRetired && this.#offeredHistory?.source !== "header";
405
384
  const headerRows = headerVisible ? this.#header.render(width) : [];
@@ -443,6 +422,52 @@ export class Composer implements TerminalFrameProvider {
443
422
  return { history, viewport: this.#paintHoverBand(mutable, spans) };
444
423
  }
445
424
 
425
+ /**
426
+ * Append one below-transcript root's rows to `after`, recording click spans
427
+ * for its row targets in `after` coordinates.
428
+ *
429
+ * Row targets usually nest one level down: chrome roots are plain
430
+ * containers (the HUD lives inside `subagentContainer`), and
431
+ * `Container.render` is a pure concatenation, so child spans tile the root
432
+ * span exactly. Render those children once and share the rows for
433
+ * composition and measurement — a second render per frame would duplicate
434
+ * render-time side effects (image placement registration). Roots with a
435
+ * custom render keep the composed output as the source of truth and measure
436
+ * up to the last target.
437
+ */
438
+ #renderBelowRoot(root: Component, width: number, after: string[], spans: ViewportClickSpan[]): void {
439
+ const start = after.length;
440
+ const plainContainer = root instanceof Container && root.render === Container.prototype.render;
441
+ const targets = root instanceof Container ? root.children : [root];
442
+ const resolves = targets.map(rowTargetCandidates);
443
+ const lastTarget = resolves.findLastIndex(resolve => resolve !== undefined);
444
+ if (plainContainer) {
445
+ let offset = start;
446
+ for (let index = 0; index < targets.length; index++) {
447
+ const childLines = targets[index]!.render(width);
448
+ after.push(...childLines);
449
+ if (index > lastTarget) continue;
450
+ const resolve = resolves[index];
451
+ if (resolve !== undefined && childLines.length > 0) {
452
+ spans.push({ start: offset, end: offset + childLines.length, candidates: resolve });
453
+ }
454
+ offset += childLines.length;
455
+ }
456
+ return;
457
+ }
458
+ after.push(...root.render(width));
459
+ if (lastTarget === -1) return;
460
+ let offset = start;
461
+ for (let index = 0; index <= lastTarget; index++) {
462
+ const childLines = targets[index] === root ? after.length - start : targets[index]!.render(width).length;
463
+ const resolve = resolves[index];
464
+ if (resolve !== undefined && childLines > 0) {
465
+ spans.push({ start: offset, end: offset + childLines, candidates: resolve });
466
+ }
467
+ offset += childLines;
468
+ }
469
+ }
470
+
446
471
  /**
447
472
  * Band the hovered click target's current rows. Id-anchored (not
448
473
  * line-anchored) so the band follows an agent whose rows shift while it
@@ -847,8 +872,10 @@ export class Composer implements TerminalFrameProvider {
847
872
  }
848
873
 
849
874
  /** Mount or replace session-aware root children while preserving the header and status hosts. */
850
- setRuntimeChildren(children: readonly Component[]): void {
875
+ setRuntimeChildren(children: readonly Component[], options: RuntimeChildrenOptions = {}): void {
851
876
  if (this.#stopped) return;
877
+ this.#transientChrome = new Set(options.transient);
878
+ this.#transientChromeFloor = undefined;
852
879
  this.ui.removeChild(this.#statusHost);
853
880
  if (this.#runtimeMounted) {
854
881
  for (const child of this.#runtimeChildren) this.ui.removeChild(child);