@oh-my-pi/pi-tui 18.2.11 → 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.
Files changed (61) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/THIRD-PARTY-NOTICES.txt +1 -1
  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/overlays/annotation-overlay.d.ts +30 -0
  7. package/dist/types/overlays/annotation-types.d.ts +84 -0
  8. package/dist/types/overlays/copy-selector.d.ts +30 -2
  9. package/dist/types/overlays/copy-targets.d.ts +2 -0
  10. package/dist/types/overlays/model-hub.d.ts +2 -2
  11. package/dist/types/overlays/oauth-selector.d.ts +3 -6
  12. package/dist/types/overlays/usage-dashboard.d.ts +2 -0
  13. package/dist/types/prompt/image-source.d.ts +9 -8
  14. package/dist/types/status-line/context-usage.d.ts +3 -1
  15. package/dist/types/theme/mermaid-cache.d.ts +4 -6
  16. package/dist/types/tools/bash.d.ts +7 -0
  17. package/dist/types/tools/daemon.d.ts +46 -0
  18. package/dist/types/tools/irc.d.ts +30 -0
  19. package/dist/types/tools/line-ranges.d.ts +1 -1
  20. package/dist/types/tools/proc-render.d.ts +34 -0
  21. package/dist/types/tools/read.d.ts +4 -1
  22. package/dist/types/tools/wait.d.ts +131 -0
  23. package/dist/types/tools/write.d.ts +4 -0
  24. package/dist/types/tools/xd-url.d.ts +8 -0
  25. package/dist/types/windows-altgr.d.ts +54 -0
  26. package/package.json +13 -10
  27. package/src/apps/debug/protocol-probe.ts +1 -1
  28. package/src/apps/ps-data.ts +1 -1
  29. package/src/apps/ps-top.ts +1 -1
  30. package/src/chat/chat-transcript-builder.ts +4 -4
  31. package/src/chat/tool-execution.ts +8 -8
  32. package/src/chat/transcript-render-helpers.ts +3 -3
  33. package/src/components/markdown.ts +1 -1
  34. package/src/desktop-notify.ts +1 -1
  35. package/src/overlays/annotation-overlay.ts +1145 -0
  36. package/src/overlays/annotation-types.ts +78 -0
  37. package/src/overlays/copy-selector.ts +45 -19
  38. package/src/overlays/copy-targets.ts +3 -3
  39. package/src/overlays/model-hub.ts +5 -4
  40. package/src/overlays/oauth-selector.ts +8 -12
  41. package/src/overlays/plan-review-overlay.ts +213 -40
  42. package/src/overlays/usage-dashboard.ts +22 -1
  43. package/src/prompt/image-source.ts +9 -8
  44. package/src/setup/scenes/sign-in.ts +1 -1
  45. package/src/status-line/context-usage.ts +15 -3
  46. package/src/terminal-capabilities.ts +2 -2
  47. package/src/terminal.ts +8 -1
  48. package/src/theme/mermaid-cache.ts +83 -25
  49. package/src/tools/bash.ts +14 -1
  50. package/src/tools/daemon.ts +51 -0
  51. package/src/tools/index.ts +2 -2
  52. package/src/tools/irc.ts +33 -0
  53. package/src/tools/line-ranges.ts +13 -3
  54. package/src/tools/proc-render.ts +294 -0
  55. package/src/tools/read.ts +22 -1
  56. package/src/tools/wait.ts +595 -0
  57. package/src/tools/write.ts +76 -0
  58. package/src/tools/xd-url.ts +9 -0
  59. package/src/windows-altgr.ts +227 -0
  60. package/dist/types/tools/hub.d.ts +0 -342
  61. 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);
