pi-supernova 0.5.0 → 0.7.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 +97 -11
- package/docs/CHANGELOG.md +150 -0
- package/docs/TOKEN_COSTS.md +71 -29
- package/index.js +126 -82
- package/package.json +2 -2
- package/src/adapters/bash.js +73 -0
- package/src/adapters/edit.js +249 -0
- package/src/adapters/errors.js +31 -0
- package/src/adapters/index.js +31 -0
- package/src/adapters/list.js +102 -0
- package/src/adapters/read.js +805 -0
- package/src/adapters/refs.js +41 -0
- package/src/adapters/write.js +96 -0
- package/src/bridge/catalog.js +30 -220
- package/src/bridge/host-bridge.js +142 -1032
- package/src/bridge/invoke.js +35 -0
- package/src/bridge/native-tools.js +1 -188
- package/src/context/evidence.js +142 -70
- package/src/context/fuzzy.js +61 -22
- package/src/context/ledger.js +43 -24
- package/src/context/outline.js +26 -12
- package/src/context/repo-index.js +242 -71
- package/src/context/search.js +189 -56
- package/src/context/snap.js +306 -150
- package/src/context/spans.js +2 -1
- package/src/context/surface.js +29 -14
- package/src/contract/bash.js +31 -0
- package/src/contract/edit.js +95 -0
- package/src/contract/read.js +220 -0
- package/src/fs/check.js +19 -7
- package/src/fs/diff.js +18 -7
- package/src/fs/json-read.js +66 -35
- package/src/fs/patch.js +97 -51
- package/src/fs/source-window.js +82 -0
- package/src/fs/text-ops.js +512 -0
- package/src/fs/vfs.js +289 -162
- package/src/fs/workspace.js +122 -105
- package/src/output/bottleneck.js +211 -107
- package/src/output/format.js +112 -63
- package/src/runtime/guest-deny-imports.js +34 -0
- package/src/runtime/guest-worker.js +306 -213
- package/src/runtime/parallel.js +99 -63
- package/src/runtime/program-batch.js +189 -69
- package/src/runtime/program-file.js +6 -3
- package/src/runtime/reference.js +13 -12
- package/src/runtime/runtime.js +327 -176
- package/src/shared/decode.js +61 -27
- package/src/ui/omp-frame.js +70 -46
- package/src/ui/render-measure.js +51 -29
- package/src/ui/render.js +242 -146
package/src/shared/decode.js
CHANGED
|
@@ -60,48 +60,82 @@ function plainFromCollection(value, seen, depth) {
|
|
|
60
60
|
return out;
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
63
|
+
function withSeen(value, seen, fn) {
|
|
64
|
+
seen.add(value);
|
|
65
|
+
|
|
66
|
+
try { return fn(); }
|
|
67
|
+
finally { seen.delete(value); }
|
|
68
|
+
}
|
|
67
69
|
|
|
68
|
-
|
|
70
|
+
function functionLabel(value) {
|
|
71
|
+
return "[Function" + (value.name ? " " + value.name : "") + "]";
|
|
72
|
+
}
|
|
69
73
|
|
|
70
|
-
|
|
74
|
+
function plainByTag(value, tag) {
|
|
75
|
+
if (tag === "[object String]") return { hit: true, out: value.valueOf() };
|
|
71
76
|
|
|
72
|
-
if (
|
|
77
|
+
if (tag === "[object Number]" || tag === "[object Boolean]") return { hit: true, out: value.valueOf() };
|
|
73
78
|
|
|
74
|
-
if (tag === "[object
|
|
79
|
+
if (tag === "[object BigInt]") return { hit: true, out: value.toString() + "n" };
|
|
75
80
|
|
|
76
|
-
if (
|
|
81
|
+
if (tag === "[object Symbol]") return { hit: true, out: value.toString() };
|
|
77
82
|
|
|
78
|
-
|
|
83
|
+
return { hit: false };
|
|
84
|
+
}
|
|
79
85
|
|
|
80
|
-
|
|
86
|
+
function plainAtom(value) {
|
|
87
|
+
if (value === null || value === undefined) return { hit: true, out: value };
|
|
88
|
+
const tagged = plainByTag(value, toStr.call(value));
|
|
81
89
|
|
|
82
|
-
if (
|
|
90
|
+
if (tagged.hit) return tagged;
|
|
83
91
|
|
|
84
|
-
if (value
|
|
85
|
-
const out = { name: value.name, message: value.message };
|
|
92
|
+
if (isFunction(value)) return { hit: true, out: functionLabel(value) };
|
|
86
93
|
|
|
87
|
-
|
|
94
|
+
return { hit: false };
|
|
95
|
+
}
|
|
88
96
|
|
|
89
|
-
|
|
90
|
-
|
|
97
|
+
function plainDate(value) {
|
|
98
|
+
return Number.isNaN(value.getTime()) ? "Invalid Date" : value.toISOString();
|
|
99
|
+
}
|
|
91
100
|
|
|
92
|
-
|
|
101
|
+
function plainHosted(value) {
|
|
102
|
+
if (value instanceof Date) return { hit: true, out: plainDate(value) };
|
|
93
103
|
|
|
94
|
-
if (
|
|
104
|
+
if (value instanceof RegExp) return { hit: true, out: value.toString() };
|
|
95
105
|
|
|
96
|
-
if (
|
|
97
|
-
seen.add(value);
|
|
106
|
+
if (value instanceof Promise) return { hit: true, out: "[Promise]" };
|
|
98
107
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
seen.delete(value);
|
|
103
|
-
}
|
|
108
|
+
if (ArrayBuffer.isView(value) || value instanceof ArrayBuffer) return { hit: true, out: plainFromBinary(value) };
|
|
109
|
+
|
|
110
|
+
return { hit: false };
|
|
104
111
|
}
|
|
105
112
|
|
|
106
|
-
|
|
113
|
+
function plainError(value, seen, depth) {
|
|
114
|
+
const out = { name: value.name, message: value.message };
|
|
115
|
+
|
|
116
|
+
if (value.cause !== undefined) out.cause = withSeen(value, seen, () => toPlain(value.cause, seen, depth + 1));
|
|
117
|
+
|
|
118
|
+
return out;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** Convert any guest value to structured-clone-safe, JSON-shaped data. */
|
|
122
|
+
export function toPlain(value, seen = new Set(), depth = 0) {
|
|
123
|
+
const atom = plainAtom(value);
|
|
124
|
+
|
|
125
|
+
if (atom.hit) return atom.out;
|
|
126
|
+
|
|
127
|
+
if (depth > MAX_DEPTH) return "[Depth]";
|
|
107
128
|
|
|
129
|
+
if (seen.has(value)) return "[Circular]";
|
|
130
|
+
|
|
131
|
+
if (value instanceof Error) return plainError(value, seen, depth);
|
|
132
|
+
const hosted = plainHosted(value);
|
|
133
|
+
|
|
134
|
+
if (hosted.hit) return hosted.out;
|
|
135
|
+
|
|
136
|
+
if (isFunction(value.toJSON)) return withSeen(value, seen, () => toPlain(value.toJSON(), seen, depth + 1));
|
|
137
|
+
|
|
138
|
+
return withSeen(value, seen, () => plainFromCollection(value, seen, depth));
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// ---- RPC to the host thread ----
|
package/src/ui/omp-frame.js
CHANGED
|
@@ -38,7 +38,10 @@ function borderPaint(theme, state, borderColor) {
|
|
|
38
38
|
|
|
39
39
|
if (theme && isFunction(theme.fg)) {
|
|
40
40
|
try {
|
|
41
|
-
return (text) =>
|
|
41
|
+
return (text) => {
|
|
42
|
+
try { return theme.fg(key, text); }
|
|
43
|
+
catch { return text; }
|
|
44
|
+
};
|
|
42
45
|
} catch {
|
|
43
46
|
/* fall through */
|
|
44
47
|
}
|
|
@@ -51,10 +54,14 @@ const STATUS_PREFIX = { error: ["error", "✗ "], running: ["dim", "… "] };
|
|
|
51
54
|
|
|
52
55
|
function statusHeader(theme, { title, description, state, icon }) {
|
|
53
56
|
const resolved = icon ?? (state === "error" ? "error" : undefined);
|
|
57
|
+
const fg = (key, text) => {
|
|
58
|
+
try { return isFunction(theme?.fg) ? theme.fg(key, text) : text; }
|
|
59
|
+
catch { return text; }
|
|
60
|
+
};
|
|
54
61
|
const prefixSpec = STATUS_PREFIX[resolved];
|
|
55
|
-
const prefix = prefixSpec ?
|
|
56
|
-
const titleText =
|
|
57
|
-
const descText = description ?
|
|
62
|
+
const prefix = prefixSpec ? fg(prefixSpec[0], prefixSpec[1]) : "";
|
|
63
|
+
const titleText = fg("accent", title);
|
|
64
|
+
const descText = description ? fg("muted", description) : "";
|
|
58
65
|
|
|
59
66
|
return descText ? `${prefix}${titleText}: ${descText}` : `${prefix}${titleText}`;
|
|
60
67
|
}
|
|
@@ -82,20 +89,17 @@ function wrapBg(paint) {
|
|
|
82
89
|
};
|
|
83
90
|
}
|
|
84
91
|
|
|
85
|
-
function
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
try {
|
|
91
|
-
if (!isString(theme.bg(key, "x"))) return undefined;
|
|
92
|
-
} catch {
|
|
93
|
-
return undefined;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
return wrapBg((text) => theme.bg(key, text));
|
|
92
|
+
function probeThemeBg(theme, key) {
|
|
93
|
+
try {
|
|
94
|
+
if (!isString(theme.bg(key, "x"))) return undefined;
|
|
95
|
+
} catch {
|
|
96
|
+
return undefined;
|
|
97
97
|
}
|
|
98
98
|
|
|
99
|
+
return wrapBg((text) => theme.bg(key, text));
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function themeAnsiBgFn(theme, key) {
|
|
99
103
|
if (!isFunction(theme.getBgAnsi)) return undefined;
|
|
100
104
|
|
|
101
105
|
try {
|
|
@@ -109,47 +113,56 @@ function bgFnForState(theme, state) {
|
|
|
109
113
|
}
|
|
110
114
|
}
|
|
111
115
|
|
|
112
|
-
function
|
|
116
|
+
function bgFnForState(theme, state) {
|
|
117
|
+
if (!state || !theme) return undefined;
|
|
118
|
+
const key = bgKeyFor(state);
|
|
119
|
+
|
|
120
|
+
if (isFunction(theme.bg)) return probeThemeBg(theme, key);
|
|
121
|
+
|
|
122
|
+
return themeAnsiBgFn(theme, key);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function frameSectionLines(section, contentWidth, box, border, bgFn, w, paintBar) {
|
|
113
126
|
const lines = [];
|
|
114
|
-
const normalized = sections.length > 0 ? sections : [{ lines: [] }];
|
|
115
|
-
const v = box.vertical;
|
|
116
127
|
|
|
117
|
-
|
|
118
|
-
if (section.label) lines.push(paintBar(box.teeRight || "├", box.teeLeft || "┤", section.label));
|
|
128
|
+
if (section.label) lines.push(paintBar(box.teeRight || "├", box.teeLeft || "┤", section.label));
|
|
119
129
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
}
|
|
130
|
+
for (const raw of section.lines || []) {
|
|
131
|
+
for (const piece of String(raw).split("\n")) {
|
|
132
|
+
const body = clampLine(piece, contentWidth);
|
|
133
|
+
const pad = Math.max(0, contentWidth - measureWidth(body));
|
|
134
|
+
lines.push(padLine(`${border(box.vertical)} ${body}${" ".repeat(pad)} ${border(box.vertical)}`, w, bgFn));
|
|
126
135
|
}
|
|
127
136
|
}
|
|
128
137
|
|
|
129
138
|
return lines;
|
|
130
139
|
}
|
|
131
140
|
|
|
132
|
-
function
|
|
133
|
-
const
|
|
141
|
+
function frameBodyLines(sections, contentWidth, box, border, bgFn, w, paintBar) {
|
|
142
|
+
const normalized = sections.length > 0 ? sections : [{ lines: [] }];
|
|
143
|
+
const lines = [];
|
|
134
144
|
|
|
135
|
-
|
|
136
|
-
const rawLines = [header];
|
|
145
|
+
for (const section of normalized) lines.push(...frameSectionLines(section, contentWidth, box, border, bgFn, w, paintBar));
|
|
137
146
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
147
|
+
return lines;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function collapseNarrowFrame(header, sections, w) {
|
|
151
|
+
const rawLines = [header];
|
|
142
152
|
|
|
143
|
-
|
|
153
|
+
for (const section of sections) {
|
|
154
|
+
if (section.label) rawLines.push(section.label);
|
|
155
|
+
rawLines.push(...(section.lines || []));
|
|
144
156
|
}
|
|
145
157
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
158
|
+
return rawLines.flatMap((line) => line ? [clampLine(line, w)] : []);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function paintBarFor(box, border, bgFn, w) {
|
|
149
162
|
const h = box.horizontal;
|
|
150
163
|
const cap = h.repeat(3);
|
|
151
164
|
|
|
152
|
-
|
|
165
|
+
return (leftChar, rightChar, label) => {
|
|
153
166
|
const left = `${leftChar}${cap}`;
|
|
154
167
|
const right = rightChar;
|
|
155
168
|
|
|
@@ -166,14 +179,25 @@ function renderPortableFrame(theme, { header, sections = [], state = "pending",
|
|
|
166
179
|
|
|
167
180
|
return padLine(`${border(left)}${trimmed}${border(h.repeat(fill))}${border(right)}`, w, bgFn);
|
|
168
181
|
};
|
|
182
|
+
}
|
|
169
183
|
|
|
170
|
-
|
|
171
|
-
const
|
|
172
|
-
lines.push(paintBar(box.topLeft, box.topRight, header));
|
|
173
|
-
lines.push(...frameBodyLines(sections, contentWidth, box, border, bgFn, w, paintBar));
|
|
174
|
-
lines.push(paintBar(box.bottomLeft, box.bottomRight, null));
|
|
184
|
+
function renderPortableFrame(theme, { header, sections = [], state = "pending", borderColor, width, paintBg = true }) {
|
|
185
|
+
const w = Math.max(1, width | 0);
|
|
175
186
|
|
|
176
|
-
return
|
|
187
|
+
if (w < 8) return collapseNarrowFrame(header, sections, w);
|
|
188
|
+
const box = boxOf(theme);
|
|
189
|
+
const border = borderPaint(theme, state, borderColor);
|
|
190
|
+
// Hosts that tint the whole tool block themselves (OMP's contentBox) must
|
|
191
|
+
// get unpainted rows: a second bg wrap here would clear the outer bg with
|
|
192
|
+
// \x1b[49m and strand the host's right pad as an unpainted bar.
|
|
193
|
+
const bgFn = paintBg ? bgFnForState(theme, state) : undefined;
|
|
194
|
+
const paintBar = paintBarFor(box, border, bgFn, w);
|
|
195
|
+
|
|
196
|
+
return [
|
|
197
|
+
paintBar(box.topLeft, box.topRight, header),
|
|
198
|
+
...frameBodyLines(sections, Math.max(1, w - 2 - 2), box, border, bgFn, w, paintBar),
|
|
199
|
+
paintBar(box.bottomLeft, box.bottomRight, null),
|
|
200
|
+
];
|
|
177
201
|
}
|
|
178
202
|
|
|
179
203
|
function createPortableFramedComponent(theme, build) {
|
package/src/ui/render-measure.js
CHANGED
|
@@ -11,6 +11,27 @@ let cachedWidthChars = 0;
|
|
|
11
11
|
|
|
12
12
|
const MAX_WIDTH_CACHE_CHARS = 512_000;
|
|
13
13
|
|
|
14
|
+
function asciiWidth(plain, normalized) {
|
|
15
|
+
return /^[\x20-\x7e\u2500-\u257f\u00b7\u00d7\u2026\u2713\u2717]*$/.test(plain)
|
|
16
|
+
? plain.length
|
|
17
|
+
: stringWidth(normalized);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function rememberWidth(raw, width) {
|
|
21
|
+
if (raw.length > 4096) return width;
|
|
22
|
+
|
|
23
|
+
while (widthCache.size >= 4096 || cachedWidthChars + raw.length > MAX_WIDTH_CACHE_CHARS) {
|
|
24
|
+
const oldest = widthCache.keys().next().value;
|
|
25
|
+
widthCache.delete(oldest);
|
|
26
|
+
cachedWidthChars -= oldest.length;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
widthCache.set(raw, width);
|
|
30
|
+
cachedWidthChars += raw.length;
|
|
31
|
+
|
|
32
|
+
return width;
|
|
33
|
+
}
|
|
34
|
+
|
|
14
35
|
export function measureWidth(text) {
|
|
15
36
|
const raw = String(text ?? "");
|
|
16
37
|
const cached = widthCache.get(raw);
|
|
@@ -22,24 +43,9 @@ export function measureWidth(text) {
|
|
|
22
43
|
|
|
23
44
|
// ASCII and these single-column chrome glyphs need no Unicode segmentation.
|
|
24
45
|
// Any other character/control/escape sequence uses the full oracle.
|
|
25
|
-
const width = /^[\x20-\x7e\u2500-\u257f\u00b7\u00d7\u2026\u2713\u2717]*$/.test(plain)
|
|
26
|
-
? plain.length
|
|
27
|
-
: stringWidth(normalized);
|
|
28
|
-
|
|
29
46
|
// Cache immutable text only, never host/theme/result objects. Bound both
|
|
30
47
|
// bookkeeping and retained text; unusually long lines bypass retention.
|
|
31
|
-
|
|
32
|
-
while (widthCache.size >= 4096 || cachedWidthChars + raw.length > MAX_WIDTH_CACHE_CHARS) {
|
|
33
|
-
const oldest = widthCache.keys().next().value;
|
|
34
|
-
widthCache.delete(oldest);
|
|
35
|
-
cachedWidthChars -= oldest.length;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
widthCache.set(raw, width);
|
|
39
|
-
cachedWidthChars += raw.length;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
return width;
|
|
48
|
+
return rememberWidth(raw, asciiWidth(plain, normalized));
|
|
43
49
|
}
|
|
44
50
|
|
|
45
51
|
function takePrefix(text, width) {
|
|
@@ -76,6 +82,24 @@ export function clampLine(line, width) {
|
|
|
76
82
|
return hardTruncate(line, width);
|
|
77
83
|
}
|
|
78
84
|
|
|
85
|
+
function pushWrapSegment(out, current, columns, segment, size, width) {
|
|
86
|
+
if (columns + size > width && current.text) {
|
|
87
|
+
out.push(current.text);
|
|
88
|
+
current.text = "";
|
|
89
|
+
columns = 0;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (size > width) {
|
|
93
|
+
out.push(ELLIPSIS);
|
|
94
|
+
|
|
95
|
+
return columns;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
current.text += segment;
|
|
99
|
+
|
|
100
|
+
return columns + size;
|
|
101
|
+
}
|
|
102
|
+
|
|
79
103
|
/** Wrap complete, already-sanitized result text without splitting graphemes. */
|
|
80
104
|
export function wrapLine(line, width) {
|
|
81
105
|
if (width <= 0) return [];
|
|
@@ -83,33 +107,31 @@ export function wrapLine(line, width) {
|
|
|
83
107
|
|
|
84
108
|
if (measureWidth(text) <= width) return [text];
|
|
85
109
|
const out = [];
|
|
86
|
-
|
|
110
|
+
const current = { text: "" };
|
|
87
111
|
let columns = 0;
|
|
88
112
|
|
|
89
113
|
for (const { segment } of segmenter.segment(text)) {
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
if (columns + size > width && current) { out.push(current); current = ""; columns = 0; }
|
|
93
|
-
|
|
94
|
-
if (size > width) { out.push(ELLIPSIS); continue; }
|
|
95
|
-
|
|
96
|
-
current += segment;
|
|
97
|
-
columns += size;
|
|
114
|
+
columns = pushWrapSegment(out, current, columns, segment, measureWidth(segment), width);
|
|
98
115
|
}
|
|
99
116
|
|
|
100
|
-
if (current) out.push(current);
|
|
117
|
+
if (current.text) out.push(current.text);
|
|
101
118
|
|
|
102
119
|
return out;
|
|
103
120
|
}
|
|
104
121
|
|
|
122
|
+
function shortenedPath(text) {
|
|
123
|
+
const parts = text.split("/").filter(Boolean);
|
|
124
|
+
const base = parts.at(-1) ?? text;
|
|
125
|
+
|
|
126
|
+
return { base, suffix: parts.length > 1 ? "…/" + base : base };
|
|
127
|
+
}
|
|
128
|
+
|
|
105
129
|
export function fitPath(pathText, budget) {
|
|
106
130
|
const width = Math.max(0, Math.floor(budget));
|
|
107
131
|
const text = String(pathText ?? "").replace(/\\/g, "/");
|
|
108
132
|
|
|
109
133
|
if (measureWidth(text) <= width) return text;
|
|
110
|
-
const
|
|
111
|
-
const base = parts.at(-1) ?? text;
|
|
112
|
-
const suffix = parts.length > 1 ? "…/" + base : base;
|
|
134
|
+
const { base, suffix } = shortenedPath(text);
|
|
113
135
|
|
|
114
136
|
return measureWidth(suffix) <= width ? suffix : hardTruncate(base, width);
|
|
115
137
|
}
|