pi-supernova 0.8.2 → 0.9.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 +188 -51
- package/docs/CHANGELOG.md +86 -1
- package/docs/TOKEN_COSTS.md +38 -0
- package/index.js +10 -175
- package/package.json +2 -1
- package/src/adapters/bash.js +14 -30
- package/src/adapters/errors.js +1 -9
- package/src/adapters/read-focus.js +98 -0
- package/src/adapters/read-image.js +51 -0
- package/src/adapters/read-json.js +42 -0
- package/src/adapters/read-text.js +71 -0
- package/src/adapters/read.js +66 -635
- package/src/bridge/catalog.js +3 -2
- package/src/bridge/host-bridge.js +35 -167
- package/src/bridge/tool-registry.js +104 -0
- package/src/bridge/trace.js +41 -0
- package/src/context/evidence-graph.js +249 -0
- package/src/context/evidence-rank.js +153 -0
- package/src/context/evidence.js +10 -424
- package/src/context/query.js +71 -0
- package/src/context/repo-index.js +8 -162
- package/src/context/search-files.js +19 -0
- package/src/context/search.js +2 -24
- package/src/context/snap-search.js +202 -0
- package/src/context/snap.js +5 -266
- package/src/context/source-entry.js +112 -0
- package/src/contract/bash.js +6 -1
- package/src/contract/program.js +36 -0
- package/src/contract/read.js +8 -53
- package/src/fs/check.js +1 -1
- package/src/fs/commit.js +161 -0
- package/src/fs/diff.js +11 -15
- package/src/fs/directory.js +79 -0
- package/src/fs/file-io.js +100 -0
- package/src/fs/glob.js +54 -0
- package/src/fs/json-size.js +54 -0
- package/src/fs/lines.js +117 -0
- package/src/fs/read-window.js +74 -0
- package/src/fs/session-resource.js +50 -0
- package/src/fs/text-ops.js +7 -227
- package/src/fs/vfs.js +5 -239
- package/src/fs/workspace.js +2 -1
- package/src/output/bottleneck.js +13 -67
- package/src/output/final.js +114 -0
- package/src/output/format.js +94 -5
- package/src/output/outcome.js +91 -0
- package/src/runtime/batch-input.js +68 -0
- package/src/runtime/guest-api.js +281 -0
- package/src/runtime/guest-worker.js +62 -333
- package/src/runtime/parallel.js +41 -39
- package/src/runtime/program-batch.js +21 -75
- package/src/runtime/program-file.js +3 -11
- package/src/runtime/program.js +141 -0
- package/src/runtime/reference.js +6 -5
- package/src/runtime/runtime.js +77 -253
- package/src/runtime/worker-pool.js +91 -0
- package/src/shared/decode.js +22 -8
- package/src/shared/image-worker.js +30 -0
- package/src/shared/image.js +78 -0
- package/src/shared/png.js +57 -0
- package/src/shared/result.js +77 -0
- package/src/shared/syntax-context.js +61 -3
- package/src/ui/host-render.js +104 -0
- package/src/ui/progress.js +51 -0
- package/src/ui/render.js +21 -421
- package/src/ui/trace.js +277 -0
package/src/ui/render.js
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import {normalizeCallRenderArgs,normalizeResultRenderArgs} from './host-render.js';
|
|
2
|
+
export {normalizeCallRenderArgs} from './host-render.js';
|
|
3
|
+
import {formatDiffRows,operationsFromTrace,formatDuration,formatOpRow,cleanBlockText} from './trace.js';
|
|
1
4
|
/**
|
|
2
5
|
* Supernova TUI renderers.
|
|
3
6
|
*
|
|
@@ -10,313 +13,13 @@
|
|
|
10
13
|
* lifecycle never duplicates; mutating operations include bounded inline diffs.
|
|
11
14
|
*/
|
|
12
15
|
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
15
|
-
import { measureWidth, hardTruncate, clampLine, fitPath, wrapLine } from "./render-measure.js";
|
|
16
|
+
import { isString, isObject } from "../shared/decode.js";
|
|
17
|
+
import { measureWidth, hardTruncate, clampLine, wrapLine } from "./render-measure.js";
|
|
16
18
|
import { novaFramedBlock, novaStatusLine } from "./omp-frame.js";
|
|
17
19
|
import { formatValue } from "../output/format.js";
|
|
18
20
|
|
|
19
21
|
export { measureWidth, hardTruncate, clampLine };
|
|
20
22
|
|
|
21
|
-
function diffGut(theme, item) {
|
|
22
|
-
const num = item.lineNum || 0;
|
|
23
|
-
|
|
24
|
-
if (item.type === "remove") {
|
|
25
|
-
const gut = theme.fg("toolDiffRemoved", `-${num}`.padStart(5));
|
|
26
|
-
|
|
27
|
-
return `${gut}${theme.fg("borderMuted", " │ ")}${theme.fg("toolDiffRemoved", `- ${cleanInlineText(item.text)}`)}`;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
if (item.type === "add") {
|
|
31
|
-
const gut = theme.fg("toolDiffAdded", `+${num}`.padStart(5));
|
|
32
|
-
|
|
33
|
-
return `${gut}${theme.fg("borderMuted", " │ ")}${theme.fg("toolDiffAdded", `+ ${cleanInlineText(item.text)}`)}`;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
const gut = theme.fg("dim", ` ${num}`.padStart(5));
|
|
37
|
-
|
|
38
|
-
return `${gut}${theme.fg("borderMuted", " │ ")}${theme.fg("toolDiffContext", ` ${cleanInlineText(item.text)}`)}`;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
function formatDiffRows(diff, theme, maxShown = 6) {
|
|
42
|
-
if (!diff || !Array.isArray(diff.lines) || diff.lines.length === 0) return [];
|
|
43
|
-
const body = [];
|
|
44
|
-
|
|
45
|
-
for (const item of diff.lines.slice(0, maxShown)) body.push(diffGut(theme, item));
|
|
46
|
-
const displayLineCount = Number.isInteger(diff.displayLineCount) ? diff.displayLineCount : diff.lines.length;
|
|
47
|
-
|
|
48
|
-
if (displayLineCount > maxShown) {
|
|
49
|
-
body.push(theme.fg("dim", ` │ … ${displayLineCount - maxShown} more lines`));
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
return body;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
function stripUnsafeControls(value) {
|
|
56
|
-
// Exactly the C0/DEL/C1 ranges previously filtered code point by code point.
|
|
57
|
-
// Native replacement avoids rebuilding every already-clean Unicode string.
|
|
58
|
-
// eslint-disable-next-line no-control-regex -- intentional terminal-control filtering
|
|
59
|
-
return value.replace(/[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f-\u009f]/g, "");
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
function cleanBlockText(value) {
|
|
63
|
-
const normalized = stripVTControlCharacters(String(value ?? "")).replace(/\r\n?/g, "\n");
|
|
64
|
-
|
|
65
|
-
return stripUnsafeControls(normalized).replace(/[\u061c\u200e\u200f\u202a-\u202e\u2066-\u2069]/g, "");
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
function cleanInlineText(value) {
|
|
69
|
-
return cleanBlockText(value).replace(/\s*\n\s*/g, " ").trim();
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
function displayOperation(tool, target, diff, ok, item) {
|
|
73
|
-
const rawName = cleanInlineText(tool);
|
|
74
|
-
|
|
75
|
-
if (!rawName) return null;
|
|
76
|
-
const normalized = rawName === "apply_patch" ? "patch" : rawName;
|
|
77
|
-
|
|
78
|
-
return { tool: normalized, target, diff, ok, ms: item?.ms, exitCode: item?.exitCode, time: item?.time, error: item?.error };
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
function isTheme(value) {
|
|
82
|
-
return isObject(value) && isFunction(value.fg);
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
/**
|
|
86
|
-
* Dual-host renderCall args:
|
|
87
|
-
* Pi: (args, theme, context)
|
|
88
|
-
* OMP: (args, options/renderState, theme)
|
|
89
|
-
*/
|
|
90
|
-
export function normalizeCallRenderArgs(a, b, c) {
|
|
91
|
-
if (isTheme(b)) {
|
|
92
|
-
const context = isObject(c) ? c : {};
|
|
93
|
-
|
|
94
|
-
if (!isObject(context.state)) context.state = {};
|
|
95
|
-
|
|
96
|
-
return { args: a, theme: b, context, host: "pi" };
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
if (isTheme(c)) {
|
|
100
|
-
const options = isObject(b) ? b : {};
|
|
101
|
-
|
|
102
|
-
if (!isObject(options.state)) options.state = {};
|
|
103
|
-
|
|
104
|
-
const context = {
|
|
105
|
-
...options,
|
|
106
|
-
state: options.state,
|
|
107
|
-
expanded: options.expanded,
|
|
108
|
-
isPartial: options.isPartial,
|
|
109
|
-
executionStarted: options.executionStarted,
|
|
110
|
-
argsComplete: options.argsComplete,
|
|
111
|
-
lastComponent: options.lastComponent,
|
|
112
|
-
invalidate: options.invalidate,
|
|
113
|
-
};
|
|
114
|
-
|
|
115
|
-
return { args: a, theme: c, context, host: "omp", options };
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
throw new Error("supernova renderCall: theme missing (expected Pi or OMP signature)");
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
/**
|
|
122
|
-
* Dual-host renderResult args:
|
|
123
|
-
* Pi: (result, {expanded,isPartial}, theme, context)
|
|
124
|
-
* OMP: (result, {expanded,isPartial}, theme, args) (4th is args, not context)
|
|
125
|
-
*
|
|
126
|
-
* Call shapes share the first three positions, so host is inferred from the
|
|
127
|
-
* fourth argument's context-versus-args shape.
|
|
128
|
-
*/
|
|
129
|
-
function contextFrom(opts, ctxOrArgs) {
|
|
130
|
-
if (isObject(ctxOrArgs) && !isTheme(ctxOrArgs)) {
|
|
131
|
-
if ("lastComponent" in ctxOrArgs || "state" in ctxOrArgs || "invalidate" in ctxOrArgs) return ctxOrArgs;
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
return { state: opts.state, lastComponent: opts.lastComponent };
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
function isRenderContext(value) {
|
|
138
|
-
return "lastComponent" in value || "invalidate" in value;
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
function isToolArgs(value) {
|
|
142
|
-
return "code" in value || "file" in value || "programs" in value || "timeoutMs" in value;
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
function detectResultHost(options, ctxOrArgs) {
|
|
146
|
-
if (isTheme(options)) return "pi";
|
|
147
|
-
|
|
148
|
-
if (!isObject(ctxOrArgs)) return "pi";
|
|
149
|
-
|
|
150
|
-
if (isRenderContext(ctxOrArgs)) return "pi";
|
|
151
|
-
|
|
152
|
-
if (isToolArgs(ctxOrArgs)) return "omp";
|
|
153
|
-
|
|
154
|
-
return "pi";
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
function ensureState(context) {
|
|
158
|
-
if (!isObject(context.state)) context.state = {};
|
|
159
|
-
|
|
160
|
-
return context;
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
function resultArgs(ctxOrArgs, context) {
|
|
164
|
-
return ctxOrArgs?.code || ctxOrArgs?.file || ctxOrArgs?.programs ? ctxOrArgs : context.args;
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
function piResultArgs(result, options, theme, ctxOrArgs) {
|
|
168
|
-
const opts = isObject(options) ? options : {};
|
|
169
|
-
const context = ensureState(contextFrom(opts, ctxOrArgs));
|
|
170
|
-
|
|
171
|
-
return {
|
|
172
|
-
result,
|
|
173
|
-
expanded: !!opts.expanded,
|
|
174
|
-
isPartial: !!opts.isPartial,
|
|
175
|
-
theme,
|
|
176
|
-
context,
|
|
177
|
-
args: resultArgs(ctxOrArgs, context),
|
|
178
|
-
host: detectResultHost(options, ctxOrArgs),
|
|
179
|
-
options: opts,
|
|
180
|
-
};
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
function oddballResultArgs(result, theme, themeOrCtx) {
|
|
184
|
-
const context = ensureState(isObject(themeOrCtx) ? themeOrCtx : {});
|
|
185
|
-
|
|
186
|
-
return {
|
|
187
|
-
result,
|
|
188
|
-
expanded: !!context.expanded,
|
|
189
|
-
isPartial: !!context.isPartial,
|
|
190
|
-
theme,
|
|
191
|
-
context,
|
|
192
|
-
args: context.args,
|
|
193
|
-
host: "pi",
|
|
194
|
-
options: {},
|
|
195
|
-
};
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
function normalizeResultRenderArgs(result, options, themeOrCtx, ctxOrArgs) {
|
|
199
|
-
if (isTheme(themeOrCtx)) return piResultArgs(result, options, themeOrCtx, ctxOrArgs);
|
|
200
|
-
|
|
201
|
-
if (isTheme(options)) return oddballResultArgs(result, options, themeOrCtx);
|
|
202
|
-
|
|
203
|
-
throw new Error("supernova renderResult: theme missing (expected Pi or OMP signature)");
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
function batchTarget(paths) {
|
|
207
|
-
const names = paths.map((p) => String(p).replace(/\\/g, "/").split("/").pop());
|
|
208
|
-
|
|
209
|
-
return `${paths.length} files: ${names.join(", ")}`;
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
const OPERATION_TARGETS = [
|
|
213
|
-
[(item) => item?.name === "snap", (item, args) => {
|
|
214
|
-
const query = args.query ? `"${args.query}"` : "";
|
|
215
|
-
|
|
216
|
-
if (!args.path) return query;
|
|
217
|
-
|
|
218
|
-
return `${query} → ${args.path}`;
|
|
219
|
-
}],
|
|
220
|
-
[(item) => item?.name === "search", (item, args) => (args.query ? `"${args.query}"` : "")],
|
|
221
|
-
[(item, args) => Array.isArray(args.path), (item, args) => batchTarget(args.path)],
|
|
222
|
-
[(item, args) => args.path, (item, args) => String(args.path)],
|
|
223
|
-
[(item) => item?.diff?.path, (item) => String(item.diff.path)],
|
|
224
|
-
[(item, args) => args.target && isString(args.target), (item, args) => args.target],
|
|
225
|
-
[(item, args) => args.command, (item, args) => String(args.command)],
|
|
226
|
-
[(item, args) => args.pattern, (item, args) => String(args.pattern)],
|
|
227
|
-
[(item, args) => args.query, (item, args) => String(args.query)],
|
|
228
|
-
];
|
|
229
|
-
|
|
230
|
-
function operationTarget(item) {
|
|
231
|
-
const args = item?.args || {};
|
|
232
|
-
|
|
233
|
-
for (const [predicate, formatter] of OPERATION_TARGETS) {
|
|
234
|
-
if (predicate(item, args)) return formatter(item, args);
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
return "";
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
function parseDiffLine(rawLine) {
|
|
241
|
-
const signed = /^([+-])\s*(\d+)\s?(.*)$/.exec(rawLine);
|
|
242
|
-
|
|
243
|
-
if (signed) return { type: signed[1] === "+" ? "add" : "remove", lineNum: Number(signed[2]), text: signed[3] };
|
|
244
|
-
const contextual = /^\s+(\d+)\s?(.*)$/.exec(rawLine);
|
|
245
|
-
|
|
246
|
-
if (contextual) return { type: "context", lineNum: Number(contextual[1]), text: contextual[2] };
|
|
247
|
-
|
|
248
|
-
return null;
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
// Text diffs are immutable, even when hosts replace trace snapshots on each frame.
|
|
252
|
-
// Cache only parsed data, not theme, paths or mutable result objects.
|
|
253
|
-
const textDiffCache = new Map();
|
|
254
|
-
|
|
255
|
-
let cachedDiffChars = 0;
|
|
256
|
-
|
|
257
|
-
const MAX_CACHED_DIFF_CHARS = 1_000_000;
|
|
258
|
-
|
|
259
|
-
function rememberDiff(diff, parsed) {
|
|
260
|
-
if (diff.length > MAX_CACHED_DIFF_CHARS) return parsed;
|
|
261
|
-
|
|
262
|
-
while (textDiffCache.size >= 24 || cachedDiffChars + diff.length > MAX_CACHED_DIFF_CHARS) {
|
|
263
|
-
const oldest = textDiffCache.keys().next().value;
|
|
264
|
-
textDiffCache.delete(oldest);
|
|
265
|
-
cachedDiffChars -= oldest.length;
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
textDiffCache.set(diff, parsed);
|
|
269
|
-
cachedDiffChars += diff.length;
|
|
270
|
-
|
|
271
|
-
return parsed;
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
function tallyDiffLine(parsed, counts) {
|
|
275
|
-
if (parsed.type === "add") counts.added += 1;
|
|
276
|
-
else if (parsed.type === "remove") counts.removed += 1;
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
function parseTraceDiffText(diff) {
|
|
280
|
-
const lines = [];
|
|
281
|
-
const counts = { added: 0, removed: 0, displayLineCount: 0 };
|
|
282
|
-
|
|
283
|
-
for (const rawLine of cleanBlockText(diff).split("\n")) {
|
|
284
|
-
const parsed = parseDiffLine(rawLine);
|
|
285
|
-
|
|
286
|
-
if (!parsed) continue;
|
|
287
|
-
tallyDiffLine(parsed, counts);
|
|
288
|
-
counts.displayLineCount++;
|
|
289
|
-
|
|
290
|
-
if (lines.length < 24) lines.push(parsed);
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
if (lines.length === 0) return undefined;
|
|
294
|
-
|
|
295
|
-
return { added: counts.added, removed: counts.removed, lines, displayLineCount: counts.displayLineCount };
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
function normalizeTraceDiff(item) {
|
|
299
|
-
const diff = item?.diff;
|
|
300
|
-
|
|
301
|
-
if (isObject(diff)) return diff;
|
|
302
|
-
|
|
303
|
-
if (!isString(diff) || !diff.trim()) return undefined;
|
|
304
|
-
const cached = textDiffCache.get(diff);
|
|
305
|
-
|
|
306
|
-
if (cached) return cached;
|
|
307
|
-
const parsed = parseTraceDiffText(diff);
|
|
308
|
-
|
|
309
|
-
return parsed ? rememberDiff(diff, parsed) : undefined;
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
function operationsFromTrace(trace) {
|
|
313
|
-
if (!Array.isArray(trace)) return [];
|
|
314
|
-
|
|
315
|
-
return trace
|
|
316
|
-
.map((item) => displayOperation(item?.name || "tool", operationTarget(item), normalizeTraceDiff(item), item?.ok, item))
|
|
317
|
-
.filter(Boolean);
|
|
318
|
-
}
|
|
319
|
-
|
|
320
23
|
// The call slot is always empty: the result card owns the whole lifecycle in both hosts.
|
|
321
24
|
const EMPTY_CALL = { render: () => [], invalidate() {} };
|
|
322
25
|
|
|
@@ -329,116 +32,6 @@ export function renderSupernovaCall(a, b, c) {
|
|
|
329
32
|
return EMPTY_CALL;
|
|
330
33
|
}
|
|
331
34
|
|
|
332
|
-
const TOOL_COL = 7;
|
|
333
|
-
|
|
334
|
-
const DURATION_COL = 6;
|
|
335
|
-
|
|
336
|
-
function formatDuration(ms) {
|
|
337
|
-
if (!Number.isFinite(ms) || ms < 0) return "";
|
|
338
|
-
|
|
339
|
-
if (ms < 1000) return `${Math.round(ms)}ms`;
|
|
340
|
-
|
|
341
|
-
if (ms < 60000) return `${(ms / 1000).toFixed(1)}s`;
|
|
342
|
-
const minutes = Math.floor(ms / 60000);
|
|
343
|
-
const seconds = Math.round((ms % 60000) / 1000);
|
|
344
|
-
|
|
345
|
-
return `${minutes}m${String(seconds).padStart(2, "0")}s`;
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
/** First meaningful line of a shell command plus a count of the hidden remainder. */
|
|
349
|
-
function summarizeCommand(raw) {
|
|
350
|
-
const lines = cleanBlockText(raw).split("\n").map((line) => line.trim()).filter(Boolean);
|
|
351
|
-
|
|
352
|
-
if (lines.length === 0) return "";
|
|
353
|
-
const first = lines[0].replace(/\s+/g, " ");
|
|
354
|
-
|
|
355
|
-
return lines.length > 1 ? `${first} …+${lines.length - 1} lines` : first;
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
const SHELL_TOOLS = ["bash", "exec"];
|
|
359
|
-
|
|
360
|
-
const SEARCH_TOOLS = ["snap", "search"];
|
|
361
|
-
|
|
362
|
-
function formatTarget(op, budget) {
|
|
363
|
-
if (SHELL_TOOLS.includes(op.tool)) return clampLine(summarizeCommand(op.target), budget);
|
|
364
|
-
const text = cleanInlineText(op.target);
|
|
365
|
-
|
|
366
|
-
if (!text) return "";
|
|
367
|
-
|
|
368
|
-
if (SEARCH_TOOLS.includes(op.tool)) return clampLine(text, budget);
|
|
369
|
-
|
|
370
|
-
return fitPath(text, budget);
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
function opMarker(theme, op, isPartial, isError) {
|
|
374
|
-
if (op.ok === false) return theme.fg("error", "×");
|
|
375
|
-
|
|
376
|
-
if (op.mutationAttempt) return theme.fg("warning", "·");
|
|
377
|
-
|
|
378
|
-
if (op.ok === true) return theme.fg("success", "✓");
|
|
379
|
-
|
|
380
|
-
if (isPartial) return theme.fg("dim", "·");
|
|
381
|
-
|
|
382
|
-
if (isError) return theme.fg("error", "×");
|
|
383
|
-
|
|
384
|
-
return theme.fg("success", "✓");
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
function opDuration(op, isPartial) {
|
|
388
|
-
if (Number.isFinite(op.ms)) return formatDuration(op.ms);
|
|
389
|
-
|
|
390
|
-
if (isPartial && op.ok === undefined && Number.isFinite(op.time)) return formatDuration(Date.now() - op.time) + "…";
|
|
391
|
-
|
|
392
|
-
return "";
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
function appendExit(theme, op) {
|
|
396
|
-
if (!Number.isInteger(op.exitCode)) return { text: "", width: 0 };
|
|
397
|
-
const exit = `exit ${op.exitCode}`;
|
|
398
|
-
|
|
399
|
-
return { text: theme.fg("error", exit) + " ", width: exit.length + 2 };
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
function appendDiffCounts(theme, op) {
|
|
403
|
-
if (!(op.diff && isObject(op.diff))) return { text: "", width: 0 };
|
|
404
|
-
const added = `+${op.diff.added || 0}`;
|
|
405
|
-
const removed = `-${op.diff.removed || 0}`;
|
|
406
|
-
|
|
407
|
-
return {
|
|
408
|
-
text: theme.fg("toolDiffAdded", added) + theme.fg("dim", "/") + theme.fg("toolDiffRemoved", removed) + " ",
|
|
409
|
-
width: added.length + 1 + removed.length + 1,
|
|
410
|
-
};
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
function opRowSuffix(theme, op, prefix, budget) {
|
|
414
|
-
const target = formatTarget(op, budget);
|
|
415
|
-
|
|
416
|
-
if (target) return prefix + theme.fg("muted", target);
|
|
417
|
-
|
|
418
|
-
if (op.ok === false && op.error) return prefix + theme.fg("error", clampLine(cleanInlineText(op.error), budget));
|
|
419
|
-
|
|
420
|
-
return prefix.trimEnd();
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
/**
|
|
424
|
-
* One aligned row: marker · tool · duration · [exit N] · [+a/-r] · target.
|
|
425
|
-
* Fixed columns keep a ledger of mixed calls scannable at a glance.
|
|
426
|
-
*/
|
|
427
|
-
function formatOpRow(theme, op, width, isPartial, isError) {
|
|
428
|
-
const marker = opMarker(theme, op, isPartial, isError);
|
|
429
|
-
const toolText = op.tool.slice(0, TOOL_COL).padEnd(TOOL_COL);
|
|
430
|
-
const tool = theme.fg("syntaxFunction", toolText);
|
|
431
|
-
const durationText = opDuration(op, isPartial).slice(-DURATION_COL);
|
|
432
|
-
const duration = theme.fg("dim", durationText.padStart(DURATION_COL));
|
|
433
|
-
const exit = appendExit(theme, op);
|
|
434
|
-
const counts = appendDiffCounts(theme, op);
|
|
435
|
-
const outcome = op.mutationAttempt ? "attempted " : "";
|
|
436
|
-
const prefix = `${marker} ${tool} ${duration} ` + exit.text + counts.text + theme.fg("warning", outcome);
|
|
437
|
-
const used = 2 + toolText.length + 1 + DURATION_COL + 2 + exit.width + counts.width + outcome.length;
|
|
438
|
-
|
|
439
|
-
return opRowSuffix(theme, op, prefix, Math.max(1, width - used));
|
|
440
|
-
}
|
|
441
|
-
|
|
442
35
|
function traceFor(payload, context) {
|
|
443
36
|
const trace = payload?.trace || context?.state?.trace;
|
|
444
37
|
|
|
@@ -472,13 +65,16 @@ function appendError(lines, theme, payload, _expanded, width) {
|
|
|
472
65
|
}
|
|
473
66
|
|
|
474
67
|
function appendMutations(lines, theme, payload, width) {
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
68
|
+
const m = payload?.mutations;
|
|
69
|
+
if (!m || !["committed", "rolledBack", "external", "pendingCommits", "recoveryFailed"].some(key => m[key])) return;
|
|
70
|
+
const extra = [
|
|
71
|
+
[m.external, "; external calls attempted=" + m.external],
|
|
72
|
+
[m.pendingCommits, "; pendingCommits=" + m.pendingCommits],
|
|
73
|
+
[m.recoveryFailed, "; recovery failed: inspect files"],
|
|
74
|
+
].filter(([enabled]) => enabled).map(([, text]) => text).join("");
|
|
75
|
+
const summary = "file versions: committed=" + (m.committed || 0) + " rolledBack=" + (m.rolledBack || 0) + extra;
|
|
76
|
+
const role = m.rolledBack || m.recoveryFailed ? "warning" : "dim";
|
|
77
|
+
for (const line of resultLines(summary, width)) lines.push(theme.fg(role, line));
|
|
482
78
|
}
|
|
483
79
|
|
|
484
80
|
function appendResult(lines, theme, payload, expanded, width) {
|
|
@@ -526,8 +122,7 @@ function buildBodyLines(theme, width, { payload, context, expanded, isPartial, i
|
|
|
526
122
|
const ops = operationsFromTrace(visibleTrace(trace, maxOps, isPartial));
|
|
527
123
|
// Trace success records an operation, not persistence of every staged version.
|
|
528
124
|
// Mixed rollback/commit counts cannot safely be attributed to individual rows.
|
|
529
|
-
for (const op of ops) op.mutationAttempt = op
|
|
530
|
-
&& (isPartial || isError || payload?.mutations?.rolledBack > 0 || payload?.mutations?.recoveryFailed);
|
|
125
|
+
for (const op of ops) op.mutationAttempt = mutationAttempt(op, payload, isPartial, isError);
|
|
531
126
|
const lines = [];
|
|
532
127
|
appendOps(lines, theme, ops, maxOps, maxDiffLines, width, isPartial, isError);
|
|
533
128
|
appendOverflow(lines, theme, trace, maxOps, isPartial);
|
|
@@ -651,3 +246,8 @@ export function renderSupernovaResult(resultArg, optionsArg, themeArg, contextAr
|
|
|
651
246
|
|
|
652
247
|
return comp;
|
|
653
248
|
}
|
|
249
|
+
|
|
250
|
+
function mutationAttempt(op, payload, isPartial, isError) {
|
|
251
|
+
return op.ok === true && ["write", "edit", "patch"].includes(op.tool)
|
|
252
|
+
&& (isPartial || isError || payload?.mutations?.rolledBack > 0 || payload?.mutations?.recoveryFailed);
|
|
253
|
+
}
|