pi-supernova 0.6.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.
Files changed (49) hide show
  1. package/README.md +27 -3
  2. package/docs/CHANGELOG.md +104 -0
  3. package/docs/TOKEN_COSTS.md +13 -5
  4. package/index.js +120 -79
  5. package/package.json +1 -1
  6. package/src/adapters/bash.js +73 -0
  7. package/src/adapters/edit.js +249 -0
  8. package/src/adapters/errors.js +31 -0
  9. package/src/adapters/index.js +31 -0
  10. package/src/adapters/list.js +102 -0
  11. package/src/adapters/read.js +805 -0
  12. package/src/adapters/refs.js +41 -0
  13. package/src/adapters/write.js +96 -0
  14. package/src/bridge/catalog.js +28 -222
  15. package/src/bridge/host-bridge.js +113 -1668
  16. package/src/bridge/invoke.js +35 -0
  17. package/src/bridge/native-tools.js +1 -198
  18. package/src/context/evidence.js +140 -76
  19. package/src/context/fuzzy.js +42 -24
  20. package/src/context/ledger.js +43 -24
  21. package/src/context/outline.js +23 -18
  22. package/src/context/repo-index.js +206 -170
  23. package/src/context/search.js +157 -77
  24. package/src/context/snap.js +240 -136
  25. package/src/context/spans.js +2 -1
  26. package/src/context/surface.js +14 -5
  27. package/src/contract/bash.js +31 -0
  28. package/src/contract/edit.js +95 -0
  29. package/src/contract/read.js +220 -0
  30. package/src/fs/check.js +12 -8
  31. package/src/fs/diff.js +18 -7
  32. package/src/fs/json-read.js +66 -35
  33. package/src/fs/patch.js +94 -50
  34. package/src/fs/source-window.js +82 -0
  35. package/src/fs/text-ops.js +512 -0
  36. package/src/fs/vfs.js +205 -175
  37. package/src/fs/workspace.js +119 -108
  38. package/src/output/bottleneck.js +195 -116
  39. package/src/output/format.js +101 -67
  40. package/src/runtime/guest-deny-imports.js +34 -0
  41. package/src/runtime/guest-worker.js +289 -292
  42. package/src/runtime/parallel.js +97 -64
  43. package/src/runtime/program-batch.js +178 -69
  44. package/src/runtime/reference.js +15 -14
  45. package/src/runtime/runtime.js +327 -187
  46. package/src/shared/decode.js +58 -36
  47. package/src/ui/omp-frame.js +59 -42
  48. package/src/ui/render-measure.js +51 -29
  49. package/src/ui/render.js +241 -145
@@ -60,60 +60,82 @@ function plainFromCollection(value, seen, depth) {
60
60
  return out;
61
61
  }
62
62
 
63
- /** Convert any guest value to structured-clone-safe, JSON-shaped data. */
64
- export function toPlain(value, seen = new Set(), depth = 0) {
65
- if (value === null || value === undefined) return value;
66
- const tag = toStr.call(value);
63
+ function withSeen(value, seen, fn) {
64
+ seen.add(value);
67
65
 
68
- if (tag === "[object String]") return value.valueOf();
66
+ try { return fn(); }
67
+ finally { seen.delete(value); }
68
+ }
69
69
 
70
- if (tag === "[object Number]" || tag === "[object Boolean]") return value.valueOf();
70
+ function functionLabel(value) {
71
+ return "[Function" + (value.name ? " " + value.name : "") + "]";
72
+ }
71
73
 
72
- if (tag === "[object BigInt]") return value.toString() + "n";
74
+ function plainByTag(value, tag) {
75
+ if (tag === "[object String]") return { hit: true, out: value.valueOf() };
73
76
 
74
- if (isFunction(value)) return "[Function" + (value.name ? " " + value.name : "") + "]";
77
+ if (tag === "[object Number]" || tag === "[object Boolean]") return { hit: true, out: value.valueOf() };
75
78
 
76
- if (tag === "[object Symbol]") return value.toString();
79
+ if (tag === "[object BigInt]") return { hit: true, out: value.toString() + "n" };
77
80
 
78
- if (depth > MAX_DEPTH) return "[Depth]";
81
+ if (tag === "[object Symbol]") return { hit: true, out: value.toString() };
79
82
 
80
- if (seen.has(value)) return "[Circular]";
83
+ return { hit: false };
84
+ }
81
85
 
82
- if (value instanceof Date) return Number.isNaN(value.getTime()) ? "Invalid Date" : value.toISOString();
86
+ function plainAtom(value) {
87
+ if (value === null || value === undefined) return { hit: true, out: value };
88
+ const tagged = plainByTag(value, toStr.call(value));
83
89
 
84
- if (value instanceof RegExp) return value.toString();
90
+ if (tagged.hit) return tagged;
85
91
 
86
- if (value instanceof Error) {
87
- const out = { name: value.name, message: value.message };
92
+ if (isFunction(value)) return { hit: true, out: functionLabel(value) };
88
93
 
89
- if (value.cause !== undefined) {
90
- seen.add(value);
94
+ return { hit: false };
95
+ }
91
96
 
92
- try { out.cause = toPlain(value.cause, seen, depth + 1); }
93
- finally { seen.delete(value); }
94
- }
97
+ function plainDate(value) {
98
+ return Number.isNaN(value.getTime()) ? "Invalid Date" : value.toISOString();
99
+ }
95
100
 
96
- return out;
97
- }
101
+ function plainHosted(value) {
102
+ if (value instanceof Date) return { hit: true, out: plainDate(value) };
98
103
 
99
- if (value instanceof Promise) return "[Promise]";
104
+ if (value instanceof RegExp) return { hit: true, out: value.toString() };
100
105
 
101
- if (ArrayBuffer.isView(value) || value instanceof ArrayBuffer) return plainFromBinary(value);
106
+ if (value instanceof Promise) return { hit: true, out: "[Promise]" };
102
107
 
103
- if (isFunction(value.toJSON)) {
104
- seen.add(value);
108
+ if (ArrayBuffer.isView(value) || value instanceof ArrayBuffer) return { hit: true, out: plainFromBinary(value) };
105
109
 
106
- try { return toPlain(value.toJSON(), seen, depth + 1); }
107
- finally { seen.delete(value); }
108
- }
109
- seen.add(value);
110
+ return { hit: false };
111
+ }
110
112
 
111
- try {
112
- return plainFromCollection(value, seen, depth);
113
- } finally {
114
- seen.delete(value);
115
- }
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;
116
119
  }
