@code-yeongyu/senpi-codemode 2026.10.10-10 → 2026.10.10-12

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 (47) hide show
  1. package/CHANGELOG.md +46 -0
  2. package/package.json +4 -4
  3. package/src/bridge/memory-protocol.ts +8 -1
  4. package/src/bridge/protocol.ts +1 -1
  5. package/src/bridges/eval-environment-schemas.ts +5 -1
  6. package/src/environments/editable-check.ts +1 -1
  7. package/src/extension/session-cwd.ts +25 -9
  8. package/src/extension/session-manager.ts +4 -4
  9. package/src/kernels/jl/runner.jl +11 -22
  10. package/src/kernels/js/group-signal-notice.js +117 -0
  11. package/src/kernels/js/inline-worker.ts +2 -0
  12. package/src/kernels/js/local-module-loader.ts +8 -0
  13. package/src/kernels/js/process-tree-host.ts +4 -8
  14. package/src/kernels/js/process-tree.d.ts +11 -0
  15. package/src/kernels/js/process-tree.js +55 -10
  16. package/src/kernels/js/windows-tree-kill-host.ts +117 -0
  17. package/src/kernels/js/windows-tree-kill.d.ts +19 -0
  18. package/src/kernels/js/windows-tree-kill.js +100 -0
  19. package/src/kernels/js/worker-cwd.js +41 -10
  20. package/src/kernels/js/worker-runtime.js +23 -4
  21. package/src/kernels/js/worker-shell-capture.js +4 -0
  22. package/src/kernels/js/worker-slot.ts +2 -3
  23. package/src/kernels/py/prelude.py +36 -25
  24. package/src/kernels/py/transport.ts +4 -0
  25. package/src/kernels/rb/runner.rb +9 -19
  26. package/src/kernels/sandbox/vendor/pi-codemode/runtime/host.ts +2 -2
  27. package/src/kernels/shared/bun-ffi.ts +24 -0
  28. package/src/kernels/shared/kernel-memory-host.ts +30 -7
  29. package/src/kernels/shared/process-cpu.ts +7 -3
  30. package/src/kernels/shared/process-group-cpu.ts +6 -3
  31. package/src/kernels/shared/subprocess-kernel.ts +29 -3
  32. package/src/kernels/shared/subprocess-memory-globals.ts +101 -0
  33. package/src/kernels/shared/subprocess-process.ts +20 -12
  34. package/src/kernels/shared/subprocess-queue.ts +1 -0
  35. package/src/tool/eval-tool.ts +2 -1
  36. package/src/tool/render-agent.ts +110 -0
  37. package/src/tool/render-blocks.ts +252 -0
  38. package/src/tool/render-call.ts +130 -0
  39. package/src/tool/render-cell.ts +165 -0
  40. package/src/tool/render-detail.ts +152 -0
  41. package/src/tool/render-live.ts +288 -0
  42. package/src/tool/render-result.ts +202 -0
  43. package/src/tool/render-status.ts +166 -0
  44. package/src/tool/render-tail.ts +139 -0
  45. package/src/tool/render.ts +3 -1128
  46. package/src/tool/run-eval-cell.ts +7 -12
  47. package/src/tool/types.ts +11 -2
