@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.
- package/CHANGELOG.md +48 -0
- package/README.md +28 -5
- package/package.json +4 -4
- package/src/extension/eval-notifier.ts +13 -2
- package/src/index.ts +2 -2
- package/src/kernels/js/cell-ownership.js +268 -0
- package/src/kernels/js/cell-run-context.js +89 -0
- package/src/kernels/js/process-entry.js +5 -1
- package/src/kernels/js/rejection-reports.js +61 -0
- package/src/kernels/js/worker-core.js +46 -6
- package/src/kernels/js/worker-indirect-eval.js +37 -13
- package/src/kernels/js/worker-require.d.ts +19 -0
- package/src/kernels/js/worker-require.js +52 -0
- package/src/kernels/js/worker-runtime.js +31 -9
- package/src/kernels/js/worker-shadow-guard.js +2 -6
- package/src/kernels/js/worker-shell-capture.js +5 -0
- package/src/kernels/js/worker-webview.js +7 -1
- package/src/kernels/sandbox/sandbox-cell.ts +26 -1
- package/src/tool/cell-runtime.ts +4 -2
- package/src/tool/detached-cell-contract.ts +3 -0
- package/src/tool/detached-cell-manager.ts +4 -8
- package/src/tool/detached-cell-notification.ts +11 -63
- package/src/tool/detached-cell-snapshot.ts +17 -0
- package/src/tool/detached-eval-result.ts +13 -7
- package/src/tool/detached-notification-queue.ts +1 -2
- package/src/tool/interrupt-note.ts +4 -5
- package/src/tool/live-headline.ts +60 -0
- package/src/tool/managed-cell.ts +0 -4
- package/src/tool/render.ts +74 -15
- package/src/tool/run-eval-cell.ts +3 -2
- package/src/tool/runtime-label.ts +1 -1
- package/src/tool/types.ts +2 -2
|
@@ -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
|
|
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 ? "" : `
|
|
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
|
|
77
|
+
const queuedBehind = snapshot.queuedBehind;
|
|
80
78
|
const text =
|
|
81
|
-
|
|
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
|
-
:
|
|
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) =>
|
|
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
|
|
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
|
|
67
|
-
return
|
|
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
|
+
}
|
package/src/tool/managed-cell.ts
CHANGED
|
@@ -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,
|
package/src/tool/render.ts
CHANGED
|
@@ -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 =
|
|
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
|
-
|
|
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 =
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
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
|
-
|
|
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
|
-
{
|
|
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
|
-
|
|
913
|
-
?
|
|
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
|
-
...(
|
|
939
|
-
code
|
|
940
|
-
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 =
|
|
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 ===
|
|
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
|
|
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>>>;
|