pi-repl-py 0.1.0 → 0.2.0
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/README.md +31 -20
- package/docs/ARCHITECTURE.md +182 -0
- package/docs/how-to-functions.md +82 -79
- package/docs/philosophy.md +67 -59
- package/index.ts +8 -31
- package/package.json +12 -6
- package/scripts/setup-venv.mjs +41 -19
- package/src/engine/index.ts +92 -433
- package/src/engine/kernel.ts +598 -0
- package/src/engine/session.ts +149 -0
- package/src/engine/zmtp.ts +251 -0
- package/src/extension/helpers.ts +46 -0
- package/src/extension/preview/candidates.ts +159 -0
- package/src/extension/preview/descriptor.ts +28 -0
- package/src/extension/preview/index.ts +31 -0
- package/src/extension/preview/scan.ts +59 -0
- package/src/extension/preview/shell.ts +156 -0
- package/src/extension/preview/types.ts +23 -0
- package/src/extension/prompt.ts +76 -0
- package/src/extension/render-core.ts +11 -31
- package/src/extension/render.ts +2 -11
- package/src/extension/session-engine.ts +9 -31
- package/src/extension/tool-meta.ts +11 -54
- package/ARCHITECTURE.md +0 -141
- package/src/engine/guest.py +0 -317
- package/src/engine/protocol.ts +0 -66
- package/src/engine/toolbox/bash.py +0 -72
- package/src/engine/toolbox/edit.py +0 -37
- package/src/engine/toolbox/read.py +0 -26
- package/src/engine/toolbox/write.py +0 -23
- package/src/extension/config.ts +0 -65
- package/src/extension/preview-core.ts +0 -518
- package/src/extension/toolbox.ts +0 -74
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
// --- scan: tokenizer source → template spans, string constants, and masks ---
|
|
2
|
+
import { BACKTICK, type Span } from "./types.js";
|
|
3
|
+
|
|
4
|
+
// --- capture the template opened at start; tracks escapes + interpolation nesting so a shell command reads whole, and an unclosed template returns the rest (partial is better than none) ---
|
|
5
|
+
export function scanTemplate(source: string, start: number): Span {
|
|
6
|
+
let depth = 0;
|
|
7
|
+
let inNested = false;
|
|
8
|
+
for (let i = start + 1; i < source.length; i++) {
|
|
9
|
+
const ch = source[i];
|
|
10
|
+
if (ch === "\\") {
|
|
11
|
+
i += 1;
|
|
12
|
+
continue;
|
|
13
|
+
}
|
|
14
|
+
if (ch === BACKTICK) {
|
|
15
|
+
if (depth === 0 && !inNested) return { start, end: i + 1, body: source.slice(start + 1, i) };
|
|
16
|
+
inNested = !inNested;
|
|
17
|
+
continue;
|
|
18
|
+
}
|
|
19
|
+
if (!inNested && ch === "$" && source[i + 1] === "{") {
|
|
20
|
+
depth += 1;
|
|
21
|
+
i += 1;
|
|
22
|
+
continue;
|
|
23
|
+
}
|
|
24
|
+
if (!inNested && depth > 0 && ch === "}") depth -= 1;
|
|
25
|
+
}
|
|
26
|
+
return { start, end: source.length, body: source.slice(start + 1) };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const CONST_STRING_PATTERN = new RegExp(
|
|
30
|
+
'(?:const|let|var)\\s+([A-Za-z_$][\\w$]*)\\s*=\\s*(?:"([^"\\n]*)"|' +
|
|
31
|
+
"'([^'\\n]*)'|" +
|
|
32
|
+
BACKTICK +
|
|
33
|
+
"([^" +
|
|
34
|
+
BACKTICK +
|
|
35
|
+
"$\\n]*)" +
|
|
36
|
+
BACKTICK +
|
|
37
|
+
")",
|
|
38
|
+
"g",
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
// --- collected simple string constants, for resolving interpolations and path args ---
|
|
42
|
+
export function stringConsts(source: string): Map<string, string> {
|
|
43
|
+
const vars = new Map<string, string>();
|
|
44
|
+
for (const match of source.matchAll(CONST_STRING_PATTERN)) {
|
|
45
|
+
const name = match[1];
|
|
46
|
+
const value = match[2] ?? match[3] ?? match[4];
|
|
47
|
+
if (name && value !== undefined) vars.set(name, value);
|
|
48
|
+
}
|
|
49
|
+
return vars;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function substituteVars(text: string, vars: ReadonlyMap<string, string>): string {
|
|
53
|
+
return text.replace(/\$\{\s*([A-Za-z_$][\w$]*)\s*\}/g, (whole, name: string) => vars.get(name) ?? whole);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// --- blank a claimed span so later detectors don't re-read what an earlier one took ---
|
|
57
|
+
export function maskSpan(source: string, span: Span): string {
|
|
58
|
+
return source.slice(0, span.start) + " ".repeat(span.end - span.start) + source.slice(span.end);
|
|
59
|
+
}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
// --- shell: resolve the strongest single line of a (possibly chained) shell command ---
|
|
2
|
+
import { descriptor } from "./descriptor.js";
|
|
3
|
+
|
|
4
|
+
const CD_PREFIX_PATTERN = /^\s*cd\s+([^&;|]+?)\s*(?:&&|;)\s*/;
|
|
5
|
+
const SHELL_SETUP_PATTERN = /^(?:export\s+\w+=|set\s+[-+]|source\s+\S+|\.\s+\S+)/;
|
|
6
|
+
const HEREDOC_PATTERN = /<<-?\s*['"]?([A-Za-z_][A-Za-z0-9_]*)['"]?/;
|
|
7
|
+
|
|
8
|
+
export function shellWords(line: string): string[] {
|
|
9
|
+
const words: string[] = [];
|
|
10
|
+
for (const match of line.matchAll(/"([^"]*)"|'([^']*)'|(\S+)/g)) {
|
|
11
|
+
words.push(match[1] ?? match[2] ?? match[3] ?? "");
|
|
12
|
+
}
|
|
13
|
+
return words;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function pathTail(path: string): string {
|
|
17
|
+
const cleaned = path.replace(/\/+$/, "");
|
|
18
|
+
const tail = cleaned.slice(cleaned.lastIndexOf("/") + 1);
|
|
19
|
+
return tail || cleaned;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function simplifyRunnerCommand(line: string): string | undefined {
|
|
23
|
+
const words = shellWords(line);
|
|
24
|
+
if (words[0] === "npm" || words[0] === "pnpm") {
|
|
25
|
+
const runIndex = words.indexOf("run");
|
|
26
|
+
if (runIndex >= 0 && words[runIndex + 1]) {
|
|
27
|
+
return `${words[0]} ${words.slice(runIndex + 1).join(" ")}`.trim();
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
if (line.includes("node_modules/.bin/")) {
|
|
31
|
+
return line.replace(/\S*node_modules\/\.bin\//g, "");
|
|
32
|
+
}
|
|
33
|
+
return undefined;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function simplifyMutationCommand(line: string): string | undefined {
|
|
37
|
+
const words = shellWords(line);
|
|
38
|
+
if (words.length === 0) return undefined;
|
|
39
|
+
if (words[0] === "cat" && words[1] === ">" && words[2]) return `write ${pathTail(words[2])}`;
|
|
40
|
+
if (words[0] === "tee" && words.at(-1)) {
|
|
41
|
+
return (words.includes("-a") ? "append " : "write ") + pathTail(words.at(-1) ?? "");
|
|
42
|
+
}
|
|
43
|
+
return undefined;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// --- collapse noisier command forms (runners, writes) down to the intent ---
|
|
47
|
+
function simplifyShellLine(line: string): string {
|
|
48
|
+
return simplifyRunnerCommand(line) ?? simplifyMutationCommand(line) ?? line;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// --- commands that prepare the ground; the shell only wins when it is the story ---
|
|
52
|
+
export const SHELL_SETUP_WORDS = new Set([
|
|
53
|
+
"mkdir",
|
|
54
|
+
"cd",
|
|
55
|
+
"export",
|
|
56
|
+
"touch",
|
|
57
|
+
"chmod",
|
|
58
|
+
"chown",
|
|
59
|
+
"ln",
|
|
60
|
+
"echo",
|
|
61
|
+
"true",
|
|
62
|
+
"sleep",
|
|
63
|
+
"which",
|
|
64
|
+
"sync",
|
|
65
|
+
]);
|
|
66
|
+
|
|
67
|
+
const SHELL_ACTION_WORDS = new Set([
|
|
68
|
+
"rm",
|
|
69
|
+
"mv",
|
|
70
|
+
"cp",
|
|
71
|
+
"git",
|
|
72
|
+
"npm",
|
|
73
|
+
"pnpm",
|
|
74
|
+
"bun",
|
|
75
|
+
"bunx",
|
|
76
|
+
"npx",
|
|
77
|
+
"make",
|
|
78
|
+
"cargo",
|
|
79
|
+
"docker",
|
|
80
|
+
"curl",
|
|
81
|
+
"gh",
|
|
82
|
+
"pi",
|
|
83
|
+
]);
|
|
84
|
+
|
|
85
|
+
function shellLineScore(line: string, index: number): number {
|
|
86
|
+
const simplified = simplifyShellLine(line);
|
|
87
|
+
const words = shellWords(line);
|
|
88
|
+
let score = 30;
|
|
89
|
+
if (simplified !== line) score += 40;
|
|
90
|
+
if (SHELL_ACTION_WORDS.has(words[0] ?? "")) score += 20;
|
|
91
|
+
if (/\b(?:rm|mv|cp|git\s+(?:add|commit|push)|sed\s+-i|perl\s+-pi|tee|cat\s*>)\b/.test(line)) score += 40;
|
|
92
|
+
return score + index;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function heredocBody(lines: readonly string[], startIndex: number, delimiter: string): string | undefined {
|
|
96
|
+
const body: string[] = [];
|
|
97
|
+
for (let i = startIndex + 1; i < lines.length; i++) {
|
|
98
|
+
if ((lines[i] ?? "").trim() === delimiter) return body.join("\n");
|
|
99
|
+
body.push(lines[i] ?? "");
|
|
100
|
+
}
|
|
101
|
+
return body.length > 0 ? body.join("\n") : undefined;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function previewHeredoc(lines: readonly string[]): string | undefined {
|
|
105
|
+
for (let i = 0; i < lines.length; i++) {
|
|
106
|
+
const line = (lines[i] ?? "").trim();
|
|
107
|
+
const delimiter = line.match(HEREDOC_PATTERN)?.[1];
|
|
108
|
+
if (!delimiter) continue;
|
|
109
|
+
const body = heredocBody(lines, i, delimiter);
|
|
110
|
+
if (!body) continue;
|
|
111
|
+
// --- the write target is the story; the body is detail for the expanded view ---
|
|
112
|
+
const catWrite = line.match(/\b(?:cat|tee)\b.*(?:>|\s)(\S+)\s*<<-?/);
|
|
113
|
+
if (catWrite?.[1]) return (line.includes("tee -a") ? "append " : "write ") + pathTail(catWrite[1]);
|
|
114
|
+
return descriptor(body);
|
|
115
|
+
}
|
|
116
|
+
return undefined;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function previewShellCommand(command: string): string {
|
|
120
|
+
return previewShellCommandScored(command).text;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// --- like previewShellCommand but keeps the winning line's strength so several shell calls can rank ---
|
|
124
|
+
export function previewShellCommandScored(command: string): { text: string; strength: number } {
|
|
125
|
+
const lines = command.split("\n");
|
|
126
|
+
const heredoc = previewHeredoc(lines);
|
|
127
|
+
if (heredoc) return { text: descriptor(heredoc), strength: 90 };
|
|
128
|
+
|
|
129
|
+
let best: { text: string; score: number } | undefined;
|
|
130
|
+
let cwdSuffix: string | undefined;
|
|
131
|
+
let index = 0;
|
|
132
|
+
for (const rawLine of lines) {
|
|
133
|
+
for (const rawPart of rawLine.split(/\s*(?:&&|;)\s*/)) {
|
|
134
|
+
let part = rawPart.trim();
|
|
135
|
+
if (!part || part.startsWith("#") || SHELL_SETUP_PATTERN.test(part)) continue;
|
|
136
|
+
const cd = part.match(CD_PREFIX_PATTERN);
|
|
137
|
+
if (cd?.[1]) {
|
|
138
|
+
cwdSuffix = pathTail(cd[1].trim());
|
|
139
|
+
part = part.replace(CD_PREFIX_PATTERN, "").trim();
|
|
140
|
+
} else if (/^cd\s+\S+$/.test(part)) {
|
|
141
|
+
cwdSuffix = pathTail(part.slice(2).trim());
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
if (!part) continue;
|
|
145
|
+
const candidate = { text: simplifyShellLine(part), score: shellLineScore(part, index) };
|
|
146
|
+
if (!best || candidate.score > best.score) best = candidate;
|
|
147
|
+
index += 1;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
if (!best) return { text: "", strength: 0 };
|
|
151
|
+
// --- trailing redirections are plumbing, not intent ---
|
|
152
|
+
const cleaned = best.text.replace(/(?:\s*(?:2>&1|[12]?>\s*\/dev\/null|&>\s*\/dev\/null))+\s*$/, "");
|
|
153
|
+
// --- a stripped cd prefix still matters when it names a non-default dir ---
|
|
154
|
+
const text = cwdSuffix && !cleaned.includes(cwdSuffix) ? `${cleaned} (${cwdSuffix})` : cleaned;
|
|
155
|
+
return { text: descriptor(text), strength: best.score };
|
|
156
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// --- shared preview types; tiny module so every consumer imports only the shape it needs ---
|
|
2
|
+
type CellPreviewKind = "shell" | "ts";
|
|
3
|
+
|
|
4
|
+
export interface CellPreview {
|
|
5
|
+
kind: CellPreviewKind;
|
|
6
|
+
text: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
// --- a [start, end) slice of the source with its captured body ---
|
|
10
|
+
export interface Span {
|
|
11
|
+
start: number;
|
|
12
|
+
end: number;
|
|
13
|
+
body: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// --- the winner of each detector, ranked by score in the orchestration ---
|
|
17
|
+
export interface Candidate {
|
|
18
|
+
kind: CellPreviewKind;
|
|
19
|
+
text: string;
|
|
20
|
+
score: number;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export const BACKTICK = "\u0060";
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
// --- prompt: the execute tool's model-facing contract (pure, no pi/helper dep) ---
|
|
2
|
+
|
|
3
|
+
export const executeToolDescription =
|
|
4
|
+
"You have one tool: a persistent Python workspace backed by a real `ipython` kernel. " +
|
|
5
|
+
"Variables, imports, functions, and data survive across cells and turns. Use this tool to read files, " +
|
|
6
|
+
"run shell commands, search code, transform data, and build up solutions — all inside Python. " +
|
|
7
|
+
"Helpers in `~/.pi/agent/pi-repl/helpers/` are loaded at boot as functions; see what's loaded with " +
|
|
8
|
+
"`[k for k in globals() if not k.startswith('_')]`. A cell returns its final expression; printed output " +
|
|
9
|
+
"is captured separately.";
|
|
10
|
+
|
|
11
|
+
export const executePromptSnippet =
|
|
12
|
+
"Use the Python workspace: keep state in variables, batch independent reads/searches in one cell, " +
|
|
13
|
+
"edit files safely, and iterate in small cells.";
|
|
14
|
+
|
|
15
|
+
// --- the workspace doctrine riding the execute tool ---
|
|
16
|
+
export function buildPromptGuidelines(preloaded: string[]): string[] {
|
|
17
|
+
return [
|
|
18
|
+
"## This workspace is your only tool",
|
|
19
|
+
"In `--repl` mode, `execute` is the only callable tool. Read files, run shell, search, and edit " +
|
|
20
|
+
"all happen inside Python.",
|
|
21
|
+
"",
|
|
22
|
+
"## The loop is generate → execute → observe → iterate",
|
|
23
|
+
"Write a cell, run it, observe the result, then write the next cell. Build solutions incrementally.",
|
|
24
|
+
"",
|
|
25
|
+
"## State persists",
|
|
26
|
+
"Variables, imports, and functions survive across cells and turns. Assign read/search results to " +
|
|
27
|
+
"named variables and reuse them.",
|
|
28
|
+
"",
|
|
29
|
+
"## Chain big tasks into verifiable steps",
|
|
30
|
+
"Break ambitious requests into independently checkable steps. Confirm assumptions before writing " +
|
|
31
|
+
"code that depends on them.",
|
|
32
|
+
"",
|
|
33
|
+
"## Shell & files are plain Python",
|
|
34
|
+
"`!cmd` / `%%bash` for fire-and-forget shell; `subprocess.run(..., timeout=...)` when you need the result back " +
|
|
35
|
+
"as a value — always set a `timeout` on anything that could hang (the evaluator does not kill a " +
|
|
36
|
+
"silent cell automatically). `open()` / `pathlib` read and write files. For safe edits: read the full " +
|
|
37
|
+
"file, modify in memory, write once, then re-read to verify.",
|
|
38
|
+
"",
|
|
39
|
+
"## Batch independent work, keep exploratory cells small",
|
|
40
|
+
"Batch independent reads, searches, and setup steps in one cell to reduce round-trips. Keep " +
|
|
41
|
+
"exploratory/iterative cells small so you can observe and adjust.",
|
|
42
|
+
"",
|
|
43
|
+
"## Search efficiently",
|
|
44
|
+
"Use `rg`, `fd`, `grep`, `find` via `subprocess.run` for deep searches, not Python loops.",
|
|
45
|
+
"",
|
|
46
|
+
...(preloaded.length
|
|
47
|
+
? [
|
|
48
|
+
"## Helpers",
|
|
49
|
+
"User helpers load from `~/.pi/agent/pi-repl/helpers/`. Their descriptions appear below. " +
|
|
50
|
+
"List what's loaded with `[k for k in globals() if not k.startswith('_')]`.",
|
|
51
|
+
"",
|
|
52
|
+
...preloaded,
|
|
53
|
+
"",
|
|
54
|
+
]
|
|
55
|
+
: []),
|
|
56
|
+
"## Compose and reuse",
|
|
57
|
+
"If the same pattern appears more than once, wrap it in a `def` and reuse it.",
|
|
58
|
+
"",
|
|
59
|
+
"## Output discipline",
|
|
60
|
+
"Printing is a context cost: everything a cell prints stays in the transcript. Print slices, " +
|
|
61
|
+
"counts, and summaries. Keep large values in variables. End a cell with `;` to suppress the " +
|
|
62
|
+
"last-expression echo.",
|
|
63
|
+
"",
|
|
64
|
+
"## Environment boundary",
|
|
65
|
+
"The evaluator runs in a project-local venv, not the system Python. Do not install a target project's " +
|
|
66
|
+
"dependencies into the evaluator just to make that project run there. Run external projects " +
|
|
67
|
+
"through their own interface and normal commands.",
|
|
68
|
+
"",
|
|
69
|
+
"## Engine reset guard",
|
|
70
|
+
"If the output begins with `<repl_engine_reset>`, the kernel was rebuilt from the last snapshot. " +
|
|
71
|
+
"Some variables may be revived, some lost, and anything defined after the snapshot is gone. " +
|
|
72
|
+
"Re-verify variables before reusing them — never interpolate a restored variable into a shell " +
|
|
73
|
+
"command until you have confirmed it still holds what you expect. Functions, classes, and live " +
|
|
74
|
+
"handles cannot be snapshotted and must be redefined.",
|
|
75
|
+
];
|
|
76
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
/** Pure layout, free of pi imports so unit tests drive it directly. */
|
|
2
2
|
|
|
3
3
|
export interface ExecuteDetails {
|
|
4
4
|
status?: "ok" | "error" | "aborted" | string;
|
|
@@ -21,7 +21,7 @@ export interface ExecuteRenderState {
|
|
|
21
21
|
hasResult: boolean;
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
-
import { previewCell } from "./preview
|
|
24
|
+
import { previewCell } from "./preview/index.js";
|
|
25
25
|
|
|
26
26
|
export type StatusKind = "error" | "aborted" | "running" | "queued" | "done";
|
|
27
27
|
export type BgKind = "toolPendingBg" | "toolSuccessBg" | "toolErrorBg";
|
|
@@ -49,11 +49,7 @@ export function formatDuration(durationMs: number | undefined): string | undefin
|
|
|
49
49
|
|
|
50
50
|
const SGR_PATTERN = /\x1b\[([0-9;]*)m/g;
|
|
51
51
|
|
|
52
|
-
/**
|
|
53
|
-
* Append a reset when `line` ends with a foreground or background color still
|
|
54
|
-
* open, so a span that wrapping split across lines cannot bleed into the
|
|
55
|
-
* trailing padding or the next row.
|
|
56
|
-
*/
|
|
52
|
+
/** Close an open color so a line wrapped across words can't bleed into padding or the next row. */
|
|
57
53
|
export function closeOpenSgr(line: string): string {
|
|
58
54
|
let fgOpen = false;
|
|
59
55
|
let bgOpen = false;
|
|
@@ -65,8 +61,7 @@ export function closeOpenSgr(line: string): string {
|
|
|
65
61
|
fgOpen = false;
|
|
66
62
|
bgOpen = false;
|
|
67
63
|
} else if (code === 38 || code === 48) {
|
|
68
|
-
//
|
|
69
|
-
// is not read as another SGR code.
|
|
64
|
+
// --- skip the 38;5;n / 38;2;r;g;b payload so a component isn't read as another SGR code ---
|
|
70
65
|
if (code === 38) fgOpen = true;
|
|
71
66
|
else bgOpen = true;
|
|
72
67
|
const mode = Number(params[i + 1]);
|
|
@@ -135,14 +130,10 @@ function outputText(state: ExecuteRenderState): string {
|
|
|
135
130
|
function topLine(state: ExecuteRenderState, width: number, deps: RenderDeps): string {
|
|
136
131
|
const code = state.code.trimEnd();
|
|
137
132
|
const preview = previewCell(code);
|
|
138
|
-
const language = preview.kind === "shell" ? "repl · shell" :
|
|
133
|
+
const language = preview.kind === "shell" ? "repl · shell" : "repl";
|
|
139
134
|
const prefix = `${marker(state, deps)} ${deps.fg("muted", language)}`;
|
|
140
135
|
|
|
141
|
-
//
|
|
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.
|
|
136
|
+
// --- suffix priority: expand hint > error > duration > counts, so truncation never hides the expand key ---
|
|
146
137
|
const suffixParts: string[] = [];
|
|
147
138
|
suffixParts.push(deps.keyHint(state.expanded));
|
|
148
139
|
|
|
@@ -170,12 +161,10 @@ function topLine(state: ExecuteRenderState, width: number, deps: RenderDeps): st
|
|
|
170
161
|
const separator = deps.fg("dim", " · ");
|
|
171
162
|
const separatorWidth = deps.visibleWidth(separator);
|
|
172
163
|
const suffix = suffixParts.join(separator);
|
|
173
|
-
//
|
|
164
|
+
// --- budget: width minus leading space, prefix, suffix, and separators ---
|
|
174
165
|
const fixed = 1 + deps.visibleWidth(prefix) + separatorWidth + deps.visibleWidth(suffix);
|
|
175
166
|
const previewBudget = Math.max(8, width - fixed - separatorWidth);
|
|
176
|
-
//
|
|
177
|
-
// code the same way the expanded block is highlighted; shell/agent previews
|
|
178
|
-
// stay accent-colored so they read as intent, not syntax.
|
|
167
|
+
// --- a semantic preview is a one-line summary; highlight Python code, accent shell intent ---
|
|
179
168
|
let middle = "";
|
|
180
169
|
if (preview.text) {
|
|
181
170
|
const previewText =
|
|
@@ -191,12 +180,7 @@ function topLine(state: ExecuteRenderState, width: number, deps: RenderDeps): st
|
|
|
191
180
|
}
|
|
192
181
|
|
|
193
182
|
function sanitizeTuiOutput(text: string): string {
|
|
194
|
-
//
|
|
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 ␍.
|
|
183
|
+
// --- strip ANSI SGR/CSI and escape control chars for a readable TUI; tabs expand, CR becomes ␍ ---
|
|
200
184
|
return text
|
|
201
185
|
.replace(/\x1b\[[0-?]*[ -/]*[@-~]/g, "")
|
|
202
186
|
.replace(/\x1b/g, "␛")
|
|
@@ -245,7 +229,7 @@ function renderCode(state: ExecuteRenderState, lines: string[], width: number, d
|
|
|
245
229
|
const highlighted = highlightLines(code, deps);
|
|
246
230
|
for (const [index, rawLine] of code.split("\n").entries()) {
|
|
247
231
|
const prefix = index === 0 ? deps.fg("dim", "› ") : deps.fg("dim", " ");
|
|
248
|
-
//
|
|
232
|
+
// --- code is already highlighted; don't strip its ANSI ---
|
|
249
233
|
addWrapped(lines, prefix, highlighted[index] ?? rawLine, width, deps, { sanitize: false });
|
|
250
234
|
}
|
|
251
235
|
return true;
|
|
@@ -263,9 +247,7 @@ function renderOutput(
|
|
|
263
247
|
const details = state.details;
|
|
264
248
|
const output: string[] = [];
|
|
265
249
|
|
|
266
|
-
// stdout/stderr/result are color-coded
|
|
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.
|
|
250
|
+
// --- stdout/stderr/result are color-coded; sanitize before section color so our ANSI isn't escaped ---
|
|
269
251
|
const sections: Array<{ text: string | undefined; color: string; label: string }> = [
|
|
270
252
|
{ text: details?.stdout, color: "toolOutput", label: "stdout" },
|
|
271
253
|
{ text: details?.stderr, color: "warning", label: "stderr" },
|
|
@@ -337,8 +319,6 @@ export function renderExecuteBody(state: ExecuteRenderState, width: number, deps
|
|
|
337
319
|
const lines: string[] = [];
|
|
338
320
|
const hasCode = renderCode(state, lines, safeWidth, deps);
|
|
339
321
|
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
322
|
const kind = statusKind(state);
|
|
343
323
|
return lines.map((line) => paintBackground(line, safeWidth, kind, deps));
|
|
344
324
|
}
|
package/src/extension/render.ts
CHANGED
|
@@ -1,9 +1,4 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* TUI adapter for the `execute` cell renderer.
|
|
3
|
-
*
|
|
4
|
-
* Binds pi's theme, syntax highlighting, key hints, and width primitives to the
|
|
5
|
-
* pure layout in render-core.ts, which is unit-tested outside pi's runtime.
|
|
6
|
-
*/
|
|
1
|
+
/** TUI adapter binding pi's theme/width to the unit-tested pure layout in render-core.ts. */
|
|
7
2
|
|
|
8
3
|
import { highlightCode, keyHint, keyText, rawKeyHint, type Theme } from "@mariozechner/pi-coding-agent";
|
|
9
4
|
import { truncateToWidth, visibleWidth, wrapTextWithAnsi } from "@mariozechner/pi-tui";
|
|
@@ -35,11 +30,7 @@ function makeDeps(theme: Theme): RenderDeps {
|
|
|
35
30
|
};
|
|
36
31
|
}
|
|
37
32
|
|
|
38
|
-
/**
|
|
39
|
-
* The layout of a cell only changes when its state or the spinner frame does,
|
|
40
|
-
* but the TUI repaints on every frame. Rendering from a key of both stops the
|
|
41
|
-
* recompute-per-frame (and with it, flicker on wide panes).
|
|
42
|
-
*/
|
|
33
|
+
/** The layout only changes on state/spinner change, but the TUI repaints every frame; key by both to avoid recompute flicker. */
|
|
43
34
|
function renderVersion(state: ExecuteRenderState): string {
|
|
44
35
|
const details = state.details ? JSON.stringify(state.details) : "";
|
|
45
36
|
return [
|
|
@@ -1,12 +1,8 @@
|
|
|
1
|
-
//
|
|
1
|
+
// revival is part of create() so a session that gets teardown without a session_start reload still revives
|
|
2
2
|
|
|
3
3
|
import type { RestoreResult } from "../engine/index.js";
|
|
4
4
|
|
|
5
|
-
/**
|
|
6
|
-
* A revived session can carry hundreds of variables; listing them all turns
|
|
7
|
-
* the banner and the reset notice into a wall. Show enough to orient, then
|
|
8
|
-
* count the rest.
|
|
9
|
-
*/
|
|
5
|
+
/** Show enough names to orient, then count the rest (a revive can carry hundreds). */
|
|
10
6
|
export function summarizeNames(names: readonly string[], limit: number): string {
|
|
11
7
|
if (names.length <= limit) return names.join(", ");
|
|
12
8
|
return `${names.slice(0, limit).join(", ")} … and ${names.length - limit} more`;
|
|
@@ -22,23 +18,15 @@ export interface EngineLifecycleDeps<E extends RevivableEngine> {
|
|
|
22
18
|
create(): E;
|
|
23
19
|
/** Tears the current engine down, flushing its final snapshot. */
|
|
24
20
|
dispose(engine: E): Promise<void>;
|
|
25
|
-
/**
|
|
26
|
-
* Tears down an engine that cannot cooperate — a wedged guest cannot serve
|
|
27
|
-
* the snapshot flush dispose would ask of it. Falls back to dispose.
|
|
28
|
-
*/
|
|
21
|
+
/** Kill-then-rebuild when a wedged engine cannot serve the snapshot flush. */
|
|
29
22
|
discard?(engine: E): Promise<void>;
|
|
30
23
|
}
|
|
31
24
|
|
|
32
|
-
/**
|
|
33
|
-
* Why an engine came into existence. `startup` is the expected path and is
|
|
34
|
-
* already announced in the transcript; `cell` means an engine had to be built
|
|
35
|
-
* to serve a tool call, which only happens when the previous one went away
|
|
36
|
-
* mid-session — the case the model needs told about in-band.
|
|
37
|
-
*/
|
|
25
|
+
/** `startup` is announced in the transcript; `cell` means an engine was rebuilt mid-session and needs an in-band notice. */
|
|
38
26
|
export type AcquireOrigin = "startup" | "cell";
|
|
39
27
|
|
|
40
28
|
function formatEngineResetNotice(restore: RestoreResult | null): string {
|
|
41
|
-
const lines = ["<
|
|
29
|
+
const lines = ["<repl_engine_reset>"];
|
|
42
30
|
if (!restore) {
|
|
43
31
|
// --- no snapshot at all: namespace is genuinely empty ---
|
|
44
32
|
lines.push(
|
|
@@ -73,7 +61,7 @@ function formatEngineResetNotice(restore: RestoreResult | null): string {
|
|
|
73
61
|
}
|
|
74
62
|
lines.push("Anything defined after the last snapshot is also gone.");
|
|
75
63
|
}
|
|
76
|
-
lines.push("Re-verify a variable before reusing it, especially inside shell interpolation.", "</
|
|
64
|
+
lines.push("Re-verify a variable before reusing it, especially inside shell interpolation.", "</repl_engine_reset>");
|
|
77
65
|
return lines.join("\n");
|
|
78
66
|
}
|
|
79
67
|
|
|
@@ -81,25 +69,19 @@ export class EngineLifecycle<E extends RevivableEngine> {
|
|
|
81
69
|
private engine?: E;
|
|
82
70
|
private revival?: Promise<RestoreResult | null>;
|
|
83
71
|
private pendingNotice?: string;
|
|
84
|
-
/** Teardown in progress; a rebuild must not overlap the final snapshot flush. */
|
|
85
72
|
private teardown?: Promise<void>;
|
|
86
|
-
/** First-build in progress
|
|
73
|
+
/** First-build in progress. */
|
|
87
74
|
private acquiring?: Promise<{ engine: E; restore: RestoreResult | null; created: boolean }>;
|
|
88
75
|
|
|
89
76
|
constructor(private readonly deps: EngineLifecycleDeps<E>) {}
|
|
90
77
|
|
|
91
|
-
/**
|
|
92
|
-
* The live engine, built and revived if it does not exist yet.
|
|
93
|
-
* Revival is awaited here so a caller never sees an un-revived namespace.
|
|
94
|
-
*/
|
|
78
|
+
/** Built and revived on demand; awaited so callers never see an un-revived namespace. */
|
|
95
79
|
async acquire(origin: AcquireOrigin): Promise<{ engine: E; restore: RestoreResult | null; created: boolean }> {
|
|
96
80
|
if (this.engine) {
|
|
97
81
|
return { engine: this.engine, restore: await this.revival!, created: false };
|
|
98
82
|
}
|
|
99
|
-
// --- two concurrent acquires on an empty engine must share one build ---
|
|
100
83
|
if (this.acquiring) return this.acquiring;
|
|
101
84
|
const build = (async () => {
|
|
102
|
-
// --- a teardown flushing its final snapshot must finish before the rebuild reads it ---
|
|
103
85
|
while (this.teardown) await this.teardown;
|
|
104
86
|
if (this.engine) {
|
|
105
87
|
const held: E = this.engine;
|
|
@@ -131,11 +113,7 @@ export class EngineLifecycle<E extends RevivableEngine> {
|
|
|
131
113
|
await this.teardownWith((engine) => this.deps.dispose(engine));
|
|
132
114
|
}
|
|
133
115
|
|
|
134
|
-
/**
|
|
135
|
-
* Teardown for an engine that cannot cooperate (e.g. wedged in synchronous
|
|
136
|
-
* code). Skips the snapshot flush a graceful dispose would attempt; the next
|
|
137
|
-
* acquire builds a fresh engine revived from the last completed snapshot.
|
|
138
|
-
*/
|
|
116
|
+
/** Kill-then-rebuild for a wedged engine; skips the final snapshot flush, uses the last good one. */
|
|
139
117
|
async discard(): Promise<void> {
|
|
140
118
|
await this.teardownWith((engine) => (this.deps.discard ?? this.deps.dispose)(engine));
|
|
141
119
|
}
|
|
@@ -1,58 +1,15 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
*
|
|
4
|
-
* pi's default system prompt is used as-is; all REPL knowledge rides on the
|
|
5
|
-
* tool via these fields, so index.ts stays thin.
|
|
6
|
-
*
|
|
7
|
-
* - description — working summary (schema card).
|
|
8
|
-
* - promptSnippet — one line in the default `Available tools`.
|
|
9
|
-
* - promptGuidelines — the function doctrine + tokens + safety.
|
|
10
|
-
*
|
|
11
|
-
* The function map is derived from the toolbox source via buildToolboxMap()
|
|
12
|
-
* (function_description docstring + def-signature regex), so it always matches
|
|
13
|
-
* what the kernel loads.
|
|
14
|
-
*/
|
|
1
|
+
// --- tool-meta: thin surface assembling the execute tool's prompt from pure modules ---
|
|
2
|
+
// --- the model contract lives in prompt.ts; only the helpers wiring stays here ---
|
|
15
3
|
|
|
16
|
-
import {
|
|
4
|
+
import { buildHelpersMap } from "./helpers.js";
|
|
5
|
+
import { buildPromptGuidelines, executePromptSnippet, executeToolDescription } from "./prompt.js";
|
|
17
6
|
|
|
18
|
-
export const EXECUTE_DESCRIPTION =
|
|
19
|
-
|
|
20
|
-
"Variables, imports, functions, and data survive across calls. There are no " +
|
|
21
|
-
"separate file or shell tools; read, write, edit, bash, and anything you build " +
|
|
22
|
-
"are Python functions you call inside a cell. A cell returns its final " +
|
|
23
|
-
"expression; anything else is printed. Build one reusable function per routine " +
|
|
24
|
-
"and call it by arguments, since a new def overwrites the previous one; don't " +
|
|
25
|
-
"narrate that machinery to the user. Runs in a project-local venv, so a command " +
|
|
26
|
-
"that starts python or pip must target that venv.";
|
|
7
|
+
export const EXECUTE_DESCRIPTION = executeToolDescription;
|
|
8
|
+
export const EXECUTE_PROMPT_SNIPPET = executePromptSnippet;
|
|
27
9
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* promptGuidelines for the execute tool. `toolboxDir` is optional; it defaults to
|
|
35
|
-
* the shipped toolbox.
|
|
36
|
-
*/
|
|
37
|
-
export function buildExecutePromptGuidelines(toolboxDir?: string): string[] {
|
|
38
|
-
const map = buildToolboxMap(toolboxDir);
|
|
39
|
-
const preloaded = map.length > 0 ? map : ["(none preloaded: define your own)"];
|
|
40
|
-
return [
|
|
41
|
-
"Preloaded functions available in every kernel:",
|
|
42
|
-
...preloaded,
|
|
43
|
-
"ls() prints what is loaded; help(name) shows a function's signature and notes. Use them instead of guessing.",
|
|
44
|
-
"Functions you define are reusable tools: one parameterized helper per task, call it by arguments. Never write a routine twice and never fork a duplicate; extend the existing `def` (a new `def` of the same name overwrites).",
|
|
45
|
-
"Before a multi-line cell, ask whether you will run that shape again with different inputs. If yes, define the function now so each later request is one call.",
|
|
46
|
-
"Good, defined once then called by arguments only:",
|
|
47
|
-
"def fetch_news(query, hl='en', gl='US', ceid='US:en', limit=15):\n <fetch + parse to a list>\nfetch_news('Turkey')\nfetch_news('Nigeria', hl='en-NG')",
|
|
48
|
-
"Don't build a near-copy (avoid fetch_news and fetch_news_region); add the varying bits to the original `def` and let it supersede the old.",
|
|
49
|
-
"Other reusable shapes build the same way:",
|
|
50
|
-
"def find_files(pred, root='.'):\n <walk root, filter by pred>\nfind_files('*.csv')\nfind_files('*.py', root='src')\ndef count_lines(paths): ... # compose: count_lines(find_files('*.csv'))",
|
|
51
|
-
"Use functions proportionally: build one when it will be reused, otherwise run it in a plain cell. Don't wrap a one-off and don't over-engineer.",
|
|
52
|
-
"Never narrate your mechanism to the user (don't say 'I defined a function' or 'I built a tool'). Do the job, then answer with the result.",
|
|
53
|
-
"Be token efficient: everything a cell prints is context for the rest of the turn. When reading or searching, print slices, matches, or counts rather than whole files, and keep large values in variables.",
|
|
54
|
-
"For whole-filesystem or large-dir scans, use the kernel's tools via bash, not a Python walk: find, du, fd, grep. Chain them (find -xdev -type f -size +100M | sort -rn | head; du -x | sort -h | tail) and prune descent by skipping node_modules, .git, caches, venvs. A Python os.walk + lstat loop pays a slow syscall per file and runs minutes to 10+ min on a big tree; reserve Python for analysing the results, not for enumerating the disk.",
|
|
55
|
-
"If the output starts with <rlm_engine_reset>, the kernel was rebuilt: only data is restored, your functions are gone. Recreate any helper you need and re-verify a variable before trusting it.",
|
|
56
|
-
"Don't install packages into the evaluator; the standard library is available. Run out-of-tree projects through their own environment.",
|
|
57
|
-
];
|
|
10
|
+
// --- build the guidelines from the one helpers dir (default ~/.pi/agent/pi-repl/helpers) ---
|
|
11
|
+
export function buildExecutePromptGuidelines(): string[] {
|
|
12
|
+
const map = buildHelpersMap();
|
|
13
|
+
const preloaded = map.length > 0 ? map : [];
|
|
14
|
+
return buildPromptGuidelines(preloaded);
|
|
58
15
|
}
|