@code-yeongyu/senpi-codemode 2026.10.10-3 → 2026.10.10-5

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.
@@ -63,6 +63,23 @@ export function currentDetachedResult(cell: DetachedCellResultSource): AgentTool
63
63
  return cell.terminalResult ?? cell.liveResult?.() ?? fallbackResult(cell.input);
64
64
  }
65
65
 
66
+ /**
67
+ * The result a cell cancelled before its handler finished settles with: its buffered output, not the live progress
68
+ * frame ("1/1 cells running"), so a stopped cell never reads as still running.
69
+ */
70
+ export function cancelledDetachedResult(cell: DetachedCellResultSource): AgentToolResult<EvalToolDetails> {
71
+ if (cell.terminalResult !== undefined) return cell.terminalResult;
72
+ const current = currentDetachedResult(cell);
73
+ const output = detachedOutputTail(current);
74
+ return {
75
+ content: [
76
+ { type: "text", text: output.length > 0 ? output : "(no output)" },
77
+ ...current.content.filter((part) => part.type === "image"),
78
+ ],
79
+ details: current.details,
80
+ };
81
+ }
82
+
66
83
  export function detachedErrorResult(cell: DetachedCellResultSource, error: Error): AgentToolResult<EvalToolDetails> {
67
84
  const current = currentDetachedResult(cell);
68
85
  const output = detachedOutputTail(current);
@@ -46,9 +46,7 @@ function createEvalListResult(cellManager: EvalDetachedCellManager): AgentToolRe
46
46
  language: snapshot.language,
47
47
  state: snapshot.state,
48
48
  startedAtMs: snapshot.startedAtMs,
49
- ...(snapshot.queuedBehind === undefined || snapshot.queuedBehind.length === 0
50
- ? {}
51
- : { queuedBehind: [...snapshot.queuedBehind] }),
49
+ ...(snapshot.queuedBehind === undefined ? {} : { queuedBehind: [...snapshot.queuedBehind] }),
52
50
  ...(summary ? { summary } : {}),
53
51
  };
54
52
  });
@@ -60,7 +58,7 @@ function createEvalListResult(cellManager: EvalDetachedCellManager): AgentToolRe
60
58
  " ",
61
59
  );
62
60
  const elapsed = Math.floor(snapshot.result.details.durationMs / 1000);
63
- const queued = cell.queuedBehind === undefined ? "" : ` queued behind ${cell.queuedBehind.join(", ")}`;
61
+ const queued = cell.queuedBehind === undefined ? "" : ` ${queuedPhrase(cell.queuedBehind, cell.language)}`;
64
62
  return `${cell.cellId} ${cell.language} ${cell.state} ${elapsed}s${queued} - ${preview}`;
65
63
  })
66
64
  .join("\n");