@@ -1,1128 +1,3 @@
1
- // allow: SIZE_OK — one eval render pipeline avoids the runtime/render TDZ that motivated this module boundary.
2
- import {
3
- type AgentToolResult,
4
- sanitizeTerminalLabel,
5
- type Theme,
6
- type ThemeColor,
7
- type ToolDefinition,
8
- type ToolRenderResultOptions,
9
- truncateToVisualLines,
10
- visibleWidth,
11
- } from "@code-yeongyu/senpi";
12
- import type { TruncationMeta } from "../output/output-meta.ts";
13
- import { highlightedCode } from "./code-preview.ts";
14
- import { displayCode } from "./display-code.ts";
15
- import { normalizeEvalSummary } from "./eval-request.ts";
16
- import {
17
- JSON_TREE_MAX_DEPTH_COLLAPSED,
18
- JSON_TREE_MAX_DEPTH_EXPANDED,
19
- JSON_TREE_MAX_LINES_COLLAPSED,
20
- JSON_TREE_MAX_LINES_EXPANDED,
21
- JSON_TREE_SCALAR_LEN_COLLAPSED,
22
- JSON_TREE_SCALAR_LEN_EXPANDED,
23
- renderJsonTreeLines,
24
- } from "./json-tree.ts";
25
- import { knownLanguage, leadsWithHeadline, liveHeadline } from "./live-headline.ts";
26
- import { formatRuntimeBadge } from "./runtime-label.ts";
27
- import { codePointPrefix, formatDuration, renderToolCallWidget } from "./tool-widgets.ts";
28
- import type {
29
- EvalCellResult,
30
- EvalInputSchema,
31
- EvalResultDetails,
32
- EvalStatusEvent,
33
- EvalToolDetails,
34
- EvalToolInput,
35
- EvalToolRequest,
36
- } from "./types.ts";
37
-
38
- type EvalToolDefinition = ToolDefinition<EvalInputSchema, EvalResultDetails>;
39
- type RenderContext = Parameters<NonNullable<EvalToolDefinition["renderCall"]>>[2];
40
- type ResultRenderContext = Parameters<NonNullable<EvalToolDefinition["renderResult"]>>[3];
41
- type CollapsibleKind = "code" | "output";
42
-
43
- export interface EvalRenderComponent {
44
- render(width: number): string[];
45
- invalidate(): void;
46
- }
47
- type ToolCallRow = {
48
- readonly summary: string;
49
- readonly error?: string;
50
- readonly color: "success" | "error";
51
- };
52
- type RenderBlock =
53
- | { readonly kind: "blank" }
54
- | {
55
- readonly kind: "text";
56
- readonly text: string;
57
- readonly maxVisualLines?: number;
58
- readonly collapseKind?: CollapsibleKind;
59
- readonly theme?: Theme;
60
- }
61
- | {
62
- readonly kind: "toolCalls";
63
- readonly calls: readonly ToolCallRow[];
64
- readonly expanded: boolean;
65
- readonly theme?: Theme;
66
- }
67
- | { readonly kind: "dynamic"; readonly render: (width: number) => readonly string[] };
68
-
69
- const CODE_PREVIEW_LINES = 4;
70
- const SUMMARY_PREVIEW_LINES = 3;
71
- const OUTPUT_PREVIEW_LINES = 8;
72
- const STATUS_PREVIEW_COUNT = 3;
73
- const SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"] as const;
74
- const TOOL_CALL_PREVIEW_COUNT = 5;
75
- const TOOL_CALL_COLLAPSED_VISUAL_LINES = 4;
76
- const TOOL_CALL_COLLAPSED_ERROR_CODE_POINTS = 512;
77
- const TOOL_ERROR_OMISSION_MARKER = "[tool error omitted]";
78
- const LIVE_RENDER_TICK_MS = 100;
79
- // A live row repaints on every tick, so this many ticks without a render means the row is gone.
80
- const LIVE_TICKER_MAX_IDLE_TICKS = 600;
81
-
82
- class PlainTextComponent implements EvalRenderComponent {
83
- #blocks: readonly RenderBlock[] = [];
84
- #ticker: ReturnType<typeof setInterval> | undefined;
85
- #live = false;
86
- #invalidate: (() => void) | undefined;
87
- #idleTicks = 0;
88
-
89
- setBlocks(blocks: readonly RenderBlock[]): void {
90
- this.#blocks = blocks;
91
- }
92
-
93
- /**
94
- * The host only animates tool rows for streaming args, `task`, and results carrying
95
- * `details.progress`; an eval row matches none of them, so nothing repaints it between
96
- * update events. While a cell is non-terminal this drives the repaint itself so the
97
- * header's spinner and elapsed time advance, with no tool updates or RPC traffic. Detached
98
- * and terminal cards never arm it, and a ticker whose row stopped rendering (transcript
99
- * rebuild, session switch) stops itself after LIVE_TICKER_MAX_IDLE_TICKS and rearms on
100
- * the next render, so dropped rows cannot accumulate intervals.
101
- */
102
- syncLiveTicker(isLive: boolean, invalidate: () => void): void {
103
- this.#live = isLive;
104
- this.#invalidate = invalidate;
105
- if (!isLive) {
106
- this.stopLiveTicker();
107
- return;
108
- }
109
- this.#armTicker();
110
- }
111
-
112
- stopLiveTicker(): void {
113
- if (this.#ticker === undefined) return;
114
- clearInterval(this.#ticker);
115
- this.#ticker = undefined;
116
- }
117
-
118
- #armTicker(): void {
119
- if (this.#ticker !== undefined || this.#invalidate === undefined) return;
120
- this.#ticker = setInterval(() => this.#tick(), LIVE_RENDER_TICK_MS);
121
- this.#ticker.unref?.();
122
- }
123
-
124
- #tick(): void {
125
- this.#idleTicks += 1;
126
- if (this.#idleTicks >= LIVE_TICKER_MAX_IDLE_TICKS) {
127
- this.stopLiveTicker();
128
- return;
129
- }
130
- this.#invalidate?.();
131
- }
132
-
133
- render(width: number): string[] {
134
- this.#idleTicks = 0;
135
- if (this.#live) this.#armTicker();
136
- const lines: string[] = [];
137
- for (const block of this.#blocks) {
138
- switch (block.kind) {
139
- case "blank":
140
- lines.push("");
141
- break;
142
- case "toolCalls":
143
- appendLines(lines, renderToolCallBlock(block, width));
144
- break;
145
- case "text":
146
- appendLines(lines, renderTextBlock(block, width));
147
- break;
148
- case "dynamic":
149
- appendLines(lines, block.render(width));
150
- break;
151
- default:
152
- assertNever(block);
153
- }
154
- }
155
- return lines;
156
- }
157
-
158
- invalidate(): void {}
159
- }
160
-
161
- function componentFor(context: RenderContext | ResultRenderContext): PlainTextComponent {
162
- const existing = context.lastComponent;
163
- if (existing instanceof PlainTextComponent) return existing;
164
- return new PlainTextComponent();
165
- }
166
-
167
- /** Render-time clock; tests inject a fixed value so elapsed output never depends on wall time. */
168
- function renderNow(context: RenderContext | ResultRenderContext): number {
169
- const injected: unknown = Reflect.get(context, "now");
170
- return typeof injected === "number" && Number.isFinite(injected) ? injected : Date.now();
171
- }
172
-
173
- function hasLiveCell(details: EvalToolDetails | undefined): boolean {
174
- return (details?.cells ?? []).some((cell) => isLiveCellStatus(cell.status) && cell.startedAt !== undefined);
175
- }
176
-
177
- function style(theme: Theme | undefined, color: ThemeColor, text: string): string {
178
- return theme ? theme.fg(color, text) : text;
179
- }
180
-
181
- function appendLines(target: string[], source: readonly string[]): void {
182
- for (const line of source) target.push(line);
183
- }
184
-
185
- function renderAllVisualLines(text: string, width: number): string[] {
186
- return truncateToVisualLines(text, Number.POSITIVE_INFINITY, width).visualLines.map((line) => line.trimEnd());
187
- }
188
-
189
- // A summary has no length limit, so a collapsed block shows its first lines and marks the cut.
190
- function summaryVisualLines(summary: string, width: number, expanded: boolean): string[] {
191
- const lines = renderAllVisualLines(summary, width);
192
- if (expanded || lines.length <= SUMMARY_PREVIEW_LINES) return lines;
193
- const kept = renderAllVisualLines(summary, Math.max(1, width - 1)).slice(0, SUMMARY_PREVIEW_LINES);
194
- kept[SUMMARY_PREVIEW_LINES - 1] = `${kept[SUMMARY_PREVIEW_LINES - 1] ?? ""}\u2026`;
195
- return kept;
196
- }
197
-
198
- function summaryBlock(summary: string, theme: Theme | undefined, expanded: boolean): RenderBlock {
199
- return {
200
- kind: "dynamic",
201
- render: (width) => summaryVisualLines(summary, width, expanded).map((line) => style(theme, "muted", line)),
202
- };
203
- }
204
-
205
- function renderTextBlock(block: Extract<RenderBlock, { kind: "text" }>, width: number): string[] {
206
- if (block.maxVisualLines === undefined) return renderAllVisualLines(block.text, width);
207
- const result = truncateToVisualLines(block.text, block.maxVisualLines, width);
208
- const visualLines = result.visualLines.map((line) => line.trimEnd());
209
- if (result.skippedCount === 0 || block.collapseKind === undefined) return visualLines;
210
- return [
211
- ...renderAllVisualLines(
212
- style(block.theme, "muted", `${result.skippedCount} earlier ${block.collapseKind} lines`),
213
- width,
214
- ),
215
- ...visualLines,
216
- ];
217
- }
218
-
219
- function renderToolCall(
220
- call: ToolCallRow,
221
- block: Extract<RenderBlock, { kind: "toolCalls" }>,
222
- width: number,
223
- ): string[] {
224
- if (call.error === undefined) return renderAllVisualLines(style(block.theme, call.color, call.summary), width);
225
- if (block.expanded)
226
- return renderAllVisualLines(style(block.theme, call.color, `${call.summary} (${call.error})`), width);
227
-
228
- const guardedError = codePointPrefix(call.error, TOOL_CALL_COLLAPSED_ERROR_CODE_POINTS);
229
- const guardedLines = renderAllVisualLines(
230
- style(block.theme, call.color, `${call.summary} (${guardedError})`),
231
- width,
232
- );
233
- if (guardedError.length === call.error.length && guardedLines.length <= TOOL_CALL_COLLAPSED_VISUAL_LINES)
234
- return guardedLines;
235
-
236
- const summaryLines = renderAllVisualLines(style(block.theme, call.color, call.summary), width);
237
- const errorLines = renderAllVisualLines(style(block.theme, call.color, ` (${guardedError})`), width);
238
- const markerLines = renderAllVisualLines(style(block.theme, "muted", TOOL_ERROR_OMISSION_MARKER), width);
239
- const lines: string[] = [];
240
- const summaryBudget = Math.max(1, TOOL_CALL_COLLAPSED_VISUAL_LINES - markerLines.length);
241
- appendLines(lines, summaryLines.slice(0, summaryBudget));
242
- const errorBudget = Math.max(0, TOOL_CALL_COLLAPSED_VISUAL_LINES - lines.length - markerLines.length);
243
- appendLines(lines, errorLines.slice(0, errorBudget));
244
- appendLines(lines, markerLines.slice(0, TOOL_CALL_COLLAPSED_VISUAL_LINES - lines.length));
245
- return lines;
246
- }
247
-
248
- function renderToolCallBlock(block: Extract<RenderBlock, { kind: "toolCalls" }>, width: number): string[] {
249
- const retainedCalls = block.expanded ? block.calls : block.calls.slice(-TOOL_CALL_PREVIEW_COUNT);
250
- const skippedCount = block.calls.length - retainedCalls.length;
251
- const toolCallNoun = skippedCount === 1 ? "call" : "calls";
252
- const lines =
253
- block.expanded || skippedCount === 0
254
- ? []
255
- : renderAllVisualLines(style(block.theme, "muted", `${skippedCount} earlier tool ${toolCallNoun}`), width);
256
- for (const call of retainedCalls) {
257
- appendLines(lines, renderToolCall(call, block, width));
258
- }
259
- return lines;
260
- }
261
-
262
- type CellStatus = EvalCellResult["status"];
263
- type AgentStatus = "pending" | "running" | "completed" | "failed" | "aborted";
264
- type StatusPresentation = { readonly label: string; readonly icon: string; readonly color: ThemeColor };
265
- type RenderEnvironment = {
266
- readonly expanded: boolean;
267
- readonly theme: Theme | undefined;
268
- readonly spinnerFrame: number | undefined;
269
- readonly width: number;
270
- readonly meta: TruncationMeta | undefined;
271
- /** Render-time clock, injected so elapsed time is deterministic under test. */
272
- readonly now: number;
273
- /** Repaints the row once a background-formatted code preview is ready; absent while args stream. */
274
- readonly repaint?: () => void;
275
- };
276
- type CellBadges = {
277
- readonly reset: boolean;
278
- readonly timeout: number | undefined;
279
- readonly throughput: CellThroughput | undefined;
280
- };
281
- type CellThroughput = { readonly calls: number; readonly wallDurationMs: number | undefined };
282
- type PrefixStyle = { readonly prefix: string; readonly continuation: string; readonly color: ThemeColor };
283
- type DetailedRenderContext = {
284
- readonly environment: RenderEnvironment;
285
- readonly args: EvalToolRequest;
286
- readonly showImageFallback: boolean;
287
- readonly isFinal: boolean;
288
- };
289
-
290
- function assertNever(value: never): never {
291
- throw new TypeError(`Unhandled eval render variant: ${String(value)}`);
292
- }
293
-
294
- function spinner(frame: number | undefined): string {
295
- return SPINNER_FRAMES.at((frame ?? 0) % SPINNER_FRAMES.length) ?? SPINNER_FRAMES[0];
296
- }
297
-
298
- function isLiveCellStatus(status: CellStatus): boolean {
299
- return status === "pending" || status === "running";
300
- }
301
-
302
- function cellPresentation(status: CellStatus, spinnerFrame: number | undefined): StatusPresentation {
303
- switch (status) {
304
- case "pending":
305
- return { label: "pending", icon: "○", color: "muted" };
306
- case "queued":
307
- return { label: "queued", icon: "○", color: "muted" };
308
- case "running":
309
- return { label: "running", icon: spinner(spinnerFrame), color: "warning" };
310
- case "detached":
311
- return { label: "detached", icon: "↗", color: "warning" };
312
- case "complete":
313
- return { label: "done", icon: "✓", color: "success" };
314
- case "error":
315
- return { label: "error", icon: "✗", color: "error" };
316
- case "cancelled":
317
- return { label: "cancelled", icon: "×", color: "error" };
318
- default:
319
- return assertNever(status);
320
- }
321
- }
322
-
323
- function renderPrefixed(text: string, environment: RenderEnvironment, prefixStyle: PrefixStyle): string[] {
324
- const bodyLines = renderAllVisualLines(text, Math.max(1, environment.width - prefixStyle.prefix.length));
325
- if (bodyLines.length === 0) return [style(environment.theme, prefixStyle.color, prefixStyle.prefix.trimEnd())];
326
- return bodyLines.map(
327
- (line, index) =>
328
- `${style(environment.theme, prefixStyle.color, index === 0 ? prefixStyle.prefix : prefixStyle.continuation)}${line}`,
329
- );
330
- }
331
-
332
- // A running cell only receives updates on output/status events, so a stored duration
333
- // freezes between them. Non-terminal cells therefore derive elapsed time from the
334
- // render-time clock; terminal cells keep their settled duration verbatim.
335
- function cellElapsedMs(cell: EvalCellResult, environment: RenderEnvironment): number | undefined {
336
- if (!isLiveCellStatus(cell.status) || cell.startedAt === undefined) return cell.durationMs;
337
- return Math.max(0, environment.now - cell.startedAt);
338
- }
339
-
340
- function cellHeader(cell: EvalCellResult, environment: RenderEnvironment, badges: CellBadges): string {
341
- const presentation = cellPresentation(
342
- cell.status,
343
- environment.spinnerFrame ?? Math.floor((cellElapsedMs(cell, environment) ?? 0) / LIVE_RENDER_TICK_MS),
344
- );
345
- const runtimeBadge = cell.runtime === undefined ? "" : ` (${formatRuntimeBadge(cell.language, cell.runtime)})`;
346
- let header = leadsWithHeadline(cell.status)
347
- ? `eval ${cell.language}${runtimeBadge} ${presentation.label}`
348
- : `eval ${cell.language}${runtimeBadge} ${presentation.label} ${presentation.icon}`;
349
- if (cell.queuedBehind !== undefined && cell.queuedBehind.length > 0)
350
- header += ` · queued behind ${cell.queuedBehind.map(sanitizeTerminalLabel).join(", ")}`;
351
- else if (cell.queuedBehind !== undefined && cell.status === "queued")
352
- header += ` · waiting for the ${cell.language} kernel to be ready`;
353
- const throughputBadge = badges.throughput === undefined ? undefined : formatThroughputBadge(badges.throughput);
354
- if (throughputBadge !== undefined) header += ` · ${throughputBadge}`;
355
- const elapsedMs = badges.throughput?.wallDurationMs ?? cellElapsedMs(cell, environment);
356
- if (elapsedMs !== undefined) header += ` · ${formatDuration(elapsedMs)}`;
357
- if (badges.reset) header += " · reset";
358
- if (badges.timeout !== undefined) header += ` · timeout ${badges.timeout}s`;
359
- if (leadsWithHeadline(cell.status))
360
- header = headlined(presentation.icon, cell.summary, cell.code, header, environment);
361
- return style(environment.theme, presentation.color, header);
362
- }
363
-
364
- // An in-progress row leads with what the cell is doing (senpi#2802); collapsed, the headline is cut so the whole
365
- // header stays on one line, and the code moves behind expand.
366
- function headlined(
367
- icon: string,
368
- summary: string | undefined,
369
- code: string | undefined,
370
- rest: string,
371
- environment: RenderEnvironment,
372
- ): string {
373
- const budget = environment.expanded ? undefined : environment.width - 3 - visibleWidth(`${icon} · ${rest}`);
374
- return `${icon} ${liveHeadline(summary, code, budget)} · ${rest}`;
375
- }
376
-
377
- function previewText(
378
- text: string,
379
- maxLines: number,
380
- width: number,
381
- ): { readonly lines: string[]; readonly skipped: number } {
382
- const preview = truncateToVisualLines(text, maxLines, Math.max(1, width));
383
- return { lines: preview.visualLines.map((line) => line.trimEnd()), skipped: preview.skippedCount };
384
- }
385
-
386
- function eventString(value: unknown): string | undefined {
387
- return typeof value === "string" && value.length > 0 ? value : undefined;
388
- }
389
-
390
- function eventNumber(value: unknown): number {
391
- return typeof value === "number" && Number.isFinite(value) ? value : 0;
392
- }
393
-
394
- function plural(count: number, singular: string, pluralNoun: string): string {
395
- return `${count} ${count === 1 ? singular : pluralNoun}`;
396
- }
397
-
398
- function formatCallsPerSecond(calls: number, wallDurationMs: number | undefined): string {
399
- const seconds = typeof wallDurationMs === "number" && Number.isFinite(wallDurationMs) ? wallDurationMs / 1_000 : 0;
400
- if (seconds <= 0) return "n/a calls/s";
401
- return `${(calls / seconds).toFixed(2)} calls/s`;
402
- }
403
-
404
- // A cell that issued no tool calls has no throughput to report: emitting
405
- // "0 calls · 0.00 calls/s" is noise, so the whole badge is dropped instead.
406
- function formatThroughputBadge(throughput: CellThroughput): string | undefined {
407
- if (throughput.calls <= 0) return undefined;
408
- return `${plural(throughput.calls, "call", "calls")} · ${formatCallsPerSecond(
409
- throughput.calls,
410
- throughput.wallDurationMs,
411
- )}`;
412
- }
413
-
414
- function statusIcon(op: string): string {
415
- if (op.startsWith("git_")) return "⌁";
416
- switch (op) {
417
- case "read":
418
- case "write":
419
- case "cat":
420
- case "touch":
421
- return "▣";
422
- case "ls":
423
- case "cd":
424
- case "pwd":
425
- case "mkdir":
426
- return "▤";
427
- case "run":
428
- case "sh":
429
- return "▶";
430
- case "completion":
431
- return "◇";
432
- case "phase":
433
- return "◆";
434
- default:
435
- return "•";
436
- }
437
- }
438
-
439
- function formatStatusEvent(event: EvalStatusEvent, theme: Theme | undefined): string {
440
- const op = event.op;
441
- const icon = style(theme, "muted", statusIcon(op));
442
- const error = eventString(event.error);
443
- if (error !== undefined) return `${icon} ${style(theme, "warning", op)}: ${style(theme, "dim", error)}`;
444
- const parts: string[] = [];
445
- switch (op) {
446
- case "read": {
447
- parts.push(`${eventNumber(event.chars ?? event.bytes)} chars`);
448
- const path = eventString(event.path);
449
- if (path !== undefined) parts.push(`from ${path}`);
450
- break;
451
- }
452
- case "write": {
453
- parts.push(`${eventNumber(event.chars ?? event.bytes)} chars`);
454
- const path = eventString(event.path);
455
- if (path !== undefined) parts.push(`to ${path}`);
456
- break;
457
- }
458
- case "cat":
459
- parts.push(plural(eventNumber(event.files), "file", "files"));
460
- parts.push(`${eventNumber(event.chars)} chars`);
461
- break;
462
- case "ls":
463
- parts.push(plural(eventNumber(event.count), "entry", "entries"));
464
- break;
465
- case "env": {
466
- const action = eventString(event.action);
467
- const key = eventString(event.key);
468
- const value = eventString(event.value) ?? "";
469
- if (action === "set" && key !== undefined) parts.push(`set ${key}=${value.slice(0, 30)}`);
470
- else if (action === "get" && key !== undefined) parts.push(`${key}=${value.slice(0, 30)}`);
471
- else parts.push(plural(eventNumber(event.count), "variable", "variables"));
472
- break;
473
- }
474
- case "git_status": {
475
- if (event.clean === true) parts.push("clean");
476
- else {
477
- const changes: string[] = [];
478
- for (const key of ["staged", "modified", "untracked"] as const) {
479
- const count = eventNumber(event[key]);
480
- if (count > 0) changes.push(`${count} ${key}`);
481
- }
482
- parts.push(changes.join(", ") || "unknown");
483
- }
484
- const branch = eventString(event.branch);
485
- if (branch !== undefined) parts.push(`on ${branch}`);
486
- break;
487
- }
488
- case "git_diff":
489
- parts.push(plural(eventNumber(event.lines), "line", "lines"));
490
- if (event.staged === true) parts.push("staged");
491
- break;
492
- case "git_log":
493
- parts.push(plural(eventNumber(event.commits), "commit", "commits"));
494
- break;
495
- case "run":
496
- case "sh": {
497
- const command = eventString(event.command ?? event.cmd);
498
- if (command !== undefined) parts.push(command);
499
- if (typeof event.exitCode === "number") parts.push(`exit ${event.exitCode}`);
500
- break;
501
- }
502
- case "completion": {
503
- const model = eventString(event.model);
504
- const tier = eventString(event.tier);
505
- if (model !== undefined) parts.push(model);
506
- if (tier !== undefined && tier !== model) parts.push(tier);
507
- parts.push(`${eventNumber(event.chars)} chars`);
508
- break;
509
- }
510
- case "log":
511
- parts.push(eventString(event.message) ?? "");
512
- break;
513
- case "phase":
514
- parts.push(eventString(event.title) ?? "");
515
- break;
516
- case "status-events-omitted":
517
- parts.push(`${eventNumber(event.count)} earlier events omitted`);
518
- break;
519
- default: {
520
- if (event.count !== undefined) parts.push(String(event.count));
521
- const path = eventString(event.path);
522
- if (path !== undefined) parts.push(path);
523
- }
524
- }
525
- const description = parts.filter((part) => part.length > 0).join(" · ");
526
- return `${icon} ${style(theme, "muted", op)}${description.length > 0 ? ` ${style(theme, "dim", description)}` : ""}`;
527
- }
528
-
529
- function renderStatusEvents(events: readonly EvalStatusEvent[], environment: RenderEnvironment): string[] {
530
- // A bounded history stores its exact omission count in a leading marker event; fold that
531
- // count into the summary line so collapsing the preview can never understate omissions.
532
- const first = events[0];
533
- const omittedByBound = first?.op === "status-events-omitted" && typeof first.count === "number" ? first.count : 0;
534
- const visible = omittedByBound > 0 ? events.slice(1) : events;
535
- const retained = environment.expanded ? visible : visible.slice(-STATUS_PREVIEW_COUNT);
536
- const skipped = visible.length - retained.length + omittedByBound;
537
- const lines: string[] = [];
538
- if (skipped > 0) lines.push(style(environment.theme, "dim", `├ … ${skipped} earlier status events`));
539
- for (const [index, event] of retained.entries()) {
540
- const branch = index === retained.length - 1 ? "└" : "├";
541
- lines.push(`${style(environment.theme, "dim", branch)} ${formatStatusEvent(event, environment.theme)}`);
542
- }
543
- return lines;
544
- }
545
-
546
- function agentStatus(value: unknown): AgentStatus {
547
- switch (value) {
548
- case "pending":
549
- case "running":
550
- case "completed":
551
- case "failed":
552
- case "aborted":
553
- return value;
554
- default:
555
- return "running";
556
- }
557
- }
558
-
559
- function coalesceAgentEvents(events: readonly EvalStatusEvent[]): EvalStatusEvent[] {
560
- const rows: EvalStatusEvent[] = [];
561
- const indexes = new Map<string, number>();
562
- for (const event of events) {
563
- const id = eventString(event.id);
564
- if (id === undefined) {
565
- rows.push(event);
566
- continue;
567
- }
568
- const index = indexes.get(id);
569
- if (index === undefined) {
570
- indexes.set(id, rows.length);
571
- rows.push(event);
572
- } else rows[index] = event;
573
- }
574
- return rows;
575
- }
576
-
577
- function agentPresentation(status: AgentStatus, spinnerFrame: number | undefined): StatusPresentation {
578
- switch (status) {
579
- case "pending":
580
- return { label: "pending", icon: "○", color: "muted" };
581
- case "running":
582
- return { label: "running", icon: spinner(spinnerFrame), color: "warning" };
583
- case "completed":
584
- return { label: "done", icon: "✓", color: "success" };
585
- case "failed":
586
- return { label: "failed", icon: "✗", color: "error" };
587
- case "aborted":
588
- return { label: "aborted", icon: "×", color: "error" };
589
- default:
590
- return assertNever(status);
591
- }
592
- }
593
-
594
- function renderAgentProgressEvents(events: readonly EvalStatusEvent[], environment: RenderEnvironment): string[] {
595
- const rows = coalesceAgentEvents(events);
596
- const lines: string[] = [];
597
- // Senpi Theme has no tree-token API; fixed ├/└/│ glyphs intentionally mirror omp.
598
- for (const [index, event] of rows.entries()) {
599
- const isLast = index === rows.length - 1;
600
- const status = agentStatus(event.status);
601
- const presentation = agentPresentation(status, environment.spinnerFrame);
602
- const id = eventString(event.id) ?? "agent";
603
- const styledId = environment.theme === undefined ? id : environment.theme.bold(id);
604
- let body = `${style(environment.theme, presentation.color, presentation.icon)} ${styledId} ${presentation.label}`;
605
- if (status === "completed" || status === "failed" || status === "aborted") {
606
- const duration = eventNumber(event.durationMs);
607
- if (duration > 0) body += ` · ${style(environment.theme, "dim", formatDuration(duration))}`;
608
- }
609
- const branch = isLast ? "└ " : "├ ";
610
- const continuation = isLast ? " " : "│ ";
611
- appendLines(
612
- lines,
613
- renderPrefixed(body, environment, { prefix: branch, continuation: continuation, color: "dim" }),
614
- );
615
- if (status !== "running") continue;
616
- const currentTool = eventString(event.currentTool);
617
- const lastIntent = eventString(event.lastIntent);
618
- if (currentTool === undefined && lastIntent === undefined) continue;
619
- const detail =
620
- currentTool === undefined
621
- ? (lastIntent ?? "")
622
- : `${currentTool}${lastIntent === undefined ? "" : `: ${lastIntent}`}`;
623
- appendLines(
624
- lines,
625
- renderPrefixed(detail, environment, {
626
- prefix: `${continuation}└ `,
627
- continuation: `${continuation} `,
628
- color: "dim",
629
- }),
630
- );
631
- }
632
- return lines;
633
- }
634
-
635
- function renderCell(cell: EvalCellResult, environment: RenderEnvironment, badges: CellBadges): string[] {
636
- if (leadsWithHeadline(cell.status) && !environment.expanded && cell.output.trim().length === 0) {
637
- const statusEvents = (cell.statusEvents ?? []).filter((event) => event.op !== "agent");
638
- if (statusEvents.length === 0) {
639
- // Nothing to frame yet: the collapsed live row is its headline alone, one line, no empty box.
640
- const lines = renderPrefixed(cellHeader(cell, environment, badges), environment, LIVE_LINE_PREFIX);
641
- const agentEvents = (cell.statusEvents ?? []).filter((event) => event.op === "agent");
642
- if (agentEvents.length > 0) appendLines(lines, renderAgentProgressEvents(agentEvents, environment));
643
- return lines;
644
- }
645
- }
646
- const lines = renderPrefixed(cellHeader(cell, environment, badges), environment, {
647
- prefix: "╭─ ",
648
- continuation: "│ ",
649
- color: "borderAccent",
650
- });
651
- const headlined = leadsWithHeadline(cell.status);
652
- if (cell.summary !== undefined && !headlined) {
653
- appendLines(
654
- lines,
655
- summaryVisualLines(cell.summary, Math.max(1, environment.width - 2), environment.expanded).map(
656
- (line) => `${style(environment.theme, "muted", "│ ")}${style(environment.theme, "muted", line)}`,
657
- ),
658
- );
659
- }
660
- const innerWidth = Math.max(1, environment.width - 2);
661
- const codePreview =
662
- headlined && !environment.expanded
663
- ? { lines: [], skipped: 0 }
664
- : previewText(
665
- highlightedCode(cell.code, cell.language, environment.theme, environment.repaint),
666
- environment.expanded ? Number.POSITIVE_INFINITY : CODE_PREVIEW_LINES,
667
- innerWidth,
668
- );
669
- if (codePreview.skipped > 0) {
670
- appendLines(
671
- lines,
672
- renderPrefixed(`${codePreview.skipped} earlier code lines`, environment, {
673
- prefix: "│ ",
674
- continuation: "│ ",
675
- color: "muted",
676
- }),
677
- );
678
- }
679
- for (const line of codePreview.lines) {
680
- appendLines(lines, renderPrefixed(line, environment, { prefix: "│ ", continuation: "│ ", color: "borderMuted" }));
681
- }
682
- const output = cell.output.trimEnd();
683
- if (output.length > 0) {
684
- appendLines(lines, renderPrefixed("output", environment, { prefix: "├─ ", continuation: "│ ", color: "dim" }));
685
- const outputColor: ThemeColor = cell.status === "error" ? "error" : "toolOutput";
686
- const styledOutput = output
687
- .split("\n")
688
- .map((line) => style(environment.theme, outputColor, line))
689
- .join("\n");
690
- const outputPreview = previewText(
691
- styledOutput,
692
- environment.expanded ? Number.POSITIVE_INFINITY : OUTPUT_PREVIEW_LINES,
693
- innerWidth,
694
- );
695
- if (outputPreview.skipped > 0) {
696
- appendLines(
697
- lines,
698
- renderPrefixed(`${outputPreview.skipped} earlier output lines`, environment, {
699
- prefix: "│ ",
700
- continuation: "│ ",
701
- color: "muted",
702
- }),
703
- );
704
- }
705
- for (const line of outputPreview.lines) {
706
- appendLines(
707
- lines,
708
- renderPrefixed(line, environment, { prefix: "│ ", continuation: "│ ", color: "borderMuted" }),
709
- );
710
- }
711
- }
712
- const allEvents = cell.statusEvents ?? [];
713
- const statusEvents = allEvents.filter((event) => event.op !== "agent");
714
- if (statusEvents.length > 0) {
715
- appendLines(lines, renderPrefixed("status", environment, { prefix: "├─ ", continuation: "│ ", color: "dim" }));
716
- for (const line of renderStatusEvents(statusEvents, environment)) {
717
- appendLines(
718
- lines,
719
- renderPrefixed(line, environment, { prefix: "│ ", continuation: "│ ", color: "borderMuted" }),
720
- );
721
- }
722
- }
723
- lines.push(style(environment.theme, "borderMuted", "╰─"));
724
- const agentEvents = allEvents.filter((event) => event.op === "agent");
725
- if (agentEvents.length > 0) appendLines(lines, renderAgentProgressEvents(agentEvents, environment));
726
- return lines;
727
- }
728
-
729
- function renderJsonOutputs(values: readonly unknown[], environment: RenderEnvironment): string[] {
730
- const lines: string[] = [];
731
- const depth = environment.expanded ? JSON_TREE_MAX_DEPTH_EXPANDED : JSON_TREE_MAX_DEPTH_COLLAPSED;
732
- const lineCap = environment.expanded ? JSON_TREE_MAX_LINES_EXPANDED : JSON_TREE_MAX_LINES_COLLAPSED;
733
- const scalarLen = environment.expanded ? JSON_TREE_SCALAR_LEN_EXPANDED : JSON_TREE_SCALAR_LEN_COLLAPSED;
734
- for (const [index, value] of values.entries()) {
735
- appendLines(
736
- lines,
737
- renderAllVisualLines(style(environment.theme, "dim", `display[${index + 1}]`), environment.width),
738
- );
739
- const tree = renderJsonTreeLines(value, environment.theme, depth, lineCap, scalarLen);
740
- for (const line of tree.lines) appendLines(lines, renderAllVisualLines(line, environment.width));
741
- if (tree.truncated)
742
- appendLines(lines, renderAllVisualLines(style(environment.theme, "dim", "…"), environment.width));
743
- }
744
- return lines;
745
- }
746
-
747
- function renderDetailedLines(
748
- details: EvalToolDetails,
749
- result: AgentToolResult<EvalResultDetails>,
750
- context: DetailedRenderContext,
751
- ): string[] {
752
- const lines: string[] = [];
753
- const cells = details.cells ?? [];
754
- for (const [index, cell] of cells.entries()) {
755
- const run = isEvalRunInput(context.args) ? context.args : undefined;
756
- const throughput =
757
- context.isFinal &&
758
- cells.length === 1 &&
759
- cell.status === "complete" &&
760
- typeof details.toolCallCount === "number"
761
- ? { calls: details.toolCallCount, wallDurationMs: details.wallDurationMs }
762
- : undefined;
763
- const badges = {
764
- reset: index === 0 && run?.reset === true,
765
- timeout: index === 0 ? run?.timeout : undefined,
766
- throughput,
767
- };
768
- appendLines(lines, renderCell(cell, context.environment, badges));
769
- if (index < cells.length - 1) lines.push("");
770
- }
771
- const jsonOutputs = details.jsonOutputs ?? [];
772
- if (jsonOutputs.length > 0) {
773
- if (lines.length > 0) lines.push("");
774
- appendLines(lines, renderJsonOutputs(jsonOutputs, context.environment));
775
- }
776
- if (context.showImageFallback) {
777
- for (const part of result.content) {
778
- if (part.type !== "image") continue;
779
- if (lines.length > 0) lines.push("");
780
- appendLines(
781
- lines,
782
- renderAllVisualLines(`[image: ${sanitizeTerminalLabel(part.mimeType)}]`, context.environment.width),
783
- );
784
- }
785
- }
786
- if (details.phase !== undefined)
787
- appendLines(
788
- lines,
789
- renderAllVisualLines(
790
- style(context.environment.theme, "muted", `phase ${details.phase}`),
791
- context.environment.width,
792
- ),
793
- );
794
- return lines;
795
- }
796
-
797
- function textOutput(result: AgentToolResult<EvalResultDetails>, showImageFallback: boolean): string {
798
- const lines: string[] = [];
799
- for (const part of result.content) {
800
- if (part.type === "text" && part.audience !== "model") lines.push(part.text);
801
- else if (showImageFallback && part.type === "image") {
802
- lines.push(`[image: ${sanitizeTerminalLabel(part.mimeType)}]`);
803
- }
804
- }
805
- return lines.join("\n");
806
- }
807
-
808
- function isEvalRunInput(args: EvalToolRequest): args is EvalToolInput {
809
- return args.action === undefined || args.action === "run";
810
- }
811
-
812
- function toolCallRows(details: EvalToolDetails | undefined): ToolCallRow[] {
813
- if (!details?.toolCalls || details.toolCalls.length === 0) return [];
814
- return details.toolCalls.map((call) => {
815
- const status = call.ok ? "ok" : "error";
816
- const row = { summary: `- tool.${call.name}: ${status}`, color: call.ok ? "success" : "error" } as const;
817
- return call.error === undefined ? row : { ...row, error: call.error };
818
- });
819
- }
820
-
821
- function nestedToolCallBlock(
822
- details: EvalToolDetails | undefined,
823
- theme: Theme | undefined,
824
- cwd: string,
825
- expanded: boolean,
826
- ): RenderBlock | undefined {
827
- const toolCalls = details?.toolCalls;
828
- if (theme === undefined || toolCalls === undefined || !toolCalls.some((call) => call.args !== undefined)) {
829
- return undefined;
830
- }
831
- const legacyRows = toolCallRows(details);
832
- return {
833
- kind: "dynamic",
834
- render: (width) => {
835
- const retainedCalls = expanded ? toolCalls : toolCalls.slice(-TOOL_CALL_PREVIEW_COUNT);
836
- const retainedRows = expanded ? legacyRows : legacyRows.slice(-TOOL_CALL_PREVIEW_COUNT);
837
- const skippedCount = toolCalls.length - retainedCalls.length;
838
- const toolCallNoun = skippedCount === 1 ? "call" : "calls";
839
- const lines =
840
- expanded || skippedCount === 0
841
- ? []
842
- : renderAllVisualLines(style(theme, "muted", `${skippedCount} earlier tool ${toolCallNoun}`), width);
843
- const legacyBlock: Extract<RenderBlock, { kind: "toolCalls" }> = {
844
- kind: "toolCalls",
845
- calls: retainedRows,
846
- expanded,
847
- theme,
848
- };
849
- for (const [index, call] of retainedCalls.entries()) {
850
- if (call.args !== undefined) {
851
- appendLines(lines, renderToolCallWidget(call, { cwd, theme, expanded, width }));
852
- } else {
853
- appendLines(lines, renderToolCall(retainedRows[index], legacyBlock, width));
854
- }
855
- }
856
- return lines;
857
- },
858
- };
859
- }
860
-
861
- function resultStatus(
862
- details: EvalToolDetails | undefined,
863
- options: ToolRenderResultOptions,
864
- hostIsError: boolean,
865
- ): "running" | "done" | "error" {
866
- if (details?.isError || hostIsError) return "error";
867
- return options.isPartial ? "running" : "done";
868
- }
869
-
870
- function resultHeader(
871
- details: EvalToolDetails | undefined,
872
- status: "running" | "done" | "error",
873
- theme: Theme | undefined,
874
- ): string {
875
- let color: ThemeColor;
876
- switch (status) {
877
- case "running":
878
- color = "warning";
879
- break;
880
- case "done":
881
- color = "success";
882
- break;
883
- case "error":
884
- color = "error";
885
- break;
886
- }
887
- const runtimeBadge =
888
- details?.runtime === undefined ? "" : ` (${formatRuntimeBadge(details.language, details.runtime)})`;
889
- return style(theme, color, `eval ${details?.language ?? "?"}${runtimeBadge} ${status}`);
890
- }
891
-
892
- function resultMetadata(
893
- details: EvalToolDetails | undefined,
894
- options: ToolRenderResultOptions,
895
- theme: Theme | undefined,
896
- status: "running" | "done" | "error",
897
- ): RenderBlock[] {
898
- const metadata: string[] = [];
899
- if (details?.phase) metadata.push(`phase ${details.phase}`);
900
- const elapsedMs = details?.wallDurationMs ?? details?.durationMs;
901
- if (!options.isPartial && typeof elapsedMs === "number") metadata.push(`took ${formatDuration(elapsedMs)}`);
902
- if (status === "done" && typeof details?.toolCallCount === "number") {
903
- const badge = formatThroughputBadge({ calls: details.toolCallCount, wallDurationMs: details.wallDurationMs });
904
- if (badge !== undefined) metadata.push(badge);
905
- }
906
- if (metadata.length === 0) return [];
907
- return [{ kind: "text", text: style(theme, "muted", metadata.join(" | ")) }];
908
- }
909
-
910
- // The call renderer reads the assistant message's raw arguments, which keep the provider's
911
- // original summary (senpi#1472 detached preparation from the message), so it normalizes here,
912
- // idempotently: an already-normalized value is returned unchanged.
913
- function displaySummary(summary: string | undefined): string | undefined {
914
- return normalizeEvalSummary(summary);
915
- }
916
-
917
- function cellIdSuffix(cellId: unknown): string {
918
- return typeof cellId === "string" && cellId !== "" ? ` ${sanitizeTerminalLabel(cellId)}` : "";
919
- }
920
-
921
- export function renderEvalCall(
922
- args: EvalToolRequest,
923
- theme: Theme | undefined,
924
- context: RenderContext,
925
- ): EvalRenderComponent {
926
- const component = componentFor(context);
927
- if (context.hasResult === true) {
928
- // The result renderer owns the full pending -> running -> done frame once a result exists.
929
- // Rendering the call frame too would stack a duplicate box, so yield to it here.
930
- component.setBlocks([]);
931
- return component;
932
- }
933
- if (!isEvalRunInput(args)) {
934
- // While a peek/stop call streams in, `cell_id` can still be missing; the title is the action alone until it arrives.
935
- const title = args.action === "list" ? "eval list" : `eval ${args.action}${cellIdSuffix(args.cell_id)}`;
936
- component.setBlocks([{ kind: "text", text: style(theme, "toolTitle", title) }]);
937
- return component;
938
- }
939
- // While the model streams the call, `code` usually arrives before `language` and `summary`; a partial call still
940
- // renders (the host would otherwise fall back to a raw key=value row).
941
- const code = typeof args.code === "string" ? args.code : "";
942
- const language = knownLanguage(args.language);
943
- if (theme === undefined && context.spinnerFrame === undefined) {
944
- const reset = args.reset === true ? " reset" : "";
945
- const timeout = args.timeout === undefined ? "" : ` timeout ${args.timeout}s`;
946
- component.setBlocks([
947
- {
948
- kind: "text",
949
- text: style(theme, "toolTitle", `eval${language === undefined ? "" : ` ${language}`}${reset}${timeout}`),
950
- },
951
- ...(displaySummary(args.summary) === undefined
952
- ? []
953
- : [summaryBlock(displaySummary(args.summary) ?? "", theme, context.expanded)]),
954
- {
955
- kind: "text",
956
- text: style(
957
- theme,
958
- "mdCodeBlock",
959
- code.trim().length === 0
960
- ? "..."
961
- : language === undefined
962
- ? code
963
- : displayCode(code, language, context.argsComplete ? context.invalidate : undefined),
964
- ),
965
- maxVisualLines: context.expanded ? undefined : CODE_PREVIEW_LINES,
966
- collapseKind: "code",
967
- theme,
968
- },
969
- ]);
970
- return component;
971
- }
972
- component.setBlocks([
973
- {
974
- kind: "dynamic",
975
- render: (width) => {
976
- const environment: RenderEnvironment = {
977
- expanded: context.expanded,
978
- theme,
979
- spinnerFrame: context.spinnerFrame,
980
- width,
981
- meta: undefined,
982
- now: renderNow(context),
983
- ...(context.argsComplete ? { repaint: context.invalidate } : {}),
984
- };
985
- const summary = displaySummary(args.summary);
986
- if (language === undefined) return streamingCallLines(summary, code, environment);
987
- const cell: EvalCellResult = {
988
- index: 0,
989
- ...(summary === undefined ? {} : { summary }),
990
- code,
991
- language,
992
- output: "",
993
- status: context.spinnerFrame === undefined ? "pending" : "running",
994
- };
995
- return renderCell(cell, environment, {
996
- reset: args.reset === true,
997
- timeout: args.timeout,
998
- throughput: undefined,
999
- });
1000
- },
1001
- },
1002
- ]);
1003
- return component;
1004
- }
1005
-
1006
- /** A call whose language has not streamed in yet: the headline row alone, until the full frame can render. */
1007
- function streamingCallLines(summary: string | undefined, code: string, environment: RenderEnvironment): string[] {
1008
- const icon = environment.spinnerFrame === undefined ? "○" : spinner(environment.spinnerFrame);
1009
- return renderPrefixed(headlined(icon, summary, code, "eval", environment), environment, LIVE_LINE_PREFIX);
1010
- }
1011
-
1012
- const LIVE_LINE_PREFIX: PrefixStyle = { prefix: "╶─ ", continuation: " ", color: "borderAccent" };
1013
-
1014
- export function renderEvalResult(
1015
- result: AgentToolResult<EvalResultDetails>,
1016
- options: ToolRenderResultOptions,
1017
- theme: Theme | undefined,
1018
- context: ResultRenderContext,
1019
- ): EvalRenderComponent {
1020
- const component = componentFor(context);
1021
- const details = result.details;
1022
- if (details && "action" in details) {
1023
- component.syncLiveTicker(false, context.invalidate);
1024
- component.setBlocks([
1025
- { kind: "text", text: style(theme, "toolTitle", "eval list") },
1026
- { kind: "text", text: style(theme, "toolOutput", textOutput(result, false)) },
1027
- ]);
1028
- return component;
1029
- }
1030
- const expanded = options.expanded || context.expanded;
1031
- const imageProtocol = context.imageProtocol ?? null;
1032
- component.syncLiveTicker(hasLiveCell(details), context.invalidate);
1033
- if (details?.cells !== undefined && details.cells.length > 0) {
1034
- const blocks: RenderBlock[] = [
1035
- {
1036
- kind: "dynamic",
1037
- render: (width) =>
1038
- renderDetailedLines(details, result, {
1039
- environment: {
1040
- expanded,
1041
- theme,
1042
- spinnerFrame: context.spinnerFrame,
1043
- width,
1044
- meta: details.meta,
1045
- now: renderNow(context),
1046
- repaint: context.invalidate,
1047
- },
1048
- args: context.args,
1049
- showImageFallback: context.showImages && imageProtocol === null,
1050
- isFinal: !options.isPartial,
1051
- }),
1052
- },
1053
- ];
1054
- const calls = toolCallRows(details);
1055
- const nestedCalls = nestedToolCallBlock(details, theme, context.cwd, expanded);
1056
- if (calls.length > 0)
1057
- blocks.push({ kind: "blank" }, nestedCalls ?? { kind: "toolCalls", calls, expanded, theme });
1058
- component.setBlocks(blocks);
1059
- return component;
1060
- }
1061
- const status = resultStatus(details, options, context.isError);
1062
- const blocks: RenderBlock[] = [
1063
- { kind: "text", text: resultHeader(details, status, theme) },
1064
- ...(details?.summary === undefined ? [] : [summaryBlock(details.summary, theme, expanded)]),
1065
- ...resultMetadata(details, options, theme, status),
1066
- { kind: "blank" },
1067
- ];
1068
- const rawOutput = textOutput(result, context.showImages && imageProtocol === null);
1069
- const output = rawOutput.trimEnd();
1070
- const hasRenderedImage =
1071
- context.showImages && imageProtocol !== null && result.content.some((part) => part.type === "image");
1072
- if (output.length > 0) {
1073
- blocks.push({
1074
- kind: "text",
1075
- text: style(theme, "toolOutput", output),
1076
- maxVisualLines: expanded ? undefined : OUTPUT_PREVIEW_LINES,
1077
- collapseKind: "output",
1078
- theme,
1079
- });
1080
- } else if (!hasRenderedImage) blocks.push({ kind: "text", text: style(theme, "muted", "(no output)") });
1081
- const statusEvents = details?.statusEvents ?? [];
1082
- const nonAgentEvents = statusEvents.filter((event) => event.op !== "agent");
1083
- const agentEvents = statusEvents.filter((event) => event.op === "agent");
1084
- if (nonAgentEvents.length > 0 || agentEvents.length > 0) {
1085
- blocks.push(
1086
- { kind: "blank" },
1087
- {
1088
- kind: "dynamic",
1089
- render: (width) => {
1090
- const environment: RenderEnvironment = {
1091
- expanded,
1092
- theme,
1093
- spinnerFrame: context.spinnerFrame,
1094
- width,
1095
- meta: details?.meta,
1096
- now: renderNow(context),
1097
- };
1098
- return [
1099
- ...renderStatusEvents(nonAgentEvents, environment),
1100
- ...renderAgentProgressEvents(agentEvents, environment),
1101
- ];
1102
- },
1103
- },
1104
- );
1105
- }
1106
- if ((details?.jsonOutputs?.length ?? 0) > 0) {
1107
- blocks.push(
1108
- { kind: "blank" },
1109
- {
1110
- kind: "dynamic",
1111
- render: (width) =>
1112
- renderJsonOutputs(details?.jsonOutputs ?? [], {
1113
- expanded,
1114
- theme,
1115
- spinnerFrame: context.spinnerFrame,
1116
- width,
1117
- meta: details?.meta,
1118
- now: renderNow(context),
1119
- }),
1120
- },
1121
- );
1122
- }
1123
- const calls = toolCallRows(details);
1124
- const nestedCalls = nestedToolCallBlock(details, theme, context.cwd, expanded);
1125
- if (calls.length > 0) blocks.push({ kind: "blank" }, nestedCalls ?? { kind: "toolCalls", calls, expanded, theme });
1126
- component.setBlocks(blocks);
1127
- return component;
1128
- }
1
+ export type { EvalRenderComponent } from "./render-blocks.ts";
2
+ export { renderEvalCall } from "./render-call.ts";
3
+ export { renderEvalResult } from "./render-result.ts";