@oh-my-pi/pi-tui 18.13.0 → 18.13.2

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 (46) hide show
  1. package/CHANGELOG.md +36 -32
  2. package/dist/types/chrome/context-map.d.ts +15 -0
  3. package/dist/types/chrome/context-presentation.d.ts +48 -0
  4. package/dist/types/chrome/context-view-model.d.ts +77 -0
  5. package/dist/types/overlays/advisor-config.d.ts +6 -0
  6. package/dist/types/overlays/ask-dialog.d.ts +12 -3
  7. package/dist/types/overlays/context-viewer-format.d.ts +42 -0
  8. package/dist/types/overlays/context-viewer-overview.d.ts +70 -0
  9. package/dist/types/overlays/context-viewer.d.ts +25 -0
  10. package/dist/types/overlays/hook-input.d.ts +1 -0
  11. package/dist/types/overlays/pause-screen.d.ts +13 -3
  12. package/dist/types/overlays/running-subagent-badge.d.ts +7 -1
  13. package/dist/types/status-line/component.d.ts +2 -2
  14. package/dist/types/status-line/context-usage.d.ts +15 -30
  15. package/dist/types/theme/symbols.d.ts +1 -1
  16. package/dist/types/tools/ask.d.ts +4 -0
  17. package/dist/types/tools/fetch.d.ts +1 -0
  18. package/dist/types/tools/github.d.ts +3 -0
  19. package/dist/types/tools/read.d.ts +3 -0
  20. package/dist/types/tools/wait.d.ts +6 -0
  21. package/dist/types/tui.d.ts +2 -0
  22. package/package.json +9 -9
  23. package/src/chrome/context-map.ts +62 -0
  24. package/src/chrome/context-presentation.ts +26 -0
  25. package/src/chrome/context-view-model.ts +73 -0
  26. package/src/components/editor.ts +23 -0
  27. package/src/components/markdown.ts +14 -2
  28. package/src/overlays/advisor-config.ts +9 -2
  29. package/src/overlays/ask-dialog.ts +122 -35
  30. package/src/overlays/context-viewer-format.ts +145 -0
  31. package/src/overlays/context-viewer-overview.ts +381 -0
  32. package/src/overlays/context-viewer.ts +728 -0
  33. package/src/overlays/hook-input.ts +2 -0
  34. package/src/overlays/pause-screen.ts +166 -37
  35. package/src/overlays/running-subagent-badge.ts +12 -1
  36. package/src/render/output-pane.ts +3 -3
  37. package/src/status-line/component.ts +6 -4
  38. package/src/status-line/context-usage.ts +56 -580
  39. package/src/theme/symbols.ts +16 -0
  40. package/src/tools/ask.ts +4 -0
  41. package/src/tools/bash.ts +5 -23
  42. package/src/tools/fetch.ts +1 -0
  43. package/src/tools/github.ts +23 -3
  44. package/src/tools/read.ts +3 -1
  45. package/src/tools/wait.ts +6 -0
  46. package/src/tui.ts +23 -3
@@ -38,6 +38,10 @@ export type SymbolKey =
38
38
  // Context gauge boundaries
39
39
  | "context.speculation"
40
40
  | "context.compaction"
41
+ | "context.content"
42
+ | "context.messages"
43
+ | "context.buffer"
44
+ | "context.free"
41
45
  // Box Drawing - Rounded
42
46
  | "boxRound.topLeft"
43
47
  | "boxRound.topRight"
