pi-supernova 0.0.15 → 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/CHANGELOG.md +31 -0
- package/README.md +87 -33
- package/bottleneck.js +98 -97
- package/catalog.js +7 -32
- package/config.js +1 -2
- package/decode.js +61 -0
- package/evidence.js +14 -5
- package/format.js +16 -17
- package/guest-worker.js +29 -150
- package/host-bridge.js +152 -67
- package/index.js +82 -101
- package/ledger.js +73 -101
- package/omp-frame.js +0 -1
- package/package.json +7 -3
- package/parallel.js +42 -34
- package/patch.js +62 -72
- package/render-measure.js +48 -118
- package/render.js +12 -13
- package/repo-index.js +16 -7
- package/runtime.js +204 -210
- package/snap.js +183 -214
- package/vfs.js +114 -70
- package/workspace.js +37 -33
package/render-measure.js
CHANGED
|
@@ -1,135 +1,65 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Width / truncate primitives used by the compact renderer.
|
|
3
|
-
* Self-contained so path-install never depends on a host truncate that can
|
|
4
|
-
* append ellipsis after cutting to maxWidth (Pi 92>91 crash class).
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
1
|
import { stripVTControlCharacters } from "node:util";
|
|
2
|
+
import stringWidth from "string-width";
|
|
8
3
|
|
|
9
4
|
const ELLIPSIS = "…";
|
|
5
|
+
const segmenter = new Intl.Segmenter(undefined, { granularity: "grapheme" });
|
|
10
6
|
|
|
11
|
-
/**
|
|
12
|
-
* Visible columns: ANSI/OSC stripped, tabs → 3 spaces.
|
|
13
|
-
* ASCII-fast; non-ASCII uses a wide-char heuristic aligned with typical terminal
|
|
14
|
-
* / pi-tui behavior (emoji & symbols like ⚡ are 2 cols; an undercount is the 92>91 crash).
|
|
15
|
-
*/
|
|
16
7
|
export function measureWidth(text) {
|
|
17
|
-
|
|
18
|
-
if (raw.length === 0) return 0;
|
|
19
|
-
const plain = raw.includes("\x1b") ? stripVTControlCharacters(raw) : raw;
|
|
20
|
-
if (/^[\x20-\x7e]*$/.test(plain)) return plain.length;
|
|
21
|
-
let width = 0;
|
|
22
|
-
for (const ch of plain) {
|
|
23
|
-
width += codePointWidth(ch.codePointAt(0));
|
|
24
|
-
}
|
|
25
|
-
return width;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
const ZERO_RANGES = [
|
|
29
|
-
[0x00, 0x1f],
|
|
30
|
-
[0x7f, 0x9f],
|
|
31
|
-
[0x0300, 0x036f],
|
|
32
|
-
[0x1ab0, 0x1aff],
|
|
33
|
-
[0x1dc0, 0x1dff],
|
|
34
|
-
[0x20d0, 0x20ff],
|
|
35
|
-
[0xfe00, 0xfe0e],
|
|
36
|
-
[0xfe20, 0xfe2f],
|
|
37
|
-
];
|
|
38
|
-
|
|
39
|
-
const WIDE_RANGES = [
|
|
40
|
-
[0x1100, 0x115f],
|
|
41
|
-
[0x2e80, 0xa4cf],
|
|
42
|
-
[0xac00, 0xd7a3],
|
|
43
|
-
[0xf900, 0xfaff],
|
|
44
|
-
[0xfe10, 0xfe19],
|
|
45
|
-
[0xfe30, 0xfe6f],
|
|
46
|
-
[0xff00, 0xff60],
|
|
47
|
-
[0xffe0, 0xffe6],
|
|
48
|
-
[0x1f000, 0x1faff],
|
|
49
|
-
[0x20000, 0x3fffd],
|
|
50
|
-
];
|
|
51
|
-
|
|
52
|
-
const WIDE_SINGLES = [0x2329, 0x232a, 0x26a1, 0x2b50, 0x2728];
|
|
53
|
-
|
|
54
|
-
function inRanges(cp, ranges) {
|
|
55
|
-
for (const [lo, hi] of ranges) {
|
|
56
|
-
if (cp >= lo && cp <= hi) return true;
|
|
57
|
-
}
|
|
58
|
-
return false;
|
|
8
|
+
return stringWidth(String(text ?? "").replace(/\t/g, " "));
|
|
59
9
|
}
|
|
60
10
|
|
|
61
|
-
function
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
11
|
+
function takePrefix(text, width) {
|
|
12
|
+
let end = 0;
|
|
13
|
+
let columns = 0;
|
|
14
|
+
for (const { segment, index } of segmenter.segment(text)) {
|
|
15
|
+
const next = measureWidth(segment);
|
|
16
|
+
if (columns + next > width) break;
|
|
17
|
+
columns += next;
|
|
18
|
+
end = index + segment.length;
|
|
19
|
+
}
|
|
20
|
+
return text.slice(0, end);
|
|
68
21
|
}
|
|
69
22
|
|
|
70
|
-
function takeChunk(text, start, width) {
|
|
71
|
-
let end = start;
|
|
72
|
-
let visible = 0;
|
|
73
|
-
let lastBreak = -1;
|
|
74
|
-
while (end < text.length) {
|
|
75
|
-
const cp = text.codePointAt(end);
|
|
76
|
-
const ch = cp > 0xffff ? text.slice(end, end + 2) : text[end];
|
|
77
|
-
const cw = measureWidth(ch);
|
|
78
|
-
if (visible + cw > width) break;
|
|
79
|
-
visible += cw;
|
|
80
|
-
end += ch.length;
|
|
81
|
-
if (ch === "/" || ch === " ") lastBreak = end;
|
|
82
|
-
}
|
|
83
|
-
return { end, lastBreak };
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
/**
|
|
87
|
-
* Truncate so the result's visible width is ALWAYS ≤ maxWidth, ellipsis included.
|
|
88
|
-
* Strips ANSI in the truncated region (crash-safety > color fidelity on overflow).
|
|
89
|
-
*/
|
|
90
23
|
export function hardTruncate(text, maxWidth, ellipsis = ELLIPSIS) {
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
return ell.slice(0, w);
|
|
100
|
-
}
|
|
101
|
-
const budget = w - ellW;
|
|
102
|
-
const plain = stripVTControlCharacters(raw);
|
|
103
|
-
const { end } = takeChunk(plain, 0, budget);
|
|
104
|
-
return plain.slice(0, end) + ell;
|
|
24
|
+
const width = Math.max(0, Math.floor(maxWidth));
|
|
25
|
+
if (!width) return "";
|
|
26
|
+
const raw = String(text ?? "").replace(/\t/g, " ");
|
|
27
|
+
if (measureWidth(raw) <= width) return raw;
|
|
28
|
+
const suffix = stripVTControlCharacters(String(ellipsis));
|
|
29
|
+
const suffixWidth = measureWidth(suffix);
|
|
30
|
+
if (suffixWidth >= width) return takePrefix(suffix, width);
|
|
31
|
+
return takePrefix(stripVTControlCharacters(raw), width - suffixWidth) + suffix;
|
|
105
32
|
}
|
|
106
33
|
|
|
107
|
-
/**
|
|
108
|
-
* Absolute clamp used by every renderer. Loops + hard truncate; never returns > width.
|
|
109
|
-
*/
|
|
110
34
|
export function clampLine(line, width) {
|
|
111
|
-
|
|
112
|
-
let out = String(line ?? "").replace(/\t/g, " ");
|
|
113
|
-
if (measureWidth(out) <= w) return out;
|
|
114
|
-
out = hardTruncate(out, w, ELLIPSIS);
|
|
115
|
-
if (measureWidth(out) <= w) return out;
|
|
116
|
-
const plain = stripVTControlCharacters(out);
|
|
117
|
-
if (plain.length <= w) return plain;
|
|
118
|
-
if (w === 1) return ELLIPSIS;
|
|
119
|
-
return plain.slice(0, Math.max(0, w - 1)) + ELLIPSIS;
|
|
35
|
+
return hardTruncate(line, width);
|
|
120
36
|
}
|
|
121
37
|
|
|
122
|
-
/**
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
38
|
+
/** Wrap complete, already-sanitized result text without splitting graphemes. */
|
|
39
|
+
export function wrapLine(line, width) {
|
|
40
|
+
if (width <= 0) return [];
|
|
41
|
+
const text = String(line).replace(/\t/g, " ");
|
|
42
|
+
if (measureWidth(text) <= width) return [text];
|
|
43
|
+
const out = [];
|
|
44
|
+
let current = "";
|
|
45
|
+
let columns = 0;
|
|
46
|
+
for (const { segment } of segmenter.segment(text)) {
|
|
47
|
+
const size = measureWidth(segment);
|
|
48
|
+
if (columns + size > width && current) { out.push(current); current = ""; columns = 0; }
|
|
49
|
+
if (size > width) { out.push(ELLIPSIS); continue; }
|
|
50
|
+
current += segment;
|
|
51
|
+
columns += size;
|
|
52
|
+
}
|
|
53
|
+
if (current) out.push(current);
|
|
54
|
+
return out;
|
|
134
55
|
}
|
|
135
56
|
|
|
57
|
+
export function fitPath(pathText, budget) {
|
|
58
|
+
const width = Math.max(0, Math.floor(budget));
|
|
59
|
+
const text = String(pathText ?? "").replace(/\\/g, "/");
|
|
60
|
+
if (measureWidth(text) <= width) return text;
|
|
61
|
+
const parts = text.split("/").filter(Boolean);
|
|
62
|
+
const base = parts.at(-1) ?? text;
|
|
63
|
+
const suffix = parts.length > 1 ? "…/" + base : base;
|
|
64
|
+
return measureWidth(suffix) <= width ? suffix : hardTruncate(base, width);
|
|
65
|
+
}
|
package/render.js
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
|
|
13
13
|
import { stripVTControlCharacters } from "node:util";
|
|
14
14
|
import { isString, isObject, isFunction } from "./decode.js";
|
|
15
|
-
import { measureWidth, hardTruncate, clampLine, fitPath } from "./render-measure.js";
|
|
15
|
+
import { measureWidth, hardTruncate, clampLine, fitPath, wrapLine } from "./render-measure.js";
|
|
16
16
|
import { novaFramedBlock, novaStatusLine } from "./omp-frame.js";
|
|
17
17
|
import { formatValue } from "./format.js";
|
|
18
18
|
|
|
@@ -232,7 +232,6 @@ export function renderSupernovaCall(a, b, c) {
|
|
|
232
232
|
|
|
233
233
|
const TOOL_COL = 7;
|
|
234
234
|
const DURATION_COL = 6;
|
|
235
|
-
const PREVIEW_LINES = 24;
|
|
236
235
|
|
|
237
236
|
function formatDuration(ms) {
|
|
238
237
|
if (!Number.isFinite(ms) || ms < 0) return "";
|
|
@@ -310,12 +309,9 @@ function operationsFor(payload, context) {
|
|
|
310
309
|
return operationsFromTrace(payload?.trace || context?.state?.trace || []);
|
|
311
310
|
}
|
|
312
311
|
|
|
313
|
-
function resultLines(value,
|
|
312
|
+
function resultLines(value, width) {
|
|
314
313
|
const text = isString(value) ? value : formatValue(value);
|
|
315
|
-
|
|
316
|
-
const shown = lines.slice(0, maxLines);
|
|
317
|
-
if (lines.length > maxLines) shown.push(`… ${lines.length - maxLines} more lines`);
|
|
318
|
-
return shown;
|
|
314
|
+
return cleanBlockText(text).split("\n").flatMap(line => wrapLine(line, width));
|
|
319
315
|
}
|
|
320
316
|
|
|
321
317
|
function appendOps(lines, theme, ops, maxOps, maxDiffLines, width, isPartial, isError) {
|
|
@@ -327,15 +323,18 @@ function appendOps(lines, theme, ops, maxOps, maxDiffLines, width, isPartial, is
|
|
|
327
323
|
if (ops.length > maxOps) lines.push(theme.fg("dim", ` … ${ops.length - maxOps} more calls`));
|
|
328
324
|
}
|
|
329
325
|
|
|
330
|
-
function appendTail(lines, theme, payload, expanded, isError) {
|
|
331
|
-
if (isError)
|
|
326
|
+
function appendTail(lines, theme, payload, expanded, isError, width) {
|
|
327
|
+
if (isError) {
|
|
328
|
+
const error = "✗ " + (payload?.error ? cleanBlockText(payload.error) : "error");
|
|
329
|
+
for (const line of expanded ? resultLines(error, width) : error.split("\n")) lines.push(theme.fg("error", line));
|
|
330
|
+
}
|
|
332
331
|
else if (expanded && payload?.result !== undefined) {
|
|
333
332
|
lines.push(theme.fg("dim", "── result ──"));
|
|
334
|
-
for (const line of resultLines(payload.result,
|
|
333
|
+
for (const line of resultLines(payload.result, width)) lines.push(theme.fg("toolOutput", line));
|
|
335
334
|
}
|
|
336
335
|
if (expanded && payload?.logs?.length) {
|
|
337
336
|
lines.push(theme.fg("dim", "── logs ──"));
|
|
338
|
-
for (const log of payload.logs
|
|
337
|
+
for (const log of payload.logs) for (const line of resultLines(log, width)) lines.push(theme.fg("dim", line));
|
|
339
338
|
}
|
|
340
339
|
}
|
|
341
340
|
|
|
@@ -345,7 +344,7 @@ function buildBodyLines(theme, width, { payload, context, args, expanded, isPart
|
|
|
345
344
|
const maxDiffLines = expanded ? 24 : 8;
|
|
346
345
|
const lines = [];
|
|
347
346
|
appendOps(lines, theme, ops, maxOps, maxDiffLines, width, isPartial, isError);
|
|
348
|
-
appendTail(lines, theme, payload, expanded, isError);
|
|
347
|
+
appendTail(lines, theme, payload, expanded, isError, width);
|
|
349
348
|
return { lines, opCount: ops.length };
|
|
350
349
|
}
|
|
351
350
|
|
|
@@ -367,7 +366,7 @@ class UnifiedResultCard {
|
|
|
367
366
|
}
|
|
368
367
|
render(width = 80) {
|
|
369
368
|
const { theme, model } = this;
|
|
370
|
-
if (!theme || !model) return [];
|
|
369
|
+
if (!theme || !model || width <= 0) return [];
|
|
371
370
|
if (this.cache?.width === width) return this.cache.lines;
|
|
372
371
|
const view = buildBodyLines(theme, Math.max(1, width - 4), model);
|
|
373
372
|
const header = novaStatusLine(theme, {
|
package/repo-index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
2
|
import * as path from "node:path";
|
|
3
3
|
import { extractStructuralSurface } from "./surface.js";
|
|
4
|
+
import { isFunction } from "./decode.js";
|
|
4
5
|
import { Frecency } from "./fuzzy.js";
|
|
5
6
|
import { relativeSlash } from "./workspace.js";
|
|
6
7
|
|
|
@@ -112,7 +113,7 @@ export class WorkspaceIndex {
|
|
|
112
113
|
this.watchers.set(root, false);
|
|
113
114
|
this.lists.clear();
|
|
114
115
|
});
|
|
115
|
-
if (
|
|
116
|
+
if (isFunction(watcher.unref)) watcher.unref();
|
|
116
117
|
ok = true;
|
|
117
118
|
} catch {
|
|
118
119
|
ok = false;
|
|
@@ -149,7 +150,7 @@ export class WorkspaceIndex {
|
|
|
149
150
|
}
|
|
150
151
|
|
|
151
152
|
/** Absolute, sorted file list for a root; gitignore-aware via rg; cached for LIST_TTL_MS. */
|
|
152
|
-
async files(root, includeHidden = false) {
|
|
153
|
+
async files(root, includeHidden = false, signal) {
|
|
153
154
|
const key = root + "\0" + (includeHidden ? "h" : "");
|
|
154
155
|
const cached = this.lists.get(key);
|
|
155
156
|
const ttl = this.watch(root) ? WATCHED_TTL_MS : LIST_TTL_MS;
|
|
@@ -158,13 +159,21 @@ export class WorkspaceIndex {
|
|
|
158
159
|
if (includeHidden) args.push("--hidden");
|
|
159
160
|
args.push("-g", "!.git/**", "-g", "!**/.git/**", root);
|
|
160
161
|
let files = [];
|
|
162
|
+
let error;
|
|
163
|
+
let truncated = false;
|
|
164
|
+
let missing = false;
|
|
161
165
|
try {
|
|
162
|
-
const res = await this.runCommand(args, { cwd: root, timeoutMs: 15_000 });
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
+
const res = await this.runCommand(args, { cwd: root, timeoutMs: 15_000, signal });
|
|
167
|
+
if (res.exitCode !== 0 && res.exitCode !== 1) error = res.stderr.trim() || "rg exited with status " + res.exitCode;
|
|
168
|
+
truncated = res.outputTruncated === true;
|
|
169
|
+
const output = truncated && !res.stdout.endsWith("\n") ? res.stdout.slice(0, res.stdout.lastIndexOf("\n") + 1) : res.stdout;
|
|
170
|
+
files = output.split("\n").filter(Boolean).map(f => path.resolve(root, f)).sort();
|
|
171
|
+
} catch (err) {
|
|
172
|
+
signal?.throwIfAborted();
|
|
173
|
+
error = err.message;
|
|
174
|
+
missing = !fs.existsSync(root);
|
|
166
175
|
}
|
|
167
|
-
this.lists.set(key, { files, at: Date.now() });
|
|
176
|
+
this.lists.set(key, { files, at: Date.now(), error, truncated, missing });
|
|
168
177
|
return files;
|
|
169
178
|
}
|
|
170
179
|
|