@code-yeongyu/senpi-codemode 2026.9.27 → 2026.9.28-3
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 +76 -0
- package/README.md +7 -0
- package/package.json +4 -4
- package/src/bridge/protocol.ts +23 -0
- package/src/bridges/schema-bridge.ts +2 -0
- package/src/extension/kernel-preludes.ts +30 -0
- package/src/index.ts +15 -1
- package/src/kernels/js/context-manager.ts +1 -1
- package/src/kernels/js/inline-worker.ts +2 -1
- package/src/kernels/js/local-module-loader.ts +18 -2
- package/src/kernels/js/webview-host.ts +59 -0
- package/src/kernels/js/worker-core.js +17 -0
- package/src/kernels/js/worker-host.ts +2 -1
- package/src/kernels/js/worker-runtime.js +2 -1
- package/src/kernels/js/worker-slot.ts +14 -5
- package/src/kernels/js/worker-webview-client.js +79 -0
- package/src/kernels/js/worker-webview.js +204 -0
- package/src/kernels/py/kernel.ts +6 -1
- package/src/kernels/py/prelude.py +15 -2
- package/src/kernels/py/transport.ts +7 -1
- package/src/kernels/shared/kernel-prelude-plan.ts +19 -0
- package/src/kernels/shared/subprocess-queue.ts +2 -0
- package/src/prompt/eval-prompt-template.ts +2 -1
- package/src/prompt/eval-prompt.ts +4 -0
- package/src/skill/bun-1-4/references/runtime-apis.md +1 -0
- package/src/tool/eval-tool-options.ts +11 -1
- package/src/tool/eval-tool.ts +1 -0
- package/src/tool/run-eval-cell.ts +1 -0
- package/src/tool/types.ts +3 -1
package/CHANGELOG.md
CHANGED
|
@@ -12,6 +12,82 @@
|
|
|
12
12
|
|
|
13
13
|
### Removed
|
|
14
14
|
|
|
15
|
+
## [2026.9.28-3] - 2026-09-28
|
|
16
|
+
|
|
17
|
+
### Breaking Changes
|
|
18
|
+
|
|
19
|
+
### Added
|
|
20
|
+
|
|
21
|
+
### Changed
|
|
22
|
+
|
|
23
|
+
### Fixed
|
|
24
|
+
|
|
25
|
+
- `new Bun.WebView()` in a JavaScript eval cell no longer fails with `Bun.WebView with backend "chrome" is only available on the main thread` (every call on Windows and Linux, `backend: "chrome"` on macOS). Cells see a `Bun` whose `WebView` (also through `import { WebView } from "bun"`) hands Chrome-backed views to the process main thread with the same API: navigation, input, `evaluate`, screenshots, `cdp()` and its events, `console` capture, `url`/`title`/`loading`, `close()` and `await using`. The macOS default (WebKit) stays a native view in the kernel worker. `Bun.WebView.closeAll()` in a cell closes only that kernel's views. ([#2248](https://github.com/code-yeongyu/senpi/issues/2248))
|
|
26
|
+
|
|
27
|
+
### Removed
|
|
28
|
+
|
|
29
|
+
## [2026.9.28-2] - 2026-09-28
|
|
30
|
+
|
|
31
|
+
### Breaking Changes
|
|
32
|
+
|
|
33
|
+
### Added
|
|
34
|
+
|
|
35
|
+
### Changed
|
|
36
|
+
|
|
37
|
+
### Fixed
|
|
38
|
+
|
|
39
|
+
### Removed
|
|
40
|
+
|
|
41
|
+
## [2026.9.28] - 2026-09-28
|
|
42
|
+
|
|
43
|
+
### Breaking Changes
|
|
44
|
+
|
|
45
|
+
### Added
|
|
46
|
+
|
|
47
|
+
### Changed
|
|
48
|
+
|
|
49
|
+
### Fixed
|
|
50
|
+
|
|
51
|
+
### Removed
|
|
52
|
+
|
|
53
|
+
## [2026.9.27-4] - 2026-09-27
|
|
54
|
+
|
|
55
|
+
### Breaking Changes
|
|
56
|
+
|
|
57
|
+
### Added
|
|
58
|
+
|
|
59
|
+
### Changed
|
|
60
|
+
|
|
61
|
+
### Fixed
|
|
62
|
+
|
|
63
|
+
### Removed
|
|
64
|
+
|
|
65
|
+
## [2026.9.27-3] - 2026-09-27
|
|
66
|
+
|
|
67
|
+
### Breaking Changes
|
|
68
|
+
|
|
69
|
+
### Added
|
|
70
|
+
|
|
71
|
+
### Changed
|
|
72
|
+
|
|
73
|
+
### Fixed
|
|
74
|
+
|
|
75
|
+
### Removed
|
|
76
|
+
|
|
77
|
+
## [2026.9.27-2] - 2026-09-27
|
|
78
|
+
|
|
79
|
+
### Breaking Changes
|
|
80
|
+
|
|
81
|
+
### Added
|
|
82
|
+
|
|
83
|
+
- The eval kernels install each active tool's `kernelPrelude`: its JavaScript or Python statements run before a cell whenever one of the prelude's exports is missing, a deactivated tool's exports are removed before the next cell, and each prelude's documentation line joins the eval prompt's helper list. Exports that would shadow a built-in helper are rejected. ([#2178](https://github.com/code-yeongyu/senpi/pull/2178))
|
|
84
|
+
|
|
85
|
+
### Changed
|
|
86
|
+
|
|
87
|
+
### Fixed
|
|
88
|
+
|
|
89
|
+
### Removed
|
|
90
|
+
|
|
15
91
|
## [2026.9.27] - 2026-09-27
|
|
16
92
|
|
|
17
93
|
### Breaking Changes
|
package/README.md
CHANGED
|
@@ -162,6 +162,13 @@ into the cell carries the tool's expected parameters, so the cell can correct th
|
|
|
162
162
|
arguments and retry instead of falling back to one-at-a-time tool calls.
|
|
163
163
|
`tool_schema()` exposes the same catalog up front.
|
|
164
164
|
|
|
165
|
+
A tool may also contribute globals of its own through `ToolDefinition.kernelPrelude`
|
|
166
|
+
(JavaScript and Python snippets that call the ordinary `tool.<name>()`, one
|
|
167
|
+
documentation line, and the exported names). While the tool is active, each
|
|
168
|
+
JavaScript and Python cell installs any missing export first and the eval prompt
|
|
169
|
+
lists the documentation line; once the tool is deactivated, the next cell deletes
|
|
170
|
+
those names. Exports that shadow a built-in helper are rejected by the host.
|
|
171
|
+
|
|
165
172
|
`agent()` is available only when the configured task tool is active in the
|
|
166
173
|
session. `output()` similarly requires the configured task-output tool and
|
|
167
174
|
returns immediately: a running task reports its current status, while completed
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@code-yeongyu/senpi-codemode",
|
|
3
|
-
"version": "2026.9.
|
|
3
|
+
"version": "2026.9.28-3",
|
|
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.9.
|
|
33
|
+
"@earendil-works/pi-ai": "npm:@code-yeongyu/senpi-ai@2026.9.28-3",
|
|
34
34
|
"typebox": "1.3.34"
|
|
35
35
|
},
|
|
36
36
|
"peerDependencies": {
|
|
37
|
-
"@code-yeongyu/senpi": "2026.9.
|
|
37
|
+
"@code-yeongyu/senpi": "2026.9.28-3"
|
|
38
38
|
},
|
|
39
39
|
"devDependencies": {
|
|
40
|
-
"@code-yeongyu/senpi": "2026.9.
|
|
40
|
+
"@code-yeongyu/senpi": "2026.9.28-3"
|
|
41
41
|
},
|
|
42
42
|
"keywords": [
|
|
43
43
|
"senpi",
|
package/src/bridge/protocol.ts
CHANGED
|
@@ -37,6 +37,15 @@ const hostToKernelMessageSchema = Type.Union([
|
|
|
37
37
|
cellId: Type.String({ minLength: 1 }),
|
|
38
38
|
code: Type.String(),
|
|
39
39
|
timeoutMs: Type.Optional(Type.Integer({ minimum: 1 })),
|
|
40
|
+
/** Python only: active tool globals to install before the cell and deactivated ones to remove. */
|
|
41
|
+
preludes: Type.Optional(
|
|
42
|
+
Type.Object({
|
|
43
|
+
install: Type.Array(
|
|
44
|
+
Type.Object({ exports: Type.Array(Type.String({ minLength: 1 })), python: Type.String() }),
|
|
45
|
+
),
|
|
46
|
+
remove: Type.Array(Type.String({ minLength: 1 })),
|
|
47
|
+
}),
|
|
48
|
+
),
|
|
40
49
|
}),
|
|
41
50
|
Type.Object({
|
|
42
51
|
type: Type.Literal("tool-reply"),
|
|
@@ -57,6 +66,19 @@ const hostToKernelMessageSchema = Type.Union([
|
|
|
57
66
|
Type.Object({
|
|
58
67
|
type: Type.Literal("close"),
|
|
59
68
|
}),
|
|
69
|
+
/** JS only: the kernel's private port to the main-thread Bun.WebView service, sent in the transfer list. */
|
|
70
|
+
Type.Object({
|
|
71
|
+
type: Type.Literal("webview-port"),
|
|
72
|
+
requestId: Type.String({ minLength: 1 }),
|
|
73
|
+
ok: Type.Literal(true),
|
|
74
|
+
port: Type.Unknown(),
|
|
75
|
+
}),
|
|
76
|
+
Type.Object({
|
|
77
|
+
type: Type.Literal("webview-port"),
|
|
78
|
+
requestId: Type.String({ minLength: 1 }),
|
|
79
|
+
ok: Type.Literal(false),
|
|
80
|
+
error: bridgeErrorSchema,
|
|
81
|
+
}),
|
|
60
82
|
...kernelToolHostToKernelSchemas,
|
|
61
83
|
]);
|
|
62
84
|
|
|
@@ -97,6 +119,7 @@ const kernelToHostMessageSchema = Type.Union([
|
|
|
97
119
|
durationMs: Type.Integer({ minimum: 0 }),
|
|
98
120
|
}),
|
|
99
121
|
Type.Object({ type: Type.Literal("closed") }),
|
|
122
|
+
Type.Object({ type: Type.Literal("webview-connect"), requestId: Type.String({ minLength: 1 }) }),
|
|
100
123
|
...kernelToolKernelToHostSchemas,
|
|
101
124
|
]);
|
|
102
125
|
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { KernelPreludeContribution } from "@code-yeongyu/senpi";
|
|
1
2
|
import { type Static, Type } from "typebox";
|
|
2
3
|
import { Check, Errors } from "typebox/value";
|
|
3
4
|
|
|
@@ -12,6 +13,7 @@ export interface EvalSchemaToolInfo {
|
|
|
12
13
|
readonly name: string;
|
|
13
14
|
readonly description?: string | undefined;
|
|
14
15
|
readonly parameters?: unknown;
|
|
16
|
+
readonly kernelPrelude?: KernelPreludeContribution | undefined;
|
|
15
17
|
}
|
|
16
18
|
|
|
17
19
|
export interface RunEvalSchemaOptions {
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { KernelPreludeContribution } from "@code-yeongyu/senpi";
|
|
2
|
+
import type { CodemodeRuntimeAPI } from "./runtime-factory.ts";
|
|
3
|
+
|
|
4
|
+
type ToolRegistry = Pick<CodemodeRuntimeAPI, "getActiveTools" | "getAllTools">;
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Kernel globals of the tools active right now. Read per cell, so a tool activated by `tool_search`, a by-name
|
|
8
|
+
* call, or a command shows up in the next cell and a deactivated one disappears, whatever the registration order.
|
|
9
|
+
*/
|
|
10
|
+
export function activeKernelPreludes(pi: ToolRegistry): readonly KernelPreludeContribution[] {
|
|
11
|
+
const active = new Set(pi.getActiveTools());
|
|
12
|
+
return pi.getAllTools().flatMap((tool) => (tool.kernelPrelude && active.has(tool.name) ? [tool.kernelPrelude] : []));
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Identity of the prompt's contribution doc lines; the eval tool re-registers only when it changes. */
|
|
16
|
+
export function kernelPreludeDocsKey(preludes: readonly KernelPreludeContribution[]): string {
|
|
17
|
+
return preludes.map((prelude) => prelude.documentation).join("\n");
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* The contributions the eval description documents. Like the monitor probe, an unreadable registry (the loader's
|
|
22
|
+
* action methods throw until the runtime binds) teaches nothing; the per-cell read still surfaces any error.
|
|
23
|
+
*/
|
|
24
|
+
export function promptKernelPreludes(pi: ToolRegistry): readonly KernelPreludeContribution[] {
|
|
25
|
+
try {
|
|
26
|
+
return activeKernelPreludes(pi);
|
|
27
|
+
} catch {
|
|
28
|
+
return [];
|
|
29
|
+
}
|
|
30
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
import { EvalNotifier } from "./extension/eval-notifier.ts";
|
|
14
14
|
import { EVAL_CELLS_STATUS_KEY } from "./extension/eval-status.ts";
|
|
15
15
|
import { EvalStatusTicker } from "./extension/eval-status-ticker.ts";
|
|
16
|
+
import { activeKernelPreludes, kernelPreludeDocsKey, promptKernelPreludes } from "./extension/kernel-preludes.ts";
|
|
16
17
|
import {
|
|
17
18
|
createExecuteTool,
|
|
18
19
|
createRuntime,
|
|
@@ -43,7 +44,7 @@ const SESSION_LIFECYCLE_EVENTS = [
|
|
|
43
44
|
|
|
44
45
|
type SessionLifecycleEvent = (typeof SESSION_LIFECYCLE_EVENTS)[number];
|
|
45
46
|
|
|
46
|
-
type CodemodeEvent = SessionLifecycleEvent | "model_select";
|
|
47
|
+
type CodemodeEvent = SessionLifecycleEvent | "model_select" | "turn_start";
|
|
47
48
|
|
|
48
49
|
export interface CodemodeExtensionAPI {
|
|
49
50
|
registerTool(tool: ReturnType<typeof createEvalTool>): void;
|
|
@@ -91,6 +92,7 @@ export default function senpiCodemode(pi: CodemodeExtensionAPI, options: SenpiCo
|
|
|
91
92
|
let activeModelId: string | undefined;
|
|
92
93
|
let activeContext: ExtensionContext | undefined;
|
|
93
94
|
let activeCells: EvalDetachedCellManager | undefined;
|
|
95
|
+
let promptPreludeDocs = "";
|
|
94
96
|
const notifier = new EvalNotifier({
|
|
95
97
|
sendMessage: (message, notifyOptions) => pi.sendMessage(message, notifyOptions),
|
|
96
98
|
getContext: () => activeContext,
|
|
@@ -134,6 +136,8 @@ export default function senpiCodemode(pi: CodemodeExtensionAPI, options: SenpiCo
|
|
|
134
136
|
// reaches this before the runtime is bound). An unreadable registry means "do not teach
|
|
135
137
|
// a tool we cannot confirm"; session_start / model_select re-register once it is live.
|
|
136
138
|
const monitor = monitorIsRegistered(pi);
|
|
139
|
+
const preludes = promptKernelPreludes(pi);
|
|
140
|
+
promptPreludeDocs = kernelPreludeDocsKey(preludes);
|
|
137
141
|
pi.registerTool(
|
|
138
142
|
createEvalTool({
|
|
139
143
|
enabledLanguages: runtime.enabledLanguages,
|
|
@@ -156,6 +160,8 @@ export default function senpiCodemode(pi: CodemodeExtensionAPI, options: SenpiCo
|
|
|
156
160
|
spawnDefaultAgent: runtime.settings.taskTools.task,
|
|
157
161
|
hostLine: hostLine(),
|
|
158
162
|
runtimes: runtime.runtimes,
|
|
163
|
+
kernelPreludes: () => activeKernelPreludes(pi),
|
|
164
|
+
promptKernelPreludes: preludes,
|
|
159
165
|
...(bunSkillPath === undefined ? {} : { bunSkillPath }),
|
|
160
166
|
...(modelId === undefined ? {} : { modelId }),
|
|
161
167
|
}),
|
|
@@ -252,6 +258,14 @@ export default function senpiCodemode(pi: CodemodeExtensionAPI, options: SenpiCo
|
|
|
252
258
|
if (cellManager === undefined) return;
|
|
253
259
|
registerEvalForRuntime(runtime, modelId, cellManager);
|
|
254
260
|
});
|
|
261
|
+
// Tool activation has no event of its own; the next turn re-documents a changed contribution set.
|
|
262
|
+
pi.on("turn_start", async () => {
|
|
263
|
+
const runtime = activeRuntime;
|
|
264
|
+
const cellManager = activeCells;
|
|
265
|
+
if (runtime === undefined || cellManager === undefined) return;
|
|
266
|
+
if (kernelPreludeDocsKey(promptKernelPreludes(pi)) === promptPreludeDocs) return;
|
|
267
|
+
registerEvalForRuntime(runtime, activeModelId, cellManager);
|
|
268
|
+
});
|
|
255
269
|
}
|
|
256
270
|
|
|
257
271
|
function hostLine(): string {
|
|
@@ -195,7 +195,7 @@ export class JavaScriptKernel {
|
|
|
195
195
|
this.#slot.postMessage({
|
|
196
196
|
type: "run",
|
|
197
197
|
cellId: next.input.cellId,
|
|
198
|
-
code: this.#moduleLoader.prepareCell(next.input.code),
|
|
198
|
+
code: this.#moduleLoader.prepareCell(next.input.code, next.input.kernelPreludes),
|
|
199
199
|
timeoutMs: next.input.timeoutMs,
|
|
200
200
|
});
|
|
201
201
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { dirname, join } from "node:path";
|
|
2
2
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
3
|
+
import type { MessagePort } from "node:worker_threads";
|
|
3
4
|
import type { HostToKernelMessage, KernelToHostMessage } from "../../bridge/protocol.ts";
|
|
4
5
|
import { type CodemodeRuntimeAssetEnvironment, requireCodemodeRuntimeAsset } from "../shared/runtime-asset.ts";
|
|
5
6
|
import type { JavaScriptKernelMode } from "./kernel-contract.ts";
|
|
@@ -18,7 +19,7 @@ export function resolveInlineWorkerEntryUrl(options: JavaScriptInlineWorkerEntry
|
|
|
18
19
|
|
|
19
20
|
export interface WorkerLike {
|
|
20
21
|
readonly mode: JavaScriptKernelMode;
|
|
21
|
-
postMessage(message: HostToKernelMessage): void;
|
|
22
|
+
postMessage(message: HostToKernelMessage, transfer?: readonly MessagePort[]): void;
|
|
22
23
|
onMessage(handler: (message: KernelToHostMessage) => void): () => void;
|
|
23
24
|
onError(handler: (error: Error) => void): () => void;
|
|
24
25
|
terminate(): Promise<void>;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { join, resolve, sep } from "node:path";
|
|
2
2
|
import { pathToFileURL } from "node:url";
|
|
3
|
+
import type { KernelPreludeContribution } from "@code-yeongyu/senpi";
|
|
3
4
|
import type { BridgeConnectionConfig } from "../../bridge/protocol.ts";
|
|
4
5
|
import {
|
|
5
6
|
RESERVED_AGENT_TOOL,
|
|
@@ -8,6 +9,7 @@ import {
|
|
|
8
9
|
TIMEOUT_PAUSE_OP,
|
|
9
10
|
TIMEOUT_RESUME_OP,
|
|
10
11
|
} from "../../bridge/reserved.ts";
|
|
12
|
+
import { type KernelPreludePlan, KernelPreludeTracker } from "../shared/kernel-prelude-plan.ts";
|
|
11
13
|
import type { JavaScriptKernelOptions as BaseJavaScriptKernelOptions } from "./kernel-contract.ts";
|
|
12
14
|
import { rewriteImports } from "./rewrite-imports.ts";
|
|
13
15
|
|
|
@@ -75,6 +77,8 @@ function loaderPrelude(context: RuntimeModuleContext): string {
|
|
|
75
77
|
"globalThis.__senpi_import__ = async (source, options) => {",
|
|
76
78
|
" const context = globalThis.__senpi_module_context__;",
|
|
77
79
|
" const specifier = String(source);",
|
|
80
|
+
" const kernelBun = globalThis[Symbol.for('senpi.kernel.bun')];",
|
|
81
|
+
" if (specifier === 'bun' && kernelBun) return { ...(await import('bun')), WebView: kernelBun.WebView, default: kernelBun };",
|
|
78
82
|
" const match = /^([a-z][a-z0-9+.-]*):\\/\\/(.*)$/i.exec(specifier);",
|
|
79
83
|
" let target = specifier;",
|
|
80
84
|
" if (match) {",
|
|
@@ -99,14 +103,26 @@ function loaderPrelude(context: RuntimeModuleContext): string {
|
|
|
99
103
|
].join("\n");
|
|
100
104
|
}
|
|
101
105
|
|
|
106
|
+
/** Removes deactivated exports, then installs each contribution only while one of its exports is missing. */
|
|
107
|
+
function contributionPrelude(plan: KernelPreludePlan): string {
|
|
108
|
+
const removals = plan.remove.map((name) => `delete globalThis[${JSON.stringify(name)}];`);
|
|
109
|
+
const installs = plan.install.map((contribution) => {
|
|
110
|
+
const missing = contribution.exports.map((name) => `!globalThis[${JSON.stringify(name)}]`).join(" || ");
|
|
111
|
+
return `if (${missing}) {\n${contribution.javascript}\n}`;
|
|
112
|
+
});
|
|
113
|
+
return [...removals, ...installs].join("\n");
|
|
114
|
+
}
|
|
115
|
+
|
|
102
116
|
export class LocalModuleLoader {
|
|
103
117
|
readonly #prelude: string;
|
|
118
|
+
readonly #contributions = new KernelPreludeTracker();
|
|
104
119
|
|
|
105
120
|
constructor(options: LocalModuleLoaderOptions) {
|
|
106
121
|
this.#prelude = loaderPrelude(runtimeContext(options));
|
|
107
122
|
}
|
|
108
123
|
|
|
109
|
-
prepareCell(code: string): string {
|
|
110
|
-
|
|
124
|
+
prepareCell(code: string, contributions: readonly KernelPreludeContribution[] = []): string {
|
|
125
|
+
const prelude = `${this.#prelude}\n${contributionPrelude(this.#contributions.plan(contributions))}`;
|
|
126
|
+
return `${PREPARED_CELL_PREFIX}${JSON.stringify({ prelude, code: rewriteImports(code) })}`;
|
|
111
127
|
}
|
|
112
128
|
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import type { MessagePort } from "node:worker_threads";
|
|
2
|
+
import type { WebViewServiceConnection } from "@code-yeongyu/senpi";
|
|
3
|
+
import type { HostToKernelMessage, KernelToHostMessage } from "../../bridge/protocol.ts";
|
|
4
|
+
import { bridgeError } from "./worker-host.ts";
|
|
5
|
+
|
|
6
|
+
export type WebViewConnector = () => Promise<WebViewServiceConnection>;
|
|
7
|
+
|
|
8
|
+
// Loaded on the first WebView a cell asks for, so starting a kernel never pays for the host barrel.
|
|
9
|
+
const connectThroughHost: WebViewConnector = async () => (await import("@code-yeongyu/senpi")).connectWebViewService();
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* The WebView clients one worker generation asked for. Chrome-backed `Bun.WebView`s only run on the
|
|
13
|
+
* process main thread, so the worker gets a private port to the main-thread service; retiring the
|
|
14
|
+
* generation (reset, close, crash, timeout) releases every view created through those ports.
|
|
15
|
+
*/
|
|
16
|
+
export class KernelWebViewClients {
|
|
17
|
+
readonly #post: (message: HostToKernelMessage, transfer: readonly MessagePort[]) => void;
|
|
18
|
+
readonly #connect: WebViewConnector;
|
|
19
|
+
readonly #connections = new Set<WebViewServiceConnection>();
|
|
20
|
+
#retired = false;
|
|
21
|
+
|
|
22
|
+
constructor(
|
|
23
|
+
post: (message: HostToKernelMessage, transfer: readonly MessagePort[]) => void,
|
|
24
|
+
connect: WebViewConnector = connectThroughHost,
|
|
25
|
+
) {
|
|
26
|
+
this.#post = post;
|
|
27
|
+
this.#connect = connect;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
consume(message: KernelToHostMessage): boolean {
|
|
31
|
+
if (message.type !== "webview-connect") return false;
|
|
32
|
+
void this.#grant(message.requestId);
|
|
33
|
+
return true;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async release(): Promise<void> {
|
|
37
|
+
this.#retired = true;
|
|
38
|
+
const connections = [...this.#connections];
|
|
39
|
+
this.#connections.clear();
|
|
40
|
+
await Promise.allSettled(connections.map((connection) => connection.release()));
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async #grant(requestId: string): Promise<void> {
|
|
44
|
+
let connection: WebViewServiceConnection;
|
|
45
|
+
try {
|
|
46
|
+
connection = await this.#connect();
|
|
47
|
+
} catch (error) {
|
|
48
|
+
const cause = error instanceof Error ? error : new Error(String(error));
|
|
49
|
+
if (!this.#retired) this.#post({ type: "webview-port", requestId, ok: false, error: bridgeError(cause) }, []);
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
if (this.#retired) {
|
|
53
|
+
await connection.release();
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
this.#connections.add(connection);
|
|
57
|
+
this.#post({ type: "webview-port", requestId, ok: true, port: connection.port }, [connection.port]);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -3,6 +3,7 @@ import { kernelToolError } from "./kernel-tools-errors.js";
|
|
|
3
3
|
import { createKernelToolPump } from "./kernel-tools-pump.js";
|
|
4
4
|
import { hostDeniedError, hostToolRefusal } from "./kernel-tools-scope.js";
|
|
5
5
|
import { JsWorkerRuntime } from "./worker-runtime.js";
|
|
6
|
+
import { installKernelWebView } from "./worker-webview.js";
|
|
6
7
|
|
|
7
8
|
// Mirrors INTERRUPT_ACK_OP and CHILD_LIFECYCLE_OP in src/bridge/reserved.ts (this worker file cannot import TypeScript).
|
|
8
9
|
const INTERRUPT_ACK_OP = "interrupt-ack";
|
|
@@ -25,6 +26,7 @@ export function createWorkerCore(transport, options) {
|
|
|
25
26
|
let runtime = null;
|
|
26
27
|
let activeCell = null;
|
|
27
28
|
const pendingTools = new Map();
|
|
29
|
+
const pendingWebViewPorts = new Map();
|
|
28
30
|
const nestedInvokes = new Map();
|
|
29
31
|
const kernelTools = createKernelToolPump({
|
|
30
32
|
getRuntime: () => runtime,
|
|
@@ -73,6 +75,13 @@ export function createWorkerCore(transport, options) {
|
|
|
73
75
|
return await promise;
|
|
74
76
|
}
|
|
75
77
|
|
|
78
|
+
function requestWebViewPort() {
|
|
79
|
+
const requestId = crypto.randomUUID();
|
|
80
|
+
const promise = new Promise((resolve, reject) => pendingWebViewPorts.set(requestId, { resolve, reject }));
|
|
81
|
+
emit({ type: "webview-connect", requestId });
|
|
82
|
+
return promise;
|
|
83
|
+
}
|
|
84
|
+
|
|
76
85
|
function interruptCell(reason) {
|
|
77
86
|
if (!activeCell || !runtime) return;
|
|
78
87
|
emit({ type: "status", event: { op: INTERRUPT_ACK_OP, cellId: activeCell.cellId } });
|
|
@@ -92,8 +101,16 @@ export function createWorkerCore(transport, options) {
|
|
|
92
101
|
runtime?.kernelTools.setCollisionNames(message.hostToolNames ?? [], message.foreignLanguageNames ?? []);
|
|
93
102
|
return;
|
|
94
103
|
}
|
|
104
|
+
if (message.type === "webview-port") {
|
|
105
|
+
const pending = pendingWebViewPorts.get(message.requestId);
|
|
106
|
+
pendingWebViewPorts.delete(message.requestId);
|
|
107
|
+
if (message.ok) pending?.resolve(message.port);
|
|
108
|
+
else pending?.reject(errorFromBridge(message.error));
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
95
111
|
if (message.type === "init") {
|
|
96
112
|
applySessionEnvironment(message.sessionEnv);
|
|
113
|
+
installKernelWebView(requestWebViewPort);
|
|
97
114
|
runtime = new JsWorkerRuntime({
|
|
98
115
|
cwd: options.cwd,
|
|
99
116
|
parallelPoolWidth: options.parallelPoolWidth,
|
|
@@ -88,7 +88,8 @@ export function bridgeError(error: Error): {
|
|
|
88
88
|
function wrapNodeWorker(worker: Worker, mode: JavaScriptKernelMode): WorkerLike {
|
|
89
89
|
return {
|
|
90
90
|
mode,
|
|
91
|
-
postMessage: (message) =>
|
|
91
|
+
postMessage: (message, transfer) =>
|
|
92
|
+
worker.postMessage(message, transfer === undefined ? undefined : [...transfer]),
|
|
92
93
|
onMessage(handler) {
|
|
93
94
|
const listener = (message: KernelToHostMessage): void => handler(message);
|
|
94
95
|
worker.on("message", listener);
|
|
@@ -6,6 +6,7 @@ import { encodeDisplayImage, resolveDisplayOps } from "./display-image.js";
|
|
|
6
6
|
import { terminateProcessTrees } from "./process-tree.js";
|
|
7
7
|
import { awaitMaybePromise, indirectEval, wrapUserCode } from "./worker-indirect-eval.js";
|
|
8
8
|
import { installShellCapture } from "./worker-shell-capture.js";
|
|
9
|
+
import { bindKernelBun } from "./worker-webview.js";
|
|
9
10
|
import { createWorkpool } from "./workpool.js";
|
|
10
11
|
import { inKernelToolInvoke } from "./kernel-tools-context.js";
|
|
11
12
|
import { kernelToolError } from "./kernel-tools-errors.js";
|
|
@@ -57,7 +58,7 @@ export class JsWorkerRuntime {
|
|
|
57
58
|
({ prelude, code: cellCode } = prepared);
|
|
58
59
|
}
|
|
59
60
|
if (prelude) indirectEval(prelude, `${cellId}:prelude`);
|
|
60
|
-
const value = await awaitMaybePromise(indirectEval(wrapUserCode(cellCode), cellId));
|
|
61
|
+
const value = await awaitMaybePromise(indirectEval(bindKernelBun(wrapUserCode(cellCode)), cellId));
|
|
61
62
|
await this.#drainPendingDisplays();
|
|
62
63
|
return value;
|
|
63
64
|
} finally {
|
|
@@ -1,8 +1,10 @@
|
|
|
1
|
+
import type { MessagePort } from "node:worker_threads";
|
|
1
2
|
import type { HostToKernelMessage, KernelToHostMessage } from "../../bridge/protocol.ts";
|
|
2
3
|
import type { WorkerLike } from "./inline-worker.ts";
|
|
3
4
|
import { retireWorker, type WorkerRetirement } from "./interrupt-bounds.ts";
|
|
4
5
|
import type { JavaScriptKernelMode } from "./kernel-contract.ts";
|
|
5
6
|
import type { JavaScriptKernelOptions } from "./local-module-loader.ts";
|
|
7
|
+
import { KernelWebViewClients } from "./webview-host.ts";
|
|
6
8
|
import { WorkerStartupCancelledError } from "./worker-host.ts";
|
|
7
9
|
import { startWorkerWithInlineFallback } from "./worker-startup.ts";
|
|
8
10
|
|
|
@@ -17,6 +19,7 @@ export class WorkerSlot {
|
|
|
17
19
|
readonly #options: JavaScriptKernelOptions;
|
|
18
20
|
readonly #listeners: WorkerSlotListeners;
|
|
19
21
|
#worker: WorkerLike | null = null;
|
|
22
|
+
#webViews: KernelWebViewClients | null = null;
|
|
20
23
|
#mode: JavaScriptKernelMode = "worker";
|
|
21
24
|
#generation = 0;
|
|
22
25
|
#ready: Promise<void> | null = null;
|
|
@@ -43,8 +46,8 @@ export class WorkerSlot {
|
|
|
43
46
|
return this.#generation;
|
|
44
47
|
}
|
|
45
48
|
|
|
46
|
-
postMessage(message: HostToKernelMessage): void {
|
|
47
|
-
this.#worker?.postMessage(message);
|
|
49
|
+
postMessage(message: HostToKernelMessage, transfer?: readonly MessagePort[]): void {
|
|
50
|
+
this.#worker?.postMessage(message, transfer);
|
|
48
51
|
}
|
|
49
52
|
|
|
50
53
|
async ensureReady(): Promise<void> {
|
|
@@ -89,16 +92,22 @@ export class WorkerSlot {
|
|
|
89
92
|
this.#startupAbort = null;
|
|
90
93
|
this.#ready = null;
|
|
91
94
|
const worker = this.#worker;
|
|
95
|
+
const webViews = this.#webViews;
|
|
92
96
|
this.#worker = null;
|
|
93
|
-
|
|
94
|
-
|
|
97
|
+
this.#webViews = null;
|
|
98
|
+
const retirement = worker ? await retireWorker(worker) : "terminated";
|
|
99
|
+
await webViews?.release();
|
|
100
|
+
return retirement;
|
|
95
101
|
}
|
|
96
102
|
|
|
97
103
|
#publish(worker: WorkerLike, generation: number): void {
|
|
98
104
|
if (!this.#listeners.isOpen() || generation !== this.#generation) throw new WorkerStartupCancelledError();
|
|
99
105
|
this.#worker = worker;
|
|
106
|
+
const webViews = new KernelWebViewClients((message, transfer) => worker.postMessage(message, transfer));
|
|
107
|
+
this.#webViews = webViews;
|
|
100
108
|
worker.onMessage((message) => {
|
|
101
|
-
if (this.#isCurrent(worker, generation)
|
|
109
|
+
if (!this.#isCurrent(worker, generation) || webViews.consume(message)) return;
|
|
110
|
+
this.#listeners.onMessage(message);
|
|
102
111
|
});
|
|
103
112
|
worker.onError((error) => {
|
|
104
113
|
if (this.#isCurrent(worker, generation)) this.#listeners.onCrash(error);
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
// The kernel worker's end of its private port to the main-thread Bun.WebView service. Mirrors
|
|
2
|
+
// the message shapes in packages/coding-agent/src/core/webview/webview-wire.ts (this worker file
|
|
3
|
+
// cannot import TypeScript); keep the two in sync.
|
|
4
|
+
|
|
5
|
+
export const DELIVER_EVENT = Symbol("senpi.webview.deliver");
|
|
6
|
+
|
|
7
|
+
export function webViewError(wire) {
|
|
8
|
+
const error = new Error(wire?.message ?? "Bun.WebView request failed");
|
|
9
|
+
if (typeof wire?.name === "string") error.name = wire.name;
|
|
10
|
+
if (typeof wire?.code === "string") error.code = wire.code;
|
|
11
|
+
return error;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export class WebViewPortClient {
|
|
15
|
+
#port;
|
|
16
|
+
#pending = new Map();
|
|
17
|
+
#views = new Map();
|
|
18
|
+
#nextId = 1;
|
|
19
|
+
#closed = false;
|
|
20
|
+
#onClosed;
|
|
21
|
+
|
|
22
|
+
constructor(port, onClosed) {
|
|
23
|
+
this.#port = port;
|
|
24
|
+
this.#onClosed = onClosed;
|
|
25
|
+
port.on("message", message => this.#receive(message));
|
|
26
|
+
port.on("close", () => this.#fail(new Error("Bun.WebView service connection closed")));
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
register(viewId, view) {
|
|
30
|
+
this.#views.set(viewId, new WeakRef(view));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
forget(viewId) {
|
|
34
|
+
this.#views.delete(viewId);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
request(message) {
|
|
38
|
+
if (this.#closed) return Promise.reject(new Error("Bun.WebView service connection closed"));
|
|
39
|
+
const id = this.#nextId++;
|
|
40
|
+
return new Promise((resolve, reject) => {
|
|
41
|
+
this.#pending.set(id, { resolve, reject });
|
|
42
|
+
try {
|
|
43
|
+
this.#port.postMessage({ ...message, id });
|
|
44
|
+
} catch (error) {
|
|
45
|
+
this.#pending.delete(id);
|
|
46
|
+
reject(error);
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
post(message) {
|
|
52
|
+
if (this.#closed) return;
|
|
53
|
+
try {
|
|
54
|
+
this.#port.postMessage(message);
|
|
55
|
+
} catch {
|
|
56
|
+
// A closed port has already released every view on the service side.
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
#receive(message) {
|
|
61
|
+
if (message?.kind === "reply") {
|
|
62
|
+
const pending = this.#pending.get(message.id);
|
|
63
|
+
if (!pending) return;
|
|
64
|
+
this.#pending.delete(message.id);
|
|
65
|
+
if (message.ok) pending.resolve({ value: message.value, state: message.state });
|
|
66
|
+
else pending.reject(webViewError(message.error));
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
if (message?.kind === "event") this.#views.get(message.viewId)?.deref()?.[DELIVER_EVENT](message.event);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
#fail(error) {
|
|
73
|
+
if (this.#closed) return;
|
|
74
|
+
this.#closed = true;
|
|
75
|
+
for (const pending of this.#pending.values()) pending.reject(error);
|
|
76
|
+
this.#pending.clear();
|
|
77
|
+
this.#onClosed();
|
|
78
|
+
}
|
|
79
|
+
}
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import { DELIVER_EVENT, WebViewPortClient, webViewError } from "./worker-webview-client.js";
|
|
2
|
+
|
|
3
|
+
// Bun constructs "chrome"-backend WebViews only on the process main thread, and this kernel runs in
|
|
4
|
+
// a worker where `globalThis.Bun` and `Bun.WebView` are non-writable. Cells therefore run with a
|
|
5
|
+
// `Bun` parameter bound to a stand-in whose `WebView` forwards Chrome-backed views to the
|
|
6
|
+
// main-thread service over a private port; every other `Bun.*` member is the real one.
|
|
7
|
+
const KERNEL_BUN = Symbol.for("senpi.kernel.bun");
|
|
8
|
+
|
|
9
|
+
export function bindKernelBun(source) {
|
|
10
|
+
if (!globalThis[KERNEL_BUN]) return source;
|
|
11
|
+
return `((Bun) => ${source})(globalThis[Symbol.for("senpi.kernel.bun")])`;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function installKernelWebView(requestPort) {
|
|
15
|
+
const bun = globalThis.Bun;
|
|
16
|
+
if (typeof bun !== "object" || bun === null || typeof bun.WebView !== "function") return;
|
|
17
|
+
globalThis[KERNEL_BUN] = shadowBun(bun, createKernelWebView(bun.WebView, requestPort));
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function shadowBun(real, WebView) {
|
|
21
|
+
return new Proxy(
|
|
22
|
+
{},
|
|
23
|
+
{
|
|
24
|
+
get: (_target, key) => (key === "WebView" ? WebView : Reflect.get(real, key)),
|
|
25
|
+
set: (_target, key, value) => Reflect.set(real, key, value),
|
|
26
|
+
has: (_target, key) => key === "WebView" || Reflect.has(real, key),
|
|
27
|
+
ownKeys: () => Reflect.ownKeys(real),
|
|
28
|
+
getOwnPropertyDescriptor: (_target, key) => {
|
|
29
|
+
if (key === "WebView") return { value: WebView, writable: false, enumerable: true, configurable: true };
|
|
30
|
+
const descriptor = Reflect.getOwnPropertyDescriptor(real, key);
|
|
31
|
+
return descriptor && { ...descriptor, configurable: true };
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// The macOS default (WebKit) stays a native worker view: its host process serves any thread.
|
|
38
|
+
function needsMainThread(options) {
|
|
39
|
+
const backend = options?.backend;
|
|
40
|
+
if (backend === undefined) return process.platform !== "darwin";
|
|
41
|
+
return backend === "chrome" || (typeof backend === "object" && backend !== null && backend.type === "chrome");
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function closedError(method) {
|
|
45
|
+
const error = new Error(`Invalid state: WebView.${method}: view is closed`);
|
|
46
|
+
error.code = "ERR_INVALID_STATE";
|
|
47
|
+
return error;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function createKernelWebView(NativeWebView, requestPort) {
|
|
51
|
+
let connecting = null;
|
|
52
|
+
const proxied = new WeakSet();
|
|
53
|
+
const live = new Set();
|
|
54
|
+
const natives = new Set();
|
|
55
|
+
const finalizer = new FinalizationRegistry(({ client, viewId }) => client.post({ kind: "close", viewId }));
|
|
56
|
+
|
|
57
|
+
function connect() {
|
|
58
|
+
if (!connecting) {
|
|
59
|
+
const attempt = requestPort().then(port => new WebViewPortClient(port, () => (connecting = null)));
|
|
60
|
+
attempt.catch(() => {
|
|
61
|
+
if (connecting === attempt) connecting = null;
|
|
62
|
+
});
|
|
63
|
+
connecting = attempt;
|
|
64
|
+
}
|
|
65
|
+
return connecting;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
class WebView extends EventTarget {
|
|
69
|
+
static closeAll() {
|
|
70
|
+
for (const view of [...live]) view.close();
|
|
71
|
+
for (const ref of [...natives]) ref.deref()?.close();
|
|
72
|
+
natives.clear();
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
static [Symbol.hasInstance](value) {
|
|
76
|
+
return value instanceof NativeWebView || proxied.has(value);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
#viewId = crypto.randomUUID();
|
|
80
|
+
#state = { url: "", title: "", loading: false };
|
|
81
|
+
#listening = new Set();
|
|
82
|
+
#console;
|
|
83
|
+
#ready;
|
|
84
|
+
#closed = false;
|
|
85
|
+
onNavigated = null;
|
|
86
|
+
onNavigationFailed = null;
|
|
87
|
+
|
|
88
|
+
constructor(options = {}) {
|
|
89
|
+
if (!needsMainThread(options)) {
|
|
90
|
+
const view = new NativeWebView(options);
|
|
91
|
+
natives.add(new WeakRef(view));
|
|
92
|
+
return view;
|
|
93
|
+
}
|
|
94
|
+
super();
|
|
95
|
+
const { console: capture, ...rest } = options;
|
|
96
|
+
this.#console = capture;
|
|
97
|
+
proxied.add(this);
|
|
98
|
+
live.add(this);
|
|
99
|
+
const viewId = this.#viewId;
|
|
100
|
+
this.#ready = connect().then(async client => {
|
|
101
|
+
client.register(viewId, this);
|
|
102
|
+
finalizer.register(this, { client, viewId }, this);
|
|
103
|
+
const reply = await client.request({ kind: "create", viewId, options: rest, captureConsole: capture !== undefined });
|
|
104
|
+
this.#apply(reply.state);
|
|
105
|
+
return client;
|
|
106
|
+
});
|
|
107
|
+
this.#ready.catch(() => {});
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
get url() {
|
|
111
|
+
return this.#state.url;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
get title() {
|
|
115
|
+
return this.#state.title;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
get loading() {
|
|
119
|
+
return this.#state.loading;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
navigate(url) { return this.#call("navigate", [url]); }
|
|
123
|
+
evaluate(script) { return this.#call("evaluate", [script]); }
|
|
124
|
+
click(...args) { return this.#call("click", args); }
|
|
125
|
+
type(text) { return this.#call("type", [text]); }
|
|
126
|
+
press(...args) { return this.#call("press", args); }
|
|
127
|
+
scroll(dx, dy) { return this.#call("scroll", [dx, dy]); }
|
|
128
|
+
scrollTo(...args) { return this.#call("scrollTo", args); }
|
|
129
|
+
resize(width, height) { return this.#call("resize", [width, height]); }
|
|
130
|
+
back() { return this.#call("back", []); }
|
|
131
|
+
forward() { return this.#call("forward", []); }
|
|
132
|
+
reload() { return this.#call("reload", []); }
|
|
133
|
+
cdp(...args) { return this.#call("cdp", args); }
|
|
134
|
+
|
|
135
|
+
async screenshot(options) {
|
|
136
|
+
const value = await this.#call("screenshot", [options]);
|
|
137
|
+
if (options?.encoding === "buffer" && value instanceof Uint8Array) return Buffer.from(value.buffer, value.byteOffset, value.byteLength);
|
|
138
|
+
return value;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
addEventListener(type, listener, options) {
|
|
142
|
+
super.addEventListener(type, listener, options);
|
|
143
|
+
if (this.#closed || this.#listening.has(type)) return;
|
|
144
|
+
this.#listening.add(type);
|
|
145
|
+
const viewId = this.#viewId;
|
|
146
|
+
this.#ready.then(client => client.post({ kind: "listen", viewId, event: type }), () => {});
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
close() {
|
|
150
|
+
if (this.#closed) return;
|
|
151
|
+
this.#closed = true;
|
|
152
|
+
live.delete(this);
|
|
153
|
+
const viewId = this.#viewId;
|
|
154
|
+
this.#ready.then(
|
|
155
|
+
client => {
|
|
156
|
+
client.post({ kind: "close", viewId });
|
|
157
|
+
client.forget(viewId);
|
|
158
|
+
finalizer.unregister(this);
|
|
159
|
+
},
|
|
160
|
+
() => {},
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
[Symbol.dispose]() {
|
|
165
|
+
this.close();
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
[Symbol.asyncDispose]() {
|
|
169
|
+
this.close();
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
[DELIVER_EVENT](event) {
|
|
173
|
+
if (event.type === "console") {
|
|
174
|
+
const [type, ...args] = event.args;
|
|
175
|
+
if (typeof this.#console === "function") this.#console(type, ...args);
|
|
176
|
+
else this.#console?.[type]?.(...args);
|
|
177
|
+
} else if (event.type === "navigated") {
|
|
178
|
+
this.#state = { ...this.#state, url: event.url, title: event.title };
|
|
179
|
+
this.onNavigated?.(event.url, event.title);
|
|
180
|
+
} else if (event.type === "navigation-failed") {
|
|
181
|
+
this.onNavigationFailed?.(webViewError(event.error));
|
|
182
|
+
} else if (event.type === "dom") {
|
|
183
|
+
this.dispatchEvent(new MessageEvent(event.name, { data: event.data }));
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
async #call(method, args) {
|
|
188
|
+
if (this.#closed) throw closedError(method);
|
|
189
|
+
const client = await this.#ready;
|
|
190
|
+
if (this.#closed) throw closedError(method);
|
|
191
|
+
const trimmed = [...args];
|
|
192
|
+
while (trimmed.length > 0 && trimmed.at(-1) === undefined) trimmed.pop();
|
|
193
|
+
const reply = await client.request({ kind: "call", viewId: this.#viewId, method, args: trimmed });
|
|
194
|
+
this.#apply(reply.state);
|
|
195
|
+
return reply.value;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
#apply(state) {
|
|
199
|
+
if (state) this.#state = state;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return WebView;
|
|
204
|
+
}
|
package/src/kernels/py/kernel.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { KernelInterruptHandle } from "../../tool/types.ts";
|
|
2
2
|
import type { KernelToolsInvokeOptions } from "../js/kernel-tools-types.ts";
|
|
3
3
|
import { rejectKernelToolsUnavailable } from "../kernel-tools-unavailable.ts";
|
|
4
|
+
import { KernelPreludeTracker } from "../shared/kernel-prelude-plan.ts";
|
|
4
5
|
import type { PendingRun, PythonKernelRunOptions, PythonKernelStartOptions, ResultMessage } from "./kernel-contract.ts";
|
|
5
6
|
import { failedPythonResult, PythonKernelTransport } from "./transport.ts";
|
|
6
7
|
|
|
@@ -20,6 +21,7 @@ export class PythonKernel {
|
|
|
20
21
|
#retirement: Promise<void> | null = null;
|
|
21
22
|
#closePromise: Promise<void> | null = null;
|
|
22
23
|
#failure: Error | null = null;
|
|
24
|
+
readonly #preludes = new KernelPreludeTracker();
|
|
23
25
|
#generation = 0;
|
|
24
26
|
#closed = false;
|
|
25
27
|
|
|
@@ -180,7 +182,10 @@ export class PythonKernel {
|
|
|
180
182
|
if (timeoutMs !== undefined)
|
|
181
183
|
pending.timeoutTimer = setTimeout(() => this.#timeoutRun(pending, timeoutMs), timeoutMs);
|
|
182
184
|
try {
|
|
183
|
-
this.#transport?.run(
|
|
185
|
+
this.#transport?.run({
|
|
186
|
+
...pending.input,
|
|
187
|
+
preludePlan: this.#preludes.plan(pending.input.kernelPreludes ?? []),
|
|
188
|
+
});
|
|
184
189
|
} catch (error) {
|
|
185
190
|
const failure = error instanceof Error ? error : new Error(String(error));
|
|
186
191
|
this.#rejectRun(pending, failure);
|
|
@@ -1020,7 +1020,19 @@ async def run_code(code: Any, want_value: bool) -> Any:
|
|
|
1020
1020
|
return None
|
|
1021
1021
|
|
|
1022
1022
|
|
|
1023
|
-
def
|
|
1023
|
+
def apply_preludes(preludes: Any) -> None:
|
|
1024
|
+
# Host-computed per cell: globals of tools deactivated since the last cell are dropped,
|
|
1025
|
+
# and an active tool's snippet runs only while one of its exports is missing.
|
|
1026
|
+
if not isinstance(preludes, dict):
|
|
1027
|
+
return
|
|
1028
|
+
for name in preludes.get("remove", []):
|
|
1029
|
+
USER_NS.pop(name, None)
|
|
1030
|
+
for contribution in preludes.get("install", []):
|
|
1031
|
+
if any(name not in USER_NS for name in contribution.get("exports", [])):
|
|
1032
|
+
exec(compile(contribution.get("python", ""), "<kernel-prelude>", "exec"), USER_NS)
|
|
1033
|
+
|
|
1034
|
+
|
|
1035
|
+
def run_cell(cell_id: str, code: str, preludes: Any = None) -> None:
|
|
1024
1036
|
start = time.monotonic()
|
|
1025
1037
|
stdout = io.StringIO()
|
|
1026
1038
|
stderr = io.StringIO()
|
|
@@ -1029,6 +1041,7 @@ def run_cell(cell_id: str, code: str) -> None:
|
|
|
1029
1041
|
signal.signal(signal.SIGINT, signal.default_int_handler)
|
|
1030
1042
|
try:
|
|
1031
1043
|
with contextlib.redirect_stdout(stdout), contextlib.redirect_stderr(stderr):
|
|
1044
|
+
apply_preludes(preludes)
|
|
1032
1045
|
body, expression = compile_cell(code)
|
|
1033
1046
|
LOOP.run_until_complete(run_code(body, False))
|
|
1034
1047
|
value = LOOP.run_until_complete(run_code(expression, True))
|
|
@@ -1075,7 +1088,7 @@ def handle(message: dict[str, Any]) -> bool:
|
|
|
1075
1088
|
emit({"type": "ready"})
|
|
1076
1089
|
return True
|
|
1077
1090
|
if message_type == "run":
|
|
1078
|
-
run_cell(str(message.get("cellId", "")), str(message.get("code", "")))
|
|
1091
|
+
run_cell(str(message.get("cellId", "")), str(message.get("code", "")), message.get("preludes"))
|
|
1079
1092
|
return True
|
|
1080
1093
|
if message_type == "close":
|
|
1081
1094
|
emit({"type": "closed"})
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
type KernelToHostMessage,
|
|
10
10
|
} from "../../bridge/protocol.ts";
|
|
11
11
|
import { applySessionEnvironment, type SessionEnvironment } from "../session-env.ts";
|
|
12
|
+
import type { KernelPreludePlan } from "../shared/kernel-prelude-plan.ts";
|
|
12
13
|
import { type CodemodeRuntimeAssetEnvironment, requireCodemodeRuntimeAsset } from "../shared/runtime-asset.ts";
|
|
13
14
|
import {
|
|
14
15
|
defaultSpawn,
|
|
@@ -30,6 +31,7 @@ export interface PythonTransportRunInput {
|
|
|
30
31
|
readonly cellId: string;
|
|
31
32
|
readonly code: string;
|
|
32
33
|
readonly timeoutMs?: number;
|
|
34
|
+
readonly preludePlan?: KernelPreludePlan;
|
|
33
35
|
}
|
|
34
36
|
|
|
35
37
|
export interface PythonTransportOptions {
|
|
@@ -115,7 +117,11 @@ export class PythonKernelTransport {
|
|
|
115
117
|
}
|
|
116
118
|
|
|
117
119
|
run(input: PythonTransportRunInput): void {
|
|
118
|
-
|
|
120
|
+
const preludes = input.preludePlan && {
|
|
121
|
+
install: input.preludePlan.install.map(({ exports, python }) => ({ exports: [...exports], python })),
|
|
122
|
+
remove: [...input.preludePlan.remove],
|
|
123
|
+
};
|
|
124
|
+
this.#write({ type: "run", cellId: input.cellId, code: input.code, timeoutMs: input.timeoutMs, preludes });
|
|
119
125
|
}
|
|
120
126
|
|
|
121
127
|
interrupt(reason: string): void {
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { KernelPreludeContribution } from "@code-yeongyu/senpi";
|
|
2
|
+
|
|
3
|
+
/** What a kernel applies before a cell: contributions to (re)install and exports whose tool is no longer active. */
|
|
4
|
+
export interface KernelPreludePlan {
|
|
5
|
+
readonly install: readonly KernelPreludeContribution[];
|
|
6
|
+
readonly remove: readonly string[];
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/** Remembers the exports one kernel was last told to hold, so a deactivated tool's globals are removed. */
|
|
10
|
+
export class KernelPreludeTracker {
|
|
11
|
+
#installed: ReadonlySet<string> = new Set();
|
|
12
|
+
|
|
13
|
+
plan(contributions: readonly KernelPreludeContribution[]): KernelPreludePlan {
|
|
14
|
+
const active = new Set(contributions.flatMap((contribution) => contribution.exports));
|
|
15
|
+
const remove = [...this.#installed].filter((name) => !active.has(name));
|
|
16
|
+
this.#installed = active;
|
|
17
|
+
return { install: contributions, remove };
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { KernelPreludeContribution } from "@code-yeongyu/senpi";
|
|
1
2
|
import { DEFAULT_MAX_DETACHED_CELLS, DEFAULT_RUN_BUDGET_SECONDS } from "../config/settings.ts";
|
|
2
3
|
import type { EvalRuntimeInfo } from "../tool/types.ts";
|
|
3
4
|
import { EVAL_PROMPT_TEMPLATE } from "./eval-prompt-template.ts";
|
|
@@ -32,6 +33,8 @@ export interface EvalPromptOptions {
|
|
|
32
33
|
readonly runBudgetSeconds?: number;
|
|
33
34
|
/** Global background capacity advertised to the model. */
|
|
34
35
|
readonly maxDetachedCells?: number;
|
|
36
|
+
/** Active tools' kernel globals; each documentation line joins the prelude helper list. */
|
|
37
|
+
readonly kernelPreludes?: readonly KernelPreludeContribution[];
|
|
35
38
|
}
|
|
36
39
|
|
|
37
40
|
/** Prompt dialect for the eval-first batching emphasis. */
|
|
@@ -99,6 +102,7 @@ export function buildEvalPrompt(
|
|
|
99
102
|
bunSkillPath: options.bunSkillPath ?? "",
|
|
100
103
|
runBudgetSeconds: String(options.runBudgetSeconds ?? DEFAULT_RUN_BUDGET_SECONDS),
|
|
101
104
|
maxDetachedCells: String(options.maxDetachedCells ?? DEFAULT_MAX_DETACHED_CELLS),
|
|
105
|
+
kernelPreludeDocs: (options.kernelPreludes ?? []).map((prelude) => prelude.documentation).join("\n"),
|
|
102
106
|
};
|
|
103
107
|
const description = renderTemplate(EVAL_PROMPT_TEMPLATE, context)
|
|
104
108
|
.replace(/\n{3,}/g, "\n\n")
|
|
@@ -34,6 +34,7 @@ await Bun.write("page.png", await view.screenshot());
|
|
|
34
34
|
```
|
|
35
35
|
|
|
36
36
|
- macOS: system WebKit, nothing to install. macOS/Linux/Windows: can drive installed Chrome/Chromium/Edge.
|
|
37
|
+
- In the eval js kernel, Chrome-backed views (the default off macOS, or `backend: "chrome"`) are served by the process main thread; the API is the same, a constructor error surfaces on the first awaited call, and `Bun.WebView.closeAll()` closes only this kernel's views. A kernel reset closes every view the kernel left open.
|
|
37
38
|
- Clicks/scrolls are trusted user input (`event.isTrusted === true`).
|
|
38
39
|
- Extends `EventTarget`; screenshots are `Blob`s; `.cdp(method, params?)` escape hatch for raw Chrome DevTools Protocol.
|
|
39
40
|
|
|
@@ -1,4 +1,10 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type {
|
|
2
|
+
AgentToolResult,
|
|
3
|
+
AgentToolUpdateCallback,
|
|
4
|
+
ExtensionContext,
|
|
5
|
+
KernelPreludeContribution,
|
|
6
|
+
ToolDefinition,
|
|
7
|
+
} from "@code-yeongyu/senpi";
|
|
2
8
|
import type { EvalSchemaToolInfo } from "../bridges/schema-bridge.ts";
|
|
3
9
|
import type { CompletionRequest, CompletionResult } from "../completion/handler.ts";
|
|
4
10
|
import type { ResolvedCodemodeSettings } from "../config/settings.ts";
|
|
@@ -58,6 +64,10 @@ export interface CreateEvalToolOptions {
|
|
|
58
64
|
readonly runtimes?: EvalRuntimes;
|
|
59
65
|
/** Absolute path of the active bun-1-4 skill; the prompt names it as MUST READ on a bun kernel. */
|
|
60
66
|
readonly bunSkillPath?: string;
|
|
67
|
+
/** Kernel globals of the tools active when a cell is submitted; read once per cell. */
|
|
68
|
+
readonly kernelPreludes?: () => readonly KernelPreludeContribution[];
|
|
69
|
+
/** Contributions whose documentation lines the description lists; snapshot taken when the tool is (re)registered. */
|
|
70
|
+
readonly promptKernelPreludes?: readonly KernelPreludeContribution[];
|
|
61
71
|
}
|
|
62
72
|
|
|
63
73
|
export interface EvalCellInvocation {
|
package/src/tool/eval-tool.ts
CHANGED
|
@@ -54,6 +54,7 @@ export function createEvalTool(options: CreateEvalToolOptions) {
|
|
|
54
54
|
...(options.hostLine === undefined ? {} : { hostLine: options.hostLine }),
|
|
55
55
|
...(options.runtimes?.js === undefined ? {} : { jsRuntime: options.runtimes.js }),
|
|
56
56
|
...(options.bunSkillPath === undefined ? {} : { bunSkillPath: options.bunSkillPath }),
|
|
57
|
+
...(options.promptKernelPreludes === undefined ? {} : { kernelPreludes: options.promptKernelPreludes }),
|
|
57
58
|
});
|
|
58
59
|
const languages = enabledLanguageList(options.enabledLanguages);
|
|
59
60
|
const cellManager =
|
package/src/tool/types.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { AgentToolResult, AgentToolUpdateCallback } from "@code-yeongyu/senpi";
|
|
1
|
+
import type { AgentToolResult, AgentToolUpdateCallback, KernelPreludeContribution } from "@code-yeongyu/senpi";
|
|
2
2
|
import { type TSchema, type TUnsafe, Type } from "typebox";
|
|
3
3
|
import type { HostToKernelMessage, KernelToHostMessage } from "../bridge/protocol.ts";
|
|
4
4
|
import {
|
|
@@ -141,6 +141,8 @@ export interface EvalKernelRunInput {
|
|
|
141
141
|
readonly timeoutMs?: number;
|
|
142
142
|
readonly onStarted?: () => void;
|
|
143
143
|
readonly onMessage?: (message: KernelToHostMessage) => void;
|
|
144
|
+
/** Globals of the tools active when the cell was submitted; kernels without preludes ignore them. */
|
|
145
|
+
readonly kernelPreludes?: readonly KernelPreludeContribution[];
|
|
144
146
|
}
|
|
145
147
|
|
|
146
148
|
export interface KernelInterruptHandle {
|