@@ -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"}`;
@@ -1,10 +1,10 @@
1
1
  /**
2
- * Original filesystem source carried on image attachments that entered from a
3
- * file on disk: path-pasted/drag-and-dropped images and generated video
4
- * contact-sheet previews. Symbol metadata stays out of serialized/model-bound
5
- * image data while traveling with the draft object, until AgentSession creates
6
- * the hidden companion message that tells the model the path (and link
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
- /** Original local file backing an image attachment. */
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 original local source of an image attachment, or undefined for payloads with no source file (clipboard bitmaps). */
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];
@@ -230,7 +230,7 @@ export class SignInTab implements SetupTab {
230
230
  this.#host.restoreFocus();
231
231
  this.#host.requestRender();
232
232
  try {
233
- await this.#authStorage.login(providerId as OAuthProvider, {
233
+ await this.#authStorage.oauth.login(providerId as OAuthProvider, {
234
234
  signal: this.#loginAbort.signal,
235
235
  onBrowserSession: (request, signal) => this.#host.ctx.captureBrowserSession(request, signal),
236
236
  onAuth: info => {
@@ -2,6 +2,7 @@ import type { AgentMessage, Tokenizer } from "@oh-my-pi/pi-agent-core";
2
2
  import type { CompactionSettings } from "@oh-my-pi/pi-agent-core/compaction";
3
3
  import { effectiveReserveTokens, resolveThresholdTokens } from "@oh-my-pi/pi-agent-core/compaction";
4
4
  import type { Tool as AiTool, Model } from "@oh-my-pi/pi-ai";
5
+ import { renderToolExamples } from "@oh-my-pi/pi-ai/dialect";
5
6
  import { toolWireSchema } from "@oh-my-pi/pi-ai/utils/schema";
6
7
  import { formatNumber } from "@oh-my-pi/pi-utils";
7
8
  import type { Theme } from "../theme";
@@ -12,7 +13,7 @@ interface ContextSkill {
12
13
  readonly hide?: boolean;
13
14
  }
14
15
 
15
- type ContextTool = Pick<AiTool, "name" | "description" | "parameters">;
16
+ type ContextTool = Pick<AiTool, "name" | "description" | "parameters" | "examples">;
16
17
 
17
18
  /** Savings computed by the host's inline-image planner, not by the renderer. */
18
19
  export interface ContextSavingsEstimate {
@@ -140,6 +141,8 @@ export interface NonMessageTokenSource {
140
141
  };
141
142
  };
142
143
  readonly skills?: readonly ContextSkill[];
144
+ /** Provider-facing, session-frozen descriptions when available. */
145
+ readonly renderedSkills?: readonly ContextSkill[];
143
146
  }
144
147
 
145
148
  /** Shared empty system-prompt part list, avoiding an allocation per render. */
@@ -244,9 +247,13 @@ export function estimateToolSchemaTokens(tools: ToolSchemaSource, tokenizer: Tok
244
247
  name,
245
248
  description,
246
249
  parameters: parameters as AiTool["parameters"],
250
+ examples: tool.examples,
247
251
  };
248
252
  const wireJson = JSON.stringify(toolWireSchema(wireTool) ?? {});
249
253
  if (typeof wireJson === "string") fragments.push(wireJson);
254
+ // The agent loop appends rendered examples to the wire description.
255
+ const examplesBlock = renderToolExamples(wireTool);
256
+ if (examplesBlock) fragments.push(examplesBlock);
250
257
  } catch {
251
258
  // Schema may contain functions or cycles; ignore.
252
259
  }
@@ -313,7 +320,7 @@ function nonMessageTokenCacheEntry(
313
320
  const systemPromptRef = session.systemPrompt ?? EMPTY_STRING_PARTS;
314
321
  const toolsRef = session.agent?.state?.tools ?? EMPTY_TOOLS;
315
322
  const toolsRevision = getToolSchemaMetadataRevision(toolsRef);
316
- const skillsRef = session.skills ?? EMPTY_SKILLS;
323
+ const skillsRef = session.renderedSkills ?? session.skills ?? EMPTY_SKILLS;
317
324
  let entry = cachedSession[NON_MESSAGE_TOKEN_CACHE];
318
325
  if (
319
326
  entry &&
@@ -378,7 +385,12 @@ export function computeNonMessageBreakdown(
378
385
  if (entry.breakdown && entry.skillful === skillful) return entry.breakdown;
379
386
  const tools = session.agent?.state?.tools ?? EMPTY_TOOLS;
380
387
  const skillsTokens =
381
- skillful === false ? 0 : estimateSkillsTokens(renderedSkills(session.skills ?? EMPTY_SKILLS, tools), tokenizer);
388
+ skillful === false
389
+ ? 0
390
+ : estimateSkillsTokens(
391
+ renderedSkills(session.renderedSkills ?? session.skills ?? EMPTY_SKILLS, tools),
392
+ tokenizer,
393
+ );
382
394
  const toolsTokens = estimateToolSchemaTokens(tools, tokenizer, sourceRevision);
383
395
  const systemPromptParts = session.systemPrompt ?? EMPTY_STRING_PARTS;
384
396
  const systemContextTokens = tokenizer.countTokens(Array.from(systemPromptParts.slice(1), part => part ?? ""));
@@ -42,7 +42,7 @@ export type TerminalId =
42
42
  | "base"
43
43
  | "trueColor";
44
44
 
45
- const CMUX_NOTIFICATION_TITLE = "Oh My Pi";
45
+ const CMUX_NOTIFICATION_TITLE = "omp";
46
46
  const CMUX_SURFACE_ID_PATTERN = /^[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12}$/iu;
47
47
 
48
48
  /** Title and body for an out-of-band multiplexer notification (cmux, Herdr). */
@@ -1433,7 +1433,7 @@ function notificationToLine(n: TerminalNotification): string {
1433
1433
  // C0/C1 control characters that are unsafe inside an OSC payload (must base64).
1434
1434
  const OSC99_UNSAFE = /[\x00-\x1f\x7f\x80-\x9f]/u;
1435
1435
  const OSC99_MAX_PAYLOAD_BYTES = 2048;
1436
- const OSC99_APP_NAME = "Oh My Pi";
1436
+ const OSC99_APP_NAME = "omp";
1437
1437
  let nextOsc99NotificationId = 1;
1438
1438
 
1439
1439
  function base64Utf8(value: string): string {
package/src/terminal.ts CHANGED
@@ -25,6 +25,7 @@ import {
25
25
  } from "./terminal-capabilities";
26
26
  import { isInsideTmux, wrapTmuxPassthrough } from "./tmux";
27
27
  import { setHangulCompatibilityJamoWidth } from "./utils";
28
+ import { translateWindowsAltGrSequence } from "./windows-altgr";
28
29
 
29
30
  const TERMINAL_PROGRESS_KEEPALIVE_MS = 1000;
30
31
  const TERMINAL_PROGRESS_ACTIVE_SEQUENCE = "\x1b]9;4;3\x07";
@@ -1463,7 +1464,13 @@ export class ProcessTerminal implements Terminal {
1463
1464
  return;
1464
1465
  }
1465
1466
  if (this.#inputHandler) {
1466
- this.#inputHandler(sequence);
1467
+ // Windows console hosts drop AltGr text under kitty (AltGr+F → `CSI 102;3u`);
1468
+ // recover it from the active layout before any keybinding sees an Alt chord.
1469
+ const altGrText =
1470
+ this.#kittyProtocolActive && this.#conpty && process.platform === "win32"
1471
+ ? translateWindowsAltGrSequence(sequence)
1472
+ : undefined;
1473
+ this.#inputHandler(altGrText ?? sequence);
1467
1474
  }
1468
1475
  });
1469
1476