@linxiraos/pi-tui 1.3.2 → 1.4.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 (116) hide show
  1. package/CHANGELOG.md +181 -181
  2. package/dist/types/apps/session-picker.d.ts +13 -1
  3. package/dist/types/autocomplete.d.ts +6 -0
  4. package/dist/types/components/sparkline.d.ts +34 -0
  5. package/dist/types/index.d.ts +1 -0
  6. package/dist/types/native/describe.d.ts +7 -1
  7. package/dist/types/native/overlay.d.ts +2 -0
  8. package/dist/types/native/tone.d.ts +2 -0
  9. package/dist/types/overlays/agent-hub.d.ts +3 -3
  10. package/dist/types/overlays/approval-dialog.d.ts +15 -1
  11. package/dist/types/overlays/jobs-panel.d.ts +3 -0
  12. package/dist/types/overlays/model-browser.d.ts +1 -1
  13. package/dist/types/overlays/resume-picker.d.ts +293 -0
  14. package/dist/types/overlays/resume-tree.d.ts +353 -0
  15. package/dist/types/overlays/session-selector.d.ts +22 -4
  16. package/dist/types/render/output-block.d.ts +2 -0
  17. package/dist/types/render/tool-card.d.ts +2 -0
  18. package/dist/types/sixel.d.ts +28 -0
  19. package/dist/types/status-line/host.d.ts +3 -0
  20. package/dist/types/terminal-capabilities.d.ts +6 -1
  21. package/dist/types/tools/bash.d.ts +2 -0
  22. package/dist/types/tools/default-renderer.d.ts +3 -0
  23. package/dist/types/tools/eval.d.ts +9 -8
  24. package/dist/types/tools/hub.d.ts +3 -1
  25. package/dist/types/tools/irc.d.ts +11 -1
  26. package/dist/types/tools/job-widget.d.ts +226 -0
  27. package/dist/types/tools/json-tree.d.ts +2 -0
  28. package/dist/types/tools/line-ranges.d.ts +8 -1
  29. package/dist/types/tools/mcp.d.ts +23 -0
  30. package/dist/types/tools/output-meta.d.ts +1 -1
  31. package/dist/types/tools/read.d.ts +8 -2
  32. package/dist/types/tools/renderer.d.ts +8 -2
  33. package/dist/types/tools/result-card.d.ts +1 -1
  34. package/dist/types/tools/streaming-output.d.ts +1 -0
  35. package/dist/types/tools/task.d.ts +12 -0
  36. package/dist/types/tools/wait.d.ts +11 -0
  37. package/package.json +9 -9
  38. package/src/apps/session-picker.ts +20 -2
  39. package/src/autocomplete.d.ts +6 -0
  40. package/src/autocomplete.ts +21 -6
  41. package/src/chat/skill-message.ts +16 -17
  42. package/src/chat/tool-execution.ts +35 -0
  43. package/src/chrome/transcript-container.ts +85 -13
  44. package/src/components/editor.ts +17 -7
  45. package/src/components/image.ts +5 -4
  46. package/src/components/markdown.ts +11 -3
  47. package/src/components/sparkline.d.ts +34 -0
  48. package/src/components/sparkline.ts +89 -0
  49. package/src/index.d.ts +1 -0
  50. package/src/index.ts +1 -0
  51. package/src/native/backend.ts +32 -6
  52. package/src/native/describe.d.ts +7 -1
  53. package/src/native/describe.ts +18 -1
  54. package/src/native/overlay.d.ts +2 -0
  55. package/src/native/overlay.ts +3 -0
  56. package/src/native/tone.d.ts +2 -0
  57. package/src/native/tone.ts +22 -0
  58. package/src/overlays/agent-hub.ts +14 -10
  59. package/src/overlays/agent-transcript-viewer.ts +2 -2
  60. package/src/overlays/agents-hub.ts +2 -1
  61. package/src/overlays/approval-dialog.ts +59 -13
  62. package/src/overlays/hub-frame.ts +2 -1
  63. package/src/overlays/jobs-panel.ts +24 -2
  64. package/src/overlays/model-browser.ts +3 -0
  65. package/src/overlays/model-hub.ts +2 -1
  66. package/src/overlays/resume-picker.ts +706 -0
  67. package/src/overlays/resume-tree.ts +1901 -0
  68. package/src/overlays/session-selector.ts +579 -39
  69. package/src/prompt/attachment-chips.ts +1 -1
  70. package/src/render/output-block.d.ts +2 -0
  71. package/src/render/output-block.ts +25 -12
  72. package/src/render/tool-card.d.ts +2 -0
  73. package/src/render/tool-card.ts +6 -0
  74. package/src/sixel.d.ts +28 -0
  75. package/src/sixel.ts +185 -0
  76. package/src/status-line/component.ts +19 -2
  77. package/src/status-line/host.ts +3 -0
  78. package/src/terminal-capabilities.d.ts +6 -1
  79. package/src/terminal-capabilities.ts +54 -3
  80. package/src/theme/symbols.ts +5 -3
  81. package/src/tools/bash.d.ts +2 -0
  82. package/src/tools/bash.ts +25 -9
  83. package/src/tools/default-renderer.d.ts +3 -0
  84. package/src/tools/default-renderer.ts +54 -17
  85. package/src/tools/eval.d.ts +9 -8
  86. package/src/tools/eval.ts +32 -14
  87. package/src/tools/find.ts +4 -5
  88. package/src/tools/glob.ts +4 -6
  89. package/src/tools/grep.ts +2 -4
  90. package/src/tools/hub.ts +4 -1
  91. package/src/tools/irc.d.ts +11 -1
  92. package/src/tools/irc.ts +11 -1
  93. package/src/tools/job-widget.d.ts +226 -0
  94. package/src/tools/job-widget.ts +1036 -0
  95. package/src/tools/json-tree.d.ts +2 -0
  96. package/src/tools/json-tree.ts +8 -0
  97. package/src/tools/line-ranges.d.ts +8 -1
  98. package/src/tools/line-ranges.ts +23 -13
  99. package/src/tools/mcp.d.ts +23 -0
  100. package/src/tools/mcp.ts +44 -1
  101. package/src/tools/output-meta.d.ts +1 -1
  102. package/src/tools/output-meta.ts +1 -1
  103. package/src/tools/proc-render.ts +1 -0
  104. package/src/tools/read.d.ts +8 -2
  105. package/src/tools/read.ts +64 -21
  106. package/src/tools/renderer.d.ts +8 -2
  107. package/src/tools/renderer.ts +8 -2
  108. package/src/tools/result-card.d.ts +1 -1
  109. package/src/tools/result-card.ts +1 -2
  110. package/src/tools/streaming-output.d.ts +1 -0
  111. package/src/tools/streaming-output.ts +11 -9
  112. package/src/tools/task.d.ts +12 -0
  113. package/src/tools/task.ts +25 -1
  114. package/src/tools/wait.d.ts +11 -0
  115. package/src/tools/wait.ts +296 -147
  116. package/src/tui.ts +45 -5
