@oh-my-pi/pi-tui 18.12.2 → 18.12.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +44 -48
- package/dist/types/autocomplete.d.ts +6 -0
- package/dist/types/chat/skill-message.d.ts +3 -4
- package/dist/types/components/sparkline.d.ts +34 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/native/describe.d.ts +7 -1
- package/dist/types/native/tone.d.ts +2 -0
- package/dist/types/overlays/agent-hub.d.ts +3 -3
- package/dist/types/overlays/approval-dialog.d.ts +15 -1
- package/dist/types/overlays/jobs-panel.d.ts +3 -0
- package/dist/types/overlays/model-browser.d.ts +1 -1
- package/dist/types/render/output-block.d.ts +2 -0
- package/dist/types/render/tool-card.d.ts +2 -0
- package/dist/types/sixel.d.ts +28 -0
- package/dist/types/status-line/host.d.ts +3 -0
- package/dist/types/terminal-capabilities.d.ts +5 -0
- package/dist/types/tools/bash.d.ts +2 -0
- package/dist/types/tools/default-renderer.d.ts +3 -0
- package/dist/types/tools/eval.d.ts +9 -8
- package/dist/types/tools/irc.d.ts +6 -0
- package/dist/types/tools/job-widget.d.ts +226 -0
- package/dist/types/tools/json-tree.d.ts +2 -0
- package/dist/types/tools/line-ranges.d.ts +8 -1
- package/dist/types/tools/mcp.d.ts +23 -0
- package/dist/types/tools/output-meta.d.ts +1 -1
- package/dist/types/tools/read.d.ts +8 -2
- package/dist/types/tools/renderer.d.ts +8 -2
- package/dist/types/tools/result-card.d.ts +1 -1
- package/dist/types/tools/streaming-output.d.ts +1 -0
- package/dist/types/tools/task.d.ts +12 -0
- package/dist/types/tools/wait.d.ts +5 -0
- package/package.json +9 -9
- package/src/autocomplete.ts +12 -1
- package/src/chat/skill-message.ts +19 -21
- package/src/chat/tool-execution.ts +35 -0
- package/src/chrome/transcript-container.ts +85 -12
- package/src/components/editor.ts +17 -7
- package/src/components/image.ts +5 -4
- package/src/components/markdown.ts +11 -3
- package/src/components/sparkline.ts +89 -0
- package/src/index.ts +1 -0
- package/src/native/backend.ts +25 -5
- package/src/native/describe.ts +18 -1
- package/src/native/tone.ts +22 -0
- package/src/overlays/agent-hub.ts +14 -10
- package/src/overlays/agent-transcript-viewer.ts +2 -2
- package/src/overlays/approval-dialog.ts +59 -13
- package/src/overlays/jobs-panel.ts +24 -2
- package/src/overlays/model-browser.ts +3 -0
- package/src/prompt/attachment-chips.ts +1 -1
- package/src/render/output-block.ts +25 -12
- package/src/render/tool-card.ts +6 -0
- package/src/sixel.ts +185 -0
- package/src/status-line/component.ts +19 -2
- package/src/status-line/host.ts +3 -0
- package/src/terminal-capabilities.ts +43 -3
- package/src/theme/symbols.ts +5 -3
- package/src/tools/bash.ts +25 -9
- package/src/tools/default-renderer.ts +54 -17
- package/src/tools/eval.ts +32 -14
- package/src/tools/find.ts +4 -5
- package/src/tools/glob.ts +4 -6
- package/src/tools/grep.ts +4 -6
- package/src/tools/irc.ts +6 -0
- package/src/tools/job-widget.ts +1036 -0
- package/src/tools/json-tree.ts +8 -0
- package/src/tools/line-ranges.ts +23 -13
- package/src/tools/mcp.ts +44 -1
- package/src/tools/output-meta.ts +1 -1
- package/src/tools/read.ts +64 -21
- package/src/tools/renderer.ts +8 -2
- package/src/tools/result-card.ts +1 -2
- package/src/tools/streaming-output.ts +11 -9
- package/src/tools/task.ts +25 -1
- package/src/tools/wait.ts +288 -147
- package/src/tui.ts +45 -5
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
renderKittyPlaceholderLines,
|
|
11
11
|
setKittyGraphics,
|
|
12
12
|
} from "./kitty-graphics";
|
|
13
|
+
import { ensureSixelRasterWidth } from "./sixel";
|
|
13
14
|
import {
|
|
14
15
|
isInsideTerminalMultiplexer,
|
|
15
16
|
isTerminalBackgroundQueryTrusted,
|
|
@@ -403,7 +404,11 @@ export function detectStyledUnderlineSupport(terminalId: TerminalId, env: NodeJS
|
|
|
403
404
|
// render. Enable only on a confirmed major.minor >= 3.5; an absent or
|
|
404
405
|
// unparseable version keeps the flat fallback so only proven terminals
|
|
405
406
|
// get the colon form.
|
|
406
|
-
|
|
407
|
+
//
|
|
408
|
+
// `LC_TERMINAL_VERSION` carries the same version over SSH, where
|
|
409
|
+
// `TERM_PROGRAM_VERSION` is stripped, so it is the fallback source.
|
|
410
|
+
const version =
|
|
411
|
+
parseMajorMinorVersion(env.TERM_PROGRAM_VERSION) ?? parseMajorMinorVersion(env.LC_TERMINAL_VERSION);
|
|
407
412
|
return version !== null && (version.major > 3 || (version.major === 3 && version.minor >= 5));
|
|
408
413
|
}
|
|
409
414
|
default:
|
|
@@ -672,6 +677,7 @@ export function detectTerminalId(env: NodeJS.ProcessEnv = Bun.env): TerminalId {
|
|
|
672
677
|
VSCODE_PID,
|
|
673
678
|
ALACRITTY_WINDOW_ID,
|
|
674
679
|
TERM_PROGRAM,
|
|
680
|
+
LC_TERMINAL,
|
|
675
681
|
TERM,
|
|
676
682
|
COLORTERM,
|
|
677
683
|
} = env;
|
|
@@ -686,6 +692,22 @@ export function detectTerminalId(env: NodeJS.ProcessEnv = Bun.env): TerminalId {
|
|
|
686
692
|
const programId = fromProgram(TERM_PROGRAM);
|
|
687
693
|
if (programId) return programId;
|
|
688
694
|
|
|
695
|
+
// iTerm2 sets `LC_TERMINAL=iTerm2` (plus `LC_TERMINAL_VERSION`) in the local
|
|
696
|
+
// session precisely so it survives SSH: openssh/mosh forward `LC_*` to the
|
|
697
|
+
// remote host, while `ITERM_SESSION_ID` and `TERM_PROGRAM` are stripped. Its
|
|
698
|
+
// own shell integration says the same: "TERM_PROGRAM is set by the local
|
|
699
|
+
// terminal but not forwarded over SSH. LC_TERMINAL is set by iTerm2 and may
|
|
700
|
+
// be forwarded over SSH." Without this branch the remote session falls to
|
|
701
|
+
// the true-color profile and loses the iTerm2 inline-image protocol, OSC 8
|
|
702
|
+
// hyperlinks and OSC 9 notifications.
|
|
703
|
+
//
|
|
704
|
+
// Gated on the absence of a multiplexer: iTerm2's OSC 1337 encoder is the
|
|
705
|
+
// only image encoder that does not wrap its payload in a tmux passthrough
|
|
706
|
+
// envelope, so inside a multiplexer the sequence would be swallowed by the
|
|
707
|
+
// multiplexer instead of reaching the outer terminal. The multiplexer path
|
|
708
|
+
// resolves the outer client on its own.
|
|
709
|
+
if (LC_TERMINAL && caseEq(LC_TERMINAL, "iTerm2") && !isInsideTerminalMultiplexer(env)) return "iterm2";
|
|
710
|
+
|
|
689
711
|
// tmux >= 3.2 replaces the pane's identity with `TERM_PROGRAM=tmux`.
|
|
690
712
|
// Its server still holds the attached client's terminal-type reply and TERM.
|
|
691
713
|
for (const clientProgram of resolveTmuxClientTerminalNames(env)) {
|
|
@@ -1087,6 +1109,19 @@ export function encodeITerm2(
|
|
|
1087
1109
|
return `\x1b]1337;File=${params.join(";")}:${base64Data}\x07`;
|
|
1088
1110
|
}
|
|
1089
1111
|
|
|
1112
|
+
/**
|
|
1113
|
+
* The encoded image `encodeITerm2` wrote into `line`, without the size
|
|
1114
|
+
* parameters a rewrap changes; undefined for a line without one.
|
|
1115
|
+
*/
|
|
1116
|
+
export function iterm2ImagePayload(line: string): string | undefined {
|
|
1117
|
+
const start = line.indexOf(ImageProtocol.Iterm2);
|
|
1118
|
+
if (start === -1) return undefined;
|
|
1119
|
+
const colon = line.indexOf(":", start + ImageProtocol.Iterm2.length);
|
|
1120
|
+
if (colon === -1) return undefined;
|
|
1121
|
+
const end = line.indexOf("\x07", colon + 1);
|
|
1122
|
+
return line.slice(colon + 1, end === -1 ? undefined : end);
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1090
1125
|
export function calculateImageRows(
|
|
1091
1126
|
imageDimensions: ImageDimensions,
|
|
1092
1127
|
targetWidthCells: number,
|
|
@@ -1281,7 +1316,9 @@ export function encodeSixelNow(
|
|
|
1281
1316
|
backgroundRgb = getTerminalBackgroundColor(),
|
|
1282
1317
|
): string | null {
|
|
1283
1318
|
try {
|
|
1284
|
-
return
|
|
1319
|
+
return ensureSixelRasterWidth(
|
|
1320
|
+
encodeSixel(new Uint8Array(Buffer.from(base64Data, "base64")), widthPx, heightPx, backgroundRgb),
|
|
1321
|
+
);
|
|
1285
1322
|
} catch {
|
|
1286
1323
|
return null;
|
|
1287
1324
|
}
|
|
@@ -1354,9 +1391,12 @@ export function renderImage(
|
|
|
1354
1391
|
return { sequence, rows };
|
|
1355
1392
|
}
|
|
1356
1393
|
if (TERMINAL.imageProtocol === ImageProtocol.Iterm2) {
|
|
1394
|
+
// The height is the rows reserved for the image, not `auto`: iTerm2
|
|
1395
|
+
// resolves `auto` only for a session on screen, so an image written to a
|
|
1396
|
+
// background tab is dropped, and its rows stay blank in scrollback too.
|
|
1357
1397
|
const sequence = encodeITerm2(base64Data, {
|
|
1358
1398
|
width: fit.columns,
|
|
1359
|
-
height:
|
|
1399
|
+
height: fit.rows,
|
|
1360
1400
|
preserveAspectRatio: options.preserveAspectRatio ?? true,
|
|
1361
1401
|
});
|
|
1362
1402
|
return { sequence, rows: fit.rows };
|
package/src/theme/symbols.ts
CHANGED
|
@@ -628,9 +628,11 @@ const UNICODE_SYMBOLS: SymbolMap = {
|
|
|
628
628
|
"lang.pdf": "📕",
|
|
629
629
|
"lang.archive": "🗜",
|
|
630
630
|
"lang.binary": "⚙",
|
|
631
|
-
// Composer attachment chips
|
|
632
|
-
|
|
633
|
-
|
|
631
|
+
// Composer attachment chips. These sit inside the editable buffer, so the caret column
|
|
632
|
+
// depends on their width: use Emoji_Presentation glyphs, which every terminal draws 2 cells
|
|
633
|
+
// wide. Text-presentation pictographs (🖼, 🎞) measure 1 cell but many terminals draw them 2.
|
|
634
|
+
"chip.image": "📷",
|
|
635
|
+
"chip.video": "🎬",
|
|
634
636
|
"chip.paste": "📄",
|
|
635
637
|
// Settings tabs
|
|
636
638
|
"tab.appearance": "🎨",
|
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
|
-
|
|
580
|
-
statsParts.
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
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
|
|
|
@@ -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 (
|
|
107
|
+
if (isTreeSafeJson(textContent)) {
|
|
108
108
|
try {
|
|
109
109
|
const parsed = JSON.parse(textContent);
|
|
110
|
-
const
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
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
|
-
|
|
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()
|
|
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 (
|
|
206
|
+
if (isTreeSafeJson(output)) {
|
|
207
|
+
let parsed: unknown;
|
|
182
208
|
try {
|
|
183
|
-
|
|
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
|
}
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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 `
|
|
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 (
|
|
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
|
-
*
|
|
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
|
|
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.
|
|
267
|
-
* cards read them, and so does
|
|
268
|
-
*
|
|
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(
|
|
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.
|
|
697
|
-
*
|
|
698
|
-
*
|
|
699
|
-
*
|
|
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: "omp.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,
|
|
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),
|
|
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,
|
|
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"),
|
|
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,
|
|
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),
|
|
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),
|
|
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"),
|
|
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
|
@@ -364,7 +364,7 @@ export const grepToolRenderer = {
|
|
|
364
364
|
{ icon: "pending", title: "Grep", titleColor: "toolTitle", description: args.pattern || "?", meta },
|
|
365
365
|
uiTheme,
|
|
366
366
|
);
|
|
367
|
-
return new Text(text,
|
|
367
|
+
return new Text(text, 0, 0);
|
|
368
368
|
},
|
|
369
369
|
|
|
370
370
|
renderResult(
|
|
@@ -377,7 +377,7 @@ export const grepToolRenderer = {
|
|
|
377
377
|
|
|
378
378
|
if (result.isError || details?.error) {
|
|
379
379
|
const errorText = details?.error || result.content?.find(c => c.type === "text")?.text || "Unknown error";
|
|
380
|
-
return new Text(formatErrorMessage(errorText, uiTheme),
|
|
380
|
+
return new Text(formatErrorMessage(errorText, uiTheme), 0, 0);
|
|
381
381
|
}
|
|
382
382
|
|
|
383
383
|
const hasDetailedData = details?.matchCount !== undefined || details?.fileCount !== undefined;
|
|
@@ -385,7 +385,7 @@ export const grepToolRenderer = {
|
|
|
385
385
|
if (!hasDetailedData) {
|
|
386
386
|
const textContent = result.details?.displayContent ?? result.content?.find(c => c.type === "text")?.text;
|
|
387
387
|
if (!textContent || textContent === "No matches found") {
|
|
388
|
-
return new Text(formatEmptyMessage("No matches found", uiTheme),
|
|
388
|
+
return new Text(formatEmptyMessage("No matches found", uiTheme), 0, 0);
|
|
389
389
|
}
|
|
390
390
|
const lines = textContent.split("\n").filter(line => line.trim() !== "");
|
|
391
391
|
const description = args?.pattern ?? undefined;
|
|
@@ -415,7 +415,6 @@ export const grepToolRenderer = {
|
|
|
415
415
|
);
|
|
416
416
|
return [header, ...listLines].map(l => truncateToWidth(l, width, Ellipsis.Omit));
|
|
417
417
|
},
|
|
418
|
-
{ paddingX: 1 },
|
|
419
418
|
);
|
|
420
419
|
}
|
|
421
420
|
const linkTargets = grepLinkTargets(details, options.renderContext?.rawDetails);
|
|
@@ -442,7 +441,7 @@ export const grepToolRenderer = {
|
|
|
442
441
|
);
|
|
443
442
|
const lines = [header, formatEmptyMessage("No matches found", uiTheme)];
|
|
444
443
|
if (missingNote) lines.push(missingNote);
|
|
445
|
-
return new Text(lines.join("\n"),
|
|
444
|
+
return new Text(lines.join("\n"), 0, 0);
|
|
446
445
|
}
|
|
447
446
|
|
|
448
447
|
const summaryParts = [formatCount("match", matchCount), formatCount("file", fileCount)];
|
|
@@ -500,7 +499,6 @@ export const grepToolRenderer = {
|
|
|
500
499
|
);
|
|
501
500
|
return [header, ...matchLines, ...extraLines].map(l => truncateToWidth(l, width, Ellipsis.Omit));
|
|
502
501
|
},
|
|
503
|
-
{ paddingX: 1 },
|
|
504
502
|
);
|
|
505
503
|
},
|
|
506
504
|
describeCall(args: GrepRenderArgs): NativeToolView {
|
package/src/tools/irc.ts
CHANGED
|
@@ -21,6 +21,12 @@ 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. */
|