pi-repl-py 0.6.1 → 0.6.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/package.json +1 -1
- package/src/engine/index.ts +16 -0
- package/src/engine/kernel.ts +28 -3
- package/src/extension/prompt.ts +1 -1
- package/src/extension/render-core.ts +67 -6
- package/src/extension/render.ts +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-repl-py",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.3",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "A pi extension with a single tool: execute, running a TypeScript host with a persistent Python (ipykernel) evaluator and a user-configurable toolbox of functions.",
|
|
6
6
|
"keywords": [
|
package/src/engine/index.ts
CHANGED
|
@@ -162,6 +162,15 @@ export class EngineManager {
|
|
|
162
162
|
env: this.options.env,
|
|
163
163
|
timeoutMs,
|
|
164
164
|
});
|
|
165
|
+
// --- an unexpected kernel death must not survive the next execute: drop the dying
|
|
166
|
+
// --- instance and clear the boot cache so start() rebuilds it on the next cell. ---
|
|
167
|
+
const current = this.kernel;
|
|
168
|
+
current.setOnUnexpectedExit(() => {
|
|
169
|
+
if (this.kernel !== current) return;
|
|
170
|
+
this.kernel = undefined;
|
|
171
|
+
this.startPromise = undefined;
|
|
172
|
+
this.lastNamespaceNames = undefined;
|
|
173
|
+
});
|
|
165
174
|
} catch (error) {
|
|
166
175
|
if (this.state === "starting") this.state = "idle";
|
|
167
176
|
liveEngines.delete(this);
|
|
@@ -215,6 +224,13 @@ export class EngineManager {
|
|
|
215
224
|
throw new Error("Engine has been shut down");
|
|
216
225
|
}
|
|
217
226
|
await this.start();
|
|
227
|
+
// --- the kernel may have died after the boot promise resolved but before the async
|
|
228
|
+
// --- exit event surfaced it; drop the zombie and rebuild so the next cell runs. ---
|
|
229
|
+
if (this.kernel && !this.kernel.isRunning) {
|
|
230
|
+
this.kernel = undefined;
|
|
231
|
+
this.startPromise = undefined;
|
|
232
|
+
await this.start();
|
|
233
|
+
}
|
|
218
234
|
if (this.isShutdown()) {
|
|
219
235
|
throw new Error("Engine has been shut down");
|
|
220
236
|
}
|
package/src/engine/kernel.ts
CHANGED
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
import { ZmtpSocket } from "./zmtp.js";
|
|
20
20
|
|
|
21
21
|
const KERNEL_READY_TIMEOUT_MS = 30_000;
|
|
22
|
+
const SILENCE_KILL_GRACE_MS = 2000;
|
|
22
23
|
const DEFAULT_MAX_OUTPUT_CHARS = 1_000_000;
|
|
23
24
|
|
|
24
25
|
export interface KernelOptions {
|
|
@@ -169,8 +170,13 @@ export class KernelClient {
|
|
|
169
170
|
private ready = false;
|
|
170
171
|
/** Serializes all kernel ops: one execute at a time, snapshots between cells. */
|
|
171
172
|
private queue: Promise<unknown> = Promise.resolve();
|
|
172
|
-
private
|
|
173
|
+
private _onUnexpectedExit?: () => void;
|
|
174
|
+
/** Engine hook: an unexpected kernel death (not a deliberate kill) should drop the instance. */
|
|
175
|
+
setOnUnexpectedExit(fn: () => void): void {
|
|
176
|
+
this._onUnexpectedExit = fn;
|
|
177
|
+
}
|
|
173
178
|
private watchdog?: ReturnType<typeof setInterval>;
|
|
179
|
+
private silenceKillTimer?: ReturnType<typeof setTimeout>;
|
|
174
180
|
private pendingReplies = new Map<
|
|
175
181
|
string,
|
|
176
182
|
{ resolve(m: ParsedMessage): void; timer?: ReturnType<typeof setTimeout> }
|
|
@@ -223,9 +229,18 @@ export class KernelClient {
|
|
|
223
229
|
kc.child = child;
|
|
224
230
|
kc.connectionFilePath = connPath;
|
|
225
231
|
child.on("exit", () => {
|
|
226
|
-
// --- a dead kernel settles the running cell; the engine rebuilds
|
|
232
|
+
// --- a dead kernel settles the running cell; the engine rebuilds. clear child/ready
|
|
233
|
+
// --- so isRunning reflects death and the engine never resumes a zombie process. ---
|
|
227
234
|
kc.settleActive(new Error("kernel process exited"));
|
|
228
|
-
kc.
|
|
235
|
+
kc.child = undefined;
|
|
236
|
+
kc.ready = false;
|
|
237
|
+
kc.shell?.close();
|
|
238
|
+
kc.control?.close();
|
|
239
|
+
kc.iopub?.close();
|
|
240
|
+
kc.shell = undefined;
|
|
241
|
+
kc.control = undefined;
|
|
242
|
+
kc.iopub = undefined;
|
|
243
|
+
kc._onUnexpectedExit?.();
|
|
229
244
|
});
|
|
230
245
|
|
|
231
246
|
try {
|
|
@@ -494,6 +509,12 @@ export class KernelClient {
|
|
|
494
509
|
if (quiet >= this.timeoutMs && !active.settled) {
|
|
495
510
|
active.timedOut = true;
|
|
496
511
|
this.interrupt();
|
|
512
|
+
// --- the interrupt is a real KeyboardInterrupt, but a cell that swallows/ignores
|
|
513
|
+
// --- it never replies; escalate to a kill so the queue is freed, mirroring index.ts. ---
|
|
514
|
+
this.silenceKillTimer ??= setTimeout(() => {
|
|
515
|
+
if (!active.settled) this.kill();
|
|
516
|
+
}, SILENCE_KILL_GRACE_MS);
|
|
517
|
+
this.silenceKillTimer.unref?.();
|
|
497
518
|
}
|
|
498
519
|
},
|
|
499
520
|
Math.min(250, this.timeoutMs),
|
|
@@ -506,6 +527,10 @@ export class KernelClient {
|
|
|
506
527
|
clearInterval(this.watchdog);
|
|
507
528
|
this.watchdog = undefined;
|
|
508
529
|
}
|
|
530
|
+
if (this.silenceKillTimer) {
|
|
531
|
+
clearTimeout(this.silenceKillTimer);
|
|
532
|
+
this.silenceKillTimer = undefined;
|
|
533
|
+
}
|
|
509
534
|
}
|
|
510
535
|
|
|
511
536
|
/** Genuine KeyboardInterrupt via control-channel interrupt_request; the kernel survives. */
|
package/src/extension/prompt.ts
CHANGED
|
@@ -32,7 +32,7 @@ export function buildPromptGuidelines(preloaded: string[]): string[] {
|
|
|
32
32
|
"Surgical old-text/new-text: read the region, fix an exact unique anchor that appears once, replace, verify. Many small edits over one big rewrite, a parse error can strand an anchor; after an error, read the file back from disk first. Make the smallest valid change, preserve conventions, never invent files, APIs, conventions, or test results. Prune generated dirs when walking trees. Pass a `timeout` to any `subprocess.run(...)`, a silent cell must die, not hang.",
|
|
33
33
|
"",
|
|
34
34
|
"## Print is expensive",
|
|
35
|
-
"
|
|
35
|
+
"Every token you print is spent from the context you need for the turns to come, and it never comes back. Treat printing as debt, not reward. Print only the exact fragment the next decision consumes and hold the whole in a variable. Every other print is waste, it buys nothing and closes the room you have left to think. Ask before you print: does this decide the next step, or is it just noise? When it is noise, cut it. When in doubt, cut it. A tight transcript is the sign you actually worked; a bloated one is the sign you did not.",
|
|
36
36
|
"",
|
|
37
37
|
...(preloaded.length
|
|
38
38
|
? [
|
|
@@ -39,7 +39,7 @@ export interface RenderDeps {
|
|
|
39
39
|
}
|
|
40
40
|
|
|
41
41
|
const OUTPUT_INDENT = " ";
|
|
42
|
-
const SPINNER_FRAMES = ["
|
|
42
|
+
const SPINNER_FRAMES = [">..", ".>.", "..>", ".>."];
|
|
43
43
|
|
|
44
44
|
export function formatDuration(durationMs: number | undefined): string | undefined {
|
|
45
45
|
if (durationMs === undefined) return undefined;
|
|
@@ -105,13 +105,68 @@ function marker(state: ExecuteRenderState, deps: RenderDeps): string {
|
|
|
105
105
|
return deps.fg("success", "✓");
|
|
106
106
|
case "running": {
|
|
107
107
|
const now = deps.now?.() ?? Date.now();
|
|
108
|
-
return deps.fg("accent", SPINNER_FRAMES[Math.floor(now /
|
|
108
|
+
return deps.fg("accent", SPINNER_FRAMES[Math.floor(now / 120) % SPINNER_FRAMES.length]);
|
|
109
109
|
}
|
|
110
110
|
default:
|
|
111
111
|
return deps.fg("muted", "◇");
|
|
112
112
|
}
|
|
113
113
|
}
|
|
114
114
|
|
|
115
|
+
/**
|
|
116
|
+
* highlight.js python emits no scope for plain identifiers, so they arrive as raw (uncolored)
|
|
117
|
+
* text mixed with SGR-colored tokens and bare punctuation. Re-color only whole identifier runs
|
|
118
|
+
* that sit outside any colored span, leaving keywords, strings, numbers, and other already
|
|
119
|
+
* colored tokens untouched.
|
|
120
|
+
*/
|
|
121
|
+
function colorBareIdentifiers(line: string, paint: (id: string) => string): string {
|
|
122
|
+
if (!line.includes("\x1b") && !/[a-zA-Z_]/.test(line)) return line;
|
|
123
|
+
const out: string[] = [];
|
|
124
|
+
let pending = "";
|
|
125
|
+
let colored = false;
|
|
126
|
+
const pushRaw = (s: string) => {
|
|
127
|
+
let last = 0;
|
|
128
|
+
for (const m of s.matchAll(/[a-zA-Z_][a-zA-Z0-9_]*/g)) {
|
|
129
|
+
const index = m.index ?? 0;
|
|
130
|
+
out.push(s.slice(last, index), paint(m[0]));
|
|
131
|
+
last = index + m[0].length;
|
|
132
|
+
}
|
|
133
|
+
out.push(s.slice(last));
|
|
134
|
+
};
|
|
135
|
+
let i = 0;
|
|
136
|
+
const isFgColor = (seq: string) => /\x1b\[(?:38|9?[0-7])/.test(seq);
|
|
137
|
+
while (i < line.length) {
|
|
138
|
+
if (line[i] === "\x1b") {
|
|
139
|
+
const end = line.indexOf("m", i) + 1;
|
|
140
|
+
const seq = line.slice(i, end);
|
|
141
|
+
if (isFgColor(seq)) {
|
|
142
|
+
if (pending) {
|
|
143
|
+
pushRaw(pending);
|
|
144
|
+
pending = "";
|
|
145
|
+
}
|
|
146
|
+
out.push(seq);
|
|
147
|
+
colored = true;
|
|
148
|
+
} else if (seq.includes("39") || seq.includes("0m")) {
|
|
149
|
+
if (colored) {
|
|
150
|
+
out.push(seq);
|
|
151
|
+
colored = false;
|
|
152
|
+
} else {
|
|
153
|
+
pending += seq;
|
|
154
|
+
}
|
|
155
|
+
} else {
|
|
156
|
+
pending += seq;
|
|
157
|
+
}
|
|
158
|
+
i = end;
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
// only raw (uncolored) text may be repainted; colored tokens pass through untouched
|
|
162
|
+
if (colored) out.push(line[i]);
|
|
163
|
+
else pending += line[i];
|
|
164
|
+
i++;
|
|
165
|
+
}
|
|
166
|
+
if (pending) pushRaw(pending);
|
|
167
|
+
return out.join("");
|
|
168
|
+
}
|
|
169
|
+
|
|
115
170
|
function highlightLines(code: string, deps: RenderDeps): string[] {
|
|
116
171
|
if (!code) return [];
|
|
117
172
|
return deps.highlight(code);
|
|
@@ -211,14 +266,15 @@ function addWrapped(
|
|
|
211
266
|
text: string,
|
|
212
267
|
width: number,
|
|
213
268
|
deps: RenderDeps,
|
|
214
|
-
options: { sanitize?: boolean } = {},
|
|
269
|
+
options: { sanitize?: boolean; indentAfter?: number } = {},
|
|
215
270
|
): void {
|
|
216
271
|
const safe = options.sanitize === false ? text : sanitizeTuiOutput(text);
|
|
217
272
|
const available = Math.max(1, width - 1 - deps.visibleWidth(prefix));
|
|
218
273
|
const wrapped = deps.wrapTextWithAnsi(safe, available);
|
|
219
274
|
for (const [index, line] of (wrapped.length > 0 ? wrapped : [""]).entries()) {
|
|
220
275
|
const linePrefix = index === 0 ? prefix : " ".repeat(deps.visibleWidth(prefix));
|
|
221
|
-
|
|
276
|
+
const continuationIndent = index > 0 && options.indentAfter ? " ".repeat(options.indentAfter) : "";
|
|
277
|
+
lines.push(deps.truncateToWidth(` ${linePrefix}${continuationIndent}${closeOpenSgr(line)}`, width, ""));
|
|
222
278
|
}
|
|
223
279
|
}
|
|
224
280
|
|
|
@@ -229,8 +285,13 @@ function renderCode(state: ExecuteRenderState, lines: string[], width: number, d
|
|
|
229
285
|
const highlighted = highlightLines(code, deps);
|
|
230
286
|
for (const [index, rawLine] of code.split("\n").entries()) {
|
|
231
287
|
const prefix = index === 0 ? deps.fg("dim", "› ") : deps.fg("dim", " ");
|
|
232
|
-
|
|
233
|
-
|
|
288
|
+
const paint = (id: string) => deps.fg("syntaxVariable", id);
|
|
289
|
+
const hlLine = colorBareIdentifiers(highlighted[index] ?? rawLine, paint);
|
|
290
|
+
const indent = /^[ \t]*/.exec(rawLine)?.[0] ?? "";
|
|
291
|
+
addWrapped(lines, prefix, hlLine, width, deps, {
|
|
292
|
+
sanitize: false,
|
|
293
|
+
indentAfter: deps.visibleWidth(indent),
|
|
294
|
+
});
|
|
234
295
|
}
|
|
235
296
|
return true;
|
|
236
297
|
}
|
package/src/extension/render.ts
CHANGED
|
@@ -43,7 +43,7 @@ function renderVersion(state: ExecuteRenderState): string {
|
|
|
43
43
|
state.executionStarted,
|
|
44
44
|
state.hasResult,
|
|
45
45
|
// --- fold the animation frame in while running so the spinner still turns ---
|
|
46
|
-
statusKind(state) === "running" ? Math.floor(Date.now() /
|
|
46
|
+
statusKind(state) === "running" ? Math.floor(Date.now() / 120) % 4 : -1,
|
|
47
47
|
].join("|");
|
|
48
48
|
}
|
|
49
49
|
|