@@ -419,6 +423,10 @@ const UNICODE_SYMBOLS: SymbolMap = {
419
423
  // Context gauge boundaries
420
424
  "context.speculation": "╎",
421
425
  "context.compaction": "┃",
426
+ "context.content": "⛁",
427
+ "context.messages": "⛃",
428
+ "context.buffer": "⛝",
429
+ "context.free": "⛶",
422
430
  // Box (rounded)
423
431
  "boxRound.topLeft": "╭",
424
432
  "boxRound.topRight": "╮",
@@ -745,6 +753,10 @@ const NERD_SYMBOLS: SymbolMap = {
745
753
  // Context gauge boundaries — vector intersection starts async speculation; auto-fix applies compaction.
746
754
  "context.speculation": "\u{f055d}",
747
755
  "context.compaction": "\u{f0068}",
756
+ "context.content": "⛁",
757
+ "context.messages": "⛃",
758
+ "context.buffer": "⛝",
759
+ "context.free": "⛶",
748
760
  // Box Drawing - Rounded (same as unicode)
749
761
  // pick: ╭ | alt: ┌ ┏ ╔
750
762
  "boxRound.topLeft": "╭",
@@ -1203,6 +1215,10 @@ const ASCII_SYMBOLS: SymbolMap = {
1203
1215
  // Context gauge boundaries
1204
1216
  "context.speculation": ":",
1205
1217
  "context.compaction": "|",
1218
+ "context.content": "#",
1219
+ "context.messages": "@",
1220
+ "context.buffer": "x",
1221
+ "context.free": ".",
1206
1222
  // Box Drawing - Rounded (ASCII fallback)
1207
1223
  "boxRound.topLeft": "+",
1208
1224
  "boxRound.topRight": "+",
package/src/tools/ask.ts CHANGED
@@ -37,6 +37,8 @@ export interface QuestionResult {
37
37
  customInput?: string;
38
38
  /** Optional note attached to the selected answer in the rich ask dialog. */
39
39
  note?: string;
40
+ /** Label of the option the note is on; unset when it is on the custom answer. */
41
+ noteOption?: string;
40
42
  /** True when the answer was auto-selected because the dialog timed out. */
41
43
  timedOut?: boolean;
42
44
  }
@@ -50,6 +52,8 @@ export interface AskToolDetails {
50
52
  customInput?: string;
51
53
  /** Optional note attached to the selected answer in the rich ask dialog. */
52
54
  note?: string;
55
+ /** Label of the option the note is on; unset when it is on the custom answer. */
56
+ noteOption?: string;
53
57
  /** True when the answer was auto-selected because the dialog timed out. */
54
58
  timedOut?: boolean;
55
59
  /** Multi-part question mode */
package/src/tools/bash.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import type { Component } from "../tui";
2
- import { getProjectDir } from "@oh-my-pi/pi-utils";
2
+ import { getProjectDir, jsonContainerEnd } from "@oh-my-pi/pi-utils";
3
3
  import { highlightCode, type Theme } from "../theme/theme";
4
4
  import { renderStatusLine } from "../render/status-line";
5
5
  import { framedToolCard, type ToolCardSnapshot } from "../render/tool-card";
@@ -174,28 +174,10 @@ function extractPartialBashEnv(partialJson: string | undefined): Readonly<Record
174
174
  const objectStart = envStart === -1 ? -1 : partialJson.indexOf("{", envStart);
175
175
  if (objectStart !== -1) {
176
176
  // Scan only the env object: stop at its closing brace (outside strings)
177
- // so the keys that follow it are not read as env assignments.
178
- let objectEnd = partialJson.length;
179
- let depth = 0;
180
- for (let i = objectStart + 1; i < partialJson.length; i++) {
181
- const ch = partialJson.charCodeAt(i);
182
- if (ch === 0x22) {
183
- // Skip the string body, honoring escapes; an unterminated string runs to the end.
184
- for (i++; i < partialJson.length; i++) {
185
- const inner = partialJson.charCodeAt(i);
186
- if (inner === 0x5c) i++;
187
- else if (inner === 0x22) break;
188
- }
189
- } else if (ch === 0x7b || ch === 0x5b) {
190
- depth++;
191
- } else if (ch === 0x7d || ch === 0x5d) {
192
- if (depth === 0) {
193
- objectEnd = i;
194
- break;
195
- }
196
- depth--;
197
- }
198
- }
177
+ // so the keys that follow it are not read as env assignments. An
178
+ // unclosed object (still streaming) runs to the end.
179
+ const end = jsonContainerEnd(partialJson, objectStart);
180
+ const objectEnd = end === -1 ? partialJson.length : end - 1;
199
181
  const envBody = partialJson.slice(objectStart + 1, objectEnd);
200
182
  const matcher = /"([A-Za-z_][A-Za-z0-9_]*)"\s*:\s*"((?:\\.|[^"\\])*)(?:"|$)/gu;
201
183
  for (const match of envBody.matchAll(matcher)) {
@@ -28,6 +28,7 @@ export interface ReadUrlToolDetails {
28
28
  finalUrl: string;
29
29
  contentType: string;
30
30
  method: string;
31
+ raw?: boolean;
31
32
  truncated: boolean;
32
33
  notes: string[];
33
34
  meta?: OutputMeta;
@@ -21,6 +21,7 @@ import {
21
21
  type ToolUIColor,
22
22
  type ToolUIStatus,
23
23
  TRUNCATE_LENGTHS,
24
+ sanitizeDisplayWarning,
24
25
  truncateToWidth as truncateVisualWidth,
25
26
  } from "../render/render-utils";
26
27
 
@@ -236,6 +237,11 @@ function renderRunBlock(run: GhRunWatchRunDetails, width: number, theme: Theme):
236
237
  return lines;
237
238
  }
238
239
 
240
+ function formatUnavailableLog(entry: GhRunWatchFailedLogDetails): string {
241
+ const reason = entry.unavailableReason ? sanitizeDisplayWarning(entry.unavailableReason) : "";
242
+ return `log tail unavailable${reason ? `: ${reason}` : ""}`;
243
+ }
244
+
239
245
  function renderFailedLogs(
240
246
  failedLogs: GhRunWatchFailedLogDetails[],
241
247
  width: number,
@@ -254,7 +260,7 @@ function renderFailedLogs(
254
260
  );
255
261
 
256
262
  if (!entry.available || !entry.tail) {
257
- lines.push(theme.fg("dim", " log tail unavailable"));
263
+ lines.push(theme.fg("dim", truncateVisualWidth(` ${formatUnavailableLog(entry)}`, Math.max(8, width))));
258
264
  continue;
259
265
  }
260
266
 
@@ -463,7 +469,9 @@ function describeWatch(watch: GhRunWatchViewDetails, isError: boolean): NativeTo
463
469
  const failed = (watch.failedLogs ?? []).flatMap((entry): NativeChild[] => {
464
470
  const context = entry.workflowName ? `${entry.workflowName} #${entry.runId}` : `run #${entry.runId}`;
465
471
  const title = text([span(plainText(entry.jobName), "error"), span(` ${plainText(context)}`, "muted")]);
466
- if (!entry.available || !entry.tail) return [title, noteText("log tail unavailable")];
472
+ if (!entry.available || !entry.tail) {
473
+ return [title, noteText(formatUnavailableLog(entry))];
474
+ }
467
475
  return [title, ansi(entry.tail, { follow: true, preview: { lines: PREVIEW_LIMITS.OUTPUT_COLLAPSED } })];
468
476
  });
469
477
  return {
@@ -659,6 +667,7 @@ export interface GhRunWatchFailedLogDetails {
659
667
  conclusion?: string;
660
668
  tail?: string;
661
669
  available: boolean;
670
+ unavailableReason?: string;
662
671
  }
663
672
 
664
673
  /** Live workflow watcher display snapshot. */
@@ -708,6 +717,8 @@ export interface GhFailedJobLog {
708
717
  full?: string;
709
718
  tail?: string;
710
719
  available: boolean;
720
+ unavailableReason?: string;
721
+ truncated?: boolean;
711
722
  }
712
723
 
713
724
  /** Choose the terminal conclusion or current job state. */
@@ -764,12 +775,21 @@ export function renderFailedJobLogs(
764
775
  lines.push("");
765
776
  const logText = options.mode === "full" ? entry.full : entry.tail;
766
777
  if (entry.available && logText) {
767
- lines.push(options.mode === "full" ? "Full log:" : `Last ${options.tail} log lines:`);
778
+ lines.push(
779
+ options.mode === "full"
780
+ ? entry.truncated
781
+ ? "Partial log (capture truncated):"
782
+ : "Full log:"
783
+ : entry.truncated
784
+ ? `Last ${options.tail} captured log lines (log capture truncated):`
785
+ : `Last ${options.tail} log lines:`,
786
+ );
768
787
  lines.push("```text");
769
788
  lines.push(logText);
770
789
  lines.push("```");
771
790
  } else {
772
791
  lines.push(options.mode === "full" ? "Full log unavailable." : "Log tail unavailable.");
792
+ if (entry.unavailableReason) pushLine(lines, "Reason", entry.unavailableReason);
773
793
  }
774
794
  lines.push("");
775
795
  }
package/src/tools/read.ts CHANGED
@@ -62,7 +62,9 @@ export interface ReadToolDetails {
62
62
  * screenshots, URL reads and other images not backed by a file. Display-only.
63
63
  */
64
64
  mediaPath?: string;
65
- suffixResolution?: { from: string; to: string };
65
+ suffixResolution?: { from: string; to: string; raw?: boolean };
66
+ /** Raw source output keeps descriptive diagnostics in details, not in the body. */
67
+ raw?: boolean;
66
68
  url?: string;
67
69
  finalUrl?: string;
68
70
  contentType?: string;
package/src/tools/wait.ts CHANGED
@@ -89,6 +89,12 @@ export interface JobSnapshot {
89
89
  * on collision, else the job id itself. See {@link AsyncJob.agentId}.
90
90
  */
91
91
  agentUrlId?: string;
92
+ /**
93
+ * Whether this job's turn published `<agentUrlId>.md`. False where the job
94
+ * body reported that it wrote none, so `agent://` still holds an earlier
95
+ * turn's output; undefined where the body does not report it.
96
+ */
97
+ agentOutputPublished?: boolean;
92
98
  }
93
99
 
94
100
  /** Outcome of cancelling one background job. */
package/src/tui.ts CHANGED
@@ -926,6 +926,7 @@ export class TUI extends Container {
926
926
  /** True from the end of start() until stop(): the terminal modes start listeners enable are live. */
927
927
  #started = false;
928
928
  #paintListeners = new Set<PaintListener>();
929
+ #frameWriteListeners = new Set<(data: string) => void>();
929
930
 
930
931
  /** Global callback for debug key (Shift+Ctrl+D). Called before input is forwarded to focused component. */
931
932
  onDebug?: () => void;
@@ -1142,6 +1143,14 @@ export class TUI extends Container {
1142
1143
  };
1143
1144
  }
1144
1145
 
1146
+ /** Observe actual ANSI frame writes, including transient resize paints; returns the unsubscribe. */
1147
+ addFrameWriteListener(listener: (data: string) => void): () => void {
1148
+ this.#frameWriteListeners.add(listener);
1149
+ return () => {
1150
+ this.#frameWriteListeners.delete(listener);
1151
+ };
1152
+ }
1153
+
1145
1154
  /** Install the product-owned bounded frame provider. */
1146
1155
  setFrameProvider(provider: TerminalFrameProvider | undefined): void {
1147
1156
  this.#frameProvider = provider;
@@ -3414,6 +3423,16 @@ export class TUI extends Container {
3414
3423
  }
3415
3424
  }
3416
3425
 
3426
+ #notifyFrameWrite(data: string): void {
3427
+ for (const listener of this.#frameWriteListeners) {
3428
+ try {
3429
+ listener(data);
3430
+ } catch (err) {
3431
+ logger.error("TUI frame write listener failed", { err });
3432
+ }
3433
+ }
3434
+ }
3435
+
3417
3436
  #renderProviderFrame(width: number, height: number): void {
3418
3437
  const provider = this.#frameProvider;
3419
3438
  if (!provider || width <= 0 || height <= 0) return;
@@ -3777,6 +3796,7 @@ export class TUI extends Container {
3777
3796
  this.terminal.write(this.#rebuildNoticeSequence(width, height));
3778
3797
  }
3779
3798
  this.terminal.write(buffer);
3799
+ this.#notifyFrameWrite(buffer);
3780
3800
  this.#debugPaint = {
3781
3801
  lines: prepared.lines,
3782
3802
  windowTop: this.#debugNextWindowTop,
@@ -4580,9 +4600,9 @@ export class TUI extends Container {
4580
4600
  } else if (hideCursor) {
4581
4601
  cursorBuffer = "\x1b[?25l";
4582
4602
  }
4583
- this.terminal.write(
4584
- `${this.#paintBeginSequence}${prelude}${full ? "\x1b[H" : ""}${rowsBuffer}${cursorBuffer}${this.#paintEndSequence}`,
4585
- );
4603
+ const frame = `${this.#paintBeginSequence}${prelude}${full ? "\x1b[H" : ""}${rowsBuffer}${cursorBuffer}${this.#paintEndSequence}`;
4604
+ this.terminal.write(frame);
4605
+ this.#notifyFrameWrite(frame);
4586
4606
  this.#debugPaint = {
4587
4607
  lines: prepared.lines,
4588
4608
  windowTop: 0,