pi-repl-py 0.1.0 → 0.1.1

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.
@@ -1,518 +1,2 @@
1
- /**
2
- * Semantic cell previews: the collapsed line of an \u0060execute\u0060 cell.
3
- *
4
- * A cell's first line is almost always plumbing (const out = await ...), so
5
- * "first non-comment line" renders every collapsed call as noise. This module
6
- * scores the whole cell for intent — the shell command inside a Bun.$ template,
7
- * the task handed to a subagent, the file a write lands on — and returns the
8
- * one line a reader would want, plus which kind of work it is.
9
- *
10
- * Pure and dependency-free so the scorer is table-testable; render-core
11
- * consumes it for the collapsed header.
12
- */
13
-
14
- /** What kind of work the winning line represents; drives the header label. */
15
- type CellPreviewKind = "shell" | "agent" | "ts";
16
-
17
- export interface CellPreview {
18
- kind: CellPreviewKind;
19
- text: string;
20
- }
21
-
22
- const BACKTICK = "\u0060";
23
- const DESCRIPTOR_MAX_WIDTH = 64;
24
-
25
- // ── descriptor hygiene ───────────────────────────────────────────────────────
26
-
27
- function collapseWhitespace(text: string): string {
28
- return text.replace(/\s+/g, " ").trim();
29
- }
30
-
31
- function truncateDescriptor(text: string): string {
32
- if (text.length <= DESCRIPTOR_MAX_WIDTH) return text;
33
- return text.slice(0, DESCRIPTOR_MAX_WIDTH - 1).trimEnd() + "…";
34
- }
35
-
36
- function redactNoise(text: string): string {
37
- return text
38
- .replace(/[A-Za-z0-9+/]{80,}={0,2}/g, "<blob>")
39
- .replace(/\b((?=\w*(?:token|key|secret|password))[A-Za-z_]\w*)\s*[=:]\s*(["'])[^"']*\2/gi, "$1=<redacted>")
40
- .replace(
41
- /\b((?=\w*(?:token|key|secret|password))[A-Za-z_]\w*)\s*[=:]\s*(?!<redacted>)(?!["'])\S+/gi,
42
- "$1=<redacted>",
43
- )
44
- .replace(/(["'])sk-[^"']+\1/g, "$1<redacted>$1")
45
- .replace(/(["']).{160,}\1/g, "$1…$1");
46
- }
47
-
48
- export function descriptor(text: string): string {
49
- return truncateDescriptor(collapseWhitespace(redactNoise(text)));
50
- }
51
-
52
- // ── source scanning ──────────────────────────────────────────────────────────
53
-
54
- interface Span {
55
- start: number;
56
- end: number;
57
- body: string;
58
- }
59
-
60
- /**
61
- * Body of the template literal opening at start (the backtick index). Tracks
62
- * escapes and interpolation nesting (including nested templates) so a shell
63
- * command containing interpolations is captured whole. An unclosed template is
64
- * returned as-is: while args stream, previewing the partial command beats
65
- * previewing nothing.
66
- */
67
- function scanTemplate(source: string, start: number): Span {
68
- let depth = 0;
69
- let inNested = false;
70
- for (let i = start + 1; i < source.length; i++) {
71
- const ch = source[i];
72
- if (ch === "\\") {
73
- i += 1;
74
- continue;
75
- }
76
- if (ch === BACKTICK) {
77
- if (depth === 0 && !inNested) return { start, end: i + 1, body: source.slice(start + 1, i) };
78
- inNested = !inNested;
79
- continue;
80
- }
81
- if (!inNested && ch === "$" && source[i + 1] === "{") {
82
- depth += 1;
83
- i += 1;
84
- continue;
85
- }
86
- if (!inNested && depth > 0 && ch === "}") depth -= 1;
87
- }
88
- return { start, end: source.length, body: source.slice(start + 1) };
89
- }
90
-
91
- const CONST_STRING_PATTERN = new RegExp(
92
- '(?:const|let|var)\\s+([A-Za-z_$][\\w$]*)\\s*=\\s*(?:"([^"\\n]*)"|' +
93
- "'([^'\\n]*)'|" +
94
- BACKTICK +
95
- "([^" +
96
- BACKTICK +
97
- "$\\n]*)" +
98
- BACKTICK +
99
- ")",
100
- "g",
101
- );
102
-
103
- /** Simple string constants, for resolving interpolations and path arguments. */
104
- function stringConsts(source: string): Map<string, string> {
105
- const vars = new Map<string, string>();
106
- for (const match of source.matchAll(CONST_STRING_PATTERN)) {
107
- const name = match[1];
108
- const value = match[2] ?? match[3] ?? match[4];
109
- if (name && value !== undefined) vars.set(name, value);
110
- }
111
- return vars;
112
- }
113
-
114
- function substituteVars(text: string, vars: ReadonlyMap<string, string>): string {
115
- return text.replace(/\$\{\s*([A-Za-z_$][\w$]*)\s*\}/g, (whole, name: string) => vars.get(name) ?? whole);
116
- }
117
-
118
- function maskSpan(source: string, span: Span): string {
119
- return source.slice(0, span.start) + " ".repeat(span.end - span.start) + source.slice(span.end);
120
- }
121
-
122
- // ── shell command simplification ─────────────────────────────────────────────
123
-
124
- const CD_PREFIX_PATTERN = /^\s*cd\s+([^&;|]+?)\s*(?:&&|;)\s*/;
125
- const SHELL_SETUP_PATTERN = /^(?:export\s+\w+=|set\s+[-+]|source\s+\S+|\.\s+\S+)/;
126
- const HEREDOC_PATTERN = /<<-?\s*['"]?([A-Za-z_][A-Za-z0-9_]*)['"]?/;
127
-
128
- function shellWords(line: string): string[] {
129
- const words: string[] = [];
130
- for (const match of line.matchAll(/"([^"]*)"|'([^']*)'|(\S+)/g)) {
131
- words.push(match[1] ?? match[2] ?? match[3] ?? "");
132
- }
133
- return words;
134
- }
135
-
136
- function pathTail(path: string): string {
137
- const cleaned = path.replace(/\/+$/, "");
138
- const tail = cleaned.slice(cleaned.lastIndexOf("/") + 1);
139
- return tail || cleaned;
140
- }
141
-
142
- function simplifyRunnerCommand(line: string): string | undefined {
143
- const words = shellWords(line);
144
- if (words[0] === "npm" || words[0] === "pnpm") {
145
- const runIndex = words.indexOf("run");
146
- if (runIndex >= 0 && words[runIndex + 1]) {
147
- return (words[0] + " " + words.slice(runIndex + 1).join(" ")).trim();
148
- }
149
- }
150
- if (line.includes("node_modules/.bin/")) {
151
- return line.replace(/\S*node_modules\/\.bin\//g, "");
152
- }
153
- return undefined;
154
- }
155
-
156
- function simplifyMutationCommand(line: string): string | undefined {
157
- const words = shellWords(line);
158
- if (words.length === 0) return undefined;
159
- if (words[0] === "cat" && words[1] === ">" && words[2]) return "write " + pathTail(words[2]);
160
- if (words[0] === "tee" && words.at(-1)) {
161
- return (words.includes("-a") ? "append " : "write ") + pathTail(words.at(-1) ?? "");
162
- }
163
- return undefined;
164
- }
165
-
166
- function simplifyShellLine(line: string): string {
167
- return simplifyRunnerCommand(line) ?? simplifyMutationCommand(line) ?? line;
168
- }
169
-
170
- /**
171
- * Commands that prepare the ground rather than do the work. A cell that mkdirs
172
- * and then writes a file is a write; the shell only wins when it is the story.
173
- */
174
- const SHELL_SETUP_WORDS = new Set([
175
- "mkdir",
176
- "cd",
177
- "export",
178
- "touch",
179
- "chmod",
180
- "chown",
181
- "ln",
182
- "echo",
183
- "true",
184
- "sleep",
185
- "which",
186
- "sync",
187
- ]);
188
-
189
- const SHELL_ACTION_WORDS = new Set([
190
- "rm",
191
- "mv",
192
- "cp",
193
- "git",
194
- "npm",
195
- "pnpm",
196
- "bun",
197
- "bunx",
198
- "npx",
199
- "make",
200
- "cargo",
201
- "docker",
202
- "curl",
203
- "gh",
204
- "pi",
205
- ]);
206
-
207
- function shellLineScore(line: string, index: number): number {
208
- const simplified = simplifyShellLine(line);
209
- const words = shellWords(line);
210
- let score = 30;
211
- if (simplified !== line) score += 40;
212
- if (SHELL_ACTION_WORDS.has(words[0] ?? "")) score += 20;
213
- if (/\b(?:rm|mv|cp|git\s+(?:add|commit|push)|sed\s+-i|perl\s+-pi|tee|cat\s*>)\b/.test(line)) score += 40;
214
- return score + index;
215
- }
216
-
217
- function heredocBody(lines: readonly string[], startIndex: number, delimiter: string): string | undefined {
218
- const body: string[] = [];
219
- for (let i = startIndex + 1; i < lines.length; i++) {
220
- if ((lines[i] ?? "").trim() === delimiter) return body.join("\n");
221
- body.push(lines[i] ?? "");
222
- }
223
- return body.length > 0 ? body.join("\n") : undefined;
224
- }
225
-
226
- function previewHeredoc(lines: readonly string[]): string | undefined {
227
- for (let i = 0; i < lines.length; i++) {
228
- const line = (lines[i] ?? "").trim();
229
- const delimiter = line.match(HEREDOC_PATTERN)?.[1];
230
- if (!delimiter) continue;
231
- const body = heredocBody(lines, i, delimiter);
232
- if (!body) continue;
233
- // The write target is the story; the body is detail for the expanded view.
234
- const catWrite = line.match(/\b(?:cat|tee)\b.*(?:>|\s)(\S+)\s*<<-?/);
235
- if (catWrite?.[1]) return (line.includes("tee -a") ? "append " : "write ") + pathTail(catWrite[1]);
236
- return descriptor(body);
237
- }
238
- return undefined;
239
- }
240
-
241
- /** Best single line of a (possibly multi-line, chained) shell command. */
242
- export function previewShellCommand(command: string): string {
243
- return previewShellCommandScored(command).text;
244
- }
245
-
246
- /** As previewShellCommand, but keeps the winning line's own strength so a cell
247
- * with several shell calls can surface the strongest one. */
248
- function previewShellCommandScored(command: string): { text: string; strength: number } {
249
- const lines = command.split("\n");
250
- const heredoc = previewHeredoc(lines);
251
- if (heredoc) return { text: descriptor(heredoc), strength: 90 };
252
-
253
- let best: { text: string; score: number } | undefined;
254
- let cwdSuffix: string | undefined;
255
- let index = 0;
256
- for (const rawLine of lines) {
257
- for (const rawPart of rawLine.split(/\s*(?:&&|;)\s*/)) {
258
- let part = rawPart.trim();
259
- if (!part || part.startsWith("#") || SHELL_SETUP_PATTERN.test(part)) continue;
260
- const cd = part.match(CD_PREFIX_PATTERN);
261
- if (cd?.[1]) {
262
- cwdSuffix = pathTail(cd[1].trim());
263
- part = part.replace(CD_PREFIX_PATTERN, "").trim();
264
- } else if (/^cd\s+\S+$/.test(part)) {
265
- cwdSuffix = pathTail(part.slice(2).trim());
266
- continue;
267
- }
268
- if (!part) continue;
269
- const candidate = { text: simplifyShellLine(part), score: shellLineScore(part, index) };
270
- if (!best || candidate.score > best.score) best = candidate;
271
- index += 1;
272
- }
273
- }
274
- if (!best) return { text: "", strength: 0 };
275
- // --- trailing redirections are plumbing, not intent ---
276
- const cleaned = best.text.replace(/(?:\s*(?:2>&1|[12]?>\s*\/dev\/null|&>\s*\/dev\/null))+\s*$/, "");
277
- // --- a stripped cd prefix still matters ---
278
- const text = cwdSuffix && !cleaned.includes(cwdSuffix) ? cleaned + " (" + cwdSuffix + ")" : cleaned;
279
- return { text: descriptor(text), strength: best.score };
280
- }
281
-
282
- // ── special construct extraction ─────────────────────────────────────────────
283
-
284
- interface Candidate {
285
- kind: CellPreviewKind;
286
- text: string;
287
- score: number;
288
- }
289
-
290
- const SHELL_OPEN_PATTERN = new RegExp("Bun\\s*\\.\\s*\\$\\s*(?:\\([^)]*\\)\\s*)?" + BACKTICK, "g");
291
-
292
- function shellCandidates(
293
- source: string,
294
- vars: ReadonlyMap<string, string>,
295
- ): { candidates: Candidate[]; masked: string } {
296
- const candidates: Candidate[] = [];
297
- let masked = source;
298
- SHELL_OPEN_PATTERN.lastIndex = 0;
299
- let match = SHELL_OPEN_PATTERN.exec(masked);
300
- while (match) {
301
- const span = scanTemplate(masked, match.index + match[0].length - 1);
302
- const command = previewShellCommandScored(substituteVars(span.body, vars));
303
- // --- the command's own strength breaks ties; setup-only drops lower ---
304
- if (command.text) {
305
- const setupOnly = SHELL_SETUP_WORDS.has(shellWords(command.text)[0] ?? "");
306
- const score = setupOnly ? 72 : 90 + Math.min(command.strength, 200) / 25;
307
- candidates.push({ kind: "shell", text: command.text, score });
308
- }
309
- masked = maskSpan(masked, span);
310
- SHELL_OPEN_PATTERN.lastIndex = span.end;
311
- match = SHELL_OPEN_PATTERN.exec(masked);
312
- }
313
- return { candidates, masked };
314
- }
315
-
316
- const STRING_ARG_PATTERN = /^\s*(?:"([^"]*)"|'([^']*)')/;
317
-
318
- function agentCandidates(
319
- source: string,
320
- vars: ReadonlyMap<string, string>,
321
- ): { candidates: Candidate[]; masked: string } {
322
- const tasks: string[] = [];
323
- let masked = source;
324
- const pattern = /rlm\s*\.\s*run\s*\(/g;
325
- let match = pattern.exec(masked);
326
- while (match) {
327
- const argsStart = match.index + match[0].length;
328
- let task: string | undefined;
329
- const rest = masked.slice(argsStart);
330
- const literal = rest.match(STRING_ARG_PATTERN);
331
- if (literal) {
332
- task = literal[1] ?? literal[2];
333
- } else if (rest.trimStart().startsWith(BACKTICK)) {
334
- const tickIndex = argsStart + rest.indexOf(BACKTICK);
335
- const span = scanTemplate(masked, tickIndex);
336
- task = substituteVars(span.body, vars);
337
- masked = maskSpan(masked, span);
338
- } else {
339
- const identifier = rest.match(/^\s*([A-Za-z_$][\w$]*)/)?.[1];
340
- task = identifier ? (vars.get(identifier) ?? identifier) : undefined;
341
- }
342
- // --- a chosen child name is identity; lead with it ---
343
- const name = masked.slice(argsStart).match(/name\s*:\s*(?:"([^"]*)"|'([^']*)')/);
344
- const label = name?.[1] ?? name?.[2];
345
- tasks.push(label && task ? label + ": " + task : (label ?? task ?? "subagent"));
346
- pattern.lastIndex = argsStart;
347
- match = pattern.exec(masked);
348
- }
349
- const candidates: Candidate[] =
350
- tasks.length === 0
351
- ? []
352
- : [
353
- {
354
- kind: "agent",
355
- text: descriptor(tasks.length === 1 ? (tasks[0] ?? "") : tasks[0] + " (+" + (tasks.length - 1) + " more)"),
356
- score: 100,
357
- },
358
- ];
359
- return { candidates, masked };
360
- }
361
-
362
- const FILE_EFFECT_PATTERN =
363
- /(?:Bun\.write|\b(?:fs|fsp|promises)\.(?:writeFileSync|writeFile|appendFileSync|appendFile|mkdirSync|mkdir|rmSync|rmdirSync|unlinkSync|unlink|renameSync|rename|copyFileSync|copyFile|cpSync|cp)|\b(?:writeFileSync|writeFile|appendFileSync|mkdirSync|rmSync|unlinkSync|renameSync|copyFileSync))\s*\(\s*([^,)\n]+)/g;
364
-
365
- const FILE_EFFECT_VERBS: ReadonlyArray<[string, string]> = [
366
- ["Bun.write", "write"],
367
- ["writeFileSync", "write"],
368
- ["writeFile", "write"],
369
- ["appendFileSync", "append"],
370
- ["appendFile", "append"],
371
- ["mkdirSync", "mkdir"],
372
- ["mkdir", "mkdir"],
373
- ["rmdirSync", "delete"],
374
- ["rmSync", "delete"],
375
- ["rm", "delete"],
376
- ["unlinkSync", "delete"],
377
- ["unlink", "delete"],
378
- ["renameSync", "rename"],
379
- ["rename", "rename"],
380
- ["copyFileSync", "copy"],
381
- ["copyFile", "copy"],
382
- ["cpSync", "copy"],
383
- ["cp", "copy"],
384
- ];
385
-
386
- function resolveArgText(arg: string, vars: ReadonlyMap<string, string>): string | undefined {
387
- const trimmed = arg.trim();
388
- const literalPattern = new RegExp("^[\"'" + BACKTICK + "]([^\"'" + BACKTICK + "]*)[\"'" + BACKTICK + "]$");
389
- const literal = trimmed.match(literalPattern);
390
- if (literal?.[1]) return literal[1];
391
- if (/^[A-Za-z_$][\w$]*$/.test(trimmed)) return vars.get(trimmed);
392
- if (trimmed.startsWith(BACKTICK)) return substituteVars(trimmed.slice(1, -1), vars);
393
- return undefined;
394
- }
395
-
396
- const FILE_READ_PATTERN = /Bun\.file\s*\(\s*([^,)\n]+?)\s*\)\s*\.\s*(?:text|json|arrayBuffer|bytes|stream)\s*\(/g;
397
-
398
- function fileCandidates(source: string, vars: ReadonlyMap<string, string>): Candidate[] {
399
- const candidates: Candidate[] = [];
400
- for (const match of source.matchAll(FILE_EFFECT_PATTERN)) {
401
- const call = match[0];
402
- const verb = FILE_EFFECT_VERBS.find(([name]) => call.includes(name))?.[1];
403
- if (!verb) continue;
404
- const path = resolveArgText(match[1] ?? "", vars);
405
- if (path) candidates.push({ kind: "ts", text: descriptor(verb + " " + path), score: 95 });
406
- }
407
- for (const match of source.matchAll(FILE_READ_PATTERN)) {
408
- const path = resolveArgText(match[1] ?? "", vars);
409
- if (path) candidates.push({ kind: "ts", text: descriptor("read " + path), score: 70 });
410
- }
411
- for (const match of source.matchAll(/\bfetch\s*\(\s*([^,)\n]+)/g)) {
412
- const url = resolveArgText(match[1] ?? "", vars);
413
- if (url) candidates.push({ kind: "ts", text: descriptor("fetch " + url), score: 75 });
414
- }
415
- return candidates;
416
- }
417
-
418
- // ── bridged host tools ───────────────────────────────────────────────────────
419
-
420
- /** Per-tool: which argument names the target, the verb shown, and the band. */
421
- const BRIDGED_TOOLS: Record<string, { arg: string; verb: string; score: number }> = {
422
- read: { arg: "path", verb: "read", score: 70 },
423
- bash: { arg: "command", verb: "", score: 88 },
424
- edit: { arg: "path", verb: "edit", score: 95 },
425
- write: { arg: "path", verb: "write", score: 95 },
426
- grep: { arg: "pattern", verb: "grep", score: 68 },
427
- find: { arg: "pattern", verb: "find", score: 68 },
428
- ls: { arg: "path", verb: "ls", score: 68 },
429
- };
430
-
431
- function bridgedToolCandidates(source: string, vars: ReadonlyMap<string, string>): Candidate[] {
432
- const candidates: Candidate[] = [];
433
- for (const match of source.matchAll(/\btools\.(\w+)\s*\(\s*\{([^}]*)\}/g)) {
434
- const spec = BRIDGED_TOOLS[match[1] ?? ""];
435
- if (!spec) continue;
436
- const props = match[2] ?? "";
437
- const argMatch = props.match(new RegExp(spec.arg + "\\s*:\\s*([^,}]+)"));
438
- const target = argMatch ? resolveArgText(argMatch[1] ?? "", vars) : undefined;
439
- if (!target) continue;
440
- // --- a bridged bash call is a command like any other ---
441
- const text = spec.verb ? spec.verb + " " + target : previewShellCommand(target) || target;
442
- candidates.push({ kind: "ts", text: descriptor(text), score: spec.score });
443
- }
444
- return candidates;
445
- }
446
-
447
- // ── generic line scoring ─────────────────────────────────────────────────────
448
-
449
- const SKIP_LINE_PATTERN = /^(?:$|\/\/|\/\*|\*|import\s|export\s+(?:type\s|\{)|[})\];,]+$)/;
450
- const DEFINITION_PATTERN = /^(?:export\s+)?(?:async\s+)?(?:function\s|class\s|interface\s|type\s+\w+\s*=)/;
451
- const ARROW_DEFINITION_PATTERN = /^(?:const|let)\s+[A-Za-z_$][\w$]*\s*=\s*(?:async\s*)?\(?[^)=]*\)?\s*=>/;
452
- const CONTROL_PATTERN = /^(?:if|for|while|switch|try|do)\b/;
453
- const CALL_STATEMENT_PATTERN = /^(?:await\s+)?[A-Za-z_$][\w$.]*\s*\(/;
454
- const ASSIGNMENT_CALL_PATTERN = /^(?:const|let|var)\s+[^=]{1,60}=\s*(?:await\s+)?(?:new\s+)?[A-Za-z_$][\w$.]*\s*\(/;
455
- const LOW_SIGNAL_CALL_PATTERN =
456
- /^(?:await\s+)?(?:console\.\w+|String|Number|Boolean|JSON\.stringify|JSON\.parse|structuredClone)\s*\(/;
457
- const LOW_SIGNAL_ASSIGNMENT_PATTERN =
458
- /=\s*(?:await\s+)?(?:JSON\.parse|JSON\.stringify|String|Number|Boolean|Object\.keys|Object\.entries)\s*\(/;
459
-
460
- function consoleInnerCall(line: string): string | undefined {
461
- const inner = line.match(/^console\.\w+\(\s*(.+)\)\s*;?\s*$/)?.[1]?.trim();
462
- return inner && CALL_STATEMENT_PATTERN.test(inner) && !LOW_SIGNAL_CALL_PATTERN.test(inner) ? inner : undefined;
463
- }
464
-
465
- function genericLineScore(line: string): number {
466
- if (SKIP_LINE_PATTERN.test(line)) return -1;
467
- if (LOW_SIGNAL_ASSIGNMENT_PATTERN.test(line)) return 25;
468
- if (consoleInnerCall(line)) return 55;
469
- if (LOW_SIGNAL_CALL_PATTERN.test(line)) return 15;
470
- if (DEFINITION_PATTERN.test(line) || ARROW_DEFINITION_PATTERN.test(line)) return 50;
471
- if (CONTROL_PATTERN.test(line)) return 20;
472
- if (/^(?:return|throw)\b/.test(line)) return 45;
473
- if (ASSIGNMENT_CALL_PATTERN.test(line)) return 60;
474
- if (CALL_STATEMENT_PATTERN.test(line)) return 65;
475
- if (/^(?:const|let|var)\s/.test(line)) return 22;
476
- return 30;
477
- }
478
-
479
- function genericCandidates(masked: string): Candidate[] {
480
- const candidates: Candidate[] = [];
481
- for (const [index, rawLine] of masked.split("\n").entries()) {
482
- const line = rawLine.trim();
483
- const score = genericLineScore(line);
484
- if (score < 0) continue;
485
- const text = consoleInnerCall(line) ?? line;
486
- // --- later lines win ties: cells read as setup-then-act, and the act is the story ---
487
-
488
- candidates.push({ kind: "ts", text: descriptor(text), score: score + Math.min(index, 90) / 100 });
489
- }
490
- return candidates;
491
- }
492
-
493
- // ── entry point ──────────────────────────────────────────────────────────────
494
-
495
- export function previewCell(code: string): CellPreview {
496
- const source = code.trimEnd();
497
- if (!source) return { kind: "ts", text: "" };
498
- const vars = stringConsts(source);
499
-
500
- // Order matters: agent spans scrub shell-looking syntax first, then
501
- // the shell scan masks shell bodies before the agent scan.
502
- // generic line scan so command text is never scored as TypeScript.
503
- const agent = agentCandidates(source, vars);
504
- const shell = shellCandidates(agent.masked, vars);
505
- const candidates: Candidate[] = [
506
- ...agent.candidates,
507
- ...shell.candidates,
508
- ...fileCandidates(shell.masked, vars),
509
- ...bridgedToolCandidates(shell.masked, vars),
510
- ...genericCandidates(shell.masked),
511
- ];
512
-
513
- let best: Candidate | undefined;
514
- for (const candidate of candidates) {
515
- if (candidate.text && (!best || candidate.score > best.score)) best = candidate;
516
- }
517
- return best ?? { kind: "ts", text: "" };
518
- }
1
+ // --- thin aggregator: the preview surface kept at this path so callers and tests stay put ---
2
+ export { type CellPreview, descriptor, previewCell, previewShellCommand } from "./preview/index.js";
@@ -0,0 +1,54 @@
1
+ // --- prompt: the execute tool's model-facing contract (pure, no pi/toolbox dependency) ---
2
+ // --- mirrors pi-robust-edit's schema/domain split: content lives here; the thin adapter in tool-meta wires it in ---
3
+
4
+ export const executeToolDescription =
5
+ "Execute Python in a persistent evaluator — the session's working memory. Variables, imports, " +
6
+ "functions, and data survive across every call. read, write, edit, and bash are Python functions " +
7
+ "available in every cell, not separate tools. A cell returns its final expression; anything else " +
8
+ "prints. Runs in the project-local venv, so a command that starts python or pip must target that venv.";
9
+
10
+ export const executePromptSnippet =
11
+ "Execute Python in a persistent evaluator whose variables, imports, and functions survive across " +
12
+ "calls; read/write/edit/bash are Python functions in every cell, plus anything you define and reuse; " +
13
+ "ls() lists them, help(name) shows usage";
14
+
15
+ // --- the function doctrine riding the execute tool; sections keep every rule findable and rankable as hard or soft ---
16
+ export function buildPromptGuidelines(preloaded: string[]): string[] {
17
+ return [
18
+ "## What's in every cell",
19
+ ...preloaded,
20
+ "Not sure what's available? Call ls() first; help(name) shows a signature and notes.",
21
+ "",
22
+ "## How to use them",
23
+ "These are your file and shell tools — call them. Don't reimplement read/write/edit/bash in Python, " +
24
+ "and don't fork a near-copy under a new name; a new def overwrites an old one by name, so extend " +
25
+ "the existing function instead.",
26
+ "Define a new function only to reuse it: if you'll run this shape again with different inputs, write " +
27
+ "it once as def and call it by arguments — otherwise just run the cell.",
28
+ "Do the job, then answer with the result. Don't tell the user you 'defined a function' or 'built a " +
29
+ "tool'; that's internal machinery.",
30
+ "",
31
+ "## Examples",
32
+ "Good — defined once, called by arguments:",
33
+ " def fetch_news(query, hl='en', gl='US', limit=15): <fetch + parse to a list>",
34
+ " fetch_news('Turkey')",
35
+ " fetch_news('Nigeria', hl='en-NG')",
36
+ "Compose them:",
37
+ " def find_files(pattern, root='.'): <walk root, filter by pattern>",
38
+ " def count_lines(paths): ...",
39
+ " count_lines(find_files('*.csv')) # one call",
40
+ "",
41
+ "## Efficiency",
42
+ "Everything a cell prints stays in context for the whole turn, so print slices, matches, or counts — " +
43
+ "never whole files — and keep large values in variables.",
44
+ "For whole-filesystem or large-directory scans, use the shell tools (find, fd, du, grep), not a " +
45
+ "Python os.walk: it pays a syscall per file and runs minutes on a big tree. Example: " +
46
+ "`find -xdev -type f -size +100M | sort -rn | head`. Reserve Python for analysing the results.",
47
+ "",
48
+ "## When it breaks",
49
+ "If the output starts with <rlm_engine_reset>, the kernel was rebuilt: data is restored but your " +
50
+ "functions are gone — recreate any helper you need and re-verify a variable before trusting it.",
51
+ "The standard library is available; don't install packages into the evaluator. Run out-of-tree " +
52
+ "projects through their own environment.",
53
+ ];
54
+ }
@@ -65,8 +65,7 @@ export function closeOpenSgr(line: string): string {
65
65
  fgOpen = false;
66
66
  bgOpen = false;
67
67
  } else if (code === 38 || code === 48) {
68
- // Skip the payload of 38;5;n / 38;2;r;g;b so a component (e.g. 38)
69
- // is not read as another SGR code.
68
+ // --- skip the 38;5;n / 38;2;r;g;b payload so a component isn't read as another SGR code ---
70
69
  if (code === 38) fgOpen = true;
71
70
  else bgOpen = true;
72
71
  const mode = Number(params[i + 1]);
@@ -138,11 +137,7 @@ function topLine(state: ExecuteRenderState, width: number, deps: RenderDeps): st
138
137
  const language = preview.kind === "shell" ? "repl · shell" : preview.kind === "agent" ? "repl · agent" : "repl";
139
138
  const prefix = `${marker(state, deps)} ${deps.fg("muted", language)}`;
140
139
 
141
- // Fixed metadata after the preview must always survive; the preview
142
- // absorbs all truncation. Counts settle-only: live updates jitter the header.
143
- // Suffix order is by priority: the expand hint must survive first, then the
144
- // error, then duration, then counts. Truncation happens from the right, so
145
- // low-priority items are elided before the user loses the expand keybinding.
140
+ // --- suffix priority: expand hint > error > duration > counts, so truncation never hides the expand key ---
146
141
  const suffixParts: string[] = [];
147
142
  suffixParts.push(deps.keyHint(state.expanded));
148
143
 
@@ -170,12 +165,10 @@ function topLine(state: ExecuteRenderState, width: number, deps: RenderDeps): st
170
165
  const separator = deps.fg("dim", " · ");
171
166
  const separatorWidth = deps.visibleWidth(separator);
172
167
  const suffix = suffixParts.join(separator);
173
- // Budget: total width minus leading space, prefix, suffix, separators.
168
+ // --- budget: width minus leading space, prefix, suffix, and separators ---
174
169
  const fixed = 1 + deps.visibleWidth(prefix) + separatorWidth + deps.visibleWidth(suffix);
175
170
  const previewBudget = Math.max(8, width - fixed - separatorWidth);
176
- // A semantic preview is a one-line summary of the code. Highlight Python
177
- // code the same way the expanded block is highlighted; shell/agent previews
178
- // stay accent-colored so they read as intent, not syntax.
171
+ // --- a semantic preview is a one-line summary; highlight Python code, accent shell/agent intent ---
179
172
  let middle = "";
180
173
  if (preview.text) {
181
174
  const previewText =
@@ -191,12 +184,7 @@ function topLine(state: ExecuteRenderState, width: number, deps: RenderDeps): st
191
184
  }
192
185
 
193
186
  function sanitizeTuiOutput(text: string): string {
194
- // Terminal escape sequences and control characters from user code output can
195
- // move the cursor, change colors, or print zero-width glyphs that break the
196
- // TUI layout. Color SGR / CSI sequences (e.g. IPython's colored tracebacks)
197
- // are STRIPPED so text stays readable; a remaining lone escape byte and other
198
- // control chars are shown as Unicode control pictures so nothing is silently
199
- // eaten. Tabs expand to 4 spaces; CR becomes ␍.
187
+ // --- strip ANSI SGR/CSI and escape control chars for a readable TUI; tabs expand, CR becomes ␍ ---
200
188
  return text
201
189
  .replace(/\x1b\[[0-?]*[ -/]*[@-~]/g, "")
202
190
  .replace(/\x1b/g, "␛")
@@ -245,7 +233,7 @@ function renderCode(state: ExecuteRenderState, lines: string[], width: number, d
245
233
  const highlighted = highlightLines(code, deps);
246
234
  for (const [index, rawLine] of code.split("\n").entries()) {
247
235
  const prefix = index === 0 ? deps.fg("dim", "› ") : deps.fg("dim", " ");
248
- // Code is already syntax-highlighted; don't strip its ANSI.
236
+ // --- code is already highlighted; don't strip its ANSI ---
249
237
  addWrapped(lines, prefix, highlighted[index] ?? rawLine, width, deps, { sanitize: false });
250
238
  }
251
239
  return true;
@@ -263,9 +251,7 @@ function renderOutput(
263
251
  const details = state.details;
264
252
  const output: string[] = [];
265
253
 
266
- // stdout/stderr/result are color-coded and labeled so you can tell which
267
- // stream a line came from at a glance. Sanitize the raw text before
268
- // applying the section color, or our own ANSI gets escaped as user output.
254
+ // --- stdout/stderr/result are color-coded; sanitize before section color so our ANSI isn't escaped ---
269
255
  const sections: Array<{ text: string | undefined; color: string; label: string }> = [
270
256
  { text: details?.stdout, color: "toolOutput", label: "stdout" },
271
257
  { text: details?.stderr, color: "warning", label: "stderr" },
@@ -337,8 +323,6 @@ export function renderExecuteBody(state: ExecuteRenderState, width: number, deps
337
323
  const lines: string[] = [];
338
324
  const hasCode = renderCode(state, lines, safeWidth, deps);
339
325
  renderOutput(state, lines, safeWidth, hasCode, deps);
340
- // A thin bottom border separates the expanded cell from whatever follows.
341
- if (lines.length > 0) lines.push(` ${deps.fg("dim", "─".repeat(Math.max(1, safeWidth - 1)))}`);
342
326
  const kind = statusKind(state);
343
327
  return lines.map((line) => paintBackground(line, safeWidth, kind, deps));
344
328
  }