117
120
 
118
- // ---- RPC to the host thread ----
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]";
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));
119
137
 
138
+ return withSeen(value, seen, () => plainFromCollection(value, seen, depth));
139
+ }
140
+
141
+ // ---- RPC to the host thread ----
@@ -89,20 +89,17 @@ function wrapBg(paint) {
89
89
  };
90
90
  }
91
91
 
92
- function bgFnForState(theme, state) {
93
- if (!state || !theme) return undefined;
94
- const key = bgKeyFor(state);
95
-
96
- if (isFunction(theme.bg)) {
97
- try {
98
- if (!isString(theme.bg(key, "x"))) return undefined;
99
- } catch {
100
- return undefined;
101
- }
102
-
103
- 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;
104
97
  }
105
98
 
99
+ return wrapBg((text) => theme.bg(key, text));
100
+ }
101
+
102
+ function themeAnsiBgFn(theme, key) {
106
103
  if (!isFunction(theme.getBgAnsi)) return undefined;
107
104
 
108
105
  try {
@@ -116,47 +113,56 @@ function bgFnForState(theme, state) {
116
113
  }
117
114
  }
118
115
 
119
- function frameBodyLines(sections, contentWidth, box, border, bgFn, w, paintBar) {
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) {
120
126
  const lines = [];
121
- const normalized = sections.length > 0 ? sections : [{ lines: [] }];
122
- const v = box.vertical;
123
127
 
124
- for (const section of normalized) {
125
- 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));
126
129
 
127
- for (const raw of section.lines || []) {
128
- for (const piece of String(raw).split("\n")) {
129
- const body = clampLine(piece, contentWidth);
130
- const pad = Math.max(0, contentWidth - measureWidth(body));
131
- lines.push(padLine(`${border(v)} ${body}${" ".repeat(pad)} ${border(v)}`, w, bgFn));
132
- }
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));
133
135
  }
134
136
  }
135
137
 
136
138
  return lines;
137
139
  }
138
140
 
139
- function renderPortableFrame(theme, { header, sections = [], state = "pending", borderColor, width }) {
140
- const w = Math.max(1, width | 0);
141
+ function frameBodyLines(sections, contentWidth, box, border, bgFn, w, paintBar) {
142
+ const normalized = sections.length > 0 ? sections : [{ lines: [] }];
143
+ const lines = [];
141
144
 
142
- if (w < 8) {
143
- const rawLines = [header];
145
+ for (const section of normalized) lines.push(...frameSectionLines(section, contentWidth, box, border, bgFn, w, paintBar));
144
146
 
145
- for (const section of sections) {
146
- if (section.label) rawLines.push(section.label);
147
- rawLines.push(...(section.lines || []));
148
- }
147
+ return lines;
148
+ }
149
+
150
+ function collapseNarrowFrame(header, sections, w) {
151
+ const rawLines = [header];
149
152
 
150
- return rawLines.flatMap((line) => line ? [clampLine(line, w)] : []);
153
+ for (const section of sections) {
154
+ if (section.label) rawLines.push(section.label);
155
+ rawLines.push(...(section.lines || []));
151
156
  }
152
157
 
153
- const box = boxOf(theme);
154
- const border = borderPaint(theme, state, borderColor);
155
- const bgFn = bgFnForState(theme, state);
158
+ return rawLines.flatMap((line) => line ? [clampLine(line, w)] : []);
159
+ }
160
+
161
+ function paintBarFor(box, border, bgFn, w) {
156
162
  const h = box.horizontal;
157
163
  const cap = h.repeat(3);
158
164
 
159
- const paintBar = (leftChar, rightChar, label) => {
165
+ return (leftChar, rightChar, label) => {
160
166
  const left = `${leftChar}${cap}`;
161
167
  const right = rightChar;
162
168
 
@@ -173,14 +179,25 @@ function renderPortableFrame(theme, { header, sections = [], state = "pending",
173
179
 
174
180
  return padLine(`${border(left)}${trimmed}${border(h.repeat(fill))}${border(right)}`, w, bgFn);
175
181
  };
182
+ }
176
183
 
177
- const contentWidth = Math.max(1, w - 2 - 2);
178
- const lines = [];
179
- lines.push(paintBar(box.topLeft, box.topRight, header));
180
- lines.push(...frameBodyLines(sections, contentWidth, box, border, bgFn, w, paintBar));
181
- 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);
182
186
 
183
- return lines;
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
+ ];
184
201
  }
185
202
 
186
203
  function createPortableFramedComponent(theme, build) {
@@ -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
- if (raw.length <= 4096) {
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
- let current = "";
110
+ const current = { text: "" };
87
111
  let columns = 0;
88
112
 
89
113
  for (const { segment } of segmenter.segment(text)) {
90
- const size = measureWidth(segment);
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 parts = text.split("/").filter(Boolean);
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
  }