@code-yeongyu/senpi-codemode 2026.7.31-2 → 2026.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -12,6 +12,24 @@
12
12
 
13
13
  ### Removed
14
14
 
15
+ ## [2026.8.1] - 2026-08-01
16
+
17
+ ### Breaking Changes
18
+
19
+ ### Added
20
+
21
+ ### Changed
22
+
23
+ ### Fixed
24
+
25
+ - Preserve rich live and terminal `eval` details when peeking detached cells,
26
+ including code, title, output, phase, status events, tool-call summaries,
27
+ duration, and structured displays; cancellation now remains authoritative
28
+ over late completion races
29
+ ([#603](https://github.com/code-yeongyu/senpi/pull/603)).
30
+
31
+ ### Removed
32
+
15
33
  ## [2026.7.31-2] - 2026-07-31
16
34
 
17
35
  ### Breaking Changes
package/README.md CHANGED
@@ -11,9 +11,12 @@ task-tool names are known.
11
11
  - Persistent JavaScript, Python, Ruby, and Julia cells. State survives later
12
12
  cells in the same language until reset, restart, or session disposal.
13
13
  - Timeout detachment for interactive `eval`: long pure-compute cells return a
14
- handle and continue in their existing kernel. Completion is injected with the
15
- final value/error and buffered output; use `eval({ action: "peek"|"stop",
16
- cell_id })` to inspect or terminate a detached cell.
14
+ handle and continue in their existing kernel. Completion is injected with the
15
+ final value/error and buffered output; use `eval({ action: "peek"|"stop",
16
+ cell_id })` to inspect or terminate a detached cell. A running peek preserves
17
+ the original code and title together with current output, phase, status
18
+ events, tool-call summaries, elapsed duration, and structured display state;
19
+ a terminal peek preserves the exact final result.
17
20
  - Loopback, bearer-authenticated kernel bridge with bounded JSONL frames.
18
21
  - Structured status events for file operations, environment access, phases,
19
22
  bridge activity, and delegated task progress.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@code-yeongyu/senpi-codemode",
3
- "version": "2026.7.31-2",
3
+ "version": "2026.8.1",
4
4
  "description": "Source-only senpi extension package for codemode evaluation tools",
5
5
  "type": "module",
6
6
  "main": "./src/index.ts",
@@ -30,14 +30,14 @@
30
30
  },
31
31
  "dependencies": {
32
32
  "@babel/parser": "8.0.4",
33
- "@earendil-works/pi-ai": "npm:@code-yeongyu/senpi-ai@^2026.7.31-2",
33
+ "@earendil-works/pi-ai": "npm:@code-yeongyu/senpi-ai@^2026.8.1",
34
34
  "typebox": "1.3.8"
35
35
  },
36
36
  "peerDependencies": {
37
37
  "@code-yeongyu/senpi": "*"
38
38
  },
39
39
  "devDependencies": {
40
- "@code-yeongyu/senpi": "2026.7.31-2"
40
+ "@code-yeongyu/senpi": "2026.8.1"
41
41
  },
42
42
  "keywords": [
43
43
  "senpi",
@@ -0,0 +1,146 @@
1
+ import { IdleTimeout, type IdleTimeoutOptions, type TimeoutPauseHandle } from "../timeouts/idle-timeout.ts";
2
+ import type { EvalKernel } from "./types.ts";
3
+
4
+ const INTERRUPT_DELIVERY_GRACE_MS = 100;
5
+
6
+ export interface EvalTimeoutFactory {
7
+ create(options: IdleTimeoutOptions): TimeoutPauseHandle & { dispose(): void };
8
+ }
9
+
10
+ export const defaultTimeoutFactory: EvalTimeoutFactory = {
11
+ create(options): IdleTimeout {
12
+ return new IdleTimeout(options);
13
+ },
14
+ };
15
+
16
+ export interface CellExecutionOptions {
17
+ readonly callerSignal: AbortSignal;
18
+ readonly cellId: string;
19
+ readonly timeoutMs: number;
20
+ readonly timeoutFactory: EvalTimeoutFactory;
21
+ readonly onTimeout: (error: Error) => void;
22
+ readonly onAbort: (error: Error) => void;
23
+ }
24
+
25
+ export class CellExecution {
26
+ readonly #callerSignal: AbortSignal;
27
+ readonly #onAbort: (error: Error) => void;
28
+ readonly #abortPromise: Promise<never>;
29
+ readonly #detachedPromise: Promise<void>;
30
+ readonly #watchdog: TimeoutPauseHandle & { dispose(): void };
31
+ #rejectAbort: ((reason?: unknown) => void) | undefined;
32
+ #resolveDetached: (() => void) | undefined;
33
+ #kernel: EvalKernel | undefined;
34
+ #interruptDeadline: ReturnType<typeof setTimeout> | undefined;
35
+ #active = true;
36
+
37
+ constructor(options: CellExecutionOptions) {
38
+ this.#callerSignal = options.callerSignal;
39
+ this.#onAbort = options.onAbort;
40
+ this.#abortPromise = new Promise<never>((_resolve, reject) => {
41
+ this.#rejectAbort = reject;
42
+ });
43
+ this.#detachedPromise = new Promise<void>((resolve) => {
44
+ this.#resolveDetached = resolve;
45
+ });
46
+ this.#watchdog = options.timeoutFactory.create({
47
+ cellId: options.cellId,
48
+ timeoutMs: options.timeoutMs,
49
+ onTimeout: ({ error }) => options.onTimeout(error),
50
+ });
51
+ this.#callerSignal.addEventListener("abort", this.#handleCallerAbort, {
52
+ once: true,
53
+ });
54
+ }
55
+
56
+ get detached(): Promise<void> {
57
+ return this.#detachedPromise;
58
+ }
59
+
60
+ pause(): void {
61
+ this.#watchdog.pause();
62
+ }
63
+
64
+ resume(): void {
65
+ this.#watchdog.resume();
66
+ }
67
+
68
+ setKernel(kernel: EvalKernel): void {
69
+ this.#kernel = kernel;
70
+ }
71
+
72
+ detach(): void {
73
+ if (!this.#active) return;
74
+ this.#watchdog.dispose();
75
+ this.#resolveDetached?.();
76
+ this.#resolveDetached = undefined;
77
+ }
78
+
79
+ cancel(reason: unknown): void {
80
+ this.#abort(reason);
81
+ }
82
+
83
+ finish(): void {
84
+ this.#active = false;
85
+ this.#cleanup();
86
+ }
87
+
88
+ async wait<Result>(operation: Promise<Result>): Promise<Result> {
89
+ const guarded = operation.then(
90
+ (value): Result | Promise<never> => (this.#active ? value : this.#abortPromise),
91
+ (reason: unknown): Promise<never> => (this.#active ? Promise.reject(reason) : this.#abortPromise),
92
+ );
93
+ return await Promise.race([guarded, this.#abortPromise]);
94
+ }
95
+
96
+ readonly #handleCallerAbort = (): void => {
97
+ this.#abort(this.#callerSignal.reason);
98
+ };
99
+
100
+ interruptStateRetained: Promise<boolean> | undefined;
101
+
102
+ #abort(reason: unknown): void {
103
+ if (!this.#active) return;
104
+ this.#active = false;
105
+ this.#cleanup();
106
+ const error = abortError(reason);
107
+ this.#onAbort(error);
108
+ const kernel = this.#kernel;
109
+ if (kernel === undefined) {
110
+ this.#settleAbort(error);
111
+ return;
112
+ }
113
+ this.#interruptDeadline = setTimeout(() => this.#settleAbort(error), INTERRUPT_DELIVERY_GRACE_MS);
114
+ void Promise.resolve()
115
+ .then(async () => {
116
+ const handle = await kernel.interrupt(error.message);
117
+ this.interruptStateRetained = handle?.stateRetained;
118
+ })
119
+ .then(
120
+ () => this.#settleAbort(error),
121
+ (interruptError: unknown) => this.#settleAbort(interruptError),
122
+ );
123
+ }
124
+
125
+ #settleAbort(reason: unknown): void {
126
+ const reject = this.#rejectAbort;
127
+ if (reject === undefined) return;
128
+ this.#rejectAbort = undefined;
129
+ if (this.#interruptDeadline !== undefined) clearTimeout(this.#interruptDeadline);
130
+ reject(reason);
131
+ }
132
+
133
+ #cleanup(): void {
134
+ this.#callerSignal.removeEventListener("abort", this.#handleCallerAbort);
135
+ this.#watchdog.dispose();
136
+ if (this.#interruptDeadline !== undefined) clearTimeout(this.#interruptDeadline);
137
+ this.#interruptDeadline = undefined;
138
+ }
139
+ }
140
+
141
+ export function abortError(reason: unknown): Error {
142
+ if (reason instanceof Error && reason.name !== "AbortError") return reason;
143
+ const error = new Error(typeof reason === "string" ? reason : "Eval interrupted", { cause: reason });
144
+ error.name = "AbortError";
145
+ return error;
146
+ }
@@ -1,9 +1,4 @@
1
- import {
2
- type AgentToolResult,
3
- type AgentToolUpdateCallback,
4
- type ExtensionContext,
5
- sanitizeTerminalLabel,
6
- } from "@code-yeongyu/senpi";
1
+ import { type AgentToolResult, type ExtensionContext, sanitizeTerminalLabel } from "@code-yeongyu/senpi";
7
2
  import type { KernelToHostMessage } from "../bridge/protocol.ts";
8
3
  import type { AgentExecuteTool } from "../bridges/agent-bridge.ts";
9
4
  import { isReservedToolName, runReservedTool } from "../bridges/reserved-dispatch.ts";
@@ -13,15 +8,12 @@ import type { CompletionRequest, CompletionResult } from "../completion/handler.
13
8
  import { handleCompletionToolCall } from "../completion/tool-bridge.ts";
14
9
  import type { ResolvedCodemodeSettings } from "../config/settings.ts";
15
10
  import { boundToolCallArgs, capCodePoints, MAX_ENRICHED_TOOL_CALLS, toolCallResultPreview } from "./call-capture.ts";
16
- import {
17
- type EvalImageResizer,
18
- EvalOutputCollector,
19
- type EvalOutputResult,
20
- marshalToolResult,
21
- toolResultIsError,
22
- } from "./image.ts";
11
+ import { CellResultBuilder, type CellState } from "./cell-runtime.ts";
12
+ import { type EvalImageResizer, marshalToolResult, toolResultIsError } from "./image.ts";
23
13
  import { upsertStatusEvent } from "./status-events.ts";
24
- import type { EvalKernel, EvalStatusEvent, EvalToolDetails, EvalToolInput } from "./types.ts";
14
+ import type { EvalKernel, EvalStatusEvent, EvalToolDetails } from "./types.ts";
15
+
16
+ export type { CellState } from "./cell-runtime.ts";
25
17
 
26
18
  type ResolvedToolReply = {
27
19
  readonly value: unknown;
@@ -37,22 +29,6 @@ type ToolCallEnrichment = {
37
29
  readonly argsTruncated?: true;
38
30
  };
39
31
 
40
- const LIVE_OUTPUT_PREVIEW_LINES = 8;
41
-
42
- export interface CellState {
43
- readonly input: EvalToolInput;
44
- readonly signal: AbortSignal;
45
- readonly onUpdate: AgentToolUpdateCallback<EvalToolDetails> | undefined;
46
- readonly toolCalls: EvalToolDetails["toolCalls"] extends readonly (infer Item)[] ? Item[] : never;
47
- readonly pendingBridgeCalls: Promise<void>[];
48
- readonly statusEvents: EvalStatusEvent[];
49
- active: boolean;
50
- output: string;
51
- phase: string | undefined;
52
- durationMs: number;
53
- status: "pending" | "running" | "complete" | "error";
54
- }
55
-
56
32
  export interface CellBridgeRuntime {
57
33
  readonly executeTool: AgentExecuteTool;
58
34
  readonly listTools?: () => readonly EvalSchemaToolInfo[];
@@ -67,46 +43,40 @@ export class CellHandler {
67
43
  readonly #kernel: EvalKernel;
68
44
  readonly #state: CellState;
69
45
  readonly #runtime: CellBridgeRuntime;
70
- readonly #output: EvalOutputCollector;
46
+ readonly #resultBuilder: CellResultBuilder;
71
47
 
72
48
  constructor(kernel: EvalKernel, state: CellState, runtime: CellBridgeRuntime) {
73
49
  this.#kernel = kernel;
74
50
  this.#state = state;
75
51
  this.#runtime = runtime;
76
52
  const settings = runtime.settings.outputSink;
77
- this.#output = new EvalOutputCollector({
53
+ this.#resultBuilder = new CellResultBuilder({
54
+ state,
78
55
  headBytes: settings.headBytes,
79
56
  maxColumns: settings.maxColumns,
80
57
  model: runtime.ctx.model,
81
58
  ...(runtime.artifactPath === undefined ? {} : { artifactPath: runtime.artifactPath }),
82
59
  ...(runtime.imageResizer === undefined ? {} : { imageResizer: runtime.imageResizer }),
83
- onChunk: (_aggregate, cell) => {
84
- state.output = cell;
85
- this.#emitUpdate(false);
86
- },
87
60
  });
88
- state.status = "running";
89
- this.#emitUpdate(false);
90
61
  }
91
62
 
92
63
  async handle(message: KernelToHostMessage): Promise<void> {
93
64
  if (!this.#state.active) return;
94
65
  switch (message.type) {
95
66
  case "text":
96
- this.#output.push(message.data);
67
+ this.#resultBuilder.push(message.data);
97
68
  return;
98
69
  case "phase":
99
- this.#state.phase = message.title;
100
- this.#emitUpdate(false);
70
+ this.#resultBuilder.setPhase(message.title);
101
71
  return;
102
72
  case "status":
103
73
  this.#recordStatus(message.event);
104
74
  return;
105
75
  case "log":
106
- this.#output.push(`${message.message}\n`);
76
+ this.#resultBuilder.push(`${message.message}\n`);
107
77
  return;
108
78
  case "display":
109
- this.#output.display(message);
79
+ this.#resultBuilder.display(message);
110
80
  return;
111
81
  case "tool-call": {
112
82
  const pending = this.#handleToolCall(message);
@@ -125,38 +95,19 @@ export class CellHandler {
125
95
  }
126
96
 
127
97
  async finalize(result: Extract<KernelToHostMessage, { type: "result" }>): Promise<AgentToolResult<EvalToolDetails>> {
128
- this.#state.durationMs = result.durationMs;
129
- if (result.ok) {
130
- if (result.valueRepr) this.#output.push(`${result.valueRepr}\n`);
131
- this.#state.status = "complete";
132
- } else {
133
- this.#output.push(`${result.error.message}\n`);
134
- this.#state.status = "error";
135
- }
136
- return await this.#finish(!result.ok);
98
+ return await this.#resultBuilder.finalize(result);
137
99
  }
138
100
 
139
101
  async finalizeCancellation(error: Error): Promise<AgentToolResult<EvalToolDetails>> {
140
- this.#output.push(`${error.message}\n`);
141
- this.#state.status = "error";
142
- return await this.#finish(true);
102
+ return await this.#resultBuilder.finalizeCancellation(error);
143
103
  }
144
104
 
145
105
  async flushOutput(): Promise<void> {
146
- await this.#output.flush();
106
+ await this.#resultBuilder.flushOutput();
147
107
  }
148
108
 
149
- async #finish(isError: boolean): Promise<AgentToolResult<EvalToolDetails>> {
150
- const output = await this.#output.finish();
151
- this.#state.output = output.output;
152
- const details = this.#details(output, isError);
153
- this.#emitUpdate(isError);
154
- const text =
155
- output.output ||
156
- (output.images.length > 0
157
- ? `(displayed ${output.images.length} image${output.images.length === 1 ? "" : "s"}; no text output)`
158
- : "(no output)");
159
- return { content: [{ type: "text", text }, ...output.images], details };
109
+ liveResult(): AgentToolResult<EvalToolDetails> {
110
+ return this.#resultBuilder.liveResult();
160
111
  }
161
112
 
162
113
  async #handleToolCall(message: Extract<KernelToHostMessage, { type: "tool-call" }>): Promise<void> {
@@ -202,7 +153,7 @@ export class CellHandler {
202
153
  ? { name: message.toolName, ok: true }
203
154
  : { name: message.toolName, ok: false, error: result.error },
204
155
  );
205
- this.#emitUpdate(false);
156
+ this.#resultBuilder.emitUpdate(false);
206
157
  return;
207
158
  }
208
159
  const capturedArgs = boundToolCallArgs(message.args);
@@ -268,7 +219,7 @@ export class CellHandler {
268
219
  error: { message: text },
269
220
  });
270
221
  }
271
- this.#emitUpdate(false);
222
+ this.#resultBuilder.emitUpdate(false);
272
223
  }
273
224
 
274
225
  #pushToolCall(
@@ -301,55 +252,6 @@ export class CellHandler {
301
252
  #recordStatus(event: EvalStatusEvent): void {
302
253
  if (!this.#runtime.settings.statusEvents) return;
303
254
  upsertStatusEvent(this.#state.statusEvents, event);
304
- this.#emitUpdate(false);
305
- }
306
-
307
- #details(output: EvalOutputResult | undefined, isError: boolean): EvalToolDetails {
308
- const statusEvents = this.#state.statusEvents.length > 0 ? [...this.#state.statusEvents] : undefined;
309
- return {
310
- language: this.#state.input.language,
311
- languages: [this.#state.input.language],
312
- ...(this.#state.input.title === undefined ? {} : { title: this.#state.input.title }),
313
- durationMs: this.#state.durationMs,
314
- toolCalls: [...this.#state.toolCalls],
315
- truncated: output?.truncated ?? false,
316
- ...(isError ? { isError: true } : {}),
317
- ...(this.#state.phase === undefined ? {} : { phase: this.#state.phase }),
318
- cells: [
319
- {
320
- index: 0,
321
- ...(this.#state.input.title === undefined ? {} : { title: this.#state.input.title }),
322
- code: this.#state.input.code,
323
- language: this.#state.input.language,
324
- output: this.#state.output,
325
- status: this.#state.status,
326
- durationMs: this.#state.durationMs,
327
- ...(statusEvents === undefined ? {} : { statusEvents }),
328
- ...(output?.hasMarkdown ? { hasMarkdown: true } : {}),
329
- },
330
- ],
331
- ...(statusEvents === undefined ? {} : { statusEvents }),
332
- ...(output === undefined || output.jsonOutputs.length === 0 ? {} : { jsonOutputs: output.jsonOutputs }),
333
- ...(output?.notice === undefined ? {} : { notice: output.notice }),
334
- ...(output?.meta === undefined ? {} : { meta: output.meta }),
335
- };
336
- }
337
-
338
- #liveUpdateText(): string {
339
- const title = this.#state.input.title === undefined ? "" : ` ${this.#state.input.title}`;
340
- const aggregateOutput = this.#output.aggregateText();
341
- const outputLines = aggregateOutput.split("\n");
342
- const hasTrailingNewline = aggregateOutput.endsWith("\n");
343
- if (hasTrailingNewline) outputLines.pop();
344
- const output = `${outputLines.slice(-LIVE_OUTPUT_PREVIEW_LINES).join("\n")}${hasTrailingNewline ? "\n" : ""}`;
345
- return `1/1 cells ${this.#state.status}\n[1] ${this.#state.input.language}${title} ${this.#state.status}${output.length === 0 ? "" : `\n${output}`}`;
346
- }
347
-
348
- #emitUpdate(isError: boolean): void {
349
- if (!this.#state.active) return;
350
- this.#state.onUpdate?.({
351
- content: [{ type: "text", text: this.#liveUpdateText() }],
352
- details: this.#details(undefined, isError),
353
- });
255
+ this.#resultBuilder.emitUpdate(false);
354
256
  }
355
257
  }
@@ -0,0 +1,157 @@
1
+ import type { AgentToolResult, AgentToolUpdateCallback, ExtensionContext } from "@code-yeongyu/senpi";
2
+ import type { KernelToHostMessage } from "../bridge/protocol.ts";
3
+ import { type EvalImageResizer, EvalOutputCollector, type EvalOutputResult } from "./image.ts";
4
+ import type { EvalStatusEvent, EvalToolDetails, EvalToolInput } from "./types.ts";
5
+
6
+ type KernelResult = Extract<KernelToHostMessage, { type: "result" }>;
7
+ type DisplayMessage = Extract<KernelToHostMessage, { type: "display" }>;
8
+ type ToolCall = EvalToolDetails["toolCalls"] extends readonly (infer Item)[] ? Item : never;
9
+
10
+ export interface CellState {
11
+ readonly input: EvalToolInput;
12
+ readonly signal: AbortSignal;
13
+ readonly onUpdate: AgentToolUpdateCallback<EvalToolDetails> | undefined;
14
+ readonly toolCalls: ToolCall[];
15
+ readonly pendingBridgeCalls: Promise<void>[];
16
+ readonly statusEvents: EvalStatusEvent[];
17
+ active: boolean;
18
+ output: string;
19
+ phase: string | undefined;
20
+ durationMs: number;
21
+ status: "pending" | "running" | "complete" | "error";
22
+ }
23
+
24
+ export interface CellResultBuilderOptions {
25
+ readonly artifactPath?: string;
26
+ readonly headBytes: number;
27
+ readonly imageResizer?: EvalImageResizer;
28
+ readonly maxColumns: number;
29
+ readonly model: ExtensionContext["model"];
30
+ readonly state: CellState;
31
+ }
32
+
33
+ export class CellResultBuilder {
34
+ readonly #output: EvalOutputCollector;
35
+ readonly #state: CellState;
36
+
37
+ constructor(options: CellResultBuilderOptions) {
38
+ this.#state = options.state;
39
+ this.#output = new EvalOutputCollector({
40
+ headBytes: options.headBytes,
41
+ maxColumns: options.maxColumns,
42
+ model: options.model,
43
+ ...(options.artifactPath === undefined ? {} : { artifactPath: options.artifactPath }),
44
+ ...(options.imageResizer === undefined ? {} : { imageResizer: options.imageResizer }),
45
+ onChunk: (_aggregate, cell) => {
46
+ options.state.output = cell;
47
+ this.emitUpdate(false);
48
+ },
49
+ });
50
+ options.state.status = "running";
51
+ this.emitUpdate(false);
52
+ }
53
+
54
+ push(text: string): void {
55
+ this.#output.push(text);
56
+ }
57
+
58
+ display(message: DisplayMessage): void {
59
+ this.#output.display(message);
60
+ }
61
+
62
+ setPhase(title: string): void {
63
+ this.#state.phase = title;
64
+ this.emitUpdate(false);
65
+ }
66
+
67
+ async finalize(result: KernelResult): Promise<AgentToolResult<EvalToolDetails>> {
68
+ this.#state.durationMs = result.durationMs;
69
+ if (result.ok) {
70
+ if (result.valueRepr) this.#output.push(`${result.valueRepr}\n`);
71
+ this.#state.status = "complete";
72
+ } else {
73
+ this.#output.push(`${result.error.message}\n`);
74
+ this.#state.status = "error";
75
+ }
76
+ return await this.#finish(!result.ok);
77
+ }
78
+
79
+ async finalizeCancellation(error: Error): Promise<AgentToolResult<EvalToolDetails>> {
80
+ this.#output.push(`${error.message}\n`);
81
+ this.#state.status = "error";
82
+ return await this.#finish(true);
83
+ }
84
+
85
+ async flushOutput(): Promise<void> {
86
+ await this.#output.flush();
87
+ }
88
+
89
+ liveResult(): AgentToolResult<EvalToolDetails> {
90
+ return {
91
+ content: [{ type: "text", text: this.#liveUpdateText() }],
92
+ details: this.#details(undefined, this.#state.status === "error"),
93
+ };
94
+ }
95
+
96
+ emitUpdate(isError: boolean): void {
97
+ if (!this.#state.active) return;
98
+ this.#state.onUpdate?.({
99
+ content: [{ type: "text", text: this.#liveUpdateText() }],
100
+ details: this.#details(undefined, isError),
101
+ });
102
+ }
103
+
104
+ async #finish(isError: boolean): Promise<AgentToolResult<EvalToolDetails>> {
105
+ const output = await this.#output.finish();
106
+ this.#state.output = output.output;
107
+ const details = this.#details(output, isError);
108
+ this.emitUpdate(isError);
109
+ const text =
110
+ output.output ||
111
+ (output.images.length > 0
112
+ ? `(displayed ${output.images.length} image${output.images.length === 1 ? "" : "s"}; no text output)`
113
+ : "(no output)");
114
+ return { content: [{ type: "text", text }, ...output.images], details };
115
+ }
116
+
117
+ #details(output: EvalOutputResult | undefined, isError: boolean): EvalToolDetails {
118
+ const statusEvents = this.#state.statusEvents.length > 0 ? [...this.#state.statusEvents] : undefined;
119
+ return {
120
+ language: this.#state.input.language,
121
+ languages: [this.#state.input.language],
122
+ ...(this.#state.input.title === undefined ? {} : { title: this.#state.input.title }),
123
+ durationMs: this.#state.durationMs,
124
+ toolCalls: [...this.#state.toolCalls],
125
+ truncated: output?.truncated ?? false,
126
+ ...(isError ? { isError: true } : {}),
127
+ ...(this.#state.phase === undefined ? {} : { phase: this.#state.phase }),
128
+ cells: [
129
+ {
130
+ index: 0,
131
+ ...(this.#state.input.title === undefined ? {} : { title: this.#state.input.title }),
132
+ code: this.#state.input.code,
133
+ language: this.#state.input.language,
134
+ output: this.#state.output,
135
+ status: this.#state.status,
136
+ durationMs: this.#state.durationMs,
137
+ ...(statusEvents === undefined ? {} : { statusEvents }),
138
+ ...(output?.hasMarkdown ? { hasMarkdown: true } : {}),
139
+ },
140
+ ],
141
+ ...(statusEvents === undefined ? {} : { statusEvents }),
142
+ ...(output === undefined || output.jsonOutputs.length === 0 ? {} : { jsonOutputs: output.jsonOutputs }),
143
+ ...(output?.notice === undefined ? {} : { notice: output.notice }),
144
+ ...(output?.meta === undefined ? {} : { meta: output.meta }),
145
+ };
146
+ }
147
+
148
+ #liveUpdateText(): string {
149
+ const title = this.#state.input.title === undefined ? "" : ` ${this.#state.input.title}`;
150
+ const aggregateOutput = this.#output.aggregateText();
151
+ const outputLines = aggregateOutput.split("\n");
152
+ const hasTrailingNewline = aggregateOutput.endsWith("\n");
153
+ if (hasTrailingNewline) outputLines.pop();
154
+ const output = `${outputLines.slice(-8).join("\n")}${hasTrailingNewline ? "\n" : ""}`;
155
+ return `1/1 cells ${this.#state.status}\n[1] ${this.#state.input.language}${title} ${this.#state.status}${output.length === 0 ? "" : `\n${output}`}`;
156
+ }
157
+ }