@code-yeongyu/senpi-codemode 2026.10.8 → 2026.10.10-10
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 +178 -0
- package/README.md +212 -14
- package/package.json +6 -5
- package/src/bridge/http-server.ts +9 -1
- package/src/bridge/protocol.ts +12 -3
- package/src/bridge/reserved.ts +2 -0
- package/src/bridges/eval-environment-schemas.ts +48 -0
- package/src/bridges/eval-virtual-schemas.ts +12 -4
- package/src/bridges/reserved-dispatch.ts +13 -1
- package/src/config/executable-settings.json +5 -0
- package/src/config/project-trust.ts +51 -0
- package/src/config/settings-overrides.ts +2 -0
- package/src/environments/js-environments.ts +208 -0
- package/src/environments/js-installer.ts +268 -0
- package/src/environments/js-revision-files.ts +91 -0
- package/src/environments/no-links.ts +132 -0
- package/src/environments/packages-install.ts +98 -0
- package/src/environments/project-resolves.ts +54 -0
- package/src/environments/py-environment.ts +7 -3
- package/src/environments/py-installer.ts +68 -4
- package/src/environments/python-environments.ts +85 -0
- package/src/environments/revision-store.ts +38 -7
- package/src/extension/bridge-tool-call.ts +7 -1
- package/src/extension/cell-kernel-tools.ts +28 -0
- package/src/extension/eval-notifier.ts +13 -2
- package/src/extension/kernel-registration.ts +7 -2
- package/src/extension/kernel-replacement.ts +20 -0
- package/src/extension/runtime-factory.ts +53 -3
- package/src/extension/runtime-info.ts +6 -2
- package/src/extension/session-manager-contract.ts +3 -0
- package/src/extension/session-manager-proxy.ts +6 -0
- package/src/extension/session-manager.ts +26 -4
- package/src/index.ts +4 -2
- package/src/interpreters/detect.ts +34 -1
- package/src/kernels/AGENTS.md +2 -0
- package/src/kernels/jl/prelude.jl +6 -1
- package/src/kernels/js/cell-dispatch.ts +44 -0
- package/src/kernels/js/cell-ownership.js +268 -0
- package/src/kernels/js/cell-run-context.js +89 -0
- package/src/kernels/js/context-manager.ts +48 -39
- package/src/kernels/js/control-frames.ts +24 -0
- package/src/kernels/js/host-entries.ts +62 -0
- package/src/kernels/js/kernel-contract.ts +16 -1
- package/src/kernels/js/kernel-memory-bridge.ts +1 -1
- package/src/kernels/js/kernel-memory.ts +79 -0
- package/src/kernels/js/kernel-tools-registry.js +1 -0
- package/src/kernels/js/local-module-loader.ts +21 -5
- package/src/kernels/js/process-drain-scan.d.ts +8 -0
- package/src/kernels/js/process-drain-scan.js +28 -0
- package/src/kernels/js/process-entry.js +466 -0
- package/src/kernels/js/process-memory-host.ts +83 -0
- package/src/kernels/js/process-worker.ts +298 -0
- package/src/kernels/js/rejection-reports.js +61 -0
- package/src/kernels/js/run-queue.ts +7 -0
- package/src/kernels/js/worker-core.js +53 -8
- package/src/kernels/js/worker-cwd.js +5 -2
- package/src/kernels/js/worker-host.ts +9 -2
- package/src/kernels/js/worker-indirect-eval.js +37 -13
- package/src/kernels/js/worker-package-resolve.js +153 -0
- 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 +45 -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-slot.ts +9 -0
- package/src/kernels/js/worker-startup.ts +49 -1
- package/src/kernels/js/worker-webview.js +15 -1
- package/src/kernels/js/workpool.js +5 -2
- package/src/kernels/py/host-cell.ts +8 -31
- package/src/kernels/py/kernel.ts +8 -2
- package/src/kernels/py/prelude.py +145 -11
- package/src/kernels/py/process.ts +4 -0
- package/src/kernels/py/transport.ts +13 -1
- package/src/kernels/rb/workpool.rb +7 -1
- package/src/kernels/sandbox/sandbox-cell.ts +141 -0
- package/src/kernels/sandbox/vendor/pi-codemode/VENDORED.md +1 -0
- package/src/kernels/sandbox/vendor/pi-codemode/runtime/prelude-source.ts +14 -1
- package/src/kernels/sandbox/vendor/pi-codemode/runtime/worker.ts +4 -1
- package/src/kernels/sandbox/vendor/pi-codemode/types.ts +2 -0
- package/src/kernels/shared/cell-source-at-start.ts +18 -0
- package/src/kernels/shared/host-cell.ts +57 -0
- package/src/kernels/shared/subprocess-process.ts +14 -0
- package/src/prompt/eval-prompt.ts +8 -1
- package/src/tool/AGENTS.md +1 -0
- package/src/tool/cell-handler.ts +13 -12
- package/src/tool/cell-reserved-dispatch.ts +1 -0
- 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/eval-request.ts +23 -1
- package/src/tool/eval-tool-options.ts +6 -0
- package/src/tool/eval-tool.ts +17 -4
- package/src/tool/interrupt-note.ts +4 -5
- package/src/tool/live-headline.ts +60 -0
- package/src/tool/load-cell.ts +88 -0
- package/src/tool/magic-cell-host.ts +123 -0
- package/src/tool/magic-cells.ts +75 -0
- package/src/tool/managed-cell.ts +0 -4
- package/src/tool/render.ts +74 -15
- package/src/tool/run-eval-cell.ts +73 -1
- package/src/tool/runtime-label.ts +2 -1
- package/src/tool/types.ts +50 -1
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { closeSync, constants, fstatSync, openSync, readSync } from "node:fs";
|
|
2
|
+
import { isAbsolute, join, relative, resolve } from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
|
|
5
|
+
export type LoadedCell =
|
|
6
|
+
| { readonly ok: true; readonly code: string; readonly sourceFile: string }
|
|
7
|
+
| { readonly ok: false; readonly message: string };
|
|
8
|
+
|
|
9
|
+
export interface LoadCellOptions {
|
|
10
|
+
readonly cwd: string;
|
|
11
|
+
readonly artifactsDir: string | undefined;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const MAX_LOAD_BYTES = 8 * 1024 * 1024;
|
|
15
|
+
const URL_SCHEME = /^([a-z][a-z0-9+.-]*):\/\//i;
|
|
16
|
+
|
|
17
|
+
function errorCode(error: unknown): string | undefined {
|
|
18
|
+
return typeof error === "object" && error !== null && "code" in error && typeof error.code === "string"
|
|
19
|
+
? error.code
|
|
20
|
+
: undefined;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function resolveTarget(target: string, options: LoadCellOptions): { path: string } | { message: string } {
|
|
24
|
+
const scheme = URL_SCHEME.exec(target)?.[1]?.toLowerCase();
|
|
25
|
+
try {
|
|
26
|
+
if (scheme === "file") return { path: fileURLToPath(target) };
|
|
27
|
+
if (scheme === "local") {
|
|
28
|
+
if (options.artifactsDir === undefined)
|
|
29
|
+
return { message: "%load local:// needs a session artifacts directory" };
|
|
30
|
+
const root = join(options.artifactsDir, "local");
|
|
31
|
+
const path = resolve(root, decodeURIComponent(target.slice("local://".length)));
|
|
32
|
+
const inside = relative(root, path);
|
|
33
|
+
if (inside.startsWith("..") || isAbsolute(inside))
|
|
34
|
+
return { message: `%load path escapes local://: ${target}` };
|
|
35
|
+
return { path };
|
|
36
|
+
}
|
|
37
|
+
} catch {
|
|
38
|
+
// fileURLToPath and decodeURIComponent throw on a malformed URL; the message names the user's own text.
|
|
39
|
+
return { message: `%load could not read the path in ${target}` };
|
|
40
|
+
}
|
|
41
|
+
if (scheme !== undefined) {
|
|
42
|
+
return {
|
|
43
|
+
message: `%load reads local files only (a path, local:// or file://); it does not fetch ${scheme}:// URLs`,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
return { path: resolve(options.cwd, target) };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Reads a `%load` target when its cell's turn comes in the kernel's queue, so a file the previous cell wrote is
|
|
51
|
+
* there. Every failure is a refusal that names the target as the user wrote it, never an absolute path.
|
|
52
|
+
*/
|
|
53
|
+
/** Reads from `fd` until EOF or `limit` bytes, whichever comes first; a file still growing cannot exceed it. */
|
|
54
|
+
export function readAtMost(fd: number, limit: number): Buffer {
|
|
55
|
+
const buffer = Buffer.alloc(limit);
|
|
56
|
+
let filled = 0;
|
|
57
|
+
while (filled < limit) {
|
|
58
|
+
const read = readSync(fd, buffer, filled, limit - filled, null);
|
|
59
|
+
if (read === 0) break;
|
|
60
|
+
filled += read;
|
|
61
|
+
}
|
|
62
|
+
return buffer.subarray(0, filled);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function loadCell(target: string, options: LoadCellOptions): LoadedCell {
|
|
66
|
+
const resolved = resolveTarget(target, options);
|
|
67
|
+
if ("message" in resolved) return { ok: false, message: resolved.message };
|
|
68
|
+
let fd: number | undefined;
|
|
69
|
+
try {
|
|
70
|
+
// One non-blocking open, then checks on that same handle: a FIFO or device swapped in after a check can
|
|
71
|
+
// neither block the host nor be read, and the size cap keeps a huge file from stalling the event loop.
|
|
72
|
+
fd = openSync(resolved.path, constants.O_RDONLY | constants.O_NONBLOCK);
|
|
73
|
+
const info = fstatSync(fd);
|
|
74
|
+
if (!info.isFile()) return { ok: false, message: `not a file: ${target}` };
|
|
75
|
+
const tooLarge = { ok: false as const, message: `%load reads files up to 8 MiB: ${target}` };
|
|
76
|
+
if (info.size > MAX_LOAD_BYTES) return tooLarge;
|
|
77
|
+
const bytes = readAtMost(fd, MAX_LOAD_BYTES + 1);
|
|
78
|
+
if (bytes.length > MAX_LOAD_BYTES) return tooLarge;
|
|
79
|
+
return { ok: true, code: bytes.toString("utf8"), sourceFile: resolved.path };
|
|
80
|
+
} catch (error) {
|
|
81
|
+
const code = errorCode(error);
|
|
82
|
+
if (code === "ENOENT" || code === "ENOTDIR") return { ok: false, message: `file not found: ${target}` };
|
|
83
|
+
if (code === "EACCES" || code === "EPERM") return { ok: false, message: `permission denied: ${target}` };
|
|
84
|
+
return { ok: false, message: `%load could not read ${target}${code === undefined ? "" : ` (${code})`}` };
|
|
85
|
+
} finally {
|
|
86
|
+
if (fd !== undefined) closeSync(fd);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import type { JsEnvironments } from "../environments/js-environments.ts";
|
|
2
|
+
import type { PythonEnvironments } from "../environments/python-environments.ts";
|
|
3
|
+
import { TIMEOUT_PAUSE_OP, TIMEOUT_RESUME_OP } from "../timeouts/bridge-timeout.ts";
|
|
4
|
+
import { MagicCellError, parseMagicCell } from "./magic-cells.ts";
|
|
5
|
+
import type { EvalLanguage, HostCellExecutor } from "./types.ts";
|
|
6
|
+
|
|
7
|
+
export type MagicCellPlan =
|
|
8
|
+
| { readonly kind: "ordinary" }
|
|
9
|
+
| { readonly kind: "host"; readonly executor: HostCellExecutor }
|
|
10
|
+
| { readonly kind: "refused"; readonly message: string }
|
|
11
|
+
| { readonly kind: "load"; readonly target: string };
|
|
12
|
+
|
|
13
|
+
export function planMagicCell(
|
|
14
|
+
language: EvalLanguage,
|
|
15
|
+
code: string,
|
|
16
|
+
environments: PythonEnvironments | undefined,
|
|
17
|
+
jsEnvironments?: JsEnvironments,
|
|
18
|
+
): MagicCellPlan {
|
|
19
|
+
let magic: ReturnType<typeof parseMagicCell>;
|
|
20
|
+
try {
|
|
21
|
+
magic = parseMagicCell(language, code);
|
|
22
|
+
} catch (error) {
|
|
23
|
+
if (error instanceof MagicCellError) return { kind: "refused", message: error.message };
|
|
24
|
+
throw error;
|
|
25
|
+
}
|
|
26
|
+
if (magic === undefined) return { kind: "ordinary" };
|
|
27
|
+
if (magic.kind === "load") return { kind: "load", target: magic.target };
|
|
28
|
+
if (language === "js") return planJsMagic(magic, jsEnvironments);
|
|
29
|
+
if (environments === undefined) {
|
|
30
|
+
return {
|
|
31
|
+
kind: "refused",
|
|
32
|
+
message: "environment_installer_unavailable: this session has no Python interpreter to install packages for",
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
if (magic.kind === "environment") {
|
|
36
|
+
const mode = magic.mode;
|
|
37
|
+
return {
|
|
38
|
+
kind: "host",
|
|
39
|
+
executor: async () => {
|
|
40
|
+
await environments.setMode(mode);
|
|
41
|
+
const where = mode === "project" ? "the session directory" : "this session's managed revisions";
|
|
42
|
+
return { ok: true, valueRepr: `environment: ${mode} (${where})` };
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
const requirements = magic.args;
|
|
47
|
+
return {
|
|
48
|
+
kind: "host",
|
|
49
|
+
// Install time is parked time for the run budget, the same accounting as a bridge call.
|
|
50
|
+
executor: async ({ signal, emit }) => {
|
|
51
|
+
emit({ type: "status", event: { op: TIMEOUT_PAUSE_OP } });
|
|
52
|
+
try {
|
|
53
|
+
const receipt = await environments.install(requirements, signal, (stream, data) =>
|
|
54
|
+
emit({ type: "text", stream, data }),
|
|
55
|
+
);
|
|
56
|
+
const packages = receipt.resolved.length > 0 ? receipt.resolved.join(", ") : "nothing new";
|
|
57
|
+
return {
|
|
58
|
+
ok: true,
|
|
59
|
+
valueRepr: `installed ${packages} into ${receipt.mode} (revision ${receipt.revision}); already-imported modules stay cached until reset`,
|
|
60
|
+
};
|
|
61
|
+
} catch (error) {
|
|
62
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
63
|
+
return { ok: false, error: { message } };
|
|
64
|
+
} finally {
|
|
65
|
+
emit({ type: "status", event: { op: TIMEOUT_RESUME_OP } });
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function planJsMagic(
|
|
72
|
+
magic: Exclude<NonNullable<ReturnType<typeof parseMagicCell>>, { kind: "load" }>,
|
|
73
|
+
environments: JsEnvironments | undefined,
|
|
74
|
+
): MagicCellPlan {
|
|
75
|
+
if (environments === undefined) {
|
|
76
|
+
return {
|
|
77
|
+
kind: "refused",
|
|
78
|
+
message: "environment_installer_unavailable: this session has no JavaScript package environment",
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
if (magic.kind === "environment") {
|
|
82
|
+
const mode = magic.mode;
|
|
83
|
+
return {
|
|
84
|
+
kind: "host",
|
|
85
|
+
executor: async () => {
|
|
86
|
+
await environments.setMode(mode);
|
|
87
|
+
const where = mode === "project" ? "the session directory" : "this session's managed revisions";
|
|
88
|
+
return { ok: true, valueRepr: `environment: ${mode} (${where})` };
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
if (magic.kind !== "js-add") return { kind: "refused", message: "this magic is not available in JavaScript" };
|
|
93
|
+
const requested = magic.args;
|
|
94
|
+
return {
|
|
95
|
+
kind: "host",
|
|
96
|
+
executor: async ({ signal, emit }) => {
|
|
97
|
+
emit({ type: "status", event: { op: TIMEOUT_PAUSE_OP } });
|
|
98
|
+
try {
|
|
99
|
+
const receipt = await environments.install(
|
|
100
|
+
requested,
|
|
101
|
+
signal,
|
|
102
|
+
(stream, data) => emit({ type: "text", stream, data }),
|
|
103
|
+
magic.installer,
|
|
104
|
+
);
|
|
105
|
+
const added = receipt.added.length > 0 ? receipt.added.join(", ") : "nothing new";
|
|
106
|
+
const where =
|
|
107
|
+
receipt.revision === undefined ? receipt.mode : `${receipt.mode} (revision ${receipt.revision})`;
|
|
108
|
+
const shadow =
|
|
109
|
+
receipt.shadowed.length > 0
|
|
110
|
+
? `; environment_resolution_conflict: ${receipt.shadowed.join(", ")} still ${receipt.shadowed.length === 1 ? "resolves" : "resolve"} from the project's node_modules first`
|
|
111
|
+
: "";
|
|
112
|
+
return {
|
|
113
|
+
ok: true,
|
|
114
|
+
valueRepr: `added ${added} with ${receipt.installer} into ${where}${shadow}; already-imported modules stay cached until reset`,
|
|
115
|
+
};
|
|
116
|
+
} catch (error) {
|
|
117
|
+
return { ok: false, error: { message: error instanceof Error ? error.message : String(error) } };
|
|
118
|
+
} finally {
|
|
119
|
+
emit({ type: "status", event: { op: TIMEOUT_RESUME_OP } });
|
|
120
|
+
}
|
|
121
|
+
},
|
|
122
|
+
};
|
|
123
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import type { EvalLanguage } from "./types.ts";
|
|
2
|
+
|
|
3
|
+
export type MagicCell =
|
|
4
|
+
| { readonly kind: "pip"; readonly args: string }
|
|
5
|
+
| { readonly kind: "environment"; readonly mode: "managed" | "project" }
|
|
6
|
+
| { readonly kind: "load"; readonly target: string }
|
|
7
|
+
| { readonly kind: "js-add"; readonly installer: "bun" | "npm"; readonly args: string };
|
|
8
|
+
|
|
9
|
+
const HOST_MAGICS = ["pip", "environment", "load", "bun", "npm"] as const;
|
|
10
|
+
const LOAD_LANGUAGES: ReadonlySet<EvalLanguage> = new Set(["py", "js"]);
|
|
11
|
+
const COMMENT_PREFIX: Partial<Record<EvalLanguage, string>> = { py: "#", js: "//" };
|
|
12
|
+
type HostMagic = (typeof HOST_MAGICS)[number];
|
|
13
|
+
|
|
14
|
+
export class MagicCellError extends Error {
|
|
15
|
+
readonly name = "MagicCellError";
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function joinContinuations(lines: readonly string[]): string[] {
|
|
19
|
+
const joined: string[] = [];
|
|
20
|
+
let pending: string | undefined;
|
|
21
|
+
for (const line of lines) {
|
|
22
|
+
const current = pending === undefined ? line : `${pending} ${line.trim()}`;
|
|
23
|
+
if (current.trimEnd().endsWith("\\")) pending = current.trimEnd().slice(0, -1).trimEnd();
|
|
24
|
+
else {
|
|
25
|
+
joined.push(current);
|
|
26
|
+
pending = undefined;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
if (pending !== undefined) joined.push(pending);
|
|
30
|
+
return joined;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function hostMagicOf(language: EvalLanguage, line: string): HostMagic | undefined {
|
|
34
|
+
const match = /^%([A-Za-z]+)(?:\s|$)/.exec(line.trim());
|
|
35
|
+
const name = match?.[1];
|
|
36
|
+
const magic = HOST_MAGICS.find((candidate) => candidate === name);
|
|
37
|
+
if (magic === "load") return LOAD_LANGUAGES.has(language) ? magic : undefined;
|
|
38
|
+
if (magic === "bun" || magic === "npm") return language === "js" ? magic : undefined;
|
|
39
|
+
if (magic === "environment") return language === "py" || language === "js" ? magic : undefined;
|
|
40
|
+
return language === "py" ? magic : undefined;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* A cell whose first code line (blank and comment lines skipped) is a host magic runs on the host instead of
|
|
45
|
+
* the interpreter: `%pip` and `%environment` in Python, `%load` in Python and JavaScript. In Python a trailing
|
|
46
|
+
* backslash continues the line. Any other cell is ordinary code, so a magic-looking line later in the cell
|
|
47
|
+
* (say, inside a string) is left alone. A magic followed by more code is refused, because the host step must
|
|
48
|
+
* finish before the code that depends on it runs.
|
|
49
|
+
*/
|
|
50
|
+
export function parseMagicCell(language: EvalLanguage, code: string): MagicCell | undefined {
|
|
51
|
+
const comment = COMMENT_PREFIX[language];
|
|
52
|
+
// Comment lines go before Python's backslash continuations are joined: Python never continues a comment line.
|
|
53
|
+
const raw = code.split("\n").filter((line) => comment === undefined || !line.trim().startsWith(comment));
|
|
54
|
+
const lines = (language === "py" ? joinContinuations(raw) : raw).filter((line) => line.trim() !== "");
|
|
55
|
+
const first = lines[0] ?? "";
|
|
56
|
+
const magic = hostMagicOf(language, first);
|
|
57
|
+
if (magic === undefined) return undefined;
|
|
58
|
+
if (lines.length > 1)
|
|
59
|
+
throw new MagicCellError(`put %${magic} on its own cell, then run the code that uses it in the next cell`);
|
|
60
|
+
const args = first.trim().slice(`%${magic}`.length).trim();
|
|
61
|
+
if (magic === "pip") return { kind: "pip", args };
|
|
62
|
+
if (magic === "bun" || magic === "npm") {
|
|
63
|
+
const [verb, ...rest] = args.split(/\s+/);
|
|
64
|
+
if (verb !== "add" && verb !== "install") {
|
|
65
|
+
throw new MagicCellError(`%${magic} supports only add: %${magic} add <package ...>`);
|
|
66
|
+
}
|
|
67
|
+
return { kind: "js-add", installer: magic, args: rest.join(" ") };
|
|
68
|
+
}
|
|
69
|
+
if (magic === "load") {
|
|
70
|
+
if (args === "") throw new MagicCellError("%load takes one argument: the path of the file to run");
|
|
71
|
+
return { kind: "load", target: args };
|
|
72
|
+
}
|
|
73
|
+
if (args === "managed" || args === "project") return { kind: "environment", mode: args };
|
|
74
|
+
throw new MagicCellError("%environment takes one argument: managed or project");
|
|
75
|
+
}
|
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,
|
|
@@ -7,12 +7,16 @@ import {
|
|
|
7
7
|
kernelToolsStorage,
|
|
8
8
|
} from "@code-yeongyu/senpi";
|
|
9
9
|
import type { KernelToHostMessage } from "../bridge/protocol.ts";
|
|
10
|
+
import { resolveSandbox } from "../config/feature-settings.ts";
|
|
10
11
|
import { DEFAULT_FOREGROUND_WINDOW_SECONDS, defaultCodemodeSettings } from "../config/settings.ts";
|
|
12
|
+
import type { JsEnvironments } from "../environments/js-environments.ts";
|
|
13
|
+
import type { PackagesInstallEnvironments } from "../environments/packages-install.ts";
|
|
11
14
|
import {
|
|
12
15
|
KERNEL_TOOLS_CAPABILITIES,
|
|
13
16
|
type KernelToolsCapability,
|
|
14
17
|
type KernelToolsDescribeResult,
|
|
15
18
|
} from "../kernels/js/kernel-tools-types.ts";
|
|
19
|
+
import { sandboxCellExecutor, sandboxRuntimeInfo } from "../kernels/sandbox/sandbox-cell.ts";
|
|
16
20
|
import { TIMEOUT_PAUSE_OP, TIMEOUT_RESUME_OP } from "../timeouts/bridge-timeout.ts";
|
|
17
21
|
import { abortError, CellExecution, defaultTimeoutFactory } from "./cell-execution.ts";
|
|
18
22
|
import { CellHandler, type CellState } from "./cell-handler.ts";
|
|
@@ -23,6 +27,8 @@ import { EvalKernelResetRefusedError } from "./eval-kernel-reset-refused-error.t
|
|
|
23
27
|
import { evalTimeoutBehavior } from "./eval-request.ts";
|
|
24
28
|
import type { CreateEvalToolOptions, EvalCellInvocation } from "./eval-tool-options.ts";
|
|
25
29
|
import { describeTimeoutState } from "./interrupt-note.ts";
|
|
30
|
+
import { loadCell } from "./load-cell.ts";
|
|
31
|
+
import { planMagicCell } from "./magic-cell-host.ts";
|
|
26
32
|
import type { EvalKernel, EvalToolDetails } from "./types.ts";
|
|
27
33
|
|
|
28
34
|
export async function runEvalCell(
|
|
@@ -40,7 +46,8 @@ export async function runEvalCell(
|
|
|
40
46
|
const bridgeAbortController = new AbortController();
|
|
41
47
|
const cellSignal = AbortSignal.any([invocation.signal, bridgeAbortController.signal]);
|
|
42
48
|
const bridgeContext: ExtensionContext = { ...invocation.ctx, signal: cellSignal };
|
|
43
|
-
const runtime =
|
|
49
|
+
const runtime =
|
|
50
|
+
invocation.input.isolate === true ? sandboxRuntimeInfo() : options.runtimes?.[invocation.input.language];
|
|
44
51
|
const state: CellState = {
|
|
45
52
|
input: invocation.input,
|
|
46
53
|
...(runtime === undefined ? {} : { runtime }),
|
|
@@ -205,11 +212,21 @@ async function executeCell(
|
|
|
205
212
|
const pending = handler.handle(message);
|
|
206
213
|
void pending.catch((error: unknown) => execution.cancel(error));
|
|
207
214
|
};
|
|
215
|
+
let releaseCellKernelTools: (() => void) | undefined;
|
|
208
216
|
try {
|
|
209
217
|
const kernel = await execution.wait(options.kernelManager.getKernel(invocation.input.language, onMessage));
|
|
210
218
|
// Computed before the handler so its construction-time snapshot captures this cell's capability.
|
|
211
219
|
// Worker messages later restore that snapshot before calling host tools (#1754, #2512).
|
|
212
220
|
const kernelTools = kernelToolsFor(kernel);
|
|
221
|
+
// Subprocess kernels call the host over the bridge, outside this async context. Their calls carry a fresh secret
|
|
222
|
+
// minted for this run and sent only to this cell's kernel, never the model-visible cell id, so code in another
|
|
223
|
+
// kernel cannot name this cell into its tools. JS calls already run inside this cell's scope (#1754).
|
|
224
|
+
const bridgeCellToken =
|
|
225
|
+
kernelTools === undefined || invocation.input.language === "js" ? undefined : randomUUID();
|
|
226
|
+
releaseCellKernelTools =
|
|
227
|
+
kernelTools === undefined || bridgeCellToken === undefined
|
|
228
|
+
? undefined
|
|
229
|
+
: options.kernelManager.bindCellKernelTools?.(bridgeCellToken, kernelTools);
|
|
213
230
|
const runBound = async (): Promise<AgentToolResult<EvalToolDetails>> => {
|
|
214
231
|
const queue = kernel.queueSnapshot();
|
|
215
232
|
state.queuedBehind = [...(queue.activeCellId === null ? [] : [queue.activeCellId]), ...queue.queuedCellIds];
|
|
@@ -226,6 +243,7 @@ async function executeCell(
|
|
|
226
243
|
...(kernelTools === undefined ? {} : { kernelTools }),
|
|
227
244
|
...(options.handles === undefined ? {} : { handles: options.handles }),
|
|
228
245
|
hardDeadlineMs: cell.startedAtMs + cell.hardLimitSeconds * 1_000,
|
|
246
|
+
...cellEnvironments(invocation.input.language, options),
|
|
229
247
|
});
|
|
230
248
|
handler = activeHandler;
|
|
231
249
|
cellManager.bindKernel(
|
|
@@ -249,10 +267,51 @@ async function executeCell(
|
|
|
249
267
|
}
|
|
250
268
|
if (invocation.input.reset) await execution.wait(kernel.reset());
|
|
251
269
|
execution.setKernel(kernel);
|
|
270
|
+
const magic = planMagicCell(
|
|
271
|
+
invocation.input.language,
|
|
272
|
+
invocation.input.code,
|
|
273
|
+
options.pythonEnvironments,
|
|
274
|
+
options.jsEnvironments,
|
|
275
|
+
);
|
|
276
|
+
// Resolved when the cell's turn comes in the kernel's queue: a %load reads the file the cells ahead of it
|
|
277
|
+
// wrote, and a refusal settles in queue order like any cell.
|
|
278
|
+
const loadOptions = { cwd: invocation.ctx.cwd, artifactsDir: options.artifactsDir };
|
|
279
|
+
// An isolated cell never runs a file outside its sandbox: %load there is refused.
|
|
280
|
+
const isolatedLoad = invocation.input.isolate === true && magic.kind === "load";
|
|
281
|
+
const resolveAtStart = isolatedLoad
|
|
282
|
+
? () => ({
|
|
283
|
+
ok: false as const,
|
|
284
|
+
message:
|
|
285
|
+
"isolate: true cannot run a %load cell: an isolated cell sees no host files, and loading one would break that isolation. Run %load in a normal cell, or paste the code into the isolated cell.",
|
|
286
|
+
})
|
|
287
|
+
: magic.kind === "load"
|
|
288
|
+
? () => loadCell(magic.target, loadOptions)
|
|
289
|
+
: magic.kind === "refused"
|
|
290
|
+
? () => ({ ok: false as const, message: magic.message })
|
|
291
|
+
: undefined;
|
|
292
|
+
const isolated =
|
|
293
|
+
invocation.input.isolate === true && resolveAtStart === undefined
|
|
294
|
+
? sandboxCellExecutor(invocation.input.code, {
|
|
295
|
+
sandbox: resolveSandbox(options.settings ?? {}),
|
|
296
|
+
executeTool: options.executeTool,
|
|
297
|
+
toolNames: () => (options.listTools?.() ?? []).map((tool) => tool.name),
|
|
298
|
+
describeTool: (name) => options.listTools?.().find((tool) => tool.name === name)?.description,
|
|
299
|
+
})
|
|
300
|
+
: undefined;
|
|
301
|
+
const environments = invocation.input.language === "py" ? options.pythonEnvironments : undefined;
|
|
302
|
+
const envRoot = environments === undefined ? undefined : () => environments.activeRoot ?? "";
|
|
303
|
+
const jsEnvironments = invocation.input.language === "js" ? options.jsEnvironments : undefined;
|
|
304
|
+
const packageRoot = jsEnvironments === undefined ? undefined : () => jsEnvironments.packageRoot;
|
|
252
305
|
const result = await execution.wait(
|
|
253
306
|
kernel.run({
|
|
254
307
|
cellId: invocation.cellId,
|
|
255
308
|
code: invocation.input.code,
|
|
309
|
+
...(resolveAtStart === undefined ? {} : { resolveAtStart }),
|
|
310
|
+
...(magic.kind === "host" ? { host: magic.executor } : {}),
|
|
311
|
+
...(isolated === undefined ? {} : { host: isolated }),
|
|
312
|
+
...(envRoot === undefined ? {} : { envRoot }),
|
|
313
|
+
...(packageRoot === undefined ? {} : { packageRoot }),
|
|
314
|
+
...(bridgeCellToken === undefined ? {} : { bridgeCellToken }),
|
|
256
315
|
kernelPreludes: options.kernelPreludes?.(),
|
|
257
316
|
onMessage,
|
|
258
317
|
onStarted: () => {
|
|
@@ -285,6 +344,8 @@ async function executeCell(
|
|
|
285
344
|
if (error instanceof Error && error.name === "TimeoutError") throw await describeTimeoutState(error, execution);
|
|
286
345
|
throw error;
|
|
287
346
|
} finally {
|
|
347
|
+
// First, so nothing below can skip it: from here on a host call carrying this run's secret gets no kernel tools.
|
|
348
|
+
releaseCellKernelTools?.();
|
|
288
349
|
state.active = false;
|
|
289
350
|
bridgeAbortController.abort();
|
|
290
351
|
execution.finish();
|
|
@@ -306,3 +367,14 @@ function kernelToolsFor(kernel: EvalKernel): KernelToolsCapability | undefined {
|
|
|
306
367
|
invoke: (request, options) => withTools.invokeKernelTool(request, options),
|
|
307
368
|
} satisfies ExtensionKernelTools;
|
|
308
369
|
}
|
|
370
|
+
|
|
371
|
+
// Only JS cells call tools in-process; a Python cell reaches packages.install() over the bridge, whose session manager
|
|
372
|
+
// holds the same Python environment its magics use.
|
|
373
|
+
function cellEnvironments(
|
|
374
|
+
language: string,
|
|
375
|
+
options: { readonly jsEnvironments?: JsEnvironments },
|
|
376
|
+
): { readonly environments?: PackagesInstallEnvironments } {
|
|
377
|
+
if (language === "js" && options.jsEnvironments !== undefined)
|
|
378
|
+
return { environments: { js: options.jsEnvironments } };
|
|
379
|
+
return {};
|
|
380
|
+
}
|
|
@@ -10,7 +10,8 @@ const ELLIPSIS = "\u2026";
|
|
|
10
10
|
* runtime name because node and bun are otherwise indistinguishable.
|
|
11
11
|
*/
|
|
12
12
|
export function formatRuntimeBadge(language: EvalLanguage, runtime: EvalRuntimeInfo, home: string = homedir()): string {
|
|
13
|
-
const
|
|
13
|
+
const base = language === "js" ? `${runtime.name} ${runtime.version}` : runtime.version;
|
|
14
|
+
const label = runtime.isolation === undefined ? base : `${base}, ${runtime.isolation}`;
|
|
14
15
|
if (runtime.path === undefined || runtime.path.length === 0) return label;
|
|
15
16
|
return `${label}, ${minifyPath(runtime.path, home)}`;
|
|
16
17
|
}
|