pi-supernova 0.10.1 → 0.11.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 +376 -221
- package/docs/CHANGELOG.md +107 -5
- package/docs/TOKEN_COSTS.md +149 -324
- package/index.js +40 -11
- package/package.json +4 -3
- package/src/adapters/bash.js +40 -10
- package/src/adapters/edit.js +77 -23
- package/src/adapters/errors.js +4 -0
- package/src/adapters/index.js +1 -0
- package/src/adapters/list.js +6 -3
- package/src/adapters/read-focus.js +6 -0
- package/src/adapters/read-image.js +17 -9
- package/src/adapters/read-indexed.js +72 -0
- package/src/adapters/read-text.js +19 -2
- package/src/adapters/read.js +57 -13
- package/src/adapters/refs.js +17 -7
- package/src/adapters/write.js +14 -21
- package/src/bridge/host-bridge.js +152 -39
- package/src/bridge/invoke.js +3 -1
- package/src/bridge/tool-registry.js +132 -16
- package/src/context/evidence-graph.js +1 -0
- package/src/context/evidence-rank.js +1 -0
- package/src/context/evidence.js +4 -2
- package/src/context/fuzzy.js +31 -8
- package/src/context/outline.js +33 -9
- package/src/context/query.js +7 -2
- package/src/context/repo-index.js +8 -3
- package/src/context/search-files.js +20 -0
- package/src/context/search.js +25 -10
- package/src/context/snap-search.js +28 -6
- package/src/context/snap.js +8 -3
- package/src/context/source-entry.js +64 -28
- package/src/context/spans.js +1 -0
- package/src/contract/bash.js +16 -7
- package/src/contract/edit.js +26 -7
- package/src/contract/program.js +2 -1
- package/src/contract/read.js +46 -46
- package/src/fs/background.js +10 -2
- package/src/fs/check.js +844 -46
- package/src/fs/commit.js +89 -15
- package/src/fs/diff.js +12 -9
- package/src/fs/directory.js +14 -4
- package/src/fs/file-io.js +28 -7
- package/src/fs/glob.js +93 -11
- package/src/fs/json-read.js +3 -2
- package/src/fs/json-size.js +2 -1
- package/src/fs/lines.js +49 -2
- package/src/fs/patch.js +26 -10
- package/src/fs/process-tree.js +19 -1
- package/src/fs/read-window.js +36 -20
- package/src/fs/source-window.js +5 -10
- package/src/fs/text-ops.js +39 -19
- package/src/fs/vfs.js +296 -41
- package/src/fs/workspace.js +24 -6
- package/src/output/bottleneck.js +50 -25
- package/src/output/final.js +20 -0
- package/src/output/format.js +97 -36
- package/src/output/outcome.js +25 -9
- package/src/runtime/batch-input.js +17 -3
- package/src/runtime/guest-api.js +32 -7
- package/src/runtime/guest-worker.js +22 -4
- package/src/runtime/parallel.js +9 -49
- package/src/runtime/program-batch.js +53 -28
- package/src/runtime/program-file.js +2 -1
- package/src/runtime/program.js +94 -41
- package/src/runtime/reference.js +10 -9
- package/src/runtime/runtime.js +86 -17
- package/src/runtime/worker-pool.js +10 -1
- package/src/shared/decode.js +9 -8
- package/src/shared/image-worker.js +7 -0
- package/src/shared/image.js +58 -15
- package/src/shared/png.js +18 -0
- package/src/shared/result.js +30 -9
- package/src/shared/syntax-context.js +7 -1
- package/src/shared/utf8.js +11 -13
- package/src/ui/host-render.js +4 -0
- package/src/ui/omp-frame.js +2 -0
- package/src/ui/progress.js +2 -1
- package/src/ui/render-measure.js +11 -0
- package/src/ui/render.js +92 -31
- package/src/ui/trace.js +8 -5
|
@@ -16,13 +16,19 @@ export function sourceContext(source, line, column, lineBreaks = JS_LINES) {
|
|
|
16
16
|
if (text === undefined) return "";
|
|
17
17
|
const located = Number.isInteger(column) && column >= 0;
|
|
18
18
|
const position = located ? Math.min(column, text.length) : 0;
|
|
19
|
+
const {shown,caret} = renderSourceWindow(text, position, located);
|
|
20
|
+
|
|
21
|
+
return "\n " + shown + (caret ? "\n " + caret : "");
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function renderSourceWindow(text, position, located) {
|
|
19
25
|
const start = Math.max(0, Math.min(position - 80, text.length - 160));
|
|
20
26
|
const end = Math.min(text.length, start + 160);
|
|
21
27
|
const prefix = start > 0 ? "…" : "";
|
|
22
28
|
const shown = prefix + displaySource(text.slice(start, end)) + (end < text.length ? "…" : "");
|
|
23
29
|
const caret = located ? " ".repeat(stringWidth(prefix + displaySource(text.slice(start, position)))) + "^" : "";
|
|
24
30
|
|
|
25
|
-
return
|
|
31
|
+
return {shown,caret};
|
|
26
32
|
}
|
|
27
33
|
|
|
28
34
|
/** acorn-style error that carries a loc, when it has one. */
|
package/src/shared/utf8.js
CHANGED
|
@@ -1,17 +1,15 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
try { return new TextDecoder("utf-8", { fatal: true }).decode(bytes); }
|
|
4
|
-
catch {
|
|
5
|
-
throw new Error("not valid UTF-8 (binary or non-UTF-8 text): " + target + "; inspect or convert it with bash (for example iconv -f latin1 -t utf8 or xxd)");
|
|
6
|
-
}
|
|
1
|
+
function invalidUtf8(target) {
|
|
2
|
+
return new Error("not valid UTF-8 (binary or non-UTF-8 text): " + target + "; inspect or convert it with bash (for example iconv -f latin1 -t utf8 or xxd)");
|
|
7
3
|
}
|
|
8
4
|
|
|
9
|
-
/**
|
|
10
|
-
export function
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
}
|
|
5
|
+
/** Strict text fidelity includes the BOM; individual formats may consume it. */
|
|
6
|
+
export function decodeUtf8Strict(bytes, target) {
|
|
7
|
+
try { return new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(bytes); }
|
|
8
|
+
catch { throw invalidUtf8(target); }
|
|
9
|
+
}
|
|
15
10
|
|
|
16
|
-
|
|
11
|
+
/** Streaming decode omits only an incomplete tail, never corrupt interior bytes. */
|
|
12
|
+
export function decodeUtf8Window(bytes, target = "read window") {
|
|
13
|
+
try { return new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(bytes, { stream: true }); }
|
|
14
|
+
catch { throw invalidUtf8(target); }
|
|
17
15
|
}
|
package/src/ui/host-render.js
CHANGED
|
@@ -66,6 +66,7 @@ function isToolArgs(value) {
|
|
|
66
66
|
|
|
67
67
|
function detectResultHost(options, ctxOrArgs) {
|
|
68
68
|
if (isTheme(options) || !isObject(ctxOrArgs) || isRenderContext(ctxOrArgs)) return "pi";
|
|
69
|
+
|
|
69
70
|
return isToolArgs(ctxOrArgs) ? "omp" : "pi";
|
|
70
71
|
}
|
|
71
72
|
|
|
@@ -82,6 +83,7 @@ function resultArgs(ctxOrArgs, context) {
|
|
|
82
83
|
function piResultArgs(result, options, theme, ctxOrArgs) {
|
|
83
84
|
const opts = isObject(options) ? options : {};
|
|
84
85
|
const context = ensureState(contextFrom(opts, ctxOrArgs));
|
|
86
|
+
|
|
85
87
|
return resultRenderModel(result, theme, context, opts, opts, detectResultHost(options, ctxOrArgs), resultArgs(ctxOrArgs, context));
|
|
86
88
|
}
|
|
87
89
|
|
|
@@ -91,6 +93,7 @@ function resultRenderModel(result, theme, context, flags, options, host, args) {
|
|
|
91
93
|
|
|
92
94
|
function oddballResultArgs(result, theme, themeOrCtx) {
|
|
93
95
|
const context = ensureState(isObject(themeOrCtx) ? themeOrCtx : {});
|
|
96
|
+
|
|
94
97
|
return resultRenderModel(result, theme, context, context, {}, "pi", context.args);
|
|
95
98
|
}
|
|
96
99
|
|
|
@@ -101,4 +104,5 @@ function normalizeResultRenderArgs(result, options, themeOrCtx, ctxOrArgs) {
|
|
|
101
104
|
|
|
102
105
|
throw new Error("supernova renderResult: theme missing (expected Pi or OMP signature)");
|
|
103
106
|
}
|
|
107
|
+
|
|
104
108
|
export {normalizeResultRenderArgs};
|
package/src/ui/omp-frame.js
CHANGED
|
@@ -54,10 +54,12 @@ const STATUS_PREFIX = { error: ["error", "✗ "], running: ["dim", "… "] };
|
|
|
54
54
|
|
|
55
55
|
function statusHeader(theme, { title, description, state, icon }) {
|
|
56
56
|
const resolved = icon ?? (state === "error" ? "error" : undefined);
|
|
57
|
+
|
|
57
58
|
const fg = (key, text) => {
|
|
58
59
|
try { return isFunction(theme?.fg) ? theme.fg(key, text) : text; }
|
|
59
60
|
catch { return text; }
|
|
60
61
|
};
|
|
62
|
+
|
|
61
63
|
const prefixSpec = STATUS_PREFIX[resolved];
|
|
62
64
|
const prefix = prefixSpec ? fg(prefixSpec[0], prefixSpec[1]) : "";
|
|
63
65
|
const titleText = fg("accent", title);
|
package/src/ui/progress.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import {isFunction} from '../shared/decode.js';
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
// Leave headroom for Pi's own render coalescing while providing ~31Hz live state.
|
|
4
|
+
const PROGRESS_FRAME_MS = 32;
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* Live trace updates for the card. The first update is immediate (seeds the result slot);
|
package/src/ui/render-measure.js
CHANGED
|
@@ -102,11 +102,22 @@ function pushWrapSegment(out, current, columns, segment, size, width) {
|
|
|
102
102
|
|
|
103
103
|
/** Wrap complete, already-sanitized result text without splitting graphemes. */
|
|
104
104
|
export function wrapLine(line, width) {
|
|
105
|
+
width = Math.floor(width);
|
|
106
|
+
|
|
105
107
|
if (width <= 0) return [];
|
|
106
108
|
const text = String(line).replace(/\t/g, " ");
|
|
107
109
|
|
|
108
110
|
if (measureWidth(text) <= width) return [text];
|
|
109
111
|
const out = [];
|
|
112
|
+
|
|
113
|
+
// Sanitized printable ASCII is one column per UTF-16 unit. Bulk slicing
|
|
114
|
+
// avoids segmenting every source character; Unicode keeps the grapheme path.
|
|
115
|
+
if (/^[\x20-\x7e]*$/.test(text)) {
|
|
116
|
+
for (let start = 0; start < text.length; start += width) out.push(text.slice(start, start + width));
|
|
117
|
+
|
|
118
|
+
return out;
|
|
119
|
+
}
|
|
120
|
+
|
|
110
121
|
const current = { text: "" };
|
|
111
122
|
let columns = 0;
|
|
112
123
|
|
package/src/ui/render.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import {normalizeCallRenderArgs,normalizeResultRenderArgs} from './host-render.js';
|
|
2
|
+
|
|
2
3
|
export {normalizeCallRenderArgs} from './host-render.js';
|
|
3
|
-
|
|
4
|
+
|
|
5
|
+
import {formatDiffRows,operationsFromTrace,formatDuration,formatOpRow,cleanBlockText,cleanInlineText} from './trace.js';
|
|
4
6
|
/**
|
|
5
7
|
* Supernova TUI renderers.
|
|
6
8
|
*
|
|
@@ -16,7 +18,9 @@ import {formatDiffRows,operationsFromTrace,formatDuration,formatOpRow,cleanBlock
|
|
|
16
18
|
import { isString, isObject } from "../shared/decode.js";
|
|
17
19
|
import { measureWidth, hardTruncate, clampLine, wrapLine } from "./render-measure.js";
|
|
18
20
|
import { novaFramedBlock, novaStatusLine } from "./omp-frame.js";
|
|
19
|
-
import {
|
|
21
|
+
import { formatBoundedValue, truncateChars } from "../output/format.js";
|
|
22
|
+
|
|
23
|
+
const MAX_UI_RESULT_CHARS = 32_000;
|
|
20
24
|
|
|
21
25
|
export { measureWidth, hardTruncate, clampLine };
|
|
22
26
|
|
|
@@ -38,53 +42,95 @@ function traceFor(payload, context) {
|
|
|
38
42
|
return Array.isArray(trace) ? trace : [];
|
|
39
43
|
}
|
|
40
44
|
|
|
41
|
-
function resultLines(value, width) {
|
|
42
|
-
|
|
45
|
+
function resultLines(value, width, layout) {
|
|
46
|
+
// Bound display text before Unicode layout. Complete machine-facing values
|
|
47
|
+
// and the execution evidence remain untouched in result.details.
|
|
48
|
+
const text = isString(value) ? truncateChars(value, MAX_UI_RESULT_CHARS, "UI preview").text
|
|
49
|
+
: formatBoundedValue(value, MAX_UI_RESULT_CHARS);
|
|
50
|
+
|
|
51
|
+
const clean = cleanBlockText(text);
|
|
43
52
|
|
|
44
|
-
|
|
53
|
+
if (layout?.width === width && layout.text === clean) return layout.lines;
|
|
54
|
+
const lines = clean.split("\n").flatMap(line => wrapLine(line, width));
|
|
55
|
+
|
|
56
|
+
if (layout) Object.assign(layout, {width, text:clean, lines});
|
|
57
|
+
|
|
58
|
+
return lines;
|
|
45
59
|
}
|
|
46
60
|
|
|
47
|
-
function appendOps(lines, theme, ops, maxOps, maxDiffLines, width, isPartial, isError) {
|
|
61
|
+
function appendOps(lines, theme, ops, maxOps, maxDiffLines, width, isPartial, isError, expanded) {
|
|
48
62
|
for (const op of ops.slice(0, maxOps)) {
|
|
49
63
|
lines.push(formatOpRow(theme, op, width, isPartial, isError));
|
|
50
64
|
|
|
51
|
-
if (maxDiffLines === 0 || !op.diff || !isObject(op.diff)) continue;
|
|
65
|
+
if (!expanded || maxDiffLines === 0 || !op.diff || !isObject(op.diff)) continue;
|
|
52
66
|
|
|
53
67
|
for (const row of formatDiffRows(op.diff, theme, maxDiffLines)) lines.push(" " + row);
|
|
54
68
|
}
|
|
55
69
|
|
|
56
|
-
if (
|
|
70
|
+
if (!expanded && maxDiffLines > 0) {
|
|
71
|
+
const latest = ops.findLast(op => op.diff && isObject(op.diff));
|
|
72
|
+
|
|
73
|
+
if (latest) {
|
|
74
|
+
const label = latest.mutationAttempt || latest.ok === false ? "latest attempted change" : "latest change";
|
|
75
|
+
lines.push(theme.fg("dim", `── ${label}: ${cleanInlineText(latest.target)} ──`));
|
|
76
|
+
|
|
77
|
+
for (const row of formatDiffRows(latest.diff, theme, maxDiffLines)) lines.push(" " + row);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
57
80
|
}
|
|
58
81
|
|
|
59
|
-
function appendError(lines, theme, payload,
|
|
82
|
+
function appendError(lines, theme, payload, expanded, width) {
|
|
60
83
|
const errors = [payload?.error || "error"];
|
|
84
|
+
|
|
61
85
|
for (const [i, program] of (payload?.programs ?? []).entries()) {
|
|
62
86
|
if (program.details?.ok === false) errors.push(`program ${i + 1}: ${program.details.error || resultTextContent(program)}`);
|
|
63
87
|
}
|
|
64
|
-
|
|
88
|
+
|
|
89
|
+
let remaining = expanded ? Infinity : 8;
|
|
90
|
+
|
|
91
|
+
for (const [i, error] of errors.entries()) {
|
|
92
|
+
const wrapped = resultLines("✗ " + error, width);
|
|
93
|
+
const shown = wrapped.slice(0, remaining);
|
|
94
|
+
|
|
95
|
+
for (const line of shown) lines.push(theme.fg("error", line));
|
|
96
|
+
remaining -= shown.length;
|
|
97
|
+
|
|
98
|
+
if (!expanded && (shown.length < wrapped.length || (remaining === 0 && i + 1 < errors.length))) {
|
|
99
|
+
lines.push(theme.fg("dim", " … more error details (expand)"));
|
|
100
|
+
break;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
65
103
|
}
|
|
66
104
|
|
|
67
105
|
function appendMutations(lines, theme, payload, width) {
|
|
68
106
|
const m = payload?.mutations;
|
|
107
|
+
|
|
69
108
|
if (!m || !["committed", "rolledBack", "external", "pendingCommits", "recoveryFailed"].some(key => m[key])) return;
|
|
109
|
+
|
|
70
110
|
const extra = [
|
|
71
111
|
[m.external, "; external calls attempted=" + m.external],
|
|
72
112
|
[m.pendingCommits, "; pendingCommits=" + m.pendingCommits],
|
|
73
113
|
[m.recoveryFailed, "; recovery failed: inspect files"],
|
|
74
|
-
].
|
|
114
|
+
].flatMap(([enabled, text]) => (enabled ? [text] : [])).join("");
|
|
115
|
+
|
|
75
116
|
const summary = "file versions: committed=" + (m.committed || 0) + " rolledBack=" + (m.rolledBack || 0) + extra;
|
|
76
117
|
const role = m.rolledBack || m.recoveryFailed ? "warning" : "dim";
|
|
118
|
+
|
|
77
119
|
for (const line of resultLines(summary, width)) lines.push(theme.fg(role, line));
|
|
78
120
|
}
|
|
79
121
|
|
|
80
|
-
function appendResult(lines, theme, payload, expanded, width) {
|
|
122
|
+
function appendResult(lines, theme, payload, expanded, width, layout) {
|
|
81
123
|
if (expanded) lines.push(theme.fg("dim", "── result ──"));
|
|
82
|
-
const wrapped = resultLines(payload.result, width);
|
|
83
|
-
const shown = expanded ? wrapped : wrapped.slice(0,
|
|
124
|
+
const wrapped = resultLines(payload.result, width, layout);
|
|
125
|
+
const shown = expanded ? wrapped : wrapped.slice(0, 5);
|
|
84
126
|
|
|
85
127
|
for (const line of shown) lines.push(theme.fg("toolOutput", line));
|
|
86
128
|
|
|
87
|
-
if (!expanded && wrapped.length > shown.length) lines.push(theme.fg("dim", ` … ${wrapped.length - shown.length} more result lines`));
|
|
129
|
+
if (!expanded && wrapped.length > shown.length) lines.push(theme.fg("dim", ` … ${wrapped.length - shown.length} more result lines (expand)`));
|
|
130
|
+
|
|
131
|
+
if (isString(payload.result) && payload.result.length > MAX_UI_RESULT_CHARS) {
|
|
132
|
+
lines.push(theme.fg("dim", " … UI preview clipped; select fields for complete values"));
|
|
133
|
+
}
|
|
88
134
|
}
|
|
89
135
|
|
|
90
136
|
function appendLogs(lines, theme, payload, width) {
|
|
@@ -93,42 +139,47 @@ function appendLogs(lines, theme, payload, width) {
|
|
|
93
139
|
for (const log of payload.logs) for (const line of resultLines(log, width)) lines.push(theme.fg("dim", line));
|
|
94
140
|
}
|
|
95
141
|
|
|
96
|
-
function appendTail(lines, theme, payload, expanded, isError, width,
|
|
142
|
+
function appendTail(lines, theme, payload, expanded, isError, width, layout, showResult) {
|
|
97
143
|
if (isError) appendError(lines, theme, payload, expanded, width);
|
|
98
|
-
else if (payload?.result !== undefined
|
|
144
|
+
else if ((expanded || showResult) && payload?.result !== undefined) appendResult(lines, theme, payload, expanded, width, layout);
|
|
99
145
|
|
|
100
146
|
if (expanded && payload?.logs?.length) appendLogs(lines, theme, payload, width);
|
|
101
147
|
}
|
|
102
148
|
|
|
103
149
|
function bodyLimits(expanded, isPartial) {
|
|
104
|
-
return { maxOps: expanded ? 24 : 8, maxDiffLines: expanded ? 24 : isPartial ? 0 :
|
|
150
|
+
return { maxOps: expanded ? 24 : 8, maxDiffLines: expanded ? 24 : isPartial ? 0 : 5 };
|
|
105
151
|
}
|
|
106
152
|
|
|
107
|
-
function visibleTrace(trace, maxOps
|
|
108
|
-
|
|
153
|
+
function visibleTrace(trace, maxOps) {
|
|
154
|
+
// Keep the viewport anchored to the same recent work when the run finishes.
|
|
155
|
+
return trace.slice(-maxOps);
|
|
109
156
|
}
|
|
110
157
|
|
|
111
|
-
function appendOverflow(lines, theme, trace, maxOps
|
|
112
|
-
if (trace.length > maxOps) lines.push(theme.fg("dim", ` … ${trace.length - maxOps}
|
|
158
|
+
function appendOverflow(lines, theme, trace, maxOps) {
|
|
159
|
+
if (trace.length > maxOps) lines.push(theme.fg("dim", ` … ${trace.length - maxOps} earlier calls (expand)`));
|
|
113
160
|
}
|
|
114
161
|
|
|
115
162
|
function appendEmptyOps(lines, theme, ops, isError, isPartial) {
|
|
116
163
|
if (ops.length === 0 && !isError && !isPartial) lines.push(theme.fg("dim", "JavaScript-only execution"));
|
|
117
164
|
}
|
|
118
165
|
|
|
119
|
-
function buildBodyLines(theme, width, { payload, context, expanded, isPartial, isError }) {
|
|
166
|
+
function buildBodyLines(theme, width, { payload, context, expanded, isPartial, isError }, layout) {
|
|
120
167
|
const trace = traceFor(payload, context);
|
|
121
168
|
const { maxOps, maxDiffLines } = bodyLimits(expanded, isPartial);
|
|
122
|
-
const ops = operationsFromTrace(visibleTrace(trace, maxOps
|
|
123
|
-
|
|
124
|
-
//
|
|
169
|
+
const ops = operationsFromTrace(visibleTrace(trace, maxOps));
|
|
170
|
+
|
|
171
|
+
// Native persistence states follow publication/checkpoint outcomes. Legacy and
|
|
172
|
+
// captured-tool traces remain conservative where persistence is unknown.
|
|
125
173
|
for (const op of ops) op.mutationAttempt = mutationAttempt(op, payload, isPartial, isError);
|
|
126
174
|
const lines = [];
|
|
127
|
-
appendOps(lines, theme, ops, maxOps, maxDiffLines, width, isPartial, isError);
|
|
128
|
-
appendOverflow(lines, theme, trace, maxOps
|
|
175
|
+
appendOps(lines, theme, ops, maxOps, maxDiffLines, width, isPartial, isError, expanded);
|
|
176
|
+
appendOverflow(lines, theme, trace, maxOps);
|
|
177
|
+
|
|
129
178
|
if (!isPartial) appendMutations(lines, theme, payload, width);
|
|
179
|
+
|
|
130
180
|
if (Array.isArray(payload?.trace)) appendEmptyOps(lines, theme, ops, isError, isPartial);
|
|
131
|
-
|
|
181
|
+
// Successful adapter calls stay compact; returned source/JSON is expansion-only.
|
|
182
|
+
appendTail(lines, theme, payload, expanded, isError, width, layout, ops.length === 0 && !isPartial);
|
|
132
183
|
|
|
133
184
|
return { lines, opCount: trace.length };
|
|
134
185
|
}
|
|
@@ -136,7 +187,8 @@ function buildBodyLines(theme, width, { payload, context, expanded, isPartial, i
|
|
|
136
187
|
function describeCard(model, opCount) {
|
|
137
188
|
const wall = model.payload?.wallMs != null ? formatDuration(model.payload.wallMs) : "";
|
|
138
189
|
const calls = opCount > 0 ? `${opCount} call${opCount === 1 ? "" : "s"}` : "";
|
|
139
|
-
const
|
|
190
|
+
const saved = model.payload?.mutations?.committed > 0 || traceFor(model.payload, model.context).some(row=>row.mutationState === "saved");
|
|
191
|
+
const status = model.isError ? saved ? "partial · failed step" : "failed" : model.isPartial ? "running" : "complete";
|
|
140
192
|
|
|
141
193
|
return [calls, status, wall].filter(Boolean).join(" · ");
|
|
142
194
|
}
|
|
@@ -179,6 +231,10 @@ function renderCardLines(theme, model, width, view) {
|
|
|
179
231
|
}
|
|
180
232
|
|
|
181
233
|
class UnifiedResultCard {
|
|
234
|
+
// Retain only one bounded, unstyled result layout per card. Theme invalidation
|
|
235
|
+
// still repaints every row; content or column changes replace the geometry.
|
|
236
|
+
resultLayout = {};
|
|
237
|
+
|
|
182
238
|
set(theme, model) {
|
|
183
239
|
this.theme = theme;
|
|
184
240
|
this.model = model;
|
|
@@ -193,7 +249,7 @@ class UnifiedResultCard {
|
|
|
193
249
|
if (!theme || !model || width <= 0) return [];
|
|
194
250
|
|
|
195
251
|
if (this.cache?.width === width) return this.cache.lines;
|
|
196
|
-
const view = buildBodyLines(theme, Math.max(1, width - 4), model);
|
|
252
|
+
const view = buildBodyLines(theme, Math.max(1, width - 4), model, this.resultLayout);
|
|
197
253
|
const lines = renderCardLines(theme, model, width, view);
|
|
198
254
|
this.cache = { width, lines };
|
|
199
255
|
|
|
@@ -215,7 +271,9 @@ function resultTextContent(result) {
|
|
|
215
271
|
|
|
216
272
|
function payloadFromResult(result, hostError) {
|
|
217
273
|
const payload = result?.details;
|
|
274
|
+
|
|
218
275
|
if (hostError) return {...payload, ok:false, error:payload?.error || resultTextContent(result) || "tool execution failed"};
|
|
276
|
+
|
|
219
277
|
return payload ?? {result:resultTextContent(result)};
|
|
220
278
|
}
|
|
221
279
|
|
|
@@ -236,6 +294,7 @@ export function renderSupernovaResult(resultArg, optionsArg, themeArg, contextAr
|
|
|
236
294
|
themeArg,
|
|
237
295
|
contextArg,
|
|
238
296
|
);
|
|
297
|
+
|
|
239
298
|
// Pi omits isError from result and supplies it through render context.
|
|
240
299
|
const hostError = result?.isError === true || context?.isError === true || options?.isError === true;
|
|
241
300
|
const payload = payloadFromResult(result, hostError);
|
|
@@ -248,6 +307,8 @@ export function renderSupernovaResult(resultArg, optionsArg, themeArg, contextAr
|
|
|
248
307
|
}
|
|
249
308
|
|
|
250
309
|
function mutationAttempt(op, payload, isPartial, isError) {
|
|
310
|
+
if (op.mutationState) return op.mutationState !== "saved";
|
|
311
|
+
|
|
251
312
|
return op.ok === true && ["write", "edit", "patch"].includes(op.tool)
|
|
252
313
|
&& (isPartial || isError || payload?.mutations?.rolledBack > 0 || payload?.mutations?.recoveryFailed);
|
|
253
314
|
}
|
package/src/ui/trace.js
CHANGED
|
@@ -5,6 +5,7 @@ import {clampLine,fitPath} from './render-measure.js';
|
|
|
5
5
|
function diffGut(theme, item) {
|
|
6
6
|
const [sign, color, textColor] = DIFF_STYLES.get(item.type) ?? [" ", "dim", "toolDiffContext"];
|
|
7
7
|
const gutter = theme.fg(color, (sign + (item.lineNum || 0)).padStart(5));
|
|
8
|
+
|
|
8
9
|
return gutter + theme.fg("borderMuted", " │ ") + theme.fg(textColor, sign + " " + cleanInlineText(item.text));
|
|
9
10
|
}
|
|
10
11
|
|
|
@@ -45,7 +46,7 @@ function displayOperation(tool, target, diff, ok, item) {
|
|
|
45
46
|
if (!rawName) return null;
|
|
46
47
|
const normalized = rawName === "apply_patch" ? "patch" : rawName;
|
|
47
48
|
|
|
48
|
-
return { tool: normalized, target, diff, ok, ms: item?.ms, exitCode: item?.exitCode, time: item?.time, error: item?.error };
|
|
49
|
+
return { tool: normalized, target, diff, ok, ms: item?.ms, exitCode: item?.exitCode, time: item?.time, error: item?.error, mutationState: item?.mutationState };
|
|
49
50
|
}
|
|
50
51
|
|
|
51
52
|
function batchTarget(paths) {
|
|
@@ -206,12 +207,13 @@ function formatTarget(op, budget) {
|
|
|
206
207
|
function opMarker(theme, op, isPartial, isError) {
|
|
207
208
|
const status = [
|
|
208
209
|
[op.ok === false, "error", "×"],
|
|
209
|
-
[op.mutationAttempt, "warning", "·"],
|
|
210
|
+
[op.mutationAttempt || op.mutationState === "rolled back" || op.mutationState === "uncertain", "warning", "·"],
|
|
210
211
|
[op.ok === true, "success", "✓"],
|
|
211
212
|
[isPartial, "dim", "·"],
|
|
212
213
|
[isError, "error", "×"],
|
|
213
214
|
[true, "success", "✓"],
|
|
214
215
|
].find(([matches]) => matches);
|
|
216
|
+
|
|
215
217
|
return theme.fg(status[1], status[2]);
|
|
216
218
|
}
|
|
217
219
|
|
|
@@ -263,13 +265,14 @@ function formatOpRow(theme, op, width, isPartial, isError) {
|
|
|
263
265
|
const duration = theme.fg("dim", durationText.padStart(DURATION_COL));
|
|
264
266
|
const exit = appendExit(theme, op);
|
|
265
267
|
const counts = appendDiffCounts(theme, op);
|
|
266
|
-
const outcome = op.mutationAttempt ? "attempted " : "";
|
|
267
|
-
const prefix = `${marker} ${tool} ${duration} ` + exit.text + counts.text + theme.fg("warning", outcome);
|
|
268
|
+
const outcome = op.mutationState ? op.mutationState + " " : op.mutationAttempt ? "attempted " : "";
|
|
269
|
+
const prefix = `${marker} ${tool} ${duration} ` + exit.text + counts.text + theme.fg(op.mutationState === "saved" ? "success" : "warning", outcome);
|
|
268
270
|
const used = 2 + toolText.length + 1 + DURATION_COL + 2 + exit.width + counts.width + outcome.length;
|
|
269
271
|
|
|
270
272
|
return opRowSuffix(theme, op, prefix, Math.max(1, width - used));
|
|
271
273
|
}
|
|
272
|
-
|
|
274
|
+
|
|
275
|
+
export { formatDiffRows, operationsFromTrace, formatDuration, formatOpRow, cleanBlockText, cleanInlineText };
|
|
273
276
|
|
|
274
277
|
const DIFF_STYLES = new Map([
|
|
275
278
|
["remove", ["-", "toolDiffRemoved", "toolDiffRemoved"]],
|