@code-yeongyu/senpi-codemode 2026.7.25-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.
- package/CHANGELOG.md +250 -0
- package/LICENSE +22 -0
- package/README.md +161 -0
- package/package.json +58 -0
- package/src/bridge/http-server.ts +236 -0
- package/src/bridge/protocol.ts +198 -0
- package/src/bridge/reserved.ts +9 -0
- package/src/bridges/agent-bridge.ts +197 -0
- package/src/bridges/output-bridge.ts +96 -0
- package/src/bridges/schema-injection.ts +3 -0
- package/src/codemode/runtime.ts +258 -0
- package/src/codemode/tools.ts +106 -0
- package/src/completion/handler.ts +192 -0
- package/src/completion/tool-bridge.ts +55 -0
- package/src/config/settings.ts +215 -0
- package/src/extension/runtime-factory.ts +114 -0
- package/src/extension/session-manager-proxy.ts +116 -0
- package/src/extension/session-manager.ts +215 -0
- package/src/host-sdk.ts +1 -0
- package/src/index.ts +181 -0
- package/src/interpreters/detect.ts +161 -0
- package/src/kernels/jl/kernel.ts +37 -0
- package/src/kernels/jl/prelude.jl +283 -0
- package/src/kernels/jl/runner.jl +327 -0
- package/src/kernels/js/context-manager.ts +296 -0
- package/src/kernels/js/inline-worker-entry.js +23 -0
- package/src/kernels/js/inline-worker.ts +15 -0
- package/src/kernels/js/kernel-contract.ts +38 -0
- package/src/kernels/js/local-module-loader.ts +108 -0
- package/src/kernels/js/prelude.ts +15 -0
- package/src/kernels/js/rewrite-imports.ts +164 -0
- package/src/kernels/js/run-queue.ts +82 -0
- package/src/kernels/js/worker-core.d.ts +18 -0
- package/src/kernels/js/worker-core.js +94 -0
- package/src/kernels/js/worker-entry.js +23 -0
- package/src/kernels/js/worker-host.ts +117 -0
- package/src/kernels/js/worker-indirect-eval.js +88 -0
- package/src/kernels/js/worker-runtime.js +401 -0
- package/src/kernels/py/kernel-contract.ts +32 -0
- package/src/kernels/py/kernel.ts +290 -0
- package/src/kernels/py/prelude.py +954 -0
- package/src/kernels/py/process.ts +119 -0
- package/src/kernels/py/transport.ts +237 -0
- package/src/kernels/rb/kernel.ts +26 -0
- package/src/kernels/rb/prelude.rb +270 -0
- package/src/kernels/rb/runner.rb +204 -0
- package/src/kernels/shared/subprocess-contract.ts +22 -0
- package/src/kernels/shared/subprocess-kernel.ts +266 -0
- package/src/kernels/shared/subprocess-process.ts +174 -0
- package/src/kernels/shared/subprocess-queue.ts +101 -0
- package/src/kernels/shared/subprocess-run.ts +98 -0
- package/src/output/output-meta.ts +89 -0
- package/src/output/streaming-output.ts +296 -0
- package/src/prompt/eval-prompt.ts +319 -0
- package/src/timeouts/bridge-timeout.ts +16 -0
- package/src/timeouts/idle-timeout.ts +84 -0
- package/src/tool/cell-handler.ts +279 -0
- package/src/tool/eval-tool.ts +285 -0
- package/src/tool/image.ts +274 -0
- package/src/tool/json-tree.ts +247 -0
- package/src/tool/render.ts +876 -0
- package/src/tool/status-events.ts +12 -0
- package/src/tool/types.ts +114 -0
|
@@ -0,0 +1,876 @@
|
|
|
1
|
+
// allow: SIZE_OK — one eval render pipeline avoids the runtime/render TDZ that motivated this module boundary.
|
|
2
|
+
import {
|
|
3
|
+
type AgentToolResult,
|
|
4
|
+
highlightCode,
|
|
5
|
+
sanitizeTerminalLabel,
|
|
6
|
+
type Theme,
|
|
7
|
+
type ThemeColor,
|
|
8
|
+
type ToolDefinition,
|
|
9
|
+
type ToolRenderResultOptions,
|
|
10
|
+
truncateToVisualLines,
|
|
11
|
+
} from "@code-yeongyu/senpi";
|
|
12
|
+
import { formatTruncationWarning, stripOutputNotice, type TruncationMeta } from "../output/output-meta.ts";
|
|
13
|
+
import {
|
|
14
|
+
JSON_TREE_MAX_DEPTH_COLLAPSED,
|
|
15
|
+
JSON_TREE_MAX_DEPTH_EXPANDED,
|
|
16
|
+
JSON_TREE_MAX_LINES_COLLAPSED,
|
|
17
|
+
JSON_TREE_MAX_LINES_EXPANDED,
|
|
18
|
+
JSON_TREE_SCALAR_LEN_COLLAPSED,
|
|
19
|
+
JSON_TREE_SCALAR_LEN_EXPANDED,
|
|
20
|
+
renderJsonTreeLines,
|
|
21
|
+
} from "./json-tree.ts";
|
|
22
|
+
import type {
|
|
23
|
+
EvalCellResult,
|
|
24
|
+
EvalInputSchema,
|
|
25
|
+
EvalLanguage,
|
|
26
|
+
EvalStatusEvent,
|
|
27
|
+
EvalToolDetails,
|
|
28
|
+
EvalToolInput,
|
|
29
|
+
} from "./types.ts";
|
|
30
|
+
|
|
31
|
+
type EvalToolDefinition = ToolDefinition<EvalInputSchema, EvalToolDetails>;
|
|
32
|
+
type RenderContext = Parameters<NonNullable<EvalToolDefinition["renderCall"]>>[2];
|
|
33
|
+
type ResultRenderContext = Parameters<NonNullable<EvalToolDefinition["renderResult"]>>[3];
|
|
34
|
+
type CollapsibleKind = "code" | "output";
|
|
35
|
+
|
|
36
|
+
export interface EvalRenderComponent {
|
|
37
|
+
render(width: number): string[];
|
|
38
|
+
invalidate(): void;
|
|
39
|
+
}
|
|
40
|
+
type ToolCallRow = {
|
|
41
|
+
readonly summary: string;
|
|
42
|
+
readonly error?: string;
|
|
43
|
+
readonly color: "success" | "error";
|
|
44
|
+
};
|
|
45
|
+
type RenderBlock =
|
|
46
|
+
| { readonly kind: "blank" }
|
|
47
|
+
| {
|
|
48
|
+
readonly kind: "text";
|
|
49
|
+
readonly text: string;
|
|
50
|
+
readonly maxVisualLines?: number;
|
|
51
|
+
readonly collapseKind?: CollapsibleKind;
|
|
52
|
+
readonly theme?: Theme;
|
|
53
|
+
}
|
|
54
|
+
| {
|
|
55
|
+
readonly kind: "toolCalls";
|
|
56
|
+
readonly calls: readonly ToolCallRow[];
|
|
57
|
+
readonly expanded: boolean;
|
|
58
|
+
readonly theme?: Theme;
|
|
59
|
+
}
|
|
60
|
+
| { readonly kind: "dynamic"; readonly render: (width: number) => readonly string[] };
|
|
61
|
+
|
|
62
|
+
const CODE_PREVIEW_LINES = 4;
|
|
63
|
+
const OUTPUT_PREVIEW_LINES = 8;
|
|
64
|
+
const STATUS_PREVIEW_COUNT = 3;
|
|
65
|
+
const SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"] as const;
|
|
66
|
+
const TOOL_CALL_PREVIEW_COUNT = 5;
|
|
67
|
+
const TOOL_CALL_COLLAPSED_VISUAL_LINES = 4;
|
|
68
|
+
const TOOL_CALL_COLLAPSED_ERROR_CODE_POINTS = 512;
|
|
69
|
+
const TOOL_ERROR_OMISSION_MARKER = "[tool error omitted]";
|
|
70
|
+
|
|
71
|
+
class PlainTextComponent implements EvalRenderComponent {
|
|
72
|
+
#blocks: readonly RenderBlock[] = [];
|
|
73
|
+
|
|
74
|
+
setBlocks(blocks: readonly RenderBlock[]): void {
|
|
75
|
+
this.#blocks = blocks;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
render(width: number): string[] {
|
|
79
|
+
const lines: string[] = [];
|
|
80
|
+
for (const block of this.#blocks) {
|
|
81
|
+
switch (block.kind) {
|
|
82
|
+
case "blank":
|
|
83
|
+
lines.push("");
|
|
84
|
+
break;
|
|
85
|
+
case "toolCalls":
|
|
86
|
+
appendLines(lines, renderToolCallBlock(block, width));
|
|
87
|
+
break;
|
|
88
|
+
case "text":
|
|
89
|
+
appendLines(lines, renderTextBlock(block, width));
|
|
90
|
+
break;
|
|
91
|
+
case "dynamic":
|
|
92
|
+
appendLines(lines, block.render(width));
|
|
93
|
+
break;
|
|
94
|
+
default:
|
|
95
|
+
assertNever(block);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return lines;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
invalidate(): void {}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function componentFor(context: RenderContext | ResultRenderContext): PlainTextComponent {
|
|
105
|
+
const existing = context.lastComponent;
|
|
106
|
+
if (existing instanceof PlainTextComponent) return existing;
|
|
107
|
+
return new PlainTextComponent();
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function style(theme: Theme | undefined, color: ThemeColor, text: string): string {
|
|
111
|
+
return theme ? theme.fg(color, text) : text;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function appendLines(target: string[], source: readonly string[]): void {
|
|
115
|
+
for (const line of source) target.push(line);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function codePointPrefix(text: string, maxCodePoints: number): string {
|
|
119
|
+
let end = 0;
|
|
120
|
+
for (let count = 0; count < maxCodePoints && end < text.length; count += 1) {
|
|
121
|
+
const firstCodeUnit = text.charCodeAt(end);
|
|
122
|
+
const secondCodeUnit = text.charCodeAt(end + 1);
|
|
123
|
+
const isSurrogatePair =
|
|
124
|
+
firstCodeUnit >= 0xd800 && firstCodeUnit <= 0xdbff && secondCodeUnit >= 0xdc00 && secondCodeUnit <= 0xdfff;
|
|
125
|
+
end += isSurrogatePair ? 2 : 1;
|
|
126
|
+
}
|
|
127
|
+
return text.slice(0, end);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function renderAllVisualLines(text: string, width: number): string[] {
|
|
131
|
+
return truncateToVisualLines(text, Number.POSITIVE_INFINITY, width).visualLines.map((line) => line.trimEnd());
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function renderTextBlock(block: Extract<RenderBlock, { kind: "text" }>, width: number): string[] {
|
|
135
|
+
if (block.maxVisualLines === undefined) return renderAllVisualLines(block.text, width);
|
|
136
|
+
const result = truncateToVisualLines(block.text, block.maxVisualLines, width);
|
|
137
|
+
const visualLines = result.visualLines.map((line) => line.trimEnd());
|
|
138
|
+
if (result.skippedCount === 0 || block.collapseKind === undefined) return visualLines;
|
|
139
|
+
return [
|
|
140
|
+
...renderAllVisualLines(
|
|
141
|
+
style(block.theme, "muted", `${result.skippedCount} earlier ${block.collapseKind} lines`),
|
|
142
|
+
width,
|
|
143
|
+
),
|
|
144
|
+
...visualLines,
|
|
145
|
+
];
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function renderToolCall(
|
|
149
|
+
call: ToolCallRow,
|
|
150
|
+
block: Extract<RenderBlock, { kind: "toolCalls" }>,
|
|
151
|
+
width: number,
|
|
152
|
+
): string[] {
|
|
153
|
+
if (call.error === undefined) return renderAllVisualLines(style(block.theme, call.color, call.summary), width);
|
|
154
|
+
if (block.expanded)
|
|
155
|
+
return renderAllVisualLines(style(block.theme, call.color, `${call.summary} (${call.error})`), width);
|
|
156
|
+
|
|
157
|
+
const guardedError = codePointPrefix(call.error, TOOL_CALL_COLLAPSED_ERROR_CODE_POINTS);
|
|
158
|
+
const guardedLines = renderAllVisualLines(
|
|
159
|
+
style(block.theme, call.color, `${call.summary} (${guardedError})`),
|
|
160
|
+
width,
|
|
161
|
+
);
|
|
162
|
+
if (guardedError.length === call.error.length && guardedLines.length <= TOOL_CALL_COLLAPSED_VISUAL_LINES)
|
|
163
|
+
return guardedLines;
|
|
164
|
+
|
|
165
|
+
const summaryLines = renderAllVisualLines(style(block.theme, call.color, call.summary), width);
|
|
166
|
+
const errorLines = renderAllVisualLines(style(block.theme, call.color, ` (${guardedError})`), width);
|
|
167
|
+
const markerLines = renderAllVisualLines(style(block.theme, "muted", TOOL_ERROR_OMISSION_MARKER), width);
|
|
168
|
+
const lines: string[] = [];
|
|
169
|
+
const summaryBudget = Math.max(1, TOOL_CALL_COLLAPSED_VISUAL_LINES - markerLines.length);
|
|
170
|
+
appendLines(lines, summaryLines.slice(0, summaryBudget));
|
|
171
|
+
const errorBudget = Math.max(0, TOOL_CALL_COLLAPSED_VISUAL_LINES - lines.length - markerLines.length);
|
|
172
|
+
appendLines(lines, errorLines.slice(0, errorBudget));
|
|
173
|
+
appendLines(lines, markerLines.slice(0, TOOL_CALL_COLLAPSED_VISUAL_LINES - lines.length));
|
|
174
|
+
return lines;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function renderToolCallBlock(block: Extract<RenderBlock, { kind: "toolCalls" }>, width: number): string[] {
|
|
178
|
+
const retainedCalls = block.expanded ? block.calls : block.calls.slice(-TOOL_CALL_PREVIEW_COUNT);
|
|
179
|
+
const skippedCount = block.calls.length - retainedCalls.length;
|
|
180
|
+
const toolCallNoun = skippedCount === 1 ? "call" : "calls";
|
|
181
|
+
const lines =
|
|
182
|
+
block.expanded || skippedCount === 0
|
|
183
|
+
? []
|
|
184
|
+
: renderAllVisualLines(style(block.theme, "muted", `${skippedCount} earlier tool ${toolCallNoun}`), width);
|
|
185
|
+
for (const call of retainedCalls) {
|
|
186
|
+
appendLines(lines, renderToolCall(call, block, width));
|
|
187
|
+
}
|
|
188
|
+
return lines;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
type CellStatus = EvalCellResult["status"];
|
|
192
|
+
type AgentStatus = "pending" | "running" | "completed" | "failed" | "aborted";
|
|
193
|
+
type StatusPresentation = { readonly label: string; readonly icon: string; readonly color: ThemeColor };
|
|
194
|
+
type RenderEnvironment = {
|
|
195
|
+
readonly expanded: boolean;
|
|
196
|
+
readonly theme: Theme | undefined;
|
|
197
|
+
readonly spinnerFrame: number | undefined;
|
|
198
|
+
readonly width: number;
|
|
199
|
+
readonly meta: TruncationMeta | undefined;
|
|
200
|
+
};
|
|
201
|
+
type CellBadges = { readonly reset: boolean; readonly timeout: number | undefined };
|
|
202
|
+
type PrefixStyle = { readonly prefix: string; readonly continuation: string; readonly color: ThemeColor };
|
|
203
|
+
type DetailedRenderContext = {
|
|
204
|
+
readonly environment: RenderEnvironment;
|
|
205
|
+
readonly args: EvalToolInput;
|
|
206
|
+
readonly showImageFallback: boolean;
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
function assertNever(value: never): never {
|
|
210
|
+
throw new TypeError(`Unhandled eval render variant: ${String(value)}`);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function languageForHighlighter(language: EvalLanguage): "python" | "javascript" | "ruby" | "julia" {
|
|
214
|
+
switch (language) {
|
|
215
|
+
case "py":
|
|
216
|
+
return "python";
|
|
217
|
+
case "js":
|
|
218
|
+
return "javascript";
|
|
219
|
+
case "rb":
|
|
220
|
+
return "ruby";
|
|
221
|
+
case "jl":
|
|
222
|
+
return "julia";
|
|
223
|
+
default:
|
|
224
|
+
return assertNever(language);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function highlightedCode(code: string, language: EvalLanguage, theme: Theme | undefined): string {
|
|
229
|
+
const normalizedCode = code.trim().length > 0 ? code : "...";
|
|
230
|
+
const lines = highlightCode(normalizedCode, languageForHighlighter(language));
|
|
231
|
+
return (theme === undefined ? lines.map((line) => line.replace(/\u001b\[[0-9;]*m/gu, "")) : lines).join("\n");
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function formatDuration(milliseconds: number): string {
|
|
235
|
+
const totalSeconds = Math.floor(Math.max(0, milliseconds) / 1_000);
|
|
236
|
+
if (totalSeconds < 1) return "<1s";
|
|
237
|
+
const seconds = totalSeconds % 60;
|
|
238
|
+
const totalMinutes = Math.floor(totalSeconds / 60);
|
|
239
|
+
if (totalMinutes < 1) return `${seconds}s`;
|
|
240
|
+
const minutes = totalMinutes % 60;
|
|
241
|
+
const hours = Math.floor(totalMinutes / 60);
|
|
242
|
+
if (hours < 1) return seconds === 0 ? `${minutes}m` : `${minutes}m ${seconds}s`;
|
|
243
|
+
return minutes === 0 ? `${hours}h` : `${hours}h ${minutes}m`;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function spinner(frame: number | undefined): string {
|
|
247
|
+
return SPINNER_FRAMES.at((frame ?? 0) % SPINNER_FRAMES.length) ?? SPINNER_FRAMES[0];
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function cellPresentation(status: CellStatus, spinnerFrame: number | undefined): StatusPresentation {
|
|
251
|
+
switch (status) {
|
|
252
|
+
case "pending":
|
|
253
|
+
return { label: "pending", icon: "○", color: "muted" };
|
|
254
|
+
case "running":
|
|
255
|
+
return { label: "running", icon: spinner(spinnerFrame), color: "warning" };
|
|
256
|
+
case "complete":
|
|
257
|
+
return { label: "done", icon: "✓", color: "success" };
|
|
258
|
+
case "error":
|
|
259
|
+
return { label: "error", icon: "✗", color: "error" };
|
|
260
|
+
default:
|
|
261
|
+
return assertNever(status);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function renderPrefixed(text: string, environment: RenderEnvironment, prefixStyle: PrefixStyle): string[] {
|
|
266
|
+
const bodyLines = renderAllVisualLines(text, Math.max(1, environment.width - prefixStyle.prefix.length));
|
|
267
|
+
if (bodyLines.length === 0) return [style(environment.theme, prefixStyle.color, prefixStyle.prefix.trimEnd())];
|
|
268
|
+
return bodyLines.map(
|
|
269
|
+
(line, index) =>
|
|
270
|
+
`${style(environment.theme, prefixStyle.color, index === 0 ? prefixStyle.prefix : prefixStyle.continuation)}${line}`,
|
|
271
|
+
);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function cellHeader(cell: EvalCellResult, environment: RenderEnvironment, badges: CellBadges): string {
|
|
275
|
+
const presentation = cellPresentation(cell.status, environment.spinnerFrame);
|
|
276
|
+
const title = cell.title === undefined ? "" : ` ${cell.title}`;
|
|
277
|
+
let header = `eval ${cell.language}${title} ${presentation.label} ${presentation.icon}`;
|
|
278
|
+
if (cell.durationMs !== undefined) header += ` · ${formatDuration(cell.durationMs)}`;
|
|
279
|
+
if (badges.reset) header += " · reset";
|
|
280
|
+
if (badges.timeout !== undefined) header += ` · timeout ${badges.timeout}s`;
|
|
281
|
+
return style(environment.theme, presentation.color, header);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function previewText(
|
|
285
|
+
text: string,
|
|
286
|
+
maxLines: number,
|
|
287
|
+
width: number,
|
|
288
|
+
): { readonly lines: string[]; readonly skipped: number } {
|
|
289
|
+
const preview = truncateToVisualLines(text, maxLines, Math.max(1, width));
|
|
290
|
+
return { lines: preview.visualLines.map((line) => line.trimEnd()), skipped: preview.skippedCount };
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function eventString(value: unknown): string | undefined {
|
|
294
|
+
return typeof value === "string" && value.length > 0 ? value : undefined;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function eventNumber(value: unknown): number {
|
|
298
|
+
return typeof value === "number" && Number.isFinite(value) ? value : 0;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function plural(count: number, singular: string, pluralNoun: string): string {
|
|
302
|
+
return `${count} ${count === 1 ? singular : pluralNoun}`;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function statusIcon(op: string): string {
|
|
306
|
+
if (op.startsWith("git_")) return "⌁";
|
|
307
|
+
switch (op) {
|
|
308
|
+
case "read":
|
|
309
|
+
case "write":
|
|
310
|
+
case "cat":
|
|
311
|
+
case "touch":
|
|
312
|
+
return "▣";
|
|
313
|
+
case "ls":
|
|
314
|
+
case "cd":
|
|
315
|
+
case "pwd":
|
|
316
|
+
case "mkdir":
|
|
317
|
+
return "▤";
|
|
318
|
+
case "run":
|
|
319
|
+
case "sh":
|
|
320
|
+
return "▶";
|
|
321
|
+
case "completion":
|
|
322
|
+
return "◇";
|
|
323
|
+
case "phase":
|
|
324
|
+
return "◆";
|
|
325
|
+
default:
|
|
326
|
+
return "•";
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function formatStatusEvent(event: EvalStatusEvent, theme: Theme | undefined): string {
|
|
331
|
+
const op = event.op;
|
|
332
|
+
const icon = style(theme, "muted", statusIcon(op));
|
|
333
|
+
const error = eventString(event.error);
|
|
334
|
+
if (error !== undefined) return `${icon} ${style(theme, "warning", op)}: ${style(theme, "dim", error)}`;
|
|
335
|
+
const parts: string[] = [];
|
|
336
|
+
switch (op) {
|
|
337
|
+
case "read": {
|
|
338
|
+
parts.push(`${eventNumber(event.chars ?? event.bytes)} chars`);
|
|
339
|
+
const path = eventString(event.path);
|
|
340
|
+
if (path !== undefined) parts.push(`from ${path}`);
|
|
341
|
+
break;
|
|
342
|
+
}
|
|
343
|
+
case "write": {
|
|
344
|
+
parts.push(`${eventNumber(event.chars ?? event.bytes)} chars`);
|
|
345
|
+
const path = eventString(event.path);
|
|
346
|
+
if (path !== undefined) parts.push(`to ${path}`);
|
|
347
|
+
break;
|
|
348
|
+
}
|
|
349
|
+
case "cat":
|
|
350
|
+
parts.push(plural(eventNumber(event.files), "file", "files"));
|
|
351
|
+
parts.push(`${eventNumber(event.chars)} chars`);
|
|
352
|
+
break;
|
|
353
|
+
case "ls":
|
|
354
|
+
parts.push(plural(eventNumber(event.count), "entry", "entries"));
|
|
355
|
+
break;
|
|
356
|
+
case "env": {
|
|
357
|
+
const action = eventString(event.action);
|
|
358
|
+
const key = eventString(event.key);
|
|
359
|
+
const value = eventString(event.value) ?? "";
|
|
360
|
+
if (action === "set" && key !== undefined) parts.push(`set ${key}=${value.slice(0, 30)}`);
|
|
361
|
+
else if (action === "get" && key !== undefined) parts.push(`${key}=${value.slice(0, 30)}`);
|
|
362
|
+
else parts.push(plural(eventNumber(event.count), "variable", "variables"));
|
|
363
|
+
break;
|
|
364
|
+
}
|
|
365
|
+
case "git_status": {
|
|
366
|
+
if (event.clean === true) parts.push("clean");
|
|
367
|
+
else {
|
|
368
|
+
const changes: string[] = [];
|
|
369
|
+
for (const key of ["staged", "modified", "untracked"] as const) {
|
|
370
|
+
const count = eventNumber(event[key]);
|
|
371
|
+
if (count > 0) changes.push(`${count} ${key}`);
|
|
372
|
+
}
|
|
373
|
+
parts.push(changes.join(", ") || "unknown");
|
|
374
|
+
}
|
|
375
|
+
const branch = eventString(event.branch);
|
|
376
|
+
if (branch !== undefined) parts.push(`on ${branch}`);
|
|
377
|
+
break;
|
|
378
|
+
}
|
|
379
|
+
case "git_diff":
|
|
380
|
+
parts.push(plural(eventNumber(event.lines), "line", "lines"));
|
|
381
|
+
if (event.staged === true) parts.push("staged");
|
|
382
|
+
break;
|
|
383
|
+
case "git_log":
|
|
384
|
+
parts.push(plural(eventNumber(event.commits), "commit", "commits"));
|
|
385
|
+
break;
|
|
386
|
+
case "run":
|
|
387
|
+
case "sh": {
|
|
388
|
+
const command = eventString(event.command ?? event.cmd);
|
|
389
|
+
if (command !== undefined) parts.push(command);
|
|
390
|
+
if (typeof event.exitCode === "number") parts.push(`exit ${event.exitCode}`);
|
|
391
|
+
break;
|
|
392
|
+
}
|
|
393
|
+
case "completion": {
|
|
394
|
+
const model = eventString(event.model);
|
|
395
|
+
const tier = eventString(event.tier);
|
|
396
|
+
if (model !== undefined) parts.push(model);
|
|
397
|
+
if (tier !== undefined && tier !== model) parts.push(tier);
|
|
398
|
+
parts.push(`${eventNumber(event.chars)} chars`);
|
|
399
|
+
break;
|
|
400
|
+
}
|
|
401
|
+
case "log":
|
|
402
|
+
parts.push(eventString(event.message) ?? "");
|
|
403
|
+
break;
|
|
404
|
+
case "phase":
|
|
405
|
+
parts.push(eventString(event.title) ?? "");
|
|
406
|
+
break;
|
|
407
|
+
default: {
|
|
408
|
+
if (event.count !== undefined) parts.push(String(event.count));
|
|
409
|
+
const path = eventString(event.path);
|
|
410
|
+
if (path !== undefined) parts.push(path);
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
const description = parts.filter((part) => part.length > 0).join(" · ");
|
|
414
|
+
return `${icon} ${style(theme, "muted", op)}${description.length > 0 ? ` ${style(theme, "dim", description)}` : ""}`;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
function renderStatusEvents(events: readonly EvalStatusEvent[], environment: RenderEnvironment): string[] {
|
|
418
|
+
const retained = environment.expanded ? events : events.slice(-STATUS_PREVIEW_COUNT);
|
|
419
|
+
const skipped = events.length - retained.length;
|
|
420
|
+
const lines: string[] = [];
|
|
421
|
+
if (skipped > 0) lines.push(style(environment.theme, "dim", `├ … ${skipped} earlier status events`));
|
|
422
|
+
for (const [index, event] of retained.entries()) {
|
|
423
|
+
const branch = index === retained.length - 1 ? "└" : "├";
|
|
424
|
+
lines.push(`${style(environment.theme, "dim", branch)} ${formatStatusEvent(event, environment.theme)}`);
|
|
425
|
+
}
|
|
426
|
+
return lines;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
function agentStatus(value: unknown): AgentStatus {
|
|
430
|
+
switch (value) {
|
|
431
|
+
case "pending":
|
|
432
|
+
case "running":
|
|
433
|
+
case "completed":
|
|
434
|
+
case "failed":
|
|
435
|
+
case "aborted":
|
|
436
|
+
return value;
|
|
437
|
+
default:
|
|
438
|
+
return "running";
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
function coalesceAgentEvents(events: readonly EvalStatusEvent[]): EvalStatusEvent[] {
|
|
443
|
+
const rows: EvalStatusEvent[] = [];
|
|
444
|
+
const indexes = new Map<string, number>();
|
|
445
|
+
for (const event of events) {
|
|
446
|
+
const id = eventString(event.id);
|
|
447
|
+
if (id === undefined) {
|
|
448
|
+
rows.push(event);
|
|
449
|
+
continue;
|
|
450
|
+
}
|
|
451
|
+
const index = indexes.get(id);
|
|
452
|
+
if (index === undefined) {
|
|
453
|
+
indexes.set(id, rows.length);
|
|
454
|
+
rows.push(event);
|
|
455
|
+
} else rows[index] = event;
|
|
456
|
+
}
|
|
457
|
+
return rows;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
function agentPresentation(status: AgentStatus, spinnerFrame: number | undefined): StatusPresentation {
|
|
461
|
+
switch (status) {
|
|
462
|
+
case "pending":
|
|
463
|
+
return { label: "pending", icon: "○", color: "muted" };
|
|
464
|
+
case "running":
|
|
465
|
+
return { label: "running", icon: spinner(spinnerFrame), color: "warning" };
|
|
466
|
+
case "completed":
|
|
467
|
+
return { label: "done", icon: "✓", color: "success" };
|
|
468
|
+
case "failed":
|
|
469
|
+
return { label: "failed", icon: "✗", color: "error" };
|
|
470
|
+
case "aborted":
|
|
471
|
+
return { label: "aborted", icon: "×", color: "error" };
|
|
472
|
+
default:
|
|
473
|
+
return assertNever(status);
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
function renderAgentProgressEvents(events: readonly EvalStatusEvent[], environment: RenderEnvironment): string[] {
|
|
478
|
+
const rows = coalesceAgentEvents(events);
|
|
479
|
+
const lines: string[] = [];
|
|
480
|
+
// Senpi Theme has no tree-token API; fixed ├/└/│ glyphs intentionally mirror omp.
|
|
481
|
+
for (const [index, event] of rows.entries()) {
|
|
482
|
+
const isLast = index === rows.length - 1;
|
|
483
|
+
const status = agentStatus(event.status);
|
|
484
|
+
const presentation = agentPresentation(status, environment.spinnerFrame);
|
|
485
|
+
const id = eventString(event.id) ?? "agent";
|
|
486
|
+
const styledId = environment.theme === undefined ? id : environment.theme.bold(id);
|
|
487
|
+
let body = `${style(environment.theme, presentation.color, presentation.icon)} ${styledId} ${presentation.label}`;
|
|
488
|
+
if (status === "completed" || status === "failed" || status === "aborted") {
|
|
489
|
+
const duration = eventNumber(event.durationMs);
|
|
490
|
+
if (duration > 0) body += ` · ${style(environment.theme, "dim", formatDuration(duration))}`;
|
|
491
|
+
}
|
|
492
|
+
const branch = isLast ? "└ " : "├ ";
|
|
493
|
+
const continuation = isLast ? " " : "│ ";
|
|
494
|
+
appendLines(
|
|
495
|
+
lines,
|
|
496
|
+
renderPrefixed(body, environment, { prefix: branch, continuation: continuation, color: "dim" }),
|
|
497
|
+
);
|
|
498
|
+
if (status !== "running") continue;
|
|
499
|
+
const currentTool = eventString(event.currentTool);
|
|
500
|
+
const lastIntent = eventString(event.lastIntent);
|
|
501
|
+
if (currentTool === undefined && lastIntent === undefined) continue;
|
|
502
|
+
const detail =
|
|
503
|
+
currentTool === undefined
|
|
504
|
+
? (lastIntent ?? "")
|
|
505
|
+
: `${currentTool}${lastIntent === undefined ? "" : `: ${lastIntent}`}`;
|
|
506
|
+
appendLines(
|
|
507
|
+
lines,
|
|
508
|
+
renderPrefixed(detail, environment, {
|
|
509
|
+
prefix: `${continuation}└ `,
|
|
510
|
+
continuation: `${continuation} `,
|
|
511
|
+
color: "dim",
|
|
512
|
+
}),
|
|
513
|
+
);
|
|
514
|
+
}
|
|
515
|
+
return lines;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
function renderCell(cell: EvalCellResult, environment: RenderEnvironment, badges: CellBadges): string[] {
|
|
519
|
+
const lines = renderPrefixed(cellHeader(cell, environment, badges), environment, {
|
|
520
|
+
prefix: "╭─ ",
|
|
521
|
+
continuation: "│ ",
|
|
522
|
+
color: "borderAccent",
|
|
523
|
+
});
|
|
524
|
+
const innerWidth = Math.max(1, environment.width - 2);
|
|
525
|
+
const codePreview = previewText(
|
|
526
|
+
highlightedCode(cell.code, cell.language, environment.theme),
|
|
527
|
+
environment.expanded ? Number.POSITIVE_INFINITY : CODE_PREVIEW_LINES,
|
|
528
|
+
innerWidth,
|
|
529
|
+
);
|
|
530
|
+
if (codePreview.skipped > 0) {
|
|
531
|
+
appendLines(
|
|
532
|
+
lines,
|
|
533
|
+
renderPrefixed(`${codePreview.skipped} earlier code lines`, environment, {
|
|
534
|
+
prefix: "│ ",
|
|
535
|
+
continuation: "│ ",
|
|
536
|
+
color: "muted",
|
|
537
|
+
}),
|
|
538
|
+
);
|
|
539
|
+
}
|
|
540
|
+
for (const line of codePreview.lines) {
|
|
541
|
+
appendLines(lines, renderPrefixed(line, environment, { prefix: "│ ", continuation: "│ ", color: "borderMuted" }));
|
|
542
|
+
}
|
|
543
|
+
const output = stripOutputNotice(cell.output, environment.meta).trimEnd();
|
|
544
|
+
if (output.length > 0) {
|
|
545
|
+
appendLines(lines, renderPrefixed("output", environment, { prefix: "├─ ", continuation: "│ ", color: "dim" }));
|
|
546
|
+
const outputColor: ThemeColor = cell.status === "error" ? "error" : "toolOutput";
|
|
547
|
+
const styledOutput = output
|
|
548
|
+
.split("\n")
|
|
549
|
+
.map((line) => style(environment.theme, outputColor, line))
|
|
550
|
+
.join("\n");
|
|
551
|
+
const outputPreview = previewText(
|
|
552
|
+
styledOutput,
|
|
553
|
+
environment.expanded ? Number.POSITIVE_INFINITY : OUTPUT_PREVIEW_LINES,
|
|
554
|
+
innerWidth,
|
|
555
|
+
);
|
|
556
|
+
if (outputPreview.skipped > 0) {
|
|
557
|
+
appendLines(
|
|
558
|
+
lines,
|
|
559
|
+
renderPrefixed(`${outputPreview.skipped} earlier output lines`, environment, {
|
|
560
|
+
prefix: "│ ",
|
|
561
|
+
continuation: "│ ",
|
|
562
|
+
color: "muted",
|
|
563
|
+
}),
|
|
564
|
+
);
|
|
565
|
+
}
|
|
566
|
+
for (const line of outputPreview.lines) {
|
|
567
|
+
appendLines(
|
|
568
|
+
lines,
|
|
569
|
+
renderPrefixed(line, environment, { prefix: "│ ", continuation: "│ ", color: "borderMuted" }),
|
|
570
|
+
);
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
const allEvents = cell.statusEvents ?? [];
|
|
574
|
+
const statusEvents = allEvents.filter((event) => event.op !== "agent");
|
|
575
|
+
if (statusEvents.length > 0) {
|
|
576
|
+
appendLines(lines, renderPrefixed("status", environment, { prefix: "├─ ", continuation: "│ ", color: "dim" }));
|
|
577
|
+
for (const line of renderStatusEvents(statusEvents, environment)) {
|
|
578
|
+
appendLines(
|
|
579
|
+
lines,
|
|
580
|
+
renderPrefixed(line, environment, { prefix: "│ ", continuation: "│ ", color: "borderMuted" }),
|
|
581
|
+
);
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
lines.push(style(environment.theme, "borderMuted", "╰─"));
|
|
585
|
+
const agentEvents = allEvents.filter((event) => event.op === "agent");
|
|
586
|
+
if (agentEvents.length > 0) appendLines(lines, renderAgentProgressEvents(agentEvents, environment));
|
|
587
|
+
return lines;
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
function renderJsonOutputs(values: readonly unknown[], environment: RenderEnvironment): string[] {
|
|
591
|
+
const lines: string[] = [];
|
|
592
|
+
const depth = environment.expanded ? JSON_TREE_MAX_DEPTH_EXPANDED : JSON_TREE_MAX_DEPTH_COLLAPSED;
|
|
593
|
+
const lineCap = environment.expanded ? JSON_TREE_MAX_LINES_EXPANDED : JSON_TREE_MAX_LINES_COLLAPSED;
|
|
594
|
+
const scalarLen = environment.expanded ? JSON_TREE_SCALAR_LEN_EXPANDED : JSON_TREE_SCALAR_LEN_COLLAPSED;
|
|
595
|
+
for (const [index, value] of values.entries()) {
|
|
596
|
+
appendLines(
|
|
597
|
+
lines,
|
|
598
|
+
renderAllVisualLines(style(environment.theme, "dim", `display[${index + 1}]`), environment.width),
|
|
599
|
+
);
|
|
600
|
+
const tree = renderJsonTreeLines(value, environment.theme, depth, lineCap, scalarLen);
|
|
601
|
+
for (const line of tree.lines) appendLines(lines, renderAllVisualLines(line, environment.width));
|
|
602
|
+
if (tree.truncated)
|
|
603
|
+
appendLines(lines, renderAllVisualLines(style(environment.theme, "dim", "…"), environment.width));
|
|
604
|
+
}
|
|
605
|
+
return lines;
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
function renderDetailedLines(
|
|
609
|
+
details: EvalToolDetails,
|
|
610
|
+
result: AgentToolResult<EvalToolDetails>,
|
|
611
|
+
context: DetailedRenderContext,
|
|
612
|
+
): string[] {
|
|
613
|
+
const lines: string[] = [];
|
|
614
|
+
const cells = details.cells ?? [];
|
|
615
|
+
for (const [index, cell] of cells.entries()) {
|
|
616
|
+
const badges = {
|
|
617
|
+
reset: index === 0 && context.args.reset === true,
|
|
618
|
+
timeout: index === 0 ? context.args.timeout : undefined,
|
|
619
|
+
};
|
|
620
|
+
appendLines(lines, renderCell(cell, context.environment, badges));
|
|
621
|
+
if (index < cells.length - 1) lines.push("");
|
|
622
|
+
}
|
|
623
|
+
const jsonOutputs = details.jsonOutputs ?? [];
|
|
624
|
+
if (jsonOutputs.length > 0) {
|
|
625
|
+
if (lines.length > 0) lines.push("");
|
|
626
|
+
appendLines(lines, renderJsonOutputs(jsonOutputs, context.environment));
|
|
627
|
+
}
|
|
628
|
+
if (context.showImageFallback) {
|
|
629
|
+
for (const part of result.content) {
|
|
630
|
+
if (part.type !== "image") continue;
|
|
631
|
+
if (lines.length > 0) lines.push("");
|
|
632
|
+
appendLines(
|
|
633
|
+
lines,
|
|
634
|
+
renderAllVisualLines(`[image: ${sanitizeTerminalLabel(part.mimeType)}]`, context.environment.width),
|
|
635
|
+
);
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
if (details.phase !== undefined)
|
|
639
|
+
appendLines(
|
|
640
|
+
lines,
|
|
641
|
+
renderAllVisualLines(
|
|
642
|
+
style(context.environment.theme, "muted", `phase ${details.phase}`),
|
|
643
|
+
context.environment.width,
|
|
644
|
+
),
|
|
645
|
+
);
|
|
646
|
+
if (details.notice !== undefined)
|
|
647
|
+
appendLines(
|
|
648
|
+
lines,
|
|
649
|
+
renderAllVisualLines(style(context.environment.theme, "dim", details.notice), context.environment.width),
|
|
650
|
+
);
|
|
651
|
+
const warning = formatTruncationWarning(details.meta) ?? (details.truncated ? "[eval output truncated]" : null);
|
|
652
|
+
if (warning !== null)
|
|
653
|
+
appendLines(
|
|
654
|
+
lines,
|
|
655
|
+
renderAllVisualLines(style(context.environment.theme, "warning", warning), context.environment.width),
|
|
656
|
+
);
|
|
657
|
+
return lines;
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
function textOutput(result: AgentToolResult<EvalToolDetails>, showImageFallback: boolean): string {
|
|
661
|
+
const lines: string[] = [];
|
|
662
|
+
for (const part of result.content) {
|
|
663
|
+
if (part.type === "text") lines.push(part.text);
|
|
664
|
+
else if (showImageFallback && part.type === "image") {
|
|
665
|
+
lines.push(`[image: ${sanitizeTerminalLabel(part.mimeType)}]`);
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
return lines.join("\n");
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
function toolCallRows(details: EvalToolDetails | undefined): ToolCallRow[] {
|
|
672
|
+
if (!details?.toolCalls || details.toolCalls.length === 0) return [];
|
|
673
|
+
return details.toolCalls.map((call) => {
|
|
674
|
+
const status = call.ok ? "ok" : "error";
|
|
675
|
+
const row = { summary: `- tool.${call.name}: ${status}`, color: call.ok ? "success" : "error" } as const;
|
|
676
|
+
return call.error === undefined ? row : { ...row, error: call.error };
|
|
677
|
+
});
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
function resultStatus(
|
|
681
|
+
details: EvalToolDetails | undefined,
|
|
682
|
+
options: ToolRenderResultOptions,
|
|
683
|
+
hostIsError: boolean,
|
|
684
|
+
): "running" | "done" | "error" {
|
|
685
|
+
if (details?.isError || hostIsError) return "error";
|
|
686
|
+
return options.isPartial ? "running" : "done";
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
function resultHeader(
|
|
690
|
+
details: EvalToolDetails | undefined,
|
|
691
|
+
status: "running" | "done" | "error",
|
|
692
|
+
theme: Theme | undefined,
|
|
693
|
+
): string {
|
|
694
|
+
const title = details?.title === undefined ? "" : ` ${details.title}`;
|
|
695
|
+
let color: ThemeColor;
|
|
696
|
+
switch (status) {
|
|
697
|
+
case "running":
|
|
698
|
+
color = "warning";
|
|
699
|
+
break;
|
|
700
|
+
case "done":
|
|
701
|
+
color = "success";
|
|
702
|
+
break;
|
|
703
|
+
case "error":
|
|
704
|
+
color = "error";
|
|
705
|
+
break;
|
|
706
|
+
}
|
|
707
|
+
return style(theme, color, `eval ${details?.language ?? "?"}${title} ${status}`);
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
function resultMetadata(
|
|
711
|
+
details: EvalToolDetails | undefined,
|
|
712
|
+
options: ToolRenderResultOptions,
|
|
713
|
+
theme: Theme | undefined,
|
|
714
|
+
): RenderBlock[] {
|
|
715
|
+
const metadata: string[] = [];
|
|
716
|
+
if (details?.phase) metadata.push(`phase ${details.phase}`);
|
|
717
|
+
if (!options.isPartial && details) metadata.push(`took ${details.durationMs}ms`);
|
|
718
|
+
if (metadata.length === 0) return [];
|
|
719
|
+
return [{ kind: "text", text: style(theme, "muted", metadata.join(" | ")) }];
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
export function renderEvalCall(
|
|
723
|
+
args: EvalToolInput,
|
|
724
|
+
theme: Theme | undefined,
|
|
725
|
+
context: RenderContext,
|
|
726
|
+
): EvalRenderComponent {
|
|
727
|
+
const component = componentFor(context);
|
|
728
|
+
if (context.hasResult === true) {
|
|
729
|
+
// The result renderer owns the full pending -> running -> done frame once a result exists.
|
|
730
|
+
// Rendering the call frame too would stack a duplicate box, so yield to it here.
|
|
731
|
+
component.setBlocks([]);
|
|
732
|
+
return component;
|
|
733
|
+
}
|
|
734
|
+
if (theme === undefined && context.spinnerFrame === undefined) {
|
|
735
|
+
const title = args.title === undefined ? "" : ` ${args.title}`;
|
|
736
|
+
const reset = args.reset === true ? " reset" : "";
|
|
737
|
+
const timeout = args.timeout === undefined ? "" : ` timeout ${args.timeout}s`;
|
|
738
|
+
component.setBlocks([
|
|
739
|
+
{ kind: "text", text: style(theme, "toolTitle", `eval ${args.language}${title}${reset}${timeout}`) },
|
|
740
|
+
{
|
|
741
|
+
kind: "text",
|
|
742
|
+
text: style(theme, "mdCodeBlock", args.code.trim().length > 0 ? args.code : "..."),
|
|
743
|
+
maxVisualLines: context.expanded ? undefined : CODE_PREVIEW_LINES,
|
|
744
|
+
collapseKind: "code",
|
|
745
|
+
theme,
|
|
746
|
+
},
|
|
747
|
+
]);
|
|
748
|
+
return component;
|
|
749
|
+
}
|
|
750
|
+
component.setBlocks([
|
|
751
|
+
{
|
|
752
|
+
kind: "dynamic",
|
|
753
|
+
render: (width) => {
|
|
754
|
+
const environment: RenderEnvironment = {
|
|
755
|
+
expanded: context.expanded,
|
|
756
|
+
theme,
|
|
757
|
+
spinnerFrame: context.spinnerFrame,
|
|
758
|
+
width,
|
|
759
|
+
meta: undefined,
|
|
760
|
+
};
|
|
761
|
+
const cell: EvalCellResult = {
|
|
762
|
+
index: 0,
|
|
763
|
+
...(args.title === undefined ? {} : { title: args.title }),
|
|
764
|
+
code: args.code,
|
|
765
|
+
language: args.language,
|
|
766
|
+
output: "",
|
|
767
|
+
status: context.spinnerFrame === undefined ? "pending" : "running",
|
|
768
|
+
};
|
|
769
|
+
return renderCell(cell, environment, { reset: args.reset === true, timeout: args.timeout });
|
|
770
|
+
},
|
|
771
|
+
},
|
|
772
|
+
]);
|
|
773
|
+
return component;
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
export function renderEvalResult(
|
|
777
|
+
result: AgentToolResult<EvalToolDetails>,
|
|
778
|
+
options: ToolRenderResultOptions,
|
|
779
|
+
theme: Theme | undefined,
|
|
780
|
+
context: ResultRenderContext,
|
|
781
|
+
): EvalRenderComponent {
|
|
782
|
+
const component = componentFor(context);
|
|
783
|
+
const details = result.details;
|
|
784
|
+
const expanded = options.expanded || context.expanded;
|
|
785
|
+
const imageProtocol = context.imageProtocol ?? null;
|
|
786
|
+
if (details?.cells !== undefined && details.cells.length > 0) {
|
|
787
|
+
const blocks: RenderBlock[] = [
|
|
788
|
+
{
|
|
789
|
+
kind: "dynamic",
|
|
790
|
+
render: (width) =>
|
|
791
|
+
renderDetailedLines(details, result, {
|
|
792
|
+
environment: {
|
|
793
|
+
expanded,
|
|
794
|
+
theme,
|
|
795
|
+
spinnerFrame: context.spinnerFrame,
|
|
796
|
+
width,
|
|
797
|
+
meta: details.meta,
|
|
798
|
+
},
|
|
799
|
+
args: context.args,
|
|
800
|
+
showImageFallback: context.showImages && imageProtocol === null,
|
|
801
|
+
}),
|
|
802
|
+
},
|
|
803
|
+
];
|
|
804
|
+
const calls = toolCallRows(details);
|
|
805
|
+
if (calls.length > 0) blocks.push({ kind: "blank" }, { kind: "toolCalls", calls, expanded, theme });
|
|
806
|
+
component.setBlocks(blocks);
|
|
807
|
+
return component;
|
|
808
|
+
}
|
|
809
|
+
const status = resultStatus(details, options, context.isError);
|
|
810
|
+
const blocks: RenderBlock[] = [
|
|
811
|
+
{ kind: "text", text: resultHeader(details, status, theme) },
|
|
812
|
+
...resultMetadata(details, options, theme),
|
|
813
|
+
{ kind: "blank" },
|
|
814
|
+
];
|
|
815
|
+
const rawOutput = textOutput(result, context.showImages && imageProtocol === null);
|
|
816
|
+
const output = stripOutputNotice(rawOutput, details?.meta).trimEnd();
|
|
817
|
+
const hasRenderedImage =
|
|
818
|
+
context.showImages && imageProtocol !== null && result.content.some((part) => part.type === "image");
|
|
819
|
+
if (output.length > 0) {
|
|
820
|
+
blocks.push({
|
|
821
|
+
kind: "text",
|
|
822
|
+
text: style(theme, "toolOutput", output),
|
|
823
|
+
maxVisualLines: expanded ? undefined : OUTPUT_PREVIEW_LINES,
|
|
824
|
+
collapseKind: "output",
|
|
825
|
+
theme,
|
|
826
|
+
});
|
|
827
|
+
} else if (!hasRenderedImage) blocks.push({ kind: "text", text: style(theme, "muted", "(no output)") });
|
|
828
|
+
const statusEvents = details?.statusEvents ?? [];
|
|
829
|
+
const nonAgentEvents = statusEvents.filter((event) => event.op !== "agent");
|
|
830
|
+
const agentEvents = statusEvents.filter((event) => event.op === "agent");
|
|
831
|
+
if (nonAgentEvents.length > 0 || agentEvents.length > 0) {
|
|
832
|
+
blocks.push(
|
|
833
|
+
{ kind: "blank" },
|
|
834
|
+
{
|
|
835
|
+
kind: "dynamic",
|
|
836
|
+
render: (width) => {
|
|
837
|
+
const environment: RenderEnvironment = {
|
|
838
|
+
expanded,
|
|
839
|
+
theme,
|
|
840
|
+
spinnerFrame: context.spinnerFrame,
|
|
841
|
+
width,
|
|
842
|
+
meta: details?.meta,
|
|
843
|
+
};
|
|
844
|
+
return [
|
|
845
|
+
...renderStatusEvents(nonAgentEvents, environment),
|
|
846
|
+
...renderAgentProgressEvents(agentEvents, environment),
|
|
847
|
+
];
|
|
848
|
+
},
|
|
849
|
+
},
|
|
850
|
+
);
|
|
851
|
+
}
|
|
852
|
+
if ((details?.jsonOutputs?.length ?? 0) > 0) {
|
|
853
|
+
blocks.push(
|
|
854
|
+
{ kind: "blank" },
|
|
855
|
+
{
|
|
856
|
+
kind: "dynamic",
|
|
857
|
+
render: (width) =>
|
|
858
|
+
renderJsonOutputs(details?.jsonOutputs ?? [], {
|
|
859
|
+
expanded,
|
|
860
|
+
theme,
|
|
861
|
+
spinnerFrame: context.spinnerFrame,
|
|
862
|
+
width,
|
|
863
|
+
meta: details?.meta,
|
|
864
|
+
}),
|
|
865
|
+
},
|
|
866
|
+
);
|
|
867
|
+
}
|
|
868
|
+
const calls = toolCallRows(details);
|
|
869
|
+
if (calls.length > 0) blocks.push({ kind: "blank" }, { kind: "toolCalls", calls, expanded, theme });
|
|
870
|
+
if (details?.notice !== undefined)
|
|
871
|
+
blocks.push({ kind: "blank" }, { kind: "text", text: style(theme, "dim", details.notice) });
|
|
872
|
+
const warning = formatTruncationWarning(details?.meta) ?? (details?.truncated ? "[eval output truncated]" : null);
|
|
873
|
+
if (warning !== null) blocks.push({ kind: "blank" }, { kind: "text", text: style(theme, "warning", warning) });
|
|
874
|
+
component.setBlocks(blocks);
|
|
875
|
+
return component;
|
|
876
|
+
}
|