@code-yeongyu/senpi-codemode 2026.8.26 → 2026.8.28-2
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 +56 -0
- package/package.json +4 -4
- package/src/kernels/js/context-manager.ts +13 -1
- package/src/kernels/js/inline-worker.ts +15 -1
- package/src/kernels/js/worker-indirect-eval.js +711 -1
- package/src/kernels/py/transport.ts +14 -1
- package/src/prompt/eval-prompt.ts +10 -10
- package/src/tool/detached-eval-result.ts +9 -3
- package/src/tool/eval-tool.ts +6 -1
package/CHANGELOG.md
CHANGED
|
@@ -12,6 +12,62 @@
|
|
|
12
12
|
|
|
13
13
|
### Removed
|
|
14
14
|
|
|
15
|
+
## [2026.8.28-2] - 2026-08-28
|
|
16
|
+
|
|
17
|
+
### Breaking Changes
|
|
18
|
+
|
|
19
|
+
### Added
|
|
20
|
+
|
|
21
|
+
### Changed
|
|
22
|
+
|
|
23
|
+
### Fixed
|
|
24
|
+
|
|
25
|
+
### Removed
|
|
26
|
+
|
|
27
|
+
## [2026.8.28] - 2026-08-28
|
|
28
|
+
|
|
29
|
+
### Breaking Changes
|
|
30
|
+
|
|
31
|
+
### Added
|
|
32
|
+
|
|
33
|
+
### Changed
|
|
34
|
+
|
|
35
|
+
### Fixed
|
|
36
|
+
|
|
37
|
+
- JavaScript and Python eval kernels resolve worker and prelude assets from the executable sidecar in Bun-compiled distributions instead of passing unusable `$bunfs` paths to `Worker` and `python3`.
|
|
38
|
+
|
|
39
|
+
### Removed
|
|
40
|
+
|
|
41
|
+
## [2026.8.27] - 2026-08-27
|
|
42
|
+
|
|
43
|
+
### Breaking Changes
|
|
44
|
+
|
|
45
|
+
### Added
|
|
46
|
+
|
|
47
|
+
### Changed
|
|
48
|
+
|
|
49
|
+
- Eval tool description examples are now a JS-first mixed set: set up once in JavaScript, fan out batched `Promise.all` session-tool calls in the next cell, then hop to Python when the JS kernel is busy with a detached cell. The detach paragraph now states in the same sentence that another language can continue.
|
|
50
|
+
|
|
51
|
+
### Fixed
|
|
52
|
+
|
|
53
|
+
- Detached-eval same-language busy errors now name each idle enabled kernel and tell the agent to continue the step there (`continue this step in an idle kernel: js`), instead of only pointing at peek and the output tail. A busy Python kernel no longer reads as "eval is unavailable", which previously sent agents to `bash`+`python3` while JavaScript (or another idle kernel) was free. Single-language sessions and fully-busy sessions omit the idle-kernel claim.
|
|
54
|
+
- JavaScript eval cells now persist only top-level declarations, including destructuring bindings and uninitialized variables, without rewriting declaration-shaped text inside literals or comments.
|
|
55
|
+
- Eval completion and detached-cell handling retain explicit lifecycle observability: nested tool counts, wall/kernel timing, detach state, `peek`, `stop`, hard limits, and crash recovery remain bounded and machine-readable for hosts and telemetry consumers.
|
|
56
|
+
|
|
57
|
+
### Removed
|
|
58
|
+
|
|
59
|
+
## [2026.8.26-2] - 2026-08-26
|
|
60
|
+
|
|
61
|
+
### Breaking Changes
|
|
62
|
+
|
|
63
|
+
### Added
|
|
64
|
+
|
|
65
|
+
### Changed
|
|
66
|
+
|
|
67
|
+
### Fixed
|
|
68
|
+
|
|
69
|
+
### Removed
|
|
70
|
+
|
|
15
71
|
## [2026.8.26] - 2026-08-26
|
|
16
72
|
|
|
17
73
|
### Breaking Changes
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@code-yeongyu/senpi-codemode",
|
|
3
|
-
"version": "2026.8.
|
|
3
|
+
"version": "2026.8.28-2",
|
|
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.8.
|
|
33
|
+
"@earendil-works/pi-ai": "npm:@code-yeongyu/senpi-ai@2026.8.28-2",
|
|
34
34
|
"typebox": "1.3.18"
|
|
35
35
|
},
|
|
36
36
|
"peerDependencies": {
|
|
37
|
-
"@code-yeongyu/senpi": "2026.8.
|
|
37
|
+
"@code-yeongyu/senpi": "2026.8.28-2"
|
|
38
38
|
},
|
|
39
39
|
"devDependencies": {
|
|
40
|
-
"@code-yeongyu/senpi": "2026.8.
|
|
40
|
+
"@code-yeongyu/senpi": "2026.8.28-2"
|
|
41
41
|
},
|
|
42
42
|
"keywords": [
|
|
43
43
|
"senpi",
|
|
@@ -1,5 +1,8 @@
|
|
|
1
|
+
import { dirname, join } from "node:path";
|
|
2
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
1
3
|
import type { HostToKernelMessage, KernelToHostMessage } from "../../bridge/protocol.ts";
|
|
2
4
|
import type { KernelInterruptHandle } from "../../tool/types.ts";
|
|
5
|
+
import { type CodemodeRuntimeAssetEnvironment, resolveCodemodeRuntimeAsset } from "../shared/runtime-asset.ts";
|
|
3
6
|
import { createInlineWorker, type WorkerLike } from "./inline-worker.ts";
|
|
4
7
|
import {
|
|
5
8
|
assertJavaScriptKernelOpen,
|
|
@@ -16,6 +19,15 @@ import { bridgeError, spawnNodeWorker, WorkerStartupCancelledError, waitForReady
|
|
|
16
19
|
export { JavaScriptKernelClosedError, type JavaScriptKernelMode, type JavaScriptRunInput } from "./kernel-contract.ts";
|
|
17
20
|
export type { JavaScriptKernelOptions } from "./local-module-loader.ts";
|
|
18
21
|
|
|
22
|
+
export interface JavaScriptWorkerEntryUrlOptions extends CodemodeRuntimeAssetEnvironment {
|
|
23
|
+
readonly localPath?: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function resolveJsWorkerEntryUrl(options: JavaScriptWorkerEntryUrlOptions = {}): URL {
|
|
27
|
+
const localPath = options.localPath ?? join(dirname(fileURLToPath(import.meta.url)), "worker-entry.js");
|
|
28
|
+
return pathToFileURL(resolveCodemodeRuntimeAsset(localPath, join("kernels", "js", "worker-entry.js"), options));
|
|
29
|
+
}
|
|
30
|
+
|
|
19
31
|
export class JavaScriptKernel {
|
|
20
32
|
readonly #options: JavaScriptKernelOptions;
|
|
21
33
|
readonly #moduleLoader: LocalModuleLoader;
|
|
@@ -161,7 +173,7 @@ export class JavaScriptKernel {
|
|
|
161
173
|
|
|
162
174
|
#spawnWorker(): WorkerLike {
|
|
163
175
|
try {
|
|
164
|
-
const url = this.#options.workerEntryUrl ??
|
|
176
|
+
const url = this.#options.workerEntryUrl ?? resolveJsWorkerEntryUrl();
|
|
165
177
|
return spawnNodeWorker(url, this.#options.cwd, this.#options.parallelPoolWidth);
|
|
166
178
|
} catch (error) {
|
|
167
179
|
if (!(error instanceof Error)) throw error;
|
|
@@ -1,7 +1,21 @@
|
|
|
1
|
+
import { dirname, join } from "node:path";
|
|
2
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
1
3
|
import type { HostToKernelMessage, KernelToHostMessage } from "../../bridge/protocol.ts";
|
|
4
|
+
import { type CodemodeRuntimeAssetEnvironment, resolveCodemodeRuntimeAsset } from "../shared/runtime-asset.ts";
|
|
2
5
|
import type { JavaScriptKernelMode } from "./kernel-contract.ts";
|
|
3
6
|
import { spawnNodeWorker } from "./worker-host.ts";
|
|
4
7
|
|
|
8
|
+
export interface JavaScriptInlineWorkerEntryUrlOptions extends CodemodeRuntimeAssetEnvironment {
|
|
9
|
+
readonly localPath?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function resolveInlineWorkerEntryUrl(options: JavaScriptInlineWorkerEntryUrlOptions = {}): URL {
|
|
13
|
+
const localPath = options.localPath ?? join(dirname(fileURLToPath(import.meta.url)), "inline-worker-entry.js");
|
|
14
|
+
return pathToFileURL(
|
|
15
|
+
resolveCodemodeRuntimeAsset(localPath, join("kernels", "js", "inline-worker-entry.js"), options),
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
|
|
5
19
|
export interface WorkerLike {
|
|
6
20
|
readonly mode: JavaScriptKernelMode;
|
|
7
21
|
postMessage(message: HostToKernelMessage): void;
|
|
@@ -11,5 +25,5 @@ export interface WorkerLike {
|
|
|
11
25
|
}
|
|
12
26
|
|
|
13
27
|
export function createInlineWorker(cwd: string, parallelPoolWidth: number): WorkerLike {
|
|
14
|
-
return spawnNodeWorker(
|
|
28
|
+
return spawnNodeWorker(resolveInlineWorkerEntryUrl(), cwd, parallelPoolWidth, "inline");
|
|
15
29
|
}
|
|
@@ -10,11 +10,721 @@ export async function awaitMaybePromise(value) {
|
|
|
10
10
|
}
|
|
11
11
|
|
|
12
12
|
export function wrapUserCode(code) {
|
|
13
|
-
const persistentCode = code
|
|
13
|
+
const persistentCode = persistTopLevelDeclarations(code);
|
|
14
14
|
if (/\breturn\b/u.test(persistentCode)) return `(async () => {\n${persistentCode}\n})()`;
|
|
15
15
|
return `(async () => {\n${captureLastExpression(persistentCode)}\n})()`;
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
+
const IDENTIFIER_START_RE = /[$_\p{ID_Start}]/u;
|
|
19
|
+
const IDENTIFIER_CONTINUE_RE = /[$_\p{ID_Continue}\u200c\u200d]/u;
|
|
20
|
+
const IDENTIFIER_RE = /^[\p{ID_Start}$_][\p{ID_Continue}\u200c\u200d]*$/u;
|
|
21
|
+
const DECLARATION_KEYWORDS = new Set(["const", "let", "var"]);
|
|
22
|
+
const REGEX_PREFIX_KEYWORDS = new Set([
|
|
23
|
+
"await",
|
|
24
|
+
"case",
|
|
25
|
+
"delete",
|
|
26
|
+
"do",
|
|
27
|
+
"else",
|
|
28
|
+
"in",
|
|
29
|
+
"instanceof",
|
|
30
|
+
"new",
|
|
31
|
+
"of",
|
|
32
|
+
"return",
|
|
33
|
+
"throw",
|
|
34
|
+
"typeof",
|
|
35
|
+
"void",
|
|
36
|
+
"yield",
|
|
37
|
+
]);
|
|
38
|
+
const EXPRESSION_PREFIX_KEYWORDS = new Set(["await", "delete", "new", "throw", "typeof", "void", "yield"]);
|
|
39
|
+
|
|
40
|
+
function persistTopLevelDeclarations(code) {
|
|
41
|
+
const edits = [];
|
|
42
|
+
let round = 0;
|
|
43
|
+
let square = 0;
|
|
44
|
+
let curly = 0;
|
|
45
|
+
let statementStart = true;
|
|
46
|
+
let canStartRegex = true;
|
|
47
|
+
|
|
48
|
+
for (let index = 0; index < code.length; index += 1) {
|
|
49
|
+
const char = code[index];
|
|
50
|
+
const next = code[index + 1];
|
|
51
|
+
if (char === "/" && next === "/") {
|
|
52
|
+
index = skipLineComment(code, index) - 1;
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
if (char === "/" && next === "*") {
|
|
56
|
+
index = skipBlockComment(code, index) - 1;
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
if (char === "'" || char === '"') {
|
|
60
|
+
index = skipQuotedLiteral(code, index) - 1;
|
|
61
|
+
statementStart = false;
|
|
62
|
+
canStartRegex = false;
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
if (char === "`") {
|
|
66
|
+
index = skipTemplateLiteral(code, index) - 1;
|
|
67
|
+
statementStart = false;
|
|
68
|
+
canStartRegex = false;
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
if (char === "/" && canStartRegex) {
|
|
72
|
+
index = skipRegexLiteral(code, index) - 1;
|
|
73
|
+
statementStart = false;
|
|
74
|
+
canStartRegex = false;
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
if (isIdentifierStart(char)) {
|
|
78
|
+
const end = readIdentifier(code, index);
|
|
79
|
+
const token = code.slice(index, end);
|
|
80
|
+
if (round === 0 && square === 0 && curly === 0 && statementStart && DECLARATION_KEYWORDS.has(token)) {
|
|
81
|
+
const declarationEnd = findDeclarationEnd(code, end);
|
|
82
|
+
const replacement = rewriteDeclaration(code, index, end, declarationEnd, token);
|
|
83
|
+
if (replacement !== undefined) edits.push({ start: index, end: declarationEnd, text: replacement });
|
|
84
|
+
}
|
|
85
|
+
statementStart = false;
|
|
86
|
+
canStartRegex = REGEX_PREFIX_KEYWORDS.has(token);
|
|
87
|
+
index = end - 1;
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
if (isDecimalDigit(char)) {
|
|
91
|
+
index = skipNumberLiteral(code, index) - 1;
|
|
92
|
+
statementStart = false;
|
|
93
|
+
canStartRegex = false;
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
if (char === "(") {
|
|
97
|
+
round += 1;
|
|
98
|
+
statementStart = false;
|
|
99
|
+
canStartRegex = true;
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
if (char === ")") {
|
|
103
|
+
round = Math.max(0, round - 1);
|
|
104
|
+
statementStart = false;
|
|
105
|
+
canStartRegex = false;
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
if (char === "[") {
|
|
109
|
+
square += 1;
|
|
110
|
+
statementStart = false;
|
|
111
|
+
canStartRegex = true;
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
if (char === "]") {
|
|
115
|
+
square = Math.max(0, square - 1);
|
|
116
|
+
statementStart = false;
|
|
117
|
+
canStartRegex = false;
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
if (char === "{") {
|
|
121
|
+
curly += 1;
|
|
122
|
+
statementStart = false;
|
|
123
|
+
canStartRegex = true;
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
if (char === "}") {
|
|
127
|
+
curly = Math.max(0, curly - 1);
|
|
128
|
+
statementStart = curly === 0 && round === 0 && square === 0;
|
|
129
|
+
canStartRegex = false;
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
if (char === ";") {
|
|
133
|
+
statementStart = round === 0 && square === 0 && curly === 0;
|
|
134
|
+
canStartRegex = true;
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
if (isLineTerminator(char) && round === 0 && square === 0 && curly === 0) {
|
|
138
|
+
statementStart = true;
|
|
139
|
+
canStartRegex = true;
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
if (!/\s/u.test(char)) {
|
|
143
|
+
statementStart = false;
|
|
144
|
+
canStartRegex = isExpressionOperator(char);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return applyTextEdits(code, edits);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function findDeclarationEnd(code, start) {
|
|
152
|
+
let round = 0;
|
|
153
|
+
let square = 0;
|
|
154
|
+
let curly = 0;
|
|
155
|
+
let canEnd = false;
|
|
156
|
+
let canStartRegex = true;
|
|
157
|
+
|
|
158
|
+
for (let index = start; index < code.length; index += 1) {
|
|
159
|
+
const char = code[index];
|
|
160
|
+
const next = code[index + 1];
|
|
161
|
+
if (char === "/" && next === "/") {
|
|
162
|
+
index = skipLineComment(code, index) - 1;
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
if (char === "/" && next === "*") {
|
|
166
|
+
index = skipBlockComment(code, index) - 1;
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
if (char === "'" || char === '"') {
|
|
170
|
+
index = skipQuotedLiteral(code, index) - 1;
|
|
171
|
+
canEnd = true;
|
|
172
|
+
canStartRegex = false;
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
if (char === "`") {
|
|
176
|
+
index = skipTemplateLiteral(code, index) - 1;
|
|
177
|
+
canEnd = true;
|
|
178
|
+
canStartRegex = false;
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
if (char === "/" && canStartRegex) {
|
|
182
|
+
index = skipRegexLiteral(code, index) - 1;
|
|
183
|
+
canEnd = true;
|
|
184
|
+
canStartRegex = false;
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
if (isIdentifierStart(char)) {
|
|
188
|
+
const end = readIdentifier(code, index);
|
|
189
|
+
const token = code.slice(index, end);
|
|
190
|
+
canEnd = !EXPRESSION_PREFIX_KEYWORDS.has(token);
|
|
191
|
+
canStartRegex = REGEX_PREFIX_KEYWORDS.has(token);
|
|
192
|
+
index = end - 1;
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
if (isDecimalDigit(char)) {
|
|
196
|
+
index = skipNumberLiteral(code, index) - 1;
|
|
197
|
+
canEnd = true;
|
|
198
|
+
canStartRegex = false;
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
if (char === "(") {
|
|
202
|
+
round += 1;
|
|
203
|
+
canEnd = false;
|
|
204
|
+
canStartRegex = true;
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
if (char === ")") {
|
|
208
|
+
round = Math.max(0, round - 1);
|
|
209
|
+
canEnd = true;
|
|
210
|
+
canStartRegex = false;
|
|
211
|
+
continue;
|
|
212
|
+
}
|
|
213
|
+
if (char === "[") {
|
|
214
|
+
square += 1;
|
|
215
|
+
canEnd = false;
|
|
216
|
+
canStartRegex = true;
|
|
217
|
+
continue;
|
|
218
|
+
}
|
|
219
|
+
if (char === "]") {
|
|
220
|
+
square = Math.max(0, square - 1);
|
|
221
|
+
canEnd = true;
|
|
222
|
+
canStartRegex = false;
|
|
223
|
+
continue;
|
|
224
|
+
}
|
|
225
|
+
if (char === "{") {
|
|
226
|
+
curly += 1;
|
|
227
|
+
canEnd = false;
|
|
228
|
+
canStartRegex = true;
|
|
229
|
+
continue;
|
|
230
|
+
}
|
|
231
|
+
if (char === "}") {
|
|
232
|
+
curly = Math.max(0, curly - 1);
|
|
233
|
+
canEnd = true;
|
|
234
|
+
canStartRegex = false;
|
|
235
|
+
continue;
|
|
236
|
+
}
|
|
237
|
+
if (char === ";" && round === 0 && square === 0 && curly === 0) return index + 1;
|
|
238
|
+
if (char === "," && round === 0 && square === 0 && curly === 0) {
|
|
239
|
+
canEnd = false;
|
|
240
|
+
canStartRegex = true;
|
|
241
|
+
continue;
|
|
242
|
+
}
|
|
243
|
+
if (isLineTerminator(char) && round === 0 && square === 0 && curly === 0 && canEnd) {
|
|
244
|
+
const nextIndex = nextSignificantIndex(code, index + 1);
|
|
245
|
+
if (nextIndex >= code.length || !isDeclarationContinuation(code[nextIndex])) return index;
|
|
246
|
+
}
|
|
247
|
+
if (!/\s/u.test(char)) {
|
|
248
|
+
canEnd = false;
|
|
249
|
+
canStartRegex = isExpressionOperator(char);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
return code.length;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function rewriteDeclaration(code, declarationStart, start, end, keyword) {
|
|
256
|
+
const source = code.slice(declarationStart, end);
|
|
257
|
+
const assignments = [];
|
|
258
|
+
const preserveDeclaration = source.includes("//") || source.includes("/*");
|
|
259
|
+
for (const [segmentStart, segmentEnd] of splitDeclarators(code, start, end)) {
|
|
260
|
+
const segment = code.slice(segmentStart, segmentEnd);
|
|
261
|
+
if (!segment.trim()) continue;
|
|
262
|
+
const initializerStart = findTopLevelEquals(segment);
|
|
263
|
+
if (initializerStart < 0 && keyword === "const") return undefined;
|
|
264
|
+
const pattern = trimPattern(initializerStart < 0 ? segment : segment.slice(0, initializerStart));
|
|
265
|
+
const bindings = [];
|
|
266
|
+
collectPatternNames(pattern, bindings);
|
|
267
|
+
if (bindings.length === 0) return undefined;
|
|
268
|
+
const target = rewriteBindingPattern(pattern);
|
|
269
|
+
if (target === undefined) return undefined;
|
|
270
|
+
const initializer = initializerStart < 0 ? "undefined" : segment.slice(initializerStart + 1).trim();
|
|
271
|
+
const [assignmentInitializer, comment] = splitTrailingLineComment(initializer);
|
|
272
|
+
const assignmentComment = preserveDeclaration ? "" : comment;
|
|
273
|
+
assignments.push(
|
|
274
|
+
`${target.startsWith("{") || target.startsWith("[") ? `(${target} = ${assignmentInitializer})` : `${target} = ${assignmentInitializer}`};${assignmentComment}`,
|
|
275
|
+
);
|
|
276
|
+
}
|
|
277
|
+
if (assignments.length === 0) return undefined;
|
|
278
|
+
return preserveDeclaration ? `${source}\n${assignments.join("\n")}` : assignments.join("\n");
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function rewriteBindingPattern(source) {
|
|
282
|
+
const pattern = trimPattern(source);
|
|
283
|
+
if (!pattern) return undefined;
|
|
284
|
+
const defaultStart = findTopLevelEquals(pattern);
|
|
285
|
+
if (defaultStart >= 0) {
|
|
286
|
+
const target = rewriteBindingPattern(pattern.slice(0, defaultStart));
|
|
287
|
+
return target === undefined ? undefined : `${target} = ${pattern.slice(defaultStart + 1).trim()}`;
|
|
288
|
+
}
|
|
289
|
+
if (IDENTIFIER_RE.test(pattern)) return `globalThis[${JSON.stringify(pattern)}]`;
|
|
290
|
+
if (pattern.startsWith("{") && pattern.endsWith("}")) {
|
|
291
|
+
const properties = splitPatternElements(pattern.slice(1, -1)).map((property) => rewriteObjectBinding(property));
|
|
292
|
+
if (properties.some((property) => property === undefined)) return undefined;
|
|
293
|
+
return `{${properties.filter((property) => property !== undefined).join(", ")}}`;
|
|
294
|
+
}
|
|
295
|
+
if (pattern.startsWith("[") && pattern.endsWith("]")) {
|
|
296
|
+
const elements = splitPatternElements(pattern.slice(1, -1)).map((element) => {
|
|
297
|
+
if (!element.trim()) return "";
|
|
298
|
+
const rest = trimPattern(element).startsWith("...");
|
|
299
|
+
const target = rewriteBindingPattern(rest ? trimPattern(element).slice(3) : element);
|
|
300
|
+
return target === undefined ? undefined : rest ? `...${target}` : target;
|
|
301
|
+
});
|
|
302
|
+
if (elements.some((element) => element === undefined)) return undefined;
|
|
303
|
+
return `[${elements.join(", ")}]`;
|
|
304
|
+
}
|
|
305
|
+
return undefined;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function rewriteObjectBinding(source) {
|
|
309
|
+
const property = trimPattern(source);
|
|
310
|
+
if (!property) return "";
|
|
311
|
+
if (property.startsWith("...")) {
|
|
312
|
+
const target = rewriteBindingPattern(property.slice(3));
|
|
313
|
+
return target === undefined ? undefined : `...${target}`;
|
|
314
|
+
}
|
|
315
|
+
const initializerStart = findTopLevelEquals(property);
|
|
316
|
+
const colon = findTopLevelColon(property);
|
|
317
|
+
if (colon >= 0 && (initializerStart < 0 || colon < initializerStart)) {
|
|
318
|
+
const target = rewriteBindingPattern(property.slice(colon + 1));
|
|
319
|
+
return target === undefined ? undefined : `${property.slice(0, colon + 1).trim()} ${target}`;
|
|
320
|
+
}
|
|
321
|
+
if (initializerStart >= 0) {
|
|
322
|
+
const target = rewriteBindingPattern(property.slice(0, initializerStart));
|
|
323
|
+
return target === undefined ? undefined : `${property.slice(0, initializerStart).trim()}: ${target} = ${property.slice(initializerStart + 1).trim()}`;
|
|
324
|
+
}
|
|
325
|
+
const target = rewriteBindingPattern(property);
|
|
326
|
+
return target === undefined ? undefined : `${property}: ${target}`;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function collectPatternNames(source, bindings) {
|
|
330
|
+
const pattern = trimPattern(source);
|
|
331
|
+
if (!pattern) return;
|
|
332
|
+
const defaultStart = findTopLevelEquals(pattern);
|
|
333
|
+
if (defaultStart >= 0) {
|
|
334
|
+
collectPatternNames(pattern.slice(0, defaultStart), bindings);
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
if (pattern.startsWith("{") && pattern.endsWith("}")) {
|
|
338
|
+
for (const property of splitPatternElements(pattern.slice(1, -1))) {
|
|
339
|
+
const trimmed = trimPattern(property);
|
|
340
|
+
if (!trimmed) continue;
|
|
341
|
+
if (trimmed.startsWith("...")) {
|
|
342
|
+
collectPatternNames(trimmed.slice(3), bindings);
|
|
343
|
+
continue;
|
|
344
|
+
}
|
|
345
|
+
const equals = findTopLevelEquals(trimmed);
|
|
346
|
+
const colon = findTopLevelColon(trimmed);
|
|
347
|
+
if (colon >= 0 && (equals < 0 || colon < equals)) collectPatternNames(trimmed.slice(colon + 1), bindings);
|
|
348
|
+
else collectPatternNames(equals < 0 ? trimmed : trimmed.slice(0, equals), bindings);
|
|
349
|
+
}
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
if (pattern.startsWith("[") && pattern.endsWith("]")) {
|
|
353
|
+
for (const element of splitPatternElements(pattern.slice(1, -1))) {
|
|
354
|
+
const trimmed = trimPattern(element);
|
|
355
|
+
if (!trimmed) continue;
|
|
356
|
+
collectPatternNames(trimmed.startsWith("...") ? trimmed.slice(3) : trimmed, bindings);
|
|
357
|
+
}
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
const equals = findTopLevelEquals(pattern);
|
|
361
|
+
const name = trimPattern(equals < 0 ? pattern : pattern.slice(0, equals));
|
|
362
|
+
if (IDENTIFIER_RE.test(name)) bindings.push(name);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
function splitDeclarators(code, start, end) {
|
|
366
|
+
const ranges = [];
|
|
367
|
+
let segmentStart = start;
|
|
368
|
+
let round = 0;
|
|
369
|
+
let square = 0;
|
|
370
|
+
let curly = 0;
|
|
371
|
+
let canStartRegex = true;
|
|
372
|
+
for (let index = start; index < end; index += 1) {
|
|
373
|
+
const char = code[index];
|
|
374
|
+
const next = code[index + 1];
|
|
375
|
+
if (char === "/" && next === "/") {
|
|
376
|
+
index = Math.min(end, skipLineComment(code, index)) - 1;
|
|
377
|
+
continue;
|
|
378
|
+
}
|
|
379
|
+
if (char === "/" && next === "*") {
|
|
380
|
+
index = Math.min(end, skipBlockComment(code, index)) - 1;
|
|
381
|
+
continue;
|
|
382
|
+
}
|
|
383
|
+
if (char === "'" || char === '"') {
|
|
384
|
+
index = Math.min(end, skipQuotedLiteral(code, index)) - 1;
|
|
385
|
+
canStartRegex = false;
|
|
386
|
+
continue;
|
|
387
|
+
}
|
|
388
|
+
if (char === "`") {
|
|
389
|
+
index = Math.min(end, skipTemplateLiteral(code, index)) - 1;
|
|
390
|
+
canStartRegex = false;
|
|
391
|
+
continue;
|
|
392
|
+
}
|
|
393
|
+
if (char === "/" && canStartRegex) {
|
|
394
|
+
index = Math.min(end, skipRegexLiteral(code, index)) - 1;
|
|
395
|
+
canStartRegex = false;
|
|
396
|
+
continue;
|
|
397
|
+
}
|
|
398
|
+
if (char === "(") round += 1;
|
|
399
|
+
else if (char === ")") round = Math.max(0, round - 1);
|
|
400
|
+
else if (char === "[") square += 1;
|
|
401
|
+
else if (char === "]") square = Math.max(0, square - 1);
|
|
402
|
+
else if (char === "{") curly += 1;
|
|
403
|
+
else if (char === "}") curly = Math.max(0, curly - 1);
|
|
404
|
+
else if (char === "," && round === 0 && square === 0 && curly === 0) {
|
|
405
|
+
ranges.push([segmentStart, index]);
|
|
406
|
+
segmentStart = index + 1;
|
|
407
|
+
}
|
|
408
|
+
if (!/\s/u.test(char)) canStartRegex = isExpressionOperator(char) || char === "(" || char === "[" || char === "{";
|
|
409
|
+
}
|
|
410
|
+
if (segmentStart < end && code.slice(segmentStart, end).trim() !== ";") ranges.push([segmentStart, end - (code[end - 1] === ";" ? 1 : 0)]);
|
|
411
|
+
return ranges;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
function splitPatternElements(source) {
|
|
415
|
+
const elements = [];
|
|
416
|
+
let start = 0;
|
|
417
|
+
let round = 0;
|
|
418
|
+
let square = 0;
|
|
419
|
+
let curly = 0;
|
|
420
|
+
for (let index = 0; index < source.length; index += 1) {
|
|
421
|
+
const char = source[index];
|
|
422
|
+
const next = source[index + 1];
|
|
423
|
+
if (char === "/" && next === "/") {
|
|
424
|
+
index = skipLineComment(source, index) - 1;
|
|
425
|
+
continue;
|
|
426
|
+
}
|
|
427
|
+
if (char === "/" && next === "*") {
|
|
428
|
+
index = skipBlockComment(source, index) - 1;
|
|
429
|
+
continue;
|
|
430
|
+
}
|
|
431
|
+
if (char === "'" || char === '"') {
|
|
432
|
+
index = skipQuotedLiteral(source, index) - 1;
|
|
433
|
+
continue;
|
|
434
|
+
}
|
|
435
|
+
if (char === "`") {
|
|
436
|
+
index = skipTemplateLiteral(source, index) - 1;
|
|
437
|
+
continue;
|
|
438
|
+
}
|
|
439
|
+
if (char === "(") round += 1;
|
|
440
|
+
else if (char === ")") round = Math.max(0, round - 1);
|
|
441
|
+
else if (char === "[") square += 1;
|
|
442
|
+
else if (char === "]") square = Math.max(0, square - 1);
|
|
443
|
+
else if (char === "{") curly += 1;
|
|
444
|
+
else if (char === "}") curly = Math.max(0, curly - 1);
|
|
445
|
+
else if (char === "," && round === 0 && square === 0 && curly === 0) {
|
|
446
|
+
elements.push(source.slice(start, index));
|
|
447
|
+
start = index + 1;
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
elements.push(source.slice(start));
|
|
451
|
+
return elements;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
function findTopLevelEquals(source) {
|
|
455
|
+
return findTopLevelCharacter(source, "=");
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
function findTopLevelColon(source) {
|
|
459
|
+
return findTopLevelCharacter(source, ":");
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
function findTopLevelCharacter(source, target) {
|
|
463
|
+
let round = 0;
|
|
464
|
+
let square = 0;
|
|
465
|
+
let curly = 0;
|
|
466
|
+
for (let index = 0; index < source.length; index += 1) {
|
|
467
|
+
const char = source[index];
|
|
468
|
+
const next = source[index + 1];
|
|
469
|
+
if (char === "/" && next === "/") {
|
|
470
|
+
index = skipLineComment(source, index) - 1;
|
|
471
|
+
continue;
|
|
472
|
+
}
|
|
473
|
+
if (char === "/" && next === "*") {
|
|
474
|
+
index = skipBlockComment(source, index) - 1;
|
|
475
|
+
continue;
|
|
476
|
+
}
|
|
477
|
+
if (char === "'" || char === '"') {
|
|
478
|
+
index = skipQuotedLiteral(source, index) - 1;
|
|
479
|
+
continue;
|
|
480
|
+
}
|
|
481
|
+
if (char === "`") {
|
|
482
|
+
index = skipTemplateLiteral(source, index) - 1;
|
|
483
|
+
continue;
|
|
484
|
+
}
|
|
485
|
+
if (char === "(") round += 1;
|
|
486
|
+
else if (char === ")") round = Math.max(0, round - 1);
|
|
487
|
+
else if (char === "[") square += 1;
|
|
488
|
+
else if (char === "]") square = Math.max(0, square - 1);
|
|
489
|
+
else if (char === "{") curly += 1;
|
|
490
|
+
else if (char === "}") curly = Math.max(0, curly - 1);
|
|
491
|
+
else if (char === target && round === 0 && square === 0 && curly === 0) return index;
|
|
492
|
+
}
|
|
493
|
+
return -1;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
function applyTextEdits(code, edits) {
|
|
497
|
+
let output = code;
|
|
498
|
+
for (const edit of edits.toSorted((left, right) => right.start - left.start)) {
|
|
499
|
+
output = output.slice(0, edit.start) + edit.text + output.slice(edit.end);
|
|
500
|
+
}
|
|
501
|
+
return output;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
function splitTrailingLineComment(source) {
|
|
505
|
+
let canStartRegex = true;
|
|
506
|
+
for (let index = 0; index < source.length; index += 1) {
|
|
507
|
+
const char = source[index];
|
|
508
|
+
const next = source[index + 1];
|
|
509
|
+
if (char === "/" && next === "/") return [source.slice(0, index).trimEnd(), source.slice(index)];
|
|
510
|
+
if (char === "/" && next === "*") {
|
|
511
|
+
index = skipBlockComment(source, index) - 1;
|
|
512
|
+
continue;
|
|
513
|
+
}
|
|
514
|
+
if (char === "'" || char === '"') {
|
|
515
|
+
index = skipQuotedLiteral(source, index) - 1;
|
|
516
|
+
canStartRegex = false;
|
|
517
|
+
continue;
|
|
518
|
+
}
|
|
519
|
+
if (char === "`") {
|
|
520
|
+
index = skipTemplateLiteral(source, index) - 1;
|
|
521
|
+
canStartRegex = false;
|
|
522
|
+
continue;
|
|
523
|
+
}
|
|
524
|
+
if (char === "/" && canStartRegex) {
|
|
525
|
+
index = skipRegexLiteral(source, index) - 1;
|
|
526
|
+
canStartRegex = false;
|
|
527
|
+
continue;
|
|
528
|
+
}
|
|
529
|
+
if (isIdentifierStart(char)) {
|
|
530
|
+
const end = readIdentifier(source, index);
|
|
531
|
+
canStartRegex = REGEX_PREFIX_KEYWORDS.has(source.slice(index, end));
|
|
532
|
+
index = end - 1;
|
|
533
|
+
continue;
|
|
534
|
+
}
|
|
535
|
+
if (isDecimalDigit(char)) {
|
|
536
|
+
index = skipNumberLiteral(source, index) - 1;
|
|
537
|
+
canStartRegex = false;
|
|
538
|
+
continue;
|
|
539
|
+
}
|
|
540
|
+
if (!/\s/u.test(char)) canStartRegex = isExpressionOperator(char) || char === "(" || char === "[" || char === "{";
|
|
541
|
+
}
|
|
542
|
+
return [source, ""];
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
function trimPattern(source) {
|
|
546
|
+
let start = 0;
|
|
547
|
+
let end = source.length;
|
|
548
|
+
while (start < end) {
|
|
549
|
+
if (/\s/u.test(source[start])) {
|
|
550
|
+
start += 1;
|
|
551
|
+
continue;
|
|
552
|
+
}
|
|
553
|
+
if (source.startsWith("//", start)) {
|
|
554
|
+
start = skipLineComment(source, start);
|
|
555
|
+
continue;
|
|
556
|
+
}
|
|
557
|
+
if (source.startsWith("/*", start)) {
|
|
558
|
+
start = skipBlockComment(source, start);
|
|
559
|
+
continue;
|
|
560
|
+
}
|
|
561
|
+
break;
|
|
562
|
+
}
|
|
563
|
+
while (true) {
|
|
564
|
+
while (end > start && /\s/u.test(source[end - 1])) end -= 1;
|
|
565
|
+
if (end < start + 2 || source.slice(end - 2, end) !== "*/") break;
|
|
566
|
+
const commentStart = source.lastIndexOf("/*", end - 2);
|
|
567
|
+
if (commentStart < start) break;
|
|
568
|
+
end = commentStart;
|
|
569
|
+
}
|
|
570
|
+
return source.slice(start, end);
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
function nextSignificantIndex(code, start) {
|
|
574
|
+
for (let index = start; index < code.length; index += 1) {
|
|
575
|
+
if (/\s/u.test(code[index])) continue;
|
|
576
|
+
if (code.startsWith("//", index)) {
|
|
577
|
+
index = skipLineComment(code, index) - 1;
|
|
578
|
+
continue;
|
|
579
|
+
}
|
|
580
|
+
if (code.startsWith("/*", index)) {
|
|
581
|
+
index = skipBlockComment(code, index) - 1;
|
|
582
|
+
continue;
|
|
583
|
+
}
|
|
584
|
+
return index;
|
|
585
|
+
}
|
|
586
|
+
return code.length;
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
function isDeclarationContinuation(char) {
|
|
590
|
+
return char !== undefined && /[.[(,+\-*/%&|^?:<>=!~]/u.test(char);
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
function isExpressionOperator(char) {
|
|
594
|
+
return /[=,+\-*/%&|^?:<>!~]/u.test(char);
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
function isIdentifierStart(char) {
|
|
598
|
+
return char !== undefined && IDENTIFIER_START_RE.test(char);
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
function readIdentifier(code, start) {
|
|
602
|
+
let index = start + 1;
|
|
603
|
+
while (index < code.length && IDENTIFIER_CONTINUE_RE.test(code[index])) index += 1;
|
|
604
|
+
return index;
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
function isDecimalDigit(char) {
|
|
608
|
+
return char !== undefined && /[0-9]/u.test(char);
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
function skipNumberLiteral(code, start) {
|
|
612
|
+
let index = start + 1;
|
|
613
|
+
while (index < code.length && /[0-9A-Fa-f_xXn.eE]/u.test(code[index])) index += 1;
|
|
614
|
+
return index;
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
function skipQuotedLiteral(code, start) {
|
|
618
|
+
const quote = code[start];
|
|
619
|
+
for (let index = start + 1; index < code.length; index += 1) {
|
|
620
|
+
if (code[index] === "\\") {
|
|
621
|
+
index += 1;
|
|
622
|
+
continue;
|
|
623
|
+
}
|
|
624
|
+
if (code[index] === quote) return index + 1;
|
|
625
|
+
}
|
|
626
|
+
return code.length;
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
function skipTemplateLiteral(code, start) {
|
|
630
|
+
for (let index = start + 1; index < code.length; index += 1) {
|
|
631
|
+
const char = code[index];
|
|
632
|
+
if (char === "\\") {
|
|
633
|
+
index += 1;
|
|
634
|
+
continue;
|
|
635
|
+
}
|
|
636
|
+
if (char === "`") return index + 1;
|
|
637
|
+
if (char === "$" && code[index + 1] === "{") {
|
|
638
|
+
index = skipTemplateExpression(code, index + 2) - 1;
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
return code.length;
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
function skipTemplateExpression(code, start) {
|
|
645
|
+
let curly = 1;
|
|
646
|
+
let canStartRegex = true;
|
|
647
|
+
for (let index = start; index < code.length; index += 1) {
|
|
648
|
+
const char = code[index];
|
|
649
|
+
const next = code[index + 1];
|
|
650
|
+
if (char === "/" && next === "/") {
|
|
651
|
+
index = skipLineComment(code, index) - 1;
|
|
652
|
+
continue;
|
|
653
|
+
}
|
|
654
|
+
if (char === "/" && next === "*") {
|
|
655
|
+
index = skipBlockComment(code, index) - 1;
|
|
656
|
+
continue;
|
|
657
|
+
}
|
|
658
|
+
if (char === "'" || char === '"') {
|
|
659
|
+
index = skipQuotedLiteral(code, index) - 1;
|
|
660
|
+
canStartRegex = false;
|
|
661
|
+
continue;
|
|
662
|
+
}
|
|
663
|
+
if (char === "`") {
|
|
664
|
+
index = skipTemplateLiteral(code, index) - 1;
|
|
665
|
+
canStartRegex = false;
|
|
666
|
+
continue;
|
|
667
|
+
}
|
|
668
|
+
if (char === "/" && canStartRegex) {
|
|
669
|
+
index = skipRegexLiteral(code, index) - 1;
|
|
670
|
+
canStartRegex = false;
|
|
671
|
+
continue;
|
|
672
|
+
}
|
|
673
|
+
if (isIdentifierStart(char)) {
|
|
674
|
+
const end = readIdentifier(code, index);
|
|
675
|
+
canStartRegex = REGEX_PREFIX_KEYWORDS.has(code.slice(index, end));
|
|
676
|
+
index = end - 1;
|
|
677
|
+
continue;
|
|
678
|
+
}
|
|
679
|
+
if (char === "{") curly += 1;
|
|
680
|
+
else if (char === "}" && --curly === 0) return index + 1;
|
|
681
|
+
canStartRegex = char === "(" || char === "[" || char === "{" || isExpressionOperator(char) || char === ",";
|
|
682
|
+
}
|
|
683
|
+
return code.length;
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
function skipRegexLiteral(code, start) {
|
|
687
|
+
let inClass = false;
|
|
688
|
+
for (let index = start + 1; index < code.length; index += 1) {
|
|
689
|
+
const char = code[index];
|
|
690
|
+
if (char === "\\") {
|
|
691
|
+
index += 1;
|
|
692
|
+
continue;
|
|
693
|
+
}
|
|
694
|
+
if (char === "[") {
|
|
695
|
+
inClass = true;
|
|
696
|
+
continue;
|
|
697
|
+
}
|
|
698
|
+
if (char === "]") {
|
|
699
|
+
inClass = false;
|
|
700
|
+
continue;
|
|
701
|
+
}
|
|
702
|
+
if (char === "/" && !inClass) {
|
|
703
|
+
let end = index + 1;
|
|
704
|
+
while (end < code.length && /[A-Za-z]/u.test(code[end])) end += 1;
|
|
705
|
+
return end;
|
|
706
|
+
}
|
|
707
|
+
if (isLineTerminator(char)) return index;
|
|
708
|
+
}
|
|
709
|
+
return code.length;
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
function skipLineComment(code, start) {
|
|
713
|
+
for (let index = start + 2; index < code.length; index += 1) {
|
|
714
|
+
if (isLineTerminator(code[index])) return index;
|
|
715
|
+
}
|
|
716
|
+
return code.length;
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
function isLineTerminator(char) {
|
|
720
|
+
return char === "\n" || char === "\r" || char === "\u2028" || char === "\u2029";
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
function skipBlockComment(code, start) {
|
|
724
|
+
const end = code.indexOf("*/", start + 2);
|
|
725
|
+
return end < 0 ? code.length : end + 2;
|
|
726
|
+
}
|
|
727
|
+
|
|
18
728
|
function captureLastExpression(code) {
|
|
19
729
|
const start = findLastTopLevelStatementStart(code);
|
|
20
730
|
const head = code.slice(0, start);
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
isKernelToHostMessage,
|
|
9
9
|
type KernelToHostMessage,
|
|
10
10
|
} from "../../bridge/protocol.ts";
|
|
11
|
+
import { type CodemodeRuntimeAssetEnvironment, resolveCodemodeRuntimeAsset } from "../shared/runtime-asset.ts";
|
|
11
12
|
import {
|
|
12
13
|
defaultSpawn,
|
|
13
14
|
hardKill,
|
|
@@ -47,6 +48,18 @@ export interface PythonTransportOptions {
|
|
|
47
48
|
|
|
48
49
|
const hardKillWaitMs = 500;
|
|
49
50
|
|
|
51
|
+
export interface PythonPreludePathOptions extends CodemodeRuntimeAssetEnvironment {
|
|
52
|
+
readonly localPath?: string;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function resolvePythonPreludePath(options: PythonPreludePathOptions = {}): string {
|
|
56
|
+
return resolveCodemodeRuntimeAsset(
|
|
57
|
+
options.localPath ?? join(dirname(fileURLToPath(import.meta.url)), "prelude.py"),
|
|
58
|
+
join("kernels", "py", "prelude.py"),
|
|
59
|
+
options,
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
50
63
|
export class PythonKernelTransport {
|
|
51
64
|
readonly #options: PythonTransportOptions;
|
|
52
65
|
readonly #child: KernelChild;
|
|
@@ -64,7 +77,7 @@ export class PythonKernelTransport {
|
|
|
64
77
|
}
|
|
65
78
|
|
|
66
79
|
static async start(options: PythonTransportOptions): Promise<PythonKernelTransport> {
|
|
67
|
-
const scriptPath =
|
|
80
|
+
const scriptPath = resolvePythonPreludePath();
|
|
68
81
|
const invocation = splitCommand(options.interpreterPath);
|
|
69
82
|
const spawnOptions: KernelSpawnOptions = {
|
|
70
83
|
command: invocation.command,
|
|
@@ -70,21 +70,21 @@ type EvalPromptExample = {
|
|
|
70
70
|
const REUSE_CHAIN_EXAMPLES = [
|
|
71
71
|
{
|
|
72
72
|
caption: "First call — set up once",
|
|
73
|
-
language: "
|
|
73
|
+
language: "js",
|
|
74
74
|
summary: "Count all TypeScript source files under src/ excluding tests",
|
|
75
|
-
code: "
|
|
75
|
+
code: "import { readdir } from 'node:fs/promises'\nimport { extname } from 'node:path'\nconst files = (await readdir('src', { recursive: true })).filter(f => extname(f) === '.ts' && !f.includes('test'))\nprint(files.length)",
|
|
76
76
|
},
|
|
77
77
|
{
|
|
78
|
-
caption: "Second call — reuse `files`,
|
|
79
|
-
language: "
|
|
80
|
-
summary: "
|
|
81
|
-
code: "
|
|
78
|
+
caption: "Second call — reuse `files`, fan out session tools in parallel",
|
|
79
|
+
language: "js",
|
|
80
|
+
summary: "Grep legacyClient per directory in one cell",
|
|
81
|
+
code: "const dirs = [...new Set(files.map(f => f.split('/')[0]))]\nconst hits = await Promise.all(dirs.map(d => tool.grep({ pattern: 'legacyClient', path: d })))\ndisplay(hits.map(h => h.matches?.length ?? 0))",
|
|
82
82
|
},
|
|
83
83
|
{
|
|
84
|
-
caption: "
|
|
84
|
+
caption: "JS kernel is busy with a detached cell — continue in py",
|
|
85
85
|
language: "py",
|
|
86
|
-
summary: "
|
|
87
|
-
code: "
|
|
86
|
+
summary: "Aggregate legacyClient hits while JS is busy",
|
|
87
|
+
code: "from pathlib import Path\nprint(sum('legacyClient' in read(p) for p in Path('src').rglob('*.ts')))",
|
|
88
88
|
},
|
|
89
89
|
] as const satisfies readonly EvalPromptExample[];
|
|
90
90
|
|
|
@@ -129,7 +129,7 @@ Fields:
|
|
|
129
129
|
- \`reset\` (optional) — wipe this language's kernel first.{{#ifAll py js}} Per-language: a \`py\` reset never touches the JS VM.{{/ifAll}}
|
|
130
130
|
- \`action\` (optional) — defaults to \`"run"\`. A detached cell returns its id: use \`eval({ action: "peek", cell_id })\` for buffered output/state or \`eval({ action: "stop", cell_id })\` to cancel it.
|
|
131
131
|
|
|
132
|
-
A detached cell keeps its language kernel busy while it finishes. Do not re-run a detached cell: the same-language busy error names its cell id and output tail
|
|
132
|
+
A detached cell keeps its language kernel busy while it finishes; another language can continue. Do not re-run a detached cell: the same-language busy error names its cell id and output tail. Completion arrives as one notification with the final value/error and buffered output. Stopping a cell interrupts its kernel; the stop result states whether kernel state survived or the kernel was restarted and its variables lost.
|
|
133
133
|
|
|
134
134
|
{{#if py}}Live event loop: use top-level \`await\` directly; \`asyncio.run(…)\` raises "cannot be called from a running event loop".{{/if}}
|
|
135
135
|
{{#if js}}JS runs under Node.js worker: top-level \`await\`/\`return\` work; \`fetch\`/\`Buffer\` available.{{/if}}
|
|
@@ -5,7 +5,7 @@ import type {
|
|
|
5
5
|
EvalDetachedCellState,
|
|
6
6
|
} from "./detached-cell-manager.ts";
|
|
7
7
|
import { interruptionStateNote } from "./interrupt-note.ts";
|
|
8
|
-
import type { EvalCellResult, EvalControlInput, EvalToolDetails, EvalToolInput } from "./types.ts";
|
|
8
|
+
import type { EvalCellResult, EvalControlInput, EvalLanguage, EvalToolDetails, EvalToolInput } from "./types.ts";
|
|
9
9
|
|
|
10
10
|
export async function executeEvalControl(
|
|
11
11
|
cellManager: EvalDetachedCellManager,
|
|
@@ -101,10 +101,16 @@ export function resultForDetachedState(
|
|
|
101
101
|
};
|
|
102
102
|
}
|
|
103
103
|
|
|
104
|
-
export function detachedKernelBusyError(
|
|
104
|
+
export function detachedKernelBusyError(
|
|
105
|
+
snapshot: EvalDetachedCellSnapshot,
|
|
106
|
+
idleLanguages: readonly EvalLanguage[] = [],
|
|
107
|
+
): Error {
|
|
105
108
|
const tail = snapshot.outputTail.length === 0 ? "(no output yet)" : snapshot.outputTail;
|
|
109
|
+
const peek = `eval({ action: "peek", cell_id: "${snapshot.cellId}" })`;
|
|
110
|
+
const idleHint =
|
|
111
|
+
idleLanguages.length === 0 ? "" : ` or continue this step in an idle kernel: ${idleLanguages.join(", ")}`;
|
|
106
112
|
return new Error(
|
|
107
|
-
`The ${snapshot.language} eval kernel is busy running detached cell ${snapshot.cellId}. Do not re-run
|
|
113
|
+
`The ${snapshot.language} eval kernel is busy running detached cell ${snapshot.cellId} - peek with ${peek}${idleHint}. Do not re-run the busy cell. Current output tail:\n${tail}`,
|
|
108
114
|
);
|
|
109
115
|
}
|
|
110
116
|
|
package/src/tool/eval-tool.ts
CHANGED
|
@@ -67,7 +67,12 @@ export function createEvalTool(options: CreateEvalToolOptions): ToolDefinition<E
|
|
|
67
67
|
`Unsupported eval language "${request.language}". Enabled languages: ${languages.join(", ")}`,
|
|
68
68
|
);
|
|
69
69
|
const busy = cellManager.busyFor(request.language);
|
|
70
|
-
if (busy !== undefined)
|
|
70
|
+
if (busy !== undefined) {
|
|
71
|
+
const idleLanguages = languages.filter(
|
|
72
|
+
(language) => language !== request.language && cellManager.busyFor(language) === undefined,
|
|
73
|
+
);
|
|
74
|
+
throw detachedKernelBusyError(busy, idleLanguages);
|
|
75
|
+
}
|
|
71
76
|
options.executionTracker?.assertEvalExecutionAllowed();
|
|
72
77
|
const lifecycleController = new AbortController();
|
|
73
78
|
const combinedSignal = signal
|