package/src/tools/bash.ts CHANGED
@@ -7,6 +7,7 @@ import { formatOutputPaneLines } from "../render/output-pane";
7
7
  import {
8
8
  capPreviewLines,
9
9
  DEFAULT_TERMINAL_PREVIEW_LINES,
10
+ formatDuration,
10
11
  formatToolWorkingDirectory,
11
12
  previewWindowRows,
12
13
  replaceTabs,
@@ -470,9 +471,14 @@ export function createShellRenderer<TArgs>(config: ShellRendererConfig<TArgs>) {
470
471
  },
471
472
  uiTheme,
472
473
  );
474
+ const elapsedMs = options.executionStarted ? options.elapsedMs : undefined;
473
475
  return {
474
476
  header,
475
477
  phase: options.spinnerFrame !== undefined ? "running" : "pending",
478
+ footerMeta:
479
+ elapsedMs === undefined || elapsedMs < 1000
480
+ ? undefined
481
+ : uiTheme.fg("dim", `elapsed ${formatDuration(Math.floor(elapsedMs / 1000) * 1000)}`),
476
482
  sections: [{ content: capPreviewLines(cmdLines, uiTheme, { expanded: options.expanded }) }],
477
483
  };
478
484
  });
@@ -528,6 +534,7 @@ export function createShellRenderer<TArgs>(config: ShellRendererConfig<TArgs>) {
528
534
  let cachedIsPartial: boolean | undefined;
529
535
  let cachedPreviewWindow: number | undefined;
530
536
  let cachedSnapshot: ToolCardSnapshot | undefined;
537
+ let cachedElapsedSecond: number | undefined;
531
538
  const dropSnapshot = () => {
532
539
  cachedSnapshot = undefined;
533
540
  cachedWidth = undefined;
@@ -536,6 +543,7 @@ export function createShellRenderer<TArgs>(config: ShellRendererConfig<TArgs>) {
536
543
  cachedRawOutput = undefined;
537
544
  cachedIsPartial = undefined;
538
545
  cachedPreviewWindow = undefined;
546
+ cachedElapsedSecond = undefined;
539
547
  };
540
548
 
541
549
  return framedToolCard(
@@ -553,6 +561,11 @@ export function createShellRenderer<TArgs>(config: ShellRendererConfig<TArgs>) {
553
561
 
554
562
  const isPartial = options.isPartial === true;
555
563
  const previewWindow = previewWindowRows();
564
+ // A running command shows its live elapsed time in the bottom
565
+ // border; key the cache on whole seconds so it advances once a
566
+ // second instead of rebuilding every spinner frame.
567
+ const liveElapsedMs = isPartial && details?.wallTimeMs === undefined ? options.elapsedMs : undefined;
568
+ const elapsedSecond = liveElapsedMs === undefined ? undefined : Math.floor(liveElapsedMs / 1000);
556
569
 
557
570
  if (
558
571
  cachedSnapshot !== undefined &&
@@ -561,7 +574,8 @@ export function createShellRenderer<TArgs>(config: ShellRendererConfig<TArgs>) {
561
574
  cachedExpanded === expanded &&
562
575
  cachedRawOutput === rawOutput &&
563
576
  cachedIsPartial === isPartial &&
564
- cachedPreviewWindow === previewWindow
577
+ cachedPreviewWindow === previewWindow &&
578
+ cachedElapsedSecond === elapsedSecond
565
579
  ) {
566
580
  return cachedSnapshot;
567
581
  }
@@ -576,13 +590,11 @@ export function createShellRenderer<TArgs>(config: ShellRendererConfig<TArgs>) {
576
590
  rawOutputArtifact.artifactId,
577
591
  isError,
578
592
  );
579
- const timeoutLine =
580
- statsParts.length > 0
581
- ? uiTheme.fg(
582
- "dim",
583
- `${uiTheme.format.bracketLeft}${statsParts.join(" | ")}${uiTheme.format.bracketRight}`,
584
- )
585
- : undefined;
593
+ if (elapsedSecond !== undefined && elapsedSecond > 0) {
594
+ statsParts.unshift(`elapsed ${formatDuration(elapsedSecond * 1000)}`);
595
+ }
596
+ const footerMeta =
597
+ statsParts.length > 0 ? uiTheme.fg("dim", statsParts.join(uiTheme.sep.dot)) : undefined;
586
598
  let warningLine: string | undefined;
587
599
  if (details?.meta?.artifactError || (details?.meta?.truncation && !showingFullOutput)) {
588
600
  warningLine = formatStyledTruncationWarning(details.meta, uiTheme) ?? undefined;
@@ -610,12 +622,12 @@ export function createShellRenderer<TArgs>(config: ShellRendererConfig<TArgs>) {
610
622
  uiTheme,
611
623
  );
612
624
  const outputLines: string[] = [...formatted.lines];
613
- if (timeoutLine) outputLines.push(timeoutLine);
614
625
  if (warningLine) outputLines.push(warningLine);
615
626
 
616
627
  const snapshot: ToolCardSnapshot = {
617
628
  header,
618
629
  phase: isPartial ? "partial" : isError ? (isTimeout ? "warning" : "error") : "success",
630
+ footerMeta,
619
631
  sections: [
620
632
  {
621
633
  // Viewport-sized tail window in every state — streaming and final
@@ -636,6 +648,7 @@ export function createShellRenderer<TArgs>(config: ShellRendererConfig<TArgs>) {
636
648
  cachedRawOutput = rawOutput;
637
649
  cachedIsPartial = isPartial;
638
650
  cachedPreviewWindow = previewWindow;
651
+ cachedElapsedSecond = elapsedSecond;
639
652
  cachedSnapshot = snapshot;
640
653
  return snapshot;
641
654
  },
@@ -714,6 +727,9 @@ export function createShellRenderer<TArgs>(config: ShellRendererConfig<TArgs>) {
714
727
  },
715
728
  mergeCallAndResult: true,
716
729
  inline: true,
730
+ // The live elapsed footer reads `options.elapsedMs` at render time; a
731
+ // once-a-second repaint keeps it moving for a quiet running command.
732
+ tickElapsed: true,
717
733
  } satisfies ToolRenderer<TArgs, BashToolDetails>;
718
734
  }
719
735
 
@@ -23,6 +23,9 @@ export declare function formatDefaultToolExecution(input: DefaultToolRenderInput
23
23
  /**
24
24
  * TSP view of the generic fallback card: the tool label with a one-line args
25
25
  * summary as the head target; the result as a JSON tree or raw `ansi` output.
26
+ * The complete output goes over the wire — the card-level disclosure clamp the
27
+ * terminal applies is the only bound, so an expanded card keeps every row. JSON
28
+ * the tree's depth, node or scalar caps would cut keeps the raw path instead.
26
29
  */
27
30
  export declare function describeDefaultToolExecution(input: DefaultToolRenderInput): NativeToolView;
28
31
  /** Render the generic fallback as the state-tinted card used by direct custom tools. */
@@ -104,21 +104,22 @@ function buildDefaultToolBody(input: DefaultToolRenderInput, uiTheme: Theme, con
104
104
  body.push(uiTheme.fg("dim", "(no output)"));
105
105
  } else {
106
106
  let renderedAsJson = false;
107
- if (textContent.startsWith("{") || textContent.startsWith("[")) {
107
+ if (isTreeSafeJson(textContent)) {
108
108
  try {
109
109
  const parsed = JSON.parse(textContent);
110
- const maxDepth = options.expanded ? JSON_TREE_MAX_DEPTH_EXPANDED : JSON_TREE_MAX_DEPTH_COLLAPSED;
111
- const maxLines = options.expanded ? JSON_TREE_MAX_LINES_EXPANDED : JSON_TREE_MAX_LINES_COLLAPSED;
112
- const maxScalarLen = options.expanded ? JSON_TREE_SCALAR_LEN_EXPANDED : JSON_TREE_SCALAR_LEN_COLLAPSED;
113
- const tree = renderJsonTreeLines(parsed, uiTheme, maxDepth, maxLines, maxScalarLen);
110
+ const tree = renderJsonTreeLines(parsed, uiTheme, {
111
+ maxDepth: options.expanded ? JSON_TREE_MAX_DEPTH_EXPANDED : JSON_TREE_MAX_DEPTH_COLLAPSED,
112
+ maxLines: options.expanded ? JSON_TREE_MAX_LINES_EXPANDED : JSON_TREE_MAX_LINES_COLLAPSED,
113
+ maxScalarLen: options.expanded ? JSON_TREE_SCALAR_LEN_EXPANDED : JSON_TREE_SCALAR_LEN_COLLAPSED,
114
+ // A result's own `i` or `__partialJson` keys are data, not call metadata.
115
+ hiddenRootKeys: [],
116
+ });
114
117
 
115
- if (tree.lines.length > 0) {
118
+ // An expanded tree that hit a cap would show less than the raw output, so
119
+ // Ctrl+O falls through to the uncapped raw view instead.
120
+ if (tree.lines.length > 0 && !(options.expanded && tree.truncated)) {
116
121
  body.push(...tree.lines);
117
- if (!options.expanded) {
118
- body.push(formatExpandHint(uiTheme, options.expanded, true));
119
- } else if (tree.truncated) {
120
- body.push(uiTheme.fg("dim", "…"));
121
- }
122
+ if (!options.expanded) body.push(formatExpandHint(uiTheme, options.expanded, true));
122
123
  renderedAsJson = true;
123
124
  }
124
125
  } catch {
@@ -126,13 +127,14 @@ function buildDefaultToolBody(input: DefaultToolRenderInput, uiTheme: Theme, con
126
127
  }
127
128
  }
128
129
  if (!renderedAsJson) {
130
+ // No expanded cap: the terminal's own disclosure clamp is the only
131
+ // bound, so Ctrl+O reveals every row the collapsed view promised.
129
132
  body.push(
130
133
  ...formatOutputPaneLines(
131
134
  {
132
135
  lines: textContent.split("\n"),
133
136
  expanded: options.expanded,
134
137
  collapsedMaxLines: 4,
135
- expandedMaxLines: 12,
136
138
  styleLine: line =>
137
139
  clipCollapsed(styleToolOutputLine(line, uiTheme), contentWidth, options.expanded),
138
140
  showExpandHintWhenUncapped: true,
@@ -157,14 +159,37 @@ export function formatDefaultToolExecution(
157
159
  return [renderStatusLine(status, uiTheme), ...buildDefaultToolBody(input, uiTheme, contentWidth)].join("\n");
158
160
  }
159
161
 
162
+ /**
163
+ * Serialized JSON past this size keeps the raw view: the tree caps out at a few
164
+ * hundred rows anyway, and parsing and indexing a huge payload only to discard
165
+ * the tree can stall the UI.
166
+ */
167
+ const JSON_TREE_MAX_CHARS = 64 * 1024;
168
+ /** An integer literal of 16+ digits may exceed `Number.MAX_SAFE_INTEGER` and change when parsed. */
169
+ const UNSAFE_INTEGER_RE = /(?<![\w.])-?\d{16,}(?![\w.])/;
170
+
171
+ /**
172
+ * Whether a tool result can render as a JSON tree without losing anything the
173
+ * raw text shows: a bracketed payload small enough to index, with no integer a
174
+ * double would round (64-bit ids).
175
+ */
176
+ function isTreeSafeJson(text: string): boolean {
177
+ return (
178
+ (text.startsWith("{") || text.startsWith("[")) &&
179
+ text.length <= JSON_TREE_MAX_CHARS &&
180
+ !UNSAFE_INTEGER_RE.test(text)
181
+ );
182
+ }
183
+
160
184
  /** Inline args summary budget in characters (a data cap, not a width). */
161
185
  const NATIVE_ARGS_SUMMARY_CHARS = 160;
162
- /** Result text kept for the native card body. */
163
- const NATIVE_RESULT_MAX_CHARS = 64 * 1024;
164
186
 
165
187
  /**
166
188
  * TSP view of the generic fallback card: the tool label with a one-line args
167
189
  * summary as the head target; the result as a JSON tree or raw `ansi` output.
190
+ * The complete output goes over the wire — the card-level disclosure clamp the
191
+ * terminal applies is the only bound, so an expanded card keeps every row. JSON
192
+ * the tree's depth, node or scalar caps would cut keeps the raw path instead.
168
193
  */
169
194
  export function describeDefaultToolExecution(input: DefaultToolRenderInput): NativeToolView {
170
195
  const { result } = input;
@@ -175,15 +200,27 @@ export function describeDefaultToolExecution(input: DefaultToolRenderInput): Nat
175
200
  : undefined;
176
201
  const tool: NativeToolHead = { title: input.label, target: summary || undefined, targetKind: "text" };
177
202
  if (!result) return { tool };
178
- const output = result.output.trimEnd().slice(0, NATIVE_RESULT_MAX_CHARS);
203
+ const output = result.output.trimEnd();
179
204
  const tone = result.skipped ? "info" : result.isError ? "error" : undefined;
180
205
  if (!output) return { tool, tone, body: [noteText("(no output)")] };
181
- if (output.startsWith("{") || output.startsWith("[")) {
206
+ if (isTreeSafeJson(output)) {
207
+ let parsed: unknown;
182
208
  try {
183
- return { tool, tone, body: [describeJsonTree(JSON.parse(output))] };
209
+ parsed = JSON.parse(output);
184
210
  } catch {
185
211
  // Not JSON: shown as raw output below.
186
212
  }
213
+ if (parsed !== undefined) {
214
+ let truncated = false;
215
+ const tree = describeJsonTree(parsed, {
216
+ // A result's own `i` or `__partialJson` keys are data, not call metadata.
217
+ hiddenRootKeys: [],
218
+ onTruncated: () => {
219
+ truncated = true;
220
+ },
221
+ });
222
+ if (!truncated) return { tool, tone, body: [tree] };
223
+ }
187
224
  }
188
225
  return { tool, tone, body: [ansi(output)] };
189
226
  }
@@ -61,7 +61,7 @@ export interface EvalCellResult {
61
61
  durationMs?: number;
62
62
  exitCode?: number;
63
63
  statusEvents?: EvalStatusEvent[];
64
- /** Discrete events dropped from the front of `statusEvents` (see {@link recordStatusEvent}). */
64
+ /** Evictable events dropped from the front of `statusEvents` (see {@link recordStatusEvent}). */
65
65
  statusEventsElided?: number;
66
66
  }
67
67
  /** Tool result detail object surfaced to the UI/transcript. */
@@ -69,7 +69,7 @@ export interface EvalToolDetails {
69
69
  cells?: EvalCellResult[];
70
70
  images?: ImageContent[];
71
71
  statusEvents?: EvalStatusEvent[];
72
- /** Discrete events dropped from the front of `statusEvents` (see {@link recordStatusEvent}). */
72
+ /** Evictable events dropped from the front of `statusEvents` (see {@link recordStatusEvent}). */
73
73
  statusEventsElided?: number;
74
74
  isError?: boolean;
75
75
  meta?: OutputMeta;
@@ -104,7 +104,7 @@ interface EvalRenderContext {
104
104
  timeout?: number;
105
105
  }
106
106
  /**
107
- * Coalescing key of a progress-snapshot event: `agent` and `judge_batch`
107
+ * Coalescing key of a progress-snapshot event: `agent`, `judge_batch`, and `mcp_progress`
108
108
  * events keyed by `id`, where only the newest snapshot matters. Discrete
109
109
  * actions (everything else) have no key. Shared by {@link upsertStatusEvent}
110
110
  * and the executors' display collectors so both coalesce identically.
@@ -114,12 +114,12 @@ export declare function statusEventKey(event: {
114
114
  [key: string]: unknown;
115
115
  }): string | undefined;
116
116
  /**
117
- * Discrete status events a log keeps. A cell polling a helper in a loop emits
117
+ * Evictable status events a log keeps. A cell polling a helper in a loop emits
118
118
  * one event per call (hundreds of thousands), and every live update and the
119
119
  * persisted tool result would otherwise carry them all.
120
120
  */
121
121
  export declare const MAX_STATUS_EVENTS = 200;
122
- /** A status event list plus the count of discrete events dropped from its front. */
122
+ /** A status event list plus the count of evictable events dropped from its front. */
123
123
  export type StatusEventLog = Pick<EvalCellResult, "statusEvents" | "statusEventsElided">;
124
124
  /**
125
125
  * Append or replace a status event. Progress snapshots (see
@@ -129,9 +129,10 @@ export type StatusEventLog = Pick<EvalCellResult, "statusEvents" | "statusEvents
129
129
  export declare function upsertStatusEvent(events: EvalStatusEvent[], event: EvalStatusEvent): void;
130
130
  /**
131
131
  * {@link upsertStatusEvent} into a bounded log. Past {@link MAX_STATUS_EVENTS},
132
- * the oldest discrete event is dropped and counted. Snapshots stay, since agent
133
- * cards read them, and so does the newest committed `todo` result, which the
134
- * session todo panel refreshes from. Renderers already show only the newest
132
+ * the oldest discrete event or MCP progress snapshot is dropped and counted.
133
+ * Agent/judgment-batch snapshots stay, since agent cards read them, and so does
134
+ * the newest committed `todo` result, which the session todo panel refreshes
135
+ * from. Renderers already show only the newest
135
136
  * events behind an "… N earlier" row.
136
137
  */
137
138
  export declare function recordStatusEvent(log: StatusEventLog, event: EvalStatusEvent): void;
package/src/tools/eval.ts CHANGED
@@ -35,6 +35,7 @@ import {
35
35
  renderJsonTreeLines,
36
36
  } from "./json-tree";
37
37
  import { formatStyledTruncationWarning, stripOutputNotice } from "./output-meta";
38
+ import { formatMCPProgress, parseMCPProgress } from "./mcp";
38
39
  import { splitPathAndSel } from "./read";
39
40
  import {
40
41
  DEFAULT_TERMINAL_PREVIEW_LINES,
@@ -108,7 +109,7 @@ export interface EvalCellResult {
108
109
  durationMs?: number;
109
110
  exitCode?: number;
110
111
  statusEvents?: EvalStatusEvent[];
111
- /** Discrete events dropped from the front of `statusEvents` (see {@link recordStatusEvent}). */
112
+ /** Evictable events dropped from the front of `statusEvents` (see {@link recordStatusEvent}). */
112
113
  statusEventsElided?: number;
113
114
  }
114
115
 
@@ -117,7 +118,7 @@ export interface EvalToolDetails {
117
118
  cells?: EvalCellResult[];
118
119
  images?: ImageContent[];
119
120
  statusEvents?: EvalStatusEvent[];
120
- /** Discrete events dropped from the front of `statusEvents` (see {@link recordStatusEvent}). */
121
+ /** Evictable events dropped from the front of `statusEvents` (see {@link recordStatusEvent}). */
121
122
  statusEventsElided?: number;
122
123
  isError?: boolean;
123
124
  meta?: OutputMeta;
@@ -222,26 +223,29 @@ function pushCodeCellLines(
222
223
  type AgentEventStatus = "pending" | "running" | "completed" | "failed" | "aborted";
223
224
 
224
225
  /**
225
- * Coalescing key of a progress-snapshot event: `agent` and `judge_batch`
226
+ * Coalescing key of a progress-snapshot event: `agent`, `judge_batch`, and `mcp_progress`
226
227
  * events keyed by `id`, where only the newest snapshot matters. Discrete
227
228
  * actions (everything else) have no key. Shared by {@link upsertStatusEvent}
228
229
  * and the executors' display collectors so both coalesce identically.
229
230
  */
230
231
  export function statusEventKey(event: { op: string; [key: string]: unknown }): string | undefined {
231
- if ((event.op === "agent" || event.op === "judge_batch") && typeof event.id === "string") {
232
+ if (
233
+ (event.op === "agent" || event.op === "judge_batch" || event.op === "mcp_progress") &&
234
+ typeof event.id === "string"
235
+ ) {
232
236
  return `${event.op}\0${event.id}`;
233
237
  }
234
238
  return undefined;
235
239
  }
236
240
 
237
241
  /**
238
- * Discrete status events a log keeps. A cell polling a helper in a loop emits
242
+ * Evictable status events a log keeps. A cell polling a helper in a loop emits
239
243
  * one event per call (hundreds of thousands), and every live update and the
240
244
  * persisted tool result would otherwise carry them all.
241
245
  */
242
246
  export const MAX_STATUS_EVENTS = 200;
243
247
 
244
- /** A status event list plus the count of discrete events dropped from its front. */
248
+ /** A status event list plus the count of evictable events dropped from its front. */
245
249
  export type StatusEventLog = Pick<EvalCellResult, "statusEvents" | "statusEventsElided">;
246
250
 
247
251
  /**
@@ -263,9 +267,10 @@ export function upsertStatusEvent(events: EvalStatusEvent[], event: EvalStatusEv
263
267
 
264
268
  /**
265
269
  * {@link upsertStatusEvent} into a bounded log. Past {@link MAX_STATUS_EVENTS},
266
- * the oldest discrete event is dropped and counted. Snapshots stay, since agent
267
- * cards read them, and so does the newest committed `todo` result, which the
268
- * session todo panel refreshes from. Renderers already show only the newest
270
+ * the oldest discrete event or MCP progress snapshot is dropped and counted.
271
+ * Agent/judgment-batch snapshots stay, since agent cards read them, and so does
272
+ * the newest committed `todo` result, which the session todo panel refreshes
273
+ * from. Renderers already show only the newest
269
274
  * events behind an "… N earlier" row.
270
275
  */
271
276
  export function recordStatusEvent(log: StatusEventLog, event: EvalStatusEvent): void {
@@ -273,7 +278,9 @@ export function recordStatusEvent(log: StatusEventLog, event: EvalStatusEvent):
273
278
  upsertStatusEvent(events, event);
274
279
  if (events.length <= MAX_STATUS_EVENTS) return;
275
280
  const keep = events.findLastIndex(e => e.op === "todo" && e.committed === true);
276
- const oldest = events.findIndex((e, i) => i !== keep && statusEventKey(e) === undefined);
281
+ const oldest = events.findIndex(
282
+ (e, i) => i !== keep && (e.op === "mcp_progress" || statusEventKey(e) === undefined),
283
+ );
277
284
  if (oldest < 0) return;
278
285
  events.splice(oldest, 1);
279
286
  log.statusEventsElided = (log.statusEventsElided ?? 0) + 1;
@@ -414,6 +421,12 @@ function formatStatusEvent(event: EvalStatusEvent, theme: Theme): string {
414
421
  }
415
422
 
416
423
  switch (op) {
424
+ case "mcp_progress": {
425
+ if (data.tool) parts.push(previewLine(sanitizeText(String(data.tool)), STATUS_VALUE_WIDTH));
426
+ const progress = parseMCPProgress(data);
427
+ if (progress) parts.push(formatMCPProgress(progress, STATUS_VALUE_WIDTH));
428
+ break;
429
+ }
417
430
  case "read":
418
431
  parts.push(`${data.chars ?? data.bytes ?? 0} chars`);
419
432
  if (data.path) parts.push(`from ${shortenPath(String(data.path))}`);
@@ -693,20 +706,25 @@ const STATUS_VALUE_WIDTH = 200;
693
706
 
694
707
  /**
695
708
  * Plain one-line summary of a status event: op plus its scalar fields, each
696
- * collapsed to one line. A `preview` (file or command text a helper returned)
697
- * never joins the line: an expanded cell shows it under the line as a code
698
- * block holding its first {@link PREVIEW_LIMITS.OUTPUT_EXPANDED} lines, folded
699
- * to {@link PREVIEW_LIMITS.COLLAPSED_LINES}.
709
+ * collapsed to one line. MCP progress shows its tool and then the shared
710
+ * {@link formatMCPProgress} text; its call id is internal. A `preview` (file or
711
+ * command text a helper returned) never joins the line: an expanded cell shows
712
+ * it under the line as a code block holding its first
713
+ * {@link PREVIEW_LIMITS.OUTPUT_EXPANDED} lines, folded to
714
+ * {@link PREVIEW_LIMITS.COLLAPSED_LINES}.
700
715
  */
701
716
  function describeStatusEvent(event: EvalStatusEvent, expanded: boolean): NativeNode {
702
717
  const spans: TspSpan[] = [span(event.op, "accent")];
718
+ const mcpProgress = event.op === "mcp_progress" ? parseMCPProgress(event) : undefined;
703
719
  for (const key in event) {
704
720
  if (key === "op" || key === "preview" || key === "resolvedThinkingLevel") continue;
721
+ if (event.op === "mcp_progress" && key !== "tool") continue;
705
722
  const value = event[key];
706
723
  if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
707
724
  spans.push(span(` ${key}=`, "muted"), span(previewLine(sanitizeText(String(value)), STATUS_VALUE_WIDTH)));
708
725
  }
709
726
  }
727
+ if (mcpProgress) spans.push(span(` ${formatMCPProgress(mcpProgress, STATUS_VALUE_WIDTH)}`));
710
728
  const line = text(spans, { truncate: "end", role: "zeta.tool.eval.status" });
711
729
  const preview = expanded ? eventString(event.preview)?.trimEnd() : undefined;
712
730
  if (!preview) return line;
package/src/tools/find.ts CHANGED
@@ -256,7 +256,7 @@ export const findToolRenderer = {
256
256
  },
257
257
  uiTheme,
258
258
  );
259
- return new Text(text, 1, 0);
259
+ return new Text(text, 0, 0);
260
260
  },
261
261
 
262
262
  renderResult(
@@ -269,7 +269,7 @@ export const findToolRenderer = {
269
269
  const text = result.content?.find(c => c.type === "text")?.text ?? "";
270
270
 
271
271
  if (result.isError) {
272
- return new Text(formatErrorMessage(text || "Unknown error", uiTheme), 1, 0);
272
+ return new Text(formatErrorMessage(text || "Unknown error", uiTheme), 0, 0);
273
273
  }
274
274
 
275
275
  // Streaming progress: the tool reports the phase it is in.
@@ -285,7 +285,7 @@ export const findToolRenderer = {
285
285
  },
286
286
  uiTheme,
287
287
  );
288
- return new Text(header, 1, 0);
288
+ return new Text(header, 0, 0);
289
289
  }
290
290
 
291
291
  const { hits, stats, threshold } = details;
@@ -317,7 +317,7 @@ export const findToolRenderer = {
317
317
  : "No relevant passages found";
318
318
  const lines = [header, formatEmptyMessage(empty, uiTheme)];
319
319
  for (const failure of stats.failures) lines.push(uiTheme.fg("warning", failure));
320
- return new Text(lines.join("\n"), 1, 0);
320
+ return new Text(lines.join("\n"), 0, 0);
321
321
  }
322
322
 
323
323
  const header = renderStatusLine(
@@ -359,7 +359,6 @@ export const findToolRenderer = {
359
359
  }
360
360
  return [header, ...hitLines, ...extra].map(line => truncateToWidth(line, width, Ellipsis.Omit));
361
361
  },
362
- { paddingX: 1 },
363
362
  );
364
363
  },
365
364
  describeCall(args: FindRenderArgs): NativeToolView {
package/src/tools/glob.ts CHANGED
@@ -151,7 +151,7 @@ export const globToolRenderer = {
151
151
  },
152
152
  uiTheme,
153
153
  );
154
- return new Text(text, 1, 0);
154
+ return new Text(text, 0, 0);
155
155
  },
156
156
 
157
157
  renderResult(
@@ -165,7 +165,7 @@ export const globToolRenderer = {
165
165
 
166
166
  if (result.isError || details?.error) {
167
167
  const errorText = details?.error || result.content?.find(c => c.type === "text")?.text || "Unknown error";
168
- return new Text(formatErrorMessage(errorText, uiTheme), 1, 0);
168
+ return new Text(formatErrorMessage(errorText, uiTheme), 0, 0);
169
169
  }
170
170
 
171
171
  const hasDetailedData = details?.fileCount !== undefined;
@@ -178,7 +178,7 @@ export const globToolRenderer = {
178
178
  textContent.includes("No files found") ||
179
179
  textContent.trim() === ""
180
180
  ) {
181
- return new Text(formatEmptyMessage("No files found", uiTheme), 1, 0);
181
+ return new Text(formatEmptyMessage("No files found", uiTheme), 0, 0);
182
182
  }
183
183
 
184
184
  const lines = textContent.split("\n").filter(l => l.trim());
@@ -207,7 +207,6 @@ export const globToolRenderer = {
207
207
  );
208
208
  return [header, ...listLines].map(l => truncateToWidth(l, width, Ellipsis.Omit));
209
209
  },
210
- { paddingX: 1 },
211
210
  );
212
211
  }
213
212
 
@@ -244,7 +243,7 @@ export const globToolRenderer = {
244
243
  );
245
244
  const lines = [header, formatEmptyMessage(emptyLabel, uiTheme)];
246
245
  if (missingNote) lines.push(missingNote);
247
- return new Text(lines.join("\n"), 1, 0);
246
+ return new Text(lines.join("\n"), 0, 0);
248
247
  }
249
248
  const meta: string[] = [formatCount("file", fileCount)];
250
249
  if (details?.scopePath) meta.push(`in ${details.scopePath}`);
@@ -298,7 +297,6 @@ export const globToolRenderer = {
298
297
  );
299
298
  return [header, ...fileLines, ...extraLines].map(l => truncateToWidth(l, width, Ellipsis.Omit));
300
299
  },
301
- { paddingX: 1 },
302
300
  );
303
301
  },
304
302
  describeCall(args: GlobRenderArgs): NativeToolView {
package/src/tools/grep.ts CHANGED
@@ -365,7 +365,7 @@ export const grepToolRenderer = {
365
365
  { icon: "pending", title: "Grep", titleColor: "toolTitle", description: args.pattern || "?", meta },
366
366
  uiTheme,
367
367
  );
368
- return new Text(text, 1, 0);
368
+ return new Text(text, 0, 0);
369
369
  },
370
370
 
371
371
  renderResult(
@@ -422,7 +422,6 @@ export const grepToolRenderer = {
422
422
  );
423
423
  return [header, ...listLines].map(l => truncateToWidth(l, width, Ellipsis.Omit));
424
424
  },
425
- { paddingX: 1 },
426
425
  );
427
426
  }
428
427
  const linkTargets = grepLinkTargets(details, options.renderContext?.rawDetails);
@@ -456,7 +455,7 @@ export const grepToolRenderer = {
456
455
  );
457
456
  const lines = [header, formatEmptyMessage(tuiText("gpNoMatches", "No matches found"), uiTheme)];
458
457
  if (missingNote) lines.push(missingNote);
459
- return new Text(lines.join("\n"), 1, 0);
458
+ return new Text(lines.join("\n"), 0, 0);
460
459
  }
461
460
 
462
461
  const summaryParts = [formatCount("match", matchCount), formatCount("file", fileCount)];
@@ -514,7 +513,6 @@ export const grepToolRenderer = {
514
513
  );
515
514
  return [header, ...matchLines, ...extraLines].map(l => truncateToWidth(l, width, Ellipsis.Omit));
516
515
  },
517
- { paddingX: 1 },
518
516
  );
519
517
  },
520
518
  describeCall(args: GrepRenderArgs): NativeToolView {
package/src/tools/hub.ts CHANGED
@@ -181,6 +181,8 @@ export interface CoordinationDetails {
181
181
  cancelled?: { id: string; status: CancelStatus }[];
182
182
  /** Running subagents not represented by a job row in this result. */
183
183
  agents?: AgentActivitySnapshot[];
184
+ /** `wait` returned early because blocking would deadlock (mesh S6). */
185
+ status?: "peer-waiting-on-you";
184
186
  }
185
187
 
186
188
  /** Hub result details: coordination snapshots or launch (process) state. */
@@ -269,7 +271,7 @@ export interface IrcMessage {
269
271
  /** Delivery outcome for one peer recipient. */
270
272
  export interface IrcDeliveryReceipt {
271
273
  to: string;
272
- outcome: "injected" | "woken" | "revived" | "failed";
274
+ outcome: "injected" | "woken" | "revived" | "queued" | "failed";
273
275
  error?: string;
274
276
  }
275
277
  /** Broker-facing launch parameters; the hub adapts its `ps` op to `list` before calling in. */
@@ -990,6 +992,7 @@ function outcomeColor(outcome: IrcDeliveryReceipt["outcome"]): ToolUIColor {
990
992
  case "woken":
991
993
  return "success";
992
994
  case "revived":
995
+ case "queued":
993
996
  return "warning";
994
997
  case "injected":
995
998
  return "accent";
@@ -19,11 +19,21 @@ export interface IrcMessage {
19
19
  * ping-pong forever.
20
20
  */
21
21
  wakeRelay?: boolean;
22
+ /**
23
+ * One leg of an `all` broadcast. Like relays, broadcasts never draw an
24
+ * automatic wake-turn reply: every woken idle peer would otherwise answer
25
+ * the broadcaster, mostly to say the message was not for it.
26
+ */
27
+ broadcast?: boolean;
22
28
  }
23
29
  /** Delivery outcome for one peer recipient. */
24
30
  export interface IrcDeliveryReceipt {
25
31
  to: string;
26
- outcome: "injected" | "woken" | "revived" | "failed";
32
+ /**
33
+ * `queued` is the cross-process offline path: the message is in the peer's
34
+ * persistent mailbox and will be injected when its process next starts.
35
+ */
36
+ outcome: "injected" | "woken" | "revived" | "queued" | "failed";
27
37
  error?: string;
28
38
  }
29
39
  /** Status ordering for peer rosters in child prompts. */
package/src/tools/irc.ts CHANGED
@@ -21,12 +21,22 @@ export interface IrcMessage {
21
21
  * ping-pong forever.
22
22
  */
23
23
  wakeRelay?: boolean;
24
+ /**
25
+ * One leg of an `all` broadcast. Like relays, broadcasts never draw an
26
+ * automatic wake-turn reply: every woken idle peer would otherwise answer
27
+ * the broadcaster, mostly to say the message was not for it.
28
+ */
29
+ broadcast?: boolean;
24
30
  }
25
31
 
26
32
  /** Delivery outcome for one peer recipient. */
27
33
  export interface IrcDeliveryReceipt {
28
34
  to: string;
29
- outcome: "injected" | "woken" | "revived" | "failed";
35
+ /**
36
+ * `queued` is the cross-process offline path: the message is in the peer's
37
+ * persistent mailbox and will be injected when its process next starts.
38
+ */
39
+ outcome: "injected" | "woken" | "revived" | "queued" | "failed";
30
40
  error?: string;
31
41
  }
32
42
  /** Status ordering for peer rosters in child prompts. */