@@ -76,11 +74,13 @@ export function resultAfterDetach(
76
74
  otherLiveCells: number,
77
75
  ): AgentToolResult<EvalToolDetails> {
78
76
  if (snapshot.state !== "detached" && snapshot.state !== "running") return createDetachedControlResult(snapshot);
79
- const predecessors = snapshot.queuedBehind?.join(", ");
77
+ const queuedBehind = snapshot.queuedBehind;
80
78
  const text =
81
- predecessors === undefined
79
+ queuedBehind === undefined
82
80
  ? `Eval cell ${snapshot.cellId} detached and is running in the ${input.language} kernel (${otherLiveCells} other live cells). Completion arrives as a notification; do not re-run it. eval({ action: "peek" | "stop", cell_id }) or eval({ action: "list" }).`
83
- : `Eval cell ${snapshot.cellId} is queued behind ${predecessors} in the ${input.language} kernel and detached; it runs after ${predecessors} and completes as one notification. peek/stop/list with eval({ action, cell_id })`;
81
+ : queuedBehind.length === 0
82
+ ? `Eval cell ${snapshot.cellId} is detached and waiting for the ${input.language} kernel to be ready; it runs first once the kernel is ready and completes as one notification. peek/stop/list with eval({ action, cell_id })`
83
+ : `Eval cell ${snapshot.cellId} is queued behind ${queuedBehind.join(", ")} in the ${input.language} kernel and detached; it runs after ${queuedBehind.join(", ")} and completes as one notification. peek/stop/list with eval({ action, cell_id })`;
84
84
  return {
85
85
  content: [
86
86
  {
@@ -164,6 +164,12 @@ export function resultForDetachedState(
164
164
  };
165
165
  }
166
166
 
167
+ function queuedPhrase(queuedBehind: readonly string[], language: string): string {
168
+ return queuedBehind.length === 0
169
+ ? `waiting for the ${language} kernel to be ready`
170
+ : `queued behind ${queuedBehind.join(", ")}`;
171
+ }
172
+
167
173
  function textContent(result: AgentToolResult<EvalToolDetails>): string {
168
174
  return result.content
169
175
  .filter((part) => part.type === "text")
@@ -3,7 +3,6 @@ import { buildDetachedCellNotification } from "./detached-cell-notification.ts";
3
3
 
4
4
  export interface PendingDetachedNotification {
5
5
  readonly snapshot: () => EvalDetachedCellSnapshot | Promise<EvalDetachedCellSnapshot>;
6
- readonly spillPath: string | undefined;
7
6
  }
8
7
 
9
8
  export class DetachedNotificationQueue {
@@ -30,7 +29,7 @@ export class DetachedNotificationQueue {
30
29
  const flush = Promise.resolve().then(async () => {
31
30
  const pending = this.#pending.splice(0);
32
31
  const notifications = await Promise.all(
33
- pending.map(async (item) => await buildDetachedCellNotification(await item.snapshot(), item.spillPath)),
32
+ pending.map(async (item) => buildDetachedCellNotification(await item.snapshot())),
34
33
  );
35
34
  this.#notifier?.notify(notifications);
36
35
  });
@@ -26,9 +26,8 @@ export async function describeTimeoutState(
26
26
  );
27
27
  if (outcome === undefined) error.message = fallbackTimeoutMessage(error.message);
28
28
  else if (outcome.retained)
29
- error.message = `${error.message} The kernel remains running; its existing variables are preserved.`;
30
- else
31
- error.message = `${error.message} The kernel was unresponsive and restarted; variables from earlier cells are lost.`;
29
+ error.message = `${error.message} The kernel was not restarted; variables from earlier cells are kept.`;
30
+ else error.message = `${error.message} The kernel was restarted; variables from earlier cells are lost.`;
32
31
  if (outcome?.note !== undefined) error.message = `${error.message} ${outcome.note.trim()}`;
33
32
  return error;
34
33
  }
@@ -63,8 +62,8 @@ const LANGUAGE_LABEL: Record<EvalLanguage, string> = {
63
62
  export function interruptionStateNote(language: EvalLanguage, stateRetained: boolean | undefined): string | undefined {
64
63
  if (stateRetained === undefined) return undefined;
65
64
  const label = LANGUAGE_LABEL[language];
66
- if (stateRetained) return `${label} was interrupted and remains running; its existing variables are preserved.`;
67
- return `${label} was unresponsive to interrupt and was restarted; variables from earlier cells are lost.`;
65
+ if (stateRetained) return `The ${label} was not restarted; variables from earlier cells are kept.`;
66
+ return `The ${label} was restarted; variables from earlier cells are lost.`;
68
67
  }
69
68
 
70
69
  export function unknownInterruptionStateNote(language: EvalLanguage): string {
@@ -0,0 +1,60 @@
1
+ import { sanitizeTerminalLabel, visibleWidth } from "@code-yeongyu/senpi";
2
+ import { type EvalCellResult, type EvalLanguage, evalLanguageOrder } from "./types.ts";
3
+
4
+ const HEADLINED_STATUSES: ReadonlySet<EvalCellResult["status"]> = new Set(["pending", "queued", "running", "detached"]);
5
+
6
+ /**
7
+ * A collapsed headline never wraps the frame: it gets what the language, state and timing leave on the line. This
8
+ * floor (in screen cells) only keeps a word or two visible in very narrow terminals.
9
+ */
10
+ const MIN_HEADLINE_CELLS = 8;
11
+
12
+ export function leadsWithHeadline(status: EvalCellResult["status"]): boolean {
13
+ return HEADLINED_STATUSES.has(status);
14
+ }
15
+
16
+ /**
17
+ * What an in-progress cell is doing, in one line: its summary, or else the first line of its code that has content once sanitized (the
18
+ * oh-my-pi fallback), or an ellipsis while the arguments are still streaming in. Collapsed rows cut it to
19
+ * `maxCells` screen cells (a wide character takes two), so the headline stays one line. Whichever is chosen is
20
+ * sanitized before it is measured, so escape and control characters never reach the row (senpi#2831, senpi#2839).
21
+ */
22
+ export function liveHeadline(
23
+ summary: string | undefined,
24
+ code: string | undefined,
25
+ maxCells: number | undefined,
26
+ ): string {
27
+ const text = (summary === undefined ? "" : sanitizeTerminalLabel(summary)) || firstCodeLine(code) || "…";
28
+ const limit = maxCells === undefined ? undefined : Math.max(MIN_HEADLINE_CELLS, maxCells);
29
+ if (limit === undefined || visibleWidth(text) <= limit) return text;
30
+ return `${cellPrefix(text, limit - 1)}…`;
31
+ }
32
+
33
+ // A plain cut by screen cells: the headline is styled by the caller, so no reset codes may be inserted here.
34
+ function cellPrefix(text: string, cells: number): string {
35
+ let kept = "";
36
+ let used = 0;
37
+ for (const { segment } of new Intl.Segmenter(undefined, { granularity: "grapheme" }).segment(text)) {
38
+ const width = visibleWidth(segment);
39
+ if (used + width > cells) break;
40
+ kept += segment;
41
+ used += width;
42
+ }
43
+ return kept;
44
+ }
45
+
46
+ export function knownLanguage(language: unknown): EvalLanguage | undefined {
47
+ return evalLanguageOrder.find((known) => known === language);
48
+ }
49
+
50
+ // The first line with something to show once it is sanitized: a line of only escape or control characters is skipped
51
+ // (senpi#2850), and the later lines never reach the row.
52
+ function firstCodeLine(code: string | undefined): string | undefined {
53
+ if (code === undefined) return undefined;
54
+ // Stops at the first line with content, so a long cell is not sanitized line by line on every frame.
55
+ for (const line of code.split("\n")) {
56
+ const clean = sanitizeTerminalLabel(line);
57
+ if (clean.length > 0) return clean;
58
+ }
59
+ return undefined;
60
+ }
@@ -1,7 +1,6 @@
1
1
  import type { AgentToolResult } from "@code-yeongyu/senpi";
2
2
  import { type CellDeadlineExpiry, CellDeadlines } from "./cell-deadlines.ts";
3
3
  import type { EvalDetachedCellSnapshot, EvalDetachedCellState } from "./detached-cell-contract.ts";
4
- import { detachedNotificationSpillPath } from "./detached-cell-notification.ts";
5
4
  import type { EvalKernel, EvalToolDetails, EvalToolInput } from "./types.ts";
6
5
 
7
6
  export type LiveResultProvider = () => AgentToolResult<EvalToolDetails>;
@@ -9,7 +8,6 @@ export type LiveResultProvider = () => AgentToolResult<EvalToolDetails>;
9
8
  export type ManagedCell = {
10
9
  readonly cellId: string;
11
10
  readonly input: EvalToolInput;
12
- readonly spillPath: string | undefined;
13
11
  readonly startedAtMs: number;
14
12
  readonly terminal: PromiseWithResolvers<EvalDetachedCellSnapshot>;
15
13
  state: Exclude<EvalDetachedCellState, "detached">;
@@ -37,7 +35,6 @@ export type ManagedCell = {
37
35
  export interface ManagedCellInit {
38
36
  readonly cellId: string;
39
37
  readonly input: EvalToolInput;
40
- readonly artifactsDir: string | undefined;
41
38
  readonly now: () => number;
42
39
  readonly defaultHardLimitSeconds: number;
43
40
  readonly defaultRunBudgetSeconds: number;
@@ -60,7 +57,6 @@ export function createManagedCell(init: ManagedCellInit): ManagedCell {
60
57
  return {
61
58
  cellId: init.cellId,
62
59
  input: init.input,
63
- spillPath: detachedNotificationSpillPath(init.artifactsDir, init.cellId),
64
60
  startedAtMs: init.now(),
65
61
  state: "queued",
66
62
  runStartedAtMs: undefined,
@@ -7,6 +7,7 @@ import {
7
7
  type ToolDefinition,
8
8
  type ToolRenderResultOptions,
9
9
  truncateToVisualLines,
10
+ visibleWidth,
10
11
  } from "@code-yeongyu/senpi";
11
12
  import type { TruncationMeta } from "../output/output-meta.ts";
12
13
  import { highlightedCode } from "./code-preview.ts";
@@ -21,6 +22,7 @@ import {
21
22
  JSON_TREE_SCALAR_LEN_EXPANDED,
22
23
  renderJsonTreeLines,
23
24
  } from "./json-tree.ts";
25
+ import { knownLanguage, leadsWithHeadline, liveHeadline } from "./live-headline.ts";
24
26
  import { formatRuntimeBadge } from "./runtime-label.ts";
25
27
  import { codePointPrefix, formatDuration, renderToolCallWidget } from "./tool-widgets.ts";
26
28
  import type {
@@ -341,18 +343,37 @@ function cellHeader(cell: EvalCellResult, environment: RenderEnvironment, badges
341
343
  environment.spinnerFrame ?? Math.floor((cellElapsedMs(cell, environment) ?? 0) / LIVE_RENDER_TICK_MS),
342
344
  );
343
345
  const runtimeBadge = cell.runtime === undefined ? "" : ` (${formatRuntimeBadge(cell.language, cell.runtime)})`;
344
- let header = `eval ${cell.language}${runtimeBadge} ${presentation.label} ${presentation.icon}`;
346
+ let header = leadsWithHeadline(cell.status)
347
+ ? `eval ${cell.language}${runtimeBadge} ${presentation.label}`
348
+ : `eval ${cell.language}${runtimeBadge} ${presentation.label} ${presentation.icon}`;
345
349
  if (cell.queuedBehind !== undefined && cell.queuedBehind.length > 0)
346
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`;
347
353
  const throughputBadge = badges.throughput === undefined ? undefined : formatThroughputBadge(badges.throughput);
348
354
  if (throughputBadge !== undefined) header += ` · ${throughputBadge}`;
349
355
  const elapsedMs = badges.throughput?.wallDurationMs ?? cellElapsedMs(cell, environment);
350
356
  if (elapsedMs !== undefined) header += ` · ${formatDuration(elapsedMs)}`;
351
357
  if (badges.reset) header += " · reset";
352
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);
353
361
  return style(environment.theme, presentation.color, header);
354
362
  }
355
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
+
356
377
  function previewText(
357
378
  text: string,
358
379
  maxLines: number,
@@ -612,12 +633,23 @@ function renderAgentProgressEvents(events: readonly EvalStatusEvent[], environme
612
633
  }
613
634
 
614
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
+ }
615
646
  const lines = renderPrefixed(cellHeader(cell, environment, badges), environment, {
616
647
  prefix: "╭─ ",
617
648
  continuation: "│ ",
618
649
  color: "borderAccent",
619
650
  });
620
- if (cell.summary !== undefined) {
651
+ const headlined = leadsWithHeadline(cell.status);
652
+ if (cell.summary !== undefined && !headlined) {
621
653
  appendLines(
622
654
  lines,
623
655
  summaryVisualLines(cell.summary, Math.max(1, environment.width - 2), environment.expanded).map(
@@ -626,11 +658,14 @@ function renderCell(cell: EvalCellResult, environment: RenderEnvironment, badges
626
658
  );
627
659
  }
628
660
  const innerWidth = Math.max(1, environment.width - 2);
629
- const codePreview = previewText(
630
- highlightedCode(cell.code, cell.language, environment.theme, environment.repaint),
631
- environment.expanded ? Number.POSITIVE_INFINITY : CODE_PREVIEW_LINES,
632
- innerWidth,
633
- );
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
+ );
634
669
  if (codePreview.skipped > 0) {
635
670
  appendLines(
636
671
  lines,
@@ -879,6 +914,10 @@ function displaySummary(summary: string | undefined): string | undefined {
879
914
  return normalizeEvalSummary(summary);
880
915
  }
881
916
 
917
+ function cellIdSuffix(cellId: unknown): string {
918
+ return typeof cellId === "string" && cellId !== "" ? ` ${sanitizeTerminalLabel(cellId)}` : "";
919
+ }
920
+
882
921
  export function renderEvalCall(
883
922
  args: EvalToolRequest,
884
923
  theme: Theme | undefined,
@@ -892,15 +931,23 @@ export function renderEvalCall(
892
931
  return component;
893
932
  }
894
933
  if (!isEvalRunInput(args)) {
895
- const title = args.action === "list" ? "eval list" : `eval ${args.action} ${args.cell_id}`;
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)}`;
896
936
  component.setBlocks([{ kind: "text", text: style(theme, "toolTitle", title) }]);
897
937
  return component;
898
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);
899
943
  if (theme === undefined && context.spinnerFrame === undefined) {
900
944
  const reset = args.reset === true ? " reset" : "";
901
945
  const timeout = args.timeout === undefined ? "" : ` timeout ${args.timeout}s`;
902
946
  component.setBlocks([
903
- { kind: "text", text: style(theme, "toolTitle", `eval ${args.language}${reset}${timeout}`) },
947
+ {
948
+ kind: "text",
949
+ text: style(theme, "toolTitle", `eval${language === undefined ? "" : ` ${language}`}${reset}${timeout}`),
950
+ },
904
951
  ...(displaySummary(args.summary) === undefined
905
952
  ? []
906
953
  : [summaryBlock(displaySummary(args.summary) ?? "", theme, context.expanded)]),
@@ -909,9 +956,11 @@ export function renderEvalCall(
909
956
  text: style(
910
957
  theme,
911
958
  "mdCodeBlock",
912
- args.code.trim().length > 0
913
- ? displayCode(args.code, args.language, context.argsComplete ? context.invalidate : undefined)
914
- : "...",
959
+ code.trim().length === 0
960
+ ? "..."
961
+ : language === undefined
962
+ ? code
963
+ : displayCode(code, language, context.argsComplete ? context.invalidate : undefined),
915
964
  ),
916
965
  maxVisualLines: context.expanded ? undefined : CODE_PREVIEW_LINES,
917
966
  collapseKind: "code",
@@ -933,11 +982,13 @@ export function renderEvalCall(
933
982
  now: renderNow(context),
934
983
  ...(context.argsComplete ? { repaint: context.invalidate } : {}),
935
984
  };
985
+ const summary = displaySummary(args.summary);
986
+ if (language === undefined) return streamingCallLines(summary, code, environment);
936
987
  const cell: EvalCellResult = {
937
988
  index: 0,
938
- ...(displaySummary(args.summary) === undefined ? {} : { summary: displaySummary(args.summary) ?? "" }),
939
- code: args.code,
940
- language: args.language,
989
+ ...(summary === undefined ? {} : { summary }),
990
+ code,
991
+ language,
941
992
  output: "",
942
993
  status: context.spinnerFrame === undefined ? "pending" : "running",
943
994
  };
@@ -952,6 +1003,14 @@ export function renderEvalCall(
952
1003
  return component;
953
1004
  }
954
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
+
955
1014
  export function renderEvalResult(
956
1015
  result: AgentToolResult<EvalResultDetails>,
957
1016
  options: ToolRenderResultOptions,
@@ -14,7 +14,7 @@ import {
14
14
  type KernelToolsCapability,
15
15
  type KernelToolsDescribeResult,
16
16
  } from "../kernels/js/kernel-tools-types.ts";
17
- import { sandboxCellExecutor } from "../kernels/sandbox/sandbox-cell.ts";
17
+ import { sandboxCellExecutor, sandboxRuntimeInfo } from "../kernels/sandbox/sandbox-cell.ts";
18
18
  import { TIMEOUT_PAUSE_OP, TIMEOUT_RESUME_OP } from "../timeouts/bridge-timeout.ts";
19
19
  import { abortError, CellExecution, defaultTimeoutFactory } from "./cell-execution.ts";
20
20
  import { CellHandler, type CellState } from "./cell-handler.ts";
@@ -44,7 +44,8 @@ export async function runEvalCell(
44
44
  const bridgeAbortController = new AbortController();
45
45
  const cellSignal = AbortSignal.any([invocation.signal, bridgeAbortController.signal]);
46
46
  const bridgeContext: ExtensionContext = { ...invocation.ctx, signal: cellSignal };
47
- const runtime = options.runtimes?.[invocation.input.language];
47
+ const runtime =
48
+ invocation.input.isolate === true ? sandboxRuntimeInfo() : options.runtimes?.[invocation.input.language];
48
49
  const state: CellState = {
49
50
  input: invocation.input,
50
51
  ...(runtime === undefined ? {} : { runtime }),
@@ -11,7 +11,7 @@ const ELLIPSIS = "\u2026";
11
11
  */
12
12
  export function formatRuntimeBadge(language: EvalLanguage, runtime: EvalRuntimeInfo, home: string = homedir()): string {
13
13
  const base = language === "js" ? `${runtime.name} ${runtime.version}` : runtime.version;
14
- const label = runtime.isolation === "process" ? `${base}, process` : base;
14
+ const label = runtime.isolation === undefined ? base : `${base}, ${runtime.isolation}`;
15
15
  if (runtime.path === undefined || runtime.path.length === 0) return label;
16
16
  return `${label}, ${minifyPath(runtime.path, home)}`;
17
17
  }
package/src/tool/types.ts CHANGED
@@ -279,8 +279,8 @@ export interface EvalRuntimeInfo {
279
279
  readonly name: string;
280
280
  readonly version: string;
281
281
  readonly path?: string;
282
- /** JS only: the kernel runs in a child process instead of a worker thread. */
283
- readonly isolation?: "process";
282
+ /** JS only: the kernel runs in a child process (`process`), or the cell ran in an isolated QuickJS VM (`sandbox`). */
283
+ readonly isolation?: "process" | "sandbox";
284
284
  }
285
285
 
286
286
  export type EvalRuntimes = Readonly<Partial<Record<EvalLanguage, EvalRuntimeInfo>>>;