pi-supernova 0.0.7 → 0.0.8
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 +26 -0
- package/README.md +22 -15
- package/bottleneck.js +32 -41
- package/catalog.js +61 -15
- package/config.default.json +2 -1
- package/config.js +19 -10
- package/diff.js +12 -4
- package/format.js +95 -0
- package/guest-worker.js +344 -0
- package/host-bridge.js +130 -439
- package/index.js +75 -97
- package/omp-frame.js +68 -51
- package/package.json +6 -1
- package/parallel.js +19 -20
- package/patch.js +106 -0
- package/render-measure.js +63 -49
- package/render.js +193 -166
- package/runtime.js +266 -241
- package/snap.js +120 -101
- package/surface.js +13 -30
- package/vfs.js +138 -0
- package/workspace.js +112 -0
package/render-measure.js
CHANGED
|
@@ -25,29 +25,64 @@ export function measureWidth(text) {
|
|
|
25
25
|
return width;
|
|
26
26
|
}
|
|
27
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;
|
|
59
|
+
}
|
|
60
|
+
|
|
28
61
|
function codePointWidth(cp) {
|
|
29
|
-
if (cp
|
|
30
|
-
if (cp ===
|
|
31
|
-
if (
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
// Fullwidth / wide ranges (CJK, Hangul, emoji blocks we actually emit).
|
|
35
|
-
if (cp >= 0x1100 && cp <= 0x115f) return 2;
|
|
36
|
-
if (cp === 0x2329 || cp === 0x232a) return 2;
|
|
37
|
-
if (cp >= 0x2e80 && cp <= 0xa4cf) return 2;
|
|
38
|
-
if (cp >= 0xac00 && cp <= 0xd7a3) return 2;
|
|
39
|
-
if (cp >= 0xf900 && cp <= 0xfaff) return 2;
|
|
40
|
-
if (cp >= 0xfe10 && cp <= 0xfe19) return 2;
|
|
41
|
-
if (cp >= 0xfe30 && cp <= 0xfe6f) return 2;
|
|
42
|
-
if (cp >= 0xff00 && cp <= 0xff60) return 2;
|
|
43
|
-
if (cp >= 0xffe0 && cp <= 0xffe6) return 2;
|
|
44
|
-
if (cp >= 0x1f000 && cp <= 0x1faff) return 2;
|
|
45
|
-
if (cp >= 0x20000 && cp <= 0x3fffd) return 2;
|
|
46
|
-
// Ambiguous emoji/symbols pi-tui treats as wide (⚡ U+26A1 was the 92>91 footgun).
|
|
47
|
-
if (cp === 0x26a1 || cp === 0x2b50 || cp === 0x2728) return 2;
|
|
62
|
+
if (cp === 0xfe0f) return 1;
|
|
63
|
+
if (cp === 0x200d) return 0;
|
|
64
|
+
if (inRanges(cp, ZERO_RANGES)) return 0;
|
|
65
|
+
if (WIDE_SINGLES.includes(cp)) return 2;
|
|
66
|
+
if (inRanges(cp, WIDE_RANGES)) return 2;
|
|
48
67
|
return 1;
|
|
49
68
|
}
|
|
50
69
|
|
|
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
|
+
|
|
51
86
|
/**
|
|
52
87
|
* Truncate so the result's visible width is ALWAYS ≤ maxWidth, ellipsis included.
|
|
53
88
|
* Strips ANSI in the truncated region (crash-safety > color fidelity on overflow).
|
|
@@ -57,25 +92,16 @@ export function hardTruncate(text, maxWidth, ellipsis = ELLIPSIS) {
|
|
|
57
92
|
if (w === 0) return "";
|
|
58
93
|
const raw = String(text ?? "").replace(/\t/g, " ");
|
|
59
94
|
if (measureWidth(raw) <= w) return raw;
|
|
60
|
-
|
|
61
95
|
const ell = String(ellipsis);
|
|
62
96
|
const ellW = measureWidth(ell);
|
|
63
97
|
if (ellW >= w) {
|
|
64
98
|
if (ellW === 0) return "";
|
|
65
99
|
return ell.slice(0, w);
|
|
66
100
|
}
|
|
67
|
-
|
|
68
101
|
const budget = w - ellW;
|
|
69
102
|
const plain = stripVTControlCharacters(raw);
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
for (const ch of plain) {
|
|
73
|
-
const cw = measureWidth(ch);
|
|
74
|
-
if (visible + cw > budget) break;
|
|
75
|
-
out += ch;
|
|
76
|
-
visible += cw;
|
|
77
|
-
}
|
|
78
|
-
return out + ell;
|
|
103
|
+
const { end } = takeChunk(plain, 0, budget);
|
|
104
|
+
return plain.slice(0, end) + ell;
|
|
79
105
|
}
|
|
80
106
|
|
|
81
107
|
/**
|
|
@@ -102,33 +128,22 @@ export function wrapPlainToWidth(plain, width) {
|
|
|
102
128
|
const text = String(plain ?? "");
|
|
103
129
|
if (text.length === 0) return [""];
|
|
104
130
|
if (measureWidth(text) <= w) return [text];
|
|
105
|
-
|
|
106
131
|
const lines = [];
|
|
107
132
|
let i = 0;
|
|
108
133
|
while (i < text.length) {
|
|
109
|
-
|
|
110
|
-
let visible = 0;
|
|
111
|
-
let lastBreak = -1;
|
|
112
|
-
while (end < text.length) {
|
|
113
|
-
const cp = text.codePointAt(end);
|
|
114
|
-
const ch = String.fromCodePoint(cp);
|
|
115
|
-
const cw = measureWidth(ch);
|
|
116
|
-
if (visible + cw > w) break;
|
|
117
|
-
visible += cw;
|
|
118
|
-
end += ch.length;
|
|
119
|
-
if (ch === "/" || ch === " ") lastBreak = end;
|
|
120
|
-
}
|
|
134
|
+
const { end, lastBreak } = takeChunk(text, i, w);
|
|
121
135
|
if (end === i) {
|
|
122
|
-
const ch =
|
|
136
|
+
const ch = text.codePointAt(i) > 0xffff ? text.slice(i, i + 2) : text[i];
|
|
123
137
|
lines.push(hardTruncate(ch, w));
|
|
124
138
|
i += ch.length;
|
|
125
139
|
continue;
|
|
126
140
|
}
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
i
|
|
141
|
+
let cut = end;
|
|
142
|
+
if (end < text.length && lastBreak > i + Math.floor(w * 0.35)) cut = lastBreak;
|
|
143
|
+
lines.push(text.slice(i, cut));
|
|
144
|
+
i = cut;
|
|
130
145
|
}
|
|
131
|
-
return lines
|
|
146
|
+
return lines;
|
|
132
147
|
}
|
|
133
148
|
|
|
134
149
|
/**
|
|
@@ -138,7 +153,6 @@ export function fitPath(pathText, budget) {
|
|
|
138
153
|
const w = Math.max(1, budget | 0);
|
|
139
154
|
let p = String(pathText ?? "").replace(/\\/g, "/");
|
|
140
155
|
if (measureWidth(p) <= w) return p;
|
|
141
|
-
|
|
142
156
|
const parts = p.split("/").filter(Boolean);
|
|
143
157
|
const base = parts.length > 0 ? parts[parts.length - 1] : p;
|
|
144
158
|
const suffix = parts.length > 1 ? `…/${base}` : base;
|
package/render.js
CHANGED
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
fitPath,
|
|
21
21
|
} from "./render-measure.js";
|
|
22
22
|
import { novaFramedBlock, novaStatusLine } from "./omp-frame.js";
|
|
23
|
+
import { formatValue } from "./format.js";
|
|
23
24
|
|
|
24
25
|
export { measureWidth, hardTruncate, clampLine, wrapPlainToWidth, fitPath };
|
|
25
26
|
|
|
@@ -153,18 +154,6 @@ function formatDiffRows(diff, theme, maxShown = 6) {
|
|
|
153
154
|
return body;
|
|
154
155
|
}
|
|
155
156
|
|
|
156
|
-
export function renderDiffBox(diff, theme, width = 60, maxShown = 6) {
|
|
157
|
-
if (!diff || !Array.isArray(diff.lines) || diff.lines.length === 0) return "";
|
|
158
|
-
const w = Math.max(20, width | 0);
|
|
159
|
-
const cleanPath = String(diff.path || "").replace(/\\/g, "/");
|
|
160
|
-
const baseName = cleanPath.split("/").pop() || cleanPath;
|
|
161
|
-
const opLabel = diff.op === "edit" ? "Edit" : diff.op === "write" ? "Write" : "Patch";
|
|
162
|
-
const stats = theme.fg("dim", "⟨") + theme.fg("toolDiffAdded", `+${diff.added}`) + theme.fg("dim", "/") + theme.fg("toolDiffRemoved", `-${diff.removed}`) + theme.fg("dim", "⟩");
|
|
163
|
-
const header = theme.fg("accent", "✎ ") + theme.fg("toolTitle", theme.bold(`${opLabel} `)) + stats + " " + theme.fg("muted", baseName);
|
|
164
|
-
const divider = theme.fg("borderMuted", "─".repeat(Math.min(70, w)));
|
|
165
|
-
return `${header}\n${divider}\n${formatDiffRows(diff, theme, maxShown).join("\n")}\n${divider}`;
|
|
166
|
-
}
|
|
167
|
-
|
|
168
157
|
function stripUnsafeControls(value) {
|
|
169
158
|
let clean = "";
|
|
170
159
|
for (const character of value) {
|
|
@@ -185,22 +174,11 @@ function cleanInlineText(value) {
|
|
|
185
174
|
return cleanBlockText(value).replace(/\s*\n\s*/g, " ").trim();
|
|
186
175
|
}
|
|
187
176
|
|
|
188
|
-
function displayOperation(tool, target, diff, ok) {
|
|
177
|
+
function displayOperation(tool, target, diff, ok, item) {
|
|
189
178
|
const rawName = cleanInlineText(tool);
|
|
190
179
|
if (!rawName) return null;
|
|
191
180
|
const normalized = rawName === "apply_patch" ? "patch" : rawName;
|
|
192
|
-
return { tool: normalized, target, diff, ok };
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
function formatOpTarget(raw, tool) {
|
|
196
|
-
const text = cleanInlineText(raw);
|
|
197
|
-
if (!text) return "";
|
|
198
|
-
if (tool === "bash") {
|
|
199
|
-
// Keep commands readable; wrap handles the rest at render time.
|
|
200
|
-
return text.length > 80 ? `${text.slice(0, 77)}…` : text;
|
|
201
|
-
}
|
|
202
|
-
// Paths: normalize separators; SafeText wraps so we keep the full relative path.
|
|
203
|
-
return text.replace(/\\/g, "/");
|
|
181
|
+
return { tool: normalized, target, diff, ok, ms: item?.ms, exitCode: item?.exitCode, time: item?.time };
|
|
204
182
|
}
|
|
205
183
|
|
|
206
184
|
function isTheme(value) {
|
|
@@ -244,36 +222,25 @@ export function normalizeCallRenderArgs(a, b, c) {
|
|
|
244
222
|
* Call shapes share the first three positions, so host is inferred from the
|
|
245
223
|
* fourth argument's context-versus-args shape.
|
|
246
224
|
*/
|
|
225
|
+
function contextFrom(opts, ctxOrArgs) {
|
|
226
|
+
if (isObject(ctxOrArgs) && !isTheme(ctxOrArgs)) {
|
|
227
|
+
if ("lastComponent" in ctxOrArgs || "state" in ctxOrArgs || "invalidate" in ctxOrArgs) return ctxOrArgs;
|
|
228
|
+
}
|
|
229
|
+
return { state: opts.state, lastComponent: opts.lastComponent };
|
|
230
|
+
}
|
|
231
|
+
|
|
247
232
|
function detectResultHost(options, ctxOrArgs) {
|
|
248
233
|
if (isTheme(options)) return "pi";
|
|
249
|
-
if (
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
) {
|
|
253
|
-
return "pi";
|
|
254
|
-
}
|
|
255
|
-
if (
|
|
256
|
-
isObject(ctxOrArgs) &&
|
|
257
|
-
("code" in ctxOrArgs || "timeoutMs" in ctxOrArgs)
|
|
258
|
-
) {
|
|
259
|
-
return "omp";
|
|
260
|
-
}
|
|
234
|
+
if (!isObject(ctxOrArgs)) return "pi";
|
|
235
|
+
if ("lastComponent" in ctxOrArgs || "invalidate" in ctxOrArgs) return "pi";
|
|
236
|
+
if ("code" in ctxOrArgs || "timeoutMs" in ctxOrArgs) return "omp";
|
|
261
237
|
return "pi";
|
|
262
238
|
}
|
|
263
239
|
|
|
264
240
|
export function normalizeResultRenderArgs(result, options, themeOrCtx, ctxOrArgs) {
|
|
265
241
|
if (isTheme(themeOrCtx)) {
|
|
266
242
|
const opts = isObject(options) ? options : {};
|
|
267
|
-
|
|
268
|
-
if (
|
|
269
|
-
isObject(ctxOrArgs) &&
|
|
270
|
-
!isTheme(ctxOrArgs) &&
|
|
271
|
-
("lastComponent" in ctxOrArgs || "state" in ctxOrArgs || "invalidate" in ctxOrArgs)
|
|
272
|
-
) {
|
|
273
|
-
context = ctxOrArgs;
|
|
274
|
-
} else {
|
|
275
|
-
context = { state: opts.state, lastComponent: opts.lastComponent };
|
|
276
|
-
}
|
|
243
|
+
const context = contextFrom(opts, ctxOrArgs);
|
|
277
244
|
if (!isObject(context.state)) context.state = {};
|
|
278
245
|
return {
|
|
279
246
|
result,
|
|
@@ -304,23 +271,37 @@ export function normalizeResultRenderArgs(result, options, themeOrCtx, ctxOrArgs
|
|
|
304
271
|
throw new Error("supernova renderResult: theme missing (expected Pi or OMP signature)");
|
|
305
272
|
}
|
|
306
273
|
|
|
274
|
+
const OPERATION_TARGETS = [
|
|
275
|
+
[(item) => item?.name === "snap", (item, args) => {
|
|
276
|
+
const query = args.query ? `"${args.query}"` : "";
|
|
277
|
+
if (!args.path) return query;
|
|
278
|
+
return `${query} → ${args.path}`;
|
|
279
|
+
}],
|
|
280
|
+
[(item) => item?.name === "search", (item, args) => (args.query ? `"${args.query}"` : "")],
|
|
281
|
+
[(item, args) => args.path, (item, args) => String(args.path)],
|
|
282
|
+
[(item) => item?.diff?.path, (item) => String(item.diff.path)],
|
|
283
|
+
[(item, args) => args.target && isString(args.target), (item, args) => args.target],
|
|
284
|
+
[(item, args) => args.command, (item, args) => String(args.command)],
|
|
285
|
+
[(item, args) => args.pattern, (item, args) => String(args.pattern)],
|
|
286
|
+
[(item, args) => args.query, (item, args) => String(args.query)],
|
|
287
|
+
];
|
|
288
|
+
|
|
307
289
|
function operationTarget(item) {
|
|
308
290
|
const args = item?.args || {};
|
|
309
|
-
const
|
|
310
|
-
|
|
311
|
-
const query = args.query ? `"${args.query}"` : "";
|
|
312
|
-
return args.path ? `${query} → ${args.path}` : query;
|
|
291
|
+
for (const [predicate, formatter] of OPERATION_TARGETS) {
|
|
292
|
+
if (predicate(item, args)) return formatter(item, args);
|
|
313
293
|
}
|
|
314
|
-
if (name === "search") return args.query ? `"${args.query}"` : "";
|
|
315
|
-
if (args.path) return String(args.path);
|
|
316
|
-
if (item?.diff?.path) return String(item.diff.path);
|
|
317
|
-
if (args.target && isString(args.target)) return args.target;
|
|
318
|
-
if (args.command) return String(args.command);
|
|
319
|
-
if (args.pattern) return String(args.pattern);
|
|
320
|
-
if (args.query) return String(args.query);
|
|
321
294
|
return "";
|
|
322
295
|
}
|
|
323
296
|
|
|
297
|
+
function parseDiffLine(rawLine) {
|
|
298
|
+
const signed = /^([+-])\s*(\d+)\s?(.*)$/.exec(rawLine);
|
|
299
|
+
if (signed) return { type: signed[1] === "+" ? "add" : "remove", lineNum: Number(signed[2]), text: signed[3] };
|
|
300
|
+
const contextual = /^\s+(\d+)\s?(.*)$/.exec(rawLine);
|
|
301
|
+
if (contextual) return { type: "context", lineNum: Number(contextual[1]), text: contextual[2] };
|
|
302
|
+
return null;
|
|
303
|
+
}
|
|
304
|
+
|
|
324
305
|
function normalizeTraceDiff(item) {
|
|
325
306
|
const diff = item?.diff;
|
|
326
307
|
if (isObject(diff)) return diff;
|
|
@@ -329,16 +310,11 @@ function normalizeTraceDiff(item) {
|
|
|
329
310
|
let added = 0;
|
|
330
311
|
let removed = 0;
|
|
331
312
|
for (const rawLine of cleanBlockText(diff).split("\n")) {
|
|
332
|
-
|
|
333
|
-
if (
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
lines.push({ type, lineNum: Number(match[2]), text: match[3] });
|
|
338
|
-
continue;
|
|
339
|
-
}
|
|
340
|
-
match = /^\s+(\d+)\s?(.*)$/.exec(rawLine);
|
|
341
|
-
if (match) lines.push({ type: "context", lineNum: Number(match[1]), text: match[2] });
|
|
313
|
+
const parsed = parseDiffLine(rawLine);
|
|
314
|
+
if (!parsed) continue;
|
|
315
|
+
if (parsed.type === "add") added += 1;
|
|
316
|
+
else if (parsed.type === "remove") removed += 1;
|
|
317
|
+
lines.push(parsed);
|
|
342
318
|
}
|
|
343
319
|
if (lines.length === 0) return undefined;
|
|
344
320
|
return { path: item?.args?.path || "", op: item?.name, added, removed, lines };
|
|
@@ -347,7 +323,7 @@ function normalizeTraceDiff(item) {
|
|
|
347
323
|
function operationsFromTrace(trace) {
|
|
348
324
|
if (!Array.isArray(trace)) return [];
|
|
349
325
|
return trace
|
|
350
|
-
.map((item) => displayOperation(item?.name || "tool", operationTarget(item), normalizeTraceDiff(item), item?.ok))
|
|
326
|
+
.map((item) => displayOperation(item?.name || "tool", operationTarget(item), normalizeTraceDiff(item), item?.ok, item))
|
|
351
327
|
.filter(Boolean);
|
|
352
328
|
}
|
|
353
329
|
|
|
@@ -360,99 +336,171 @@ export function renderSupernovaCall(a, b, c) {
|
|
|
360
336
|
return comp;
|
|
361
337
|
}
|
|
362
338
|
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
const
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
339
|
+
const TOOL_COL = 7;
|
|
340
|
+
const DURATION_COL = 6;
|
|
341
|
+
const PREVIEW_LINES = 24;
|
|
342
|
+
|
|
343
|
+
export function formatDuration(ms) {
|
|
344
|
+
if (!Number.isFinite(ms) || ms < 0) return "";
|
|
345
|
+
if (ms < 1000) return `${Math.round(ms)}ms`;
|
|
346
|
+
if (ms < 60000) return `${(ms / 1000).toFixed(1)}s`;
|
|
347
|
+
const minutes = Math.floor(ms / 60000);
|
|
348
|
+
const seconds = Math.round((ms % 60000) / 1000);
|
|
349
|
+
return `${minutes}m${String(seconds).padStart(2, "0")}s`;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/** First meaningful line of a shell command plus a count of the hidden remainder. */
|
|
353
|
+
function summarizeCommand(raw) {
|
|
354
|
+
const lines = cleanBlockText(raw).split("\n").map((line) => line.trim()).filter(Boolean);
|
|
355
|
+
if (lines.length === 0) return "";
|
|
356
|
+
const first = lines[0].replace(/\s+/g, " ");
|
|
357
|
+
return lines.length > 1 ? `${first} …+${lines.length - 1} lines` : first;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
const SHELL_TOOLS = ["bash", "exec"];
|
|
361
|
+
const SEARCH_TOOLS = ["snap", "search"];
|
|
362
|
+
|
|
363
|
+
function formatTarget(op, budget) {
|
|
364
|
+
if (SHELL_TOOLS.includes(op.tool)) return clampLine(summarizeCommand(op.target), budget);
|
|
365
|
+
const text = cleanInlineText(op.target);
|
|
366
|
+
if (!text) return "";
|
|
367
|
+
if (SEARCH_TOOLS.includes(op.tool)) return clampLine(text, budget);
|
|
368
|
+
return fitPath(text, budget);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
function opMarker(theme, op, isPartial, isError) {
|
|
372
|
+
if (op.ok === false) return theme.fg("error", "×");
|
|
373
|
+
if (op.ok === true) return theme.fg("success", "✓");
|
|
374
|
+
if (isPartial) return theme.fg("dim", "·");
|
|
375
|
+
if (isError) return theme.fg("error", "×");
|
|
376
|
+
return theme.fg("success", "✓");
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
function opDuration(op, isPartial) {
|
|
380
|
+
if (Number.isFinite(op.ms)) return formatDuration(op.ms);
|
|
381
|
+
if (isPartial && op.ok === undefined && Number.isFinite(op.time)) return formatDuration(Date.now() - op.time) + "…";
|
|
382
|
+
return "";
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* One aligned row: marker · tool · duration · [exit N] · [+a/-r] · target.
|
|
387
|
+
* Fixed columns keep a ledger of mixed calls scannable at a glance.
|
|
388
|
+
*/
|
|
389
|
+
function formatOpRow(theme, op, width, isPartial, isError) {
|
|
390
|
+
const marker = opMarker(theme, op, isPartial, isError);
|
|
391
|
+
const toolText = op.tool.padEnd(TOOL_COL);
|
|
392
|
+
const tool = theme.fg("syntaxFunction", toolText);
|
|
393
|
+
const durationText = opDuration(op, isPartial);
|
|
394
|
+
const duration = theme.fg("dim", durationText.padStart(DURATION_COL));
|
|
395
|
+
let prefix = `${marker} ${tool} ${duration} `;
|
|
396
|
+
let used = 2 + toolText.length + 1 + DURATION_COL + 2;
|
|
397
|
+
if (Number.isInteger(op.exitCode)) {
|
|
398
|
+
const exit = `exit ${op.exitCode}`;
|
|
399
|
+
prefix += theme.fg("error", exit) + " ";
|
|
400
|
+
used += exit.length + 2;
|
|
389
401
|
}
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
402
|
+
if (op.diff && isObject(op.diff)) {
|
|
403
|
+
const added = `+${op.diff.added || 0}`;
|
|
404
|
+
const removed = `-${op.diff.removed || 0}`;
|
|
405
|
+
prefix += theme.fg("toolDiffAdded", added) + theme.fg("dim", "/") + theme.fg("toolDiffRemoved", removed) + " ";
|
|
406
|
+
used += added.length + 1 + removed.length + 1;
|
|
407
|
+
}
|
|
408
|
+
const target = formatTarget(op, Math.max(1, width - used));
|
|
409
|
+
return prefix + (target ? theme.fg("muted", target) : theme.fg("dim", "done"));
|
|
394
410
|
}
|
|
395
411
|
|
|
396
|
-
function
|
|
397
|
-
let out = "";
|
|
412
|
+
function operationsFor(payload, context, args) {
|
|
398
413
|
const trace = payload?.trace || context?.state?.trace || [];
|
|
399
|
-
const
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
const
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
414
|
+
const traced = operationsFromTrace(trace);
|
|
415
|
+
if (traced.length > 0) return traced;
|
|
416
|
+
return extractOperationsFromCode(args?.code).map((op) => displayOperation(op.tool, op.target)).filter(Boolean);
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
function resultLines(value, maxLines) {
|
|
420
|
+
const text = isString(value) ? value : formatValue(value);
|
|
421
|
+
const lines = cleanBlockText(text).split("\n");
|
|
422
|
+
const shown = lines.slice(0, maxLines);
|
|
423
|
+
if (lines.length > maxLines) shown.push(`… ${lines.length - maxLines} more lines`);
|
|
424
|
+
return shown;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
function appendOps(lines, theme, ops, maxOps, maxDiffLines, width, isPartial, isError) {
|
|
428
|
+
for (const op of ops.slice(0, maxOps)) {
|
|
429
|
+
lines.push(formatOpRow(theme, op, width, isPartial, isError));
|
|
430
|
+
if (!op.diff || !isObject(op.diff)) continue;
|
|
431
|
+
for (const row of formatDiffRows(op.diff, theme, maxDiffLines)) lines.push(" " + row);
|
|
415
432
|
}
|
|
416
|
-
if (ops.length > maxOps)
|
|
433
|
+
if (ops.length > maxOps) lines.push(theme.fg("dim", ` … ${ops.length - maxOps} more calls`));
|
|
434
|
+
}
|
|
417
435
|
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
if (!isError && payload?.logs?.length) {
|
|
424
|
-
out += (out ? "\n" : "") + theme.fg("dim", "── logs ──");
|
|
425
|
-
for (const log of payload.logs.slice(0, 24)) out += `\n ${theme.fg("dim", cleanBlockText(log))}`;
|
|
426
|
-
}
|
|
436
|
+
function appendTail(lines, theme, payload, expanded, isError) {
|
|
437
|
+
if (isError) lines.push(theme.fg("error", "✗ " + (payload?.error ? cleanBlockText(payload.error) : "error")));
|
|
438
|
+
else if (expanded && payload?.result !== undefined) {
|
|
439
|
+
lines.push(theme.fg("dim", "── result ──"));
|
|
440
|
+
for (const line of resultLines(payload.result, PREVIEW_LINES)) lines.push(theme.fg("toolOutput", line));
|
|
427
441
|
}
|
|
428
|
-
|
|
442
|
+
if (expanded && payload?.logs?.length) {
|
|
443
|
+
lines.push(theme.fg("dim", "── logs ──"));
|
|
444
|
+
for (const log of payload.logs.slice(0, PREVIEW_LINES)) lines.push(theme.fg("dim", cleanBlockText(log)));
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
function buildBodyLines(theme, width, { payload, context, args, expanded, isPartial, isError }) {
|
|
449
|
+
const ops = operationsFor(payload, context, args);
|
|
450
|
+
const maxOps = expanded ? 24 : 8;
|
|
451
|
+
const maxDiffLines = expanded ? 24 : 8;
|
|
452
|
+
const lines = [];
|
|
453
|
+
appendOps(lines, theme, ops, maxOps, maxDiffLines, width, isPartial, isError);
|
|
454
|
+
appendTail(lines, theme, payload, expanded, isError);
|
|
455
|
+
return { lines, opCount: ops.length };
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
function describeCard(model, opCount) {
|
|
459
|
+
const wall = model.payload?.wallMs != null ? formatDuration(model.payload.wallMs) : "";
|
|
460
|
+
const calls = opCount > 0 ? `${opCount} call${opCount === 1 ? "" : "s"}` : "";
|
|
461
|
+
const status = model.isError ? "failed" : model.isPartial ? "running" : calls ? "" : "complete";
|
|
462
|
+
return [calls, status, wall].filter(Boolean).join(" · ");
|
|
429
463
|
}
|
|
430
464
|
|
|
431
465
|
class UnifiedResultCard {
|
|
432
466
|
set(theme, model) {
|
|
433
467
|
this.theme = theme;
|
|
434
468
|
this.model = model;
|
|
469
|
+
this.frame = undefined;
|
|
470
|
+
}
|
|
471
|
+
invalidate() {
|
|
472
|
+
this.frame = undefined;
|
|
435
473
|
}
|
|
436
|
-
invalidate() {}
|
|
437
474
|
render(width = 80) {
|
|
438
475
|
const { theme, model } = this;
|
|
439
476
|
if (!theme || !model) return [];
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
477
|
+
if (!this.frame) {
|
|
478
|
+
this.frame = novaFramedBlock(theme, (frameWidth) => {
|
|
479
|
+
const contentWidth = Math.max(1, frameWidth - 4);
|
|
480
|
+
const view = buildBodyLines(theme, contentWidth, model);
|
|
481
|
+
return {
|
|
482
|
+
header: novaStatusLine(theme, {
|
|
483
|
+
icon: model.isError ? "error" : model.isPartial ? "running" : undefined,
|
|
484
|
+
title: "nova",
|
|
485
|
+
description: describeCard(model, view.opCount),
|
|
486
|
+
}),
|
|
487
|
+
sections: view.lines.length > 0 ? [{ lines: view.lines }] : [],
|
|
488
|
+
state: model.isError ? "error" : model.isPartial ? "pending" : "success",
|
|
489
|
+
borderColor: model.isError ? "error" : "borderMuted",
|
|
490
|
+
width: frameWidth,
|
|
491
|
+
};
|
|
492
|
+
});
|
|
493
|
+
}
|
|
494
|
+
return this.frame.render(width);
|
|
453
495
|
}
|
|
454
496
|
}
|
|
455
497
|
|
|
498
|
+
function syncState(context, payload) {
|
|
499
|
+
if (!context?.state || !payload) return;
|
|
500
|
+
if (Array.isArray(payload.trace) && context.state.trace !== payload.trace) context.state.trace = payload.trace;
|
|
501
|
+
if (payload.wallMs != null && context.state.wallMs !== payload.wallMs) context.state.wallMs = payload.wallMs;
|
|
502
|
+
}
|
|
503
|
+
|
|
456
504
|
export function renderSupernovaResult(resultArg, optionsArg, themeArg, contextArg) {
|
|
457
505
|
const { result, expanded, isPartial, theme, context, args, options, host } = normalizeResultRenderArgs(
|
|
458
506
|
resultArg,
|
|
@@ -462,34 +510,13 @@ export function renderSupernovaResult(resultArg, optionsArg, themeArg, contextAr
|
|
|
462
510
|
);
|
|
463
511
|
|
|
464
512
|
const payload = result?.details;
|
|
465
|
-
|
|
466
|
-
if (Array.isArray(payload.trace) && context.state.trace !== payload.trace) {
|
|
467
|
-
context.state.trace = payload.trace;
|
|
468
|
-
}
|
|
469
|
-
if (payload.wallMs != null && context.state.wallMs !== payload.wallMs) {
|
|
470
|
-
context.state.wallMs = payload.wallMs;
|
|
471
|
-
}
|
|
472
|
-
}
|
|
473
|
-
|
|
474
|
-
const isErr = result?.isError || payload?.ok === false;
|
|
475
|
-
const view = buildResultBody(theme, { payload, context, args, expanded, isPartial, isError: isErr });
|
|
513
|
+
syncState(context, payload);
|
|
476
514
|
|
|
477
|
-
|
|
478
|
-
if (isErr) {
|
|
479
|
-
body += (body ? "\n" : "") + theme.fg("error", payload?.error ? cleanBlockText(payload.error) : "error");
|
|
480
|
-
if (expanded && payload?.logs?.length) {
|
|
481
|
-
body += `\n${theme.fg("dim", "── logs ──")}`;
|
|
482
|
-
for (const log of payload.logs.slice(0, 24)) body += `\n${theme.fg("dim", cleanBlockText(log))}`;
|
|
483
|
-
}
|
|
484
|
-
}
|
|
485
|
-
const wall = payload?.wallMs != null ? `${payload.wallMs}ms` : "";
|
|
486
|
-
const calls = view.opCount > 0 ? `${view.opCount} call${view.opCount === 1 ? "" : "s"}` : "";
|
|
487
|
-
const status = isErr ? "failed" : isPartial ? "running" : calls ? "" : "complete";
|
|
488
|
-
const description = [calls, status, wall].filter(Boolean).join(" · ");
|
|
515
|
+
const isError = result?.isError || payload?.ok === false;
|
|
489
516
|
const previous = host === "omp" ? options?.lastComponent : context?.lastComponent;
|
|
490
517
|
const comp = previous instanceof UnifiedResultCard ? previous : new UnifiedResultCard();
|
|
491
518
|
if (host === "omp" && options) options.lastComponent = comp;
|
|
492
519
|
else if (context) context.lastComponent = comp;
|
|
493
|
-
comp.set(theme, {
|
|
520
|
+
comp.set(theme, { payload, context, args, expanded, isPartial, isError });
|
|
494
521
|
return comp;
|
|
495
522
|
}
|