pi-supernova 0.0.4 → 0.0.6
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 +23 -0
- package/README.md +17 -6
- package/catalog.js +4 -1
- package/diff.js +60 -46
- package/host-bridge.js +76 -28
- package/index.js +39 -5
- package/omp-frame.js +20 -27
- package/package.json +2 -11
- package/render-measure.js +15 -9
- package/render.js +210 -436
- package/runtime.js +38 -19
- package/snap.js +29 -3
package/render.js
CHANGED
|
@@ -6,12 +6,12 @@
|
|
|
6
6
|
* so every width/truncate path here is self-contained and must never trust a host
|
|
7
7
|
* truncate that appends ellipsis after cutting to maxWidth.
|
|
8
8
|
*
|
|
9
|
-
* OMP
|
|
10
|
-
*
|
|
9
|
+
* Pi and OMP share one self-owned result card. The call slot stays empty so the
|
|
10
|
+
* lifecycle never duplicates; mutating operations include bounded inline diffs.
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
import { stripVTControlCharacters } from "node:util";
|
|
14
|
-
import { isString, isObject } from "./decode.js";
|
|
14
|
+
import { isString, isObject, isFunction } from "./decode.js";
|
|
15
15
|
import {
|
|
16
16
|
measureWidth,
|
|
17
17
|
hardTruncate,
|
|
@@ -43,83 +43,18 @@ function fitOutputLines(text, width) {
|
|
|
43
43
|
return out.length > 0 ? out : [""];
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
-
/**
|
|
47
|
-
* Soft violet / grey-blue wash — same structure as Pi's standard tool Box
|
|
48
|
-
* (padding + background), just purple-tinted instead of green.
|
|
49
|
-
* Tuned for dark themes (tokyo-night and friends).
|
|
50
|
-
*/
|
|
51
|
-
export const NOVA_CHROME = {
|
|
52
|
-
pendingBg: [26, 28, 42], // deep grey-blue
|
|
53
|
-
successBg: [24, 30, 46], // muted blue-purple
|
|
54
|
-
errorBg: [40, 26, 34], // muted rose-purple
|
|
55
|
-
};
|
|
56
|
-
|
|
57
|
-
function bgRgb(rgb, text) {
|
|
58
|
-
const [r, g, b] = rgb;
|
|
59
|
-
return `\x1b[48;2;${r};${g};${b}m${text}\x1b[49m`;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
function chromeBg(tone) {
|
|
63
|
-
if (tone === "error") return NOVA_CHROME.errorBg;
|
|
64
|
-
if (tone === "success") return NOVA_CHROME.successBg;
|
|
65
|
-
return NOVA_CHROME.pendingBg;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
/**
|
|
69
|
-
* Paint one row like Pi's Box: 1-col pad + content, washed to full width.
|
|
70
|
-
* No side-rail characters — the background block is the "border".
|
|
71
|
-
* Final visible width is always ≤ `width` (Pi crash contract).
|
|
72
|
-
*/
|
|
73
|
-
export function paintNovaRow(content, width, tone = "pending") {
|
|
74
|
-
const w = Math.max(1, width | 0);
|
|
75
|
-
const bg = chromeBg(tone);
|
|
76
|
-
const padX = 1;
|
|
77
|
-
const inner = Math.max(1, w - padX * 2);
|
|
78
|
-
const body = clampLine(content, inner);
|
|
79
|
-
const row = `${" ".repeat(padX)}${body}`;
|
|
80
|
-
const pad = Math.max(0, w - measureWidth(row));
|
|
81
|
-
const painted = bgRgb(bg, row + " ".repeat(pad));
|
|
82
|
-
if (measureWidth(painted) <= w) return painted;
|
|
83
|
-
return clampLine(stripVTControlCharacters(painted), w);
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
/**
|
|
87
|
-
* Framed Text for supernova. Uses renderShell: "self" so we own the chrome
|
|
88
|
-
* color (muted purple/grey-blue) instead of the host's green tool panels.
|
|
89
|
-
* Structure matches Pi's standard Box (pad + bg), without side-rail characters.
|
|
90
|
-
*/
|
|
46
|
+
/** Compact bounded text; the host supplies the card background and borders. */
|
|
91
47
|
export class SafeText {
|
|
92
48
|
constructor(text = "") {
|
|
93
49
|
this.text = text;
|
|
94
|
-
this.tone = "pending";
|
|
95
|
-
this.framing = true;
|
|
96
50
|
}
|
|
97
51
|
setText(text) {
|
|
98
52
|
this.text = text;
|
|
99
53
|
}
|
|
100
|
-
setTone(tone) {
|
|
101
|
-
if (tone === "error" || tone === "success" || tone === "pending") this.tone = tone;
|
|
102
|
-
}
|
|
103
|
-
setFraming(enabled) {
|
|
104
|
-
this.framing = !!enabled;
|
|
105
|
-
}
|
|
106
54
|
invalidate() {}
|
|
107
55
|
render(width = 80) {
|
|
108
|
-
const w = Math.max(1, width | 0);
|
|
109
56
|
const raw = String(this.text ?? "");
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
if (!this.framing) {
|
|
113
|
-
return fitOutputLines(raw, w);
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
// Match Pi Box: 1-col horizontal pad, 1-row vertical pad, purple bg wash.
|
|
117
|
-
const padX = 1;
|
|
118
|
-
const inner = Math.max(1, w - padX * 2);
|
|
119
|
-
const bodyLines = fitOutputLines(raw, inner);
|
|
120
|
-
const empty = paintNovaRow("", w, this.tone);
|
|
121
|
-
const painted = bodyLines.map((line) => paintNovaRow(line, w, this.tone));
|
|
122
|
-
return [empty, ...painted, empty];
|
|
57
|
+
return raw.trim() ? fitOutputLines(raw, Math.max(1, width | 0)) : [];
|
|
123
58
|
}
|
|
124
59
|
}
|
|
125
60
|
|
|
@@ -127,24 +62,6 @@ export class SafeText {
|
|
|
127
62
|
export const visibleWidth = measureWidth;
|
|
128
63
|
export const truncateToWidth = hardTruncate;
|
|
129
64
|
|
|
130
|
-
const ACTION_ICONS = {
|
|
131
|
-
write: "✎ ",
|
|
132
|
-
edit: "✎ ",
|
|
133
|
-
apply_patch: "✎ ",
|
|
134
|
-
patch: "✎ ",
|
|
135
|
-
bash: "❯ ",
|
|
136
|
-
exec: "❯ ",
|
|
137
|
-
read: "▤ ",
|
|
138
|
-
surface: "▤ ",
|
|
139
|
-
search: "⌕ ",
|
|
140
|
-
grep: "⌕ ",
|
|
141
|
-
find: "⌕ ",
|
|
142
|
-
ls: "▤ ",
|
|
143
|
-
// Avoid double-width emoji (⚡) — measure disagreements with Pi caused 92>91 crashes.
|
|
144
|
-
speculate: "✶ ",
|
|
145
|
-
snap: "⌖ ",
|
|
146
|
-
};
|
|
147
|
-
|
|
148
65
|
export function extractOperationsFromCode(code) {
|
|
149
66
|
const trimmed = String(code || "").trim();
|
|
150
67
|
if (!trimmed) return [];
|
|
@@ -185,18 +102,20 @@ export function extractOperationsFromCode(code) {
|
|
|
185
102
|
|
|
186
103
|
const namedCalls = [
|
|
187
104
|
{ regex: /(?:^|[^\w$.])(?:nova\.)?read\s*\(\s*["'`]([^"'`]+)["'`]/gm, tool: "read", wrap: (p) => p },
|
|
188
|
-
{ regex: /(?:^|[^\w$.])(?:nova\.)?write\s*\(\s*["'`]([^"'`]+)["'`]/gm, tool: "
|
|
105
|
+
{ regex: /(?:^|[^\w$.])(?:nova\.)?write\s*\(\s*["'`]([^"'`]+)["'`]/gm, tool: "write", wrap: (p) => p },
|
|
189
106
|
{ regex: /(?:^|[^\w$.])(?:nova\.)?edit\s*\(\s*["'`]([^"'`]+)["'`]/gm, tool: "edit", wrap: (p) => p },
|
|
190
|
-
{ regex: /(?:^|[^\w$.])(?:nova\.)?patch\s*\(\s*["'`]([^"'`]+)["'`]/gm, tool: "
|
|
107
|
+
{ regex: /(?:^|[^\w$.])(?:nova\.)?patch\s*\(\s*["'`]([^"'`]+)["'`]/gm, tool: "patch", wrap: (p) => p },
|
|
191
108
|
{
|
|
192
109
|
regex: /(?:^|[^\w$.])(?:nova\.)?bash\s*\(\s*["'`]([^"'`]+)["'`]/gm,
|
|
193
110
|
tool: "bash",
|
|
194
111
|
wrap: (c) => (c.length > 32 ? c.slice(0, 29) + "…" : c),
|
|
195
112
|
},
|
|
196
|
-
{ regex: /(?:^|[^\w$.])(?:nova\.)?exec\s*\(\s*["'`]([^"'`]+)["'`]/gm, tool: "
|
|
197
|
-
{ regex: /(?:nova\.)?search\s*\(\s*["'`]([^"'`]+)["'`]/
|
|
198
|
-
{ regex: /(
|
|
199
|
-
{ regex: /(
|
|
113
|
+
{ regex: /(?:^|[^\w$.])(?:nova\.)?exec\s*\(\s*["'`]([^"'`]+)["'`]/gm, tool: "exec", wrap: (c) => c },
|
|
114
|
+
{ regex: /(?:^|[^\w$.])(?:nova\.)?search\s*\(\s*["'`]([^"'`]+)["'`]/gm, tool: "search", wrap: (q) => `"${q}"` },
|
|
115
|
+
{ regex: /(?:^|[^\w$.])nova\.describe\s*\(\s*["'`]([^"'`]+)["'`]/gm, tool: "describe", wrap: (name) => name },
|
|
116
|
+
{ regex: /(?:^|[^\w$.])nova\.has\s*\(\s*["'`]([^"'`]+)["'`]/gm, tool: "has", wrap: (name) => name },
|
|
117
|
+
{ regex: /(?:^|[^\w$.])(?:nova\.)?surface\s*\(\s*["'`]([^"'`]+)["'`]/gm, tool: "surface", wrap: (p) => p },
|
|
118
|
+
{ regex: /(?:^|[^\w$.])(?:nova\.)?snap\s*\(\s*["'`]([^"'`]+)["'`]/gm, tool: "snap", wrap: (q) => `"${q}"` },
|
|
200
119
|
];
|
|
201
120
|
for (const item of namedCalls) {
|
|
202
121
|
while ((match = item.regex.exec(trimmed)) !== null) {
|
|
@@ -211,77 +130,70 @@ export function extractOperationsFromCode(code) {
|
|
|
211
130
|
return ops;
|
|
212
131
|
}
|
|
213
132
|
|
|
214
|
-
|
|
215
|
-
if (!diff || !Array.isArray(diff.lines) || diff.lines.length === 0) return
|
|
216
|
-
|
|
217
|
-
const w = Math.max(20, width | 0);
|
|
218
|
-
const cleanPath = String(diff.path || "").replace(/\\/g, "/");
|
|
219
|
-
const baseName = cleanPath.split("/").pop() || cleanPath;
|
|
220
|
-
const opLabel = diff.op === "edit" ? "Edit" : diff.op === "write" ? "Write" : "Patch";
|
|
221
|
-
|
|
222
|
-
// Stats BEFORE path so +N/-N survive narrow-terminal truncation (prior test/crash footgun).
|
|
223
|
-
const stats =
|
|
224
|
-
theme.fg("dim", "⟨") +
|
|
225
|
-
theme.fg("toolDiffAdded", `+${diff.added}`) +
|
|
226
|
-
theme.fg("dim", "/") +
|
|
227
|
-
theme.fg("toolDiffRemoved", `-${diff.removed}`) +
|
|
228
|
-
theme.fg("dim", "⟩");
|
|
229
|
-
// Do not clamp here — SafeText.render(terminalWidth) is the single choke point.
|
|
230
|
-
// Pre-clamping with a guessed width ate filenames under mock/ANSI-marker themes.
|
|
231
|
-
const header =
|
|
232
|
-
theme.fg("accent", "✎ ") +
|
|
233
|
-
theme.fg("toolTitle", theme.bold(`${opLabel} `)) +
|
|
234
|
-
stats +
|
|
235
|
-
" " +
|
|
236
|
-
theme.fg("muted", baseName);
|
|
237
|
-
|
|
238
|
-
const divWidth = Math.min(w, Math.max(20, Math.min(70, w)));
|
|
239
|
-
const divider = theme.fg("borderMuted", "─".repeat(divWidth));
|
|
240
|
-
|
|
241
|
-
const maxShown = 8;
|
|
242
|
-
const shownLines = diff.lines.slice(0, maxShown);
|
|
133
|
+
function formatDiffRows(diff, theme, maxShown = 6) {
|
|
134
|
+
if (!diff || !Array.isArray(diff.lines) || diff.lines.length === 0) return [];
|
|
243
135
|
const body = [];
|
|
244
|
-
|
|
245
|
-
for (const item of shownLines) {
|
|
136
|
+
for (const item of diff.lines.slice(0, maxShown)) {
|
|
246
137
|
const num = item.lineNum || 0;
|
|
247
|
-
let row;
|
|
248
138
|
if (item.type === "remove") {
|
|
249
139
|
const gut = theme.fg("toolDiffRemoved", `-${num}`.padStart(5));
|
|
250
|
-
|
|
251
|
-
const txt = theme.fg("toolDiffRemoved", `- ${item.text}`);
|
|
252
|
-
row = `${gut}${sep}${txt}`;
|
|
140
|
+
body.push(`${gut}${theme.fg("borderMuted", " │ ")}${theme.fg("toolDiffRemoved", `- ${cleanInlineText(item.text)}`)}`);
|
|
253
141
|
} else if (item.type === "add") {
|
|
254
142
|
const gut = theme.fg("toolDiffAdded", `+${num}`.padStart(5));
|
|
255
|
-
|
|
256
|
-
const txt = theme.fg("toolDiffAdded", `+ ${item.text}`);
|
|
257
|
-
row = `${gut}${sep}${txt}`;
|
|
143
|
+
body.push(`${gut}${theme.fg("borderMuted", " │ ")}${theme.fg("toolDiffAdded", `+ ${cleanInlineText(item.text)}`)}`);
|
|
258
144
|
} else {
|
|
259
145
|
const gut = theme.fg("dim", ` ${num}`.padStart(5));
|
|
260
|
-
|
|
261
|
-
const txt = theme.fg("toolDiffContext", ` ${item.text}`);
|
|
262
|
-
row = `${gut}${sep}${txt}`;
|
|
146
|
+
body.push(`${gut}${theme.fg("borderMuted", " │ ")}${theme.fg("toolDiffContext", ` ${cleanInlineText(item.text)}`)}`);
|
|
263
147
|
}
|
|
264
|
-
body.push(row);
|
|
265
148
|
}
|
|
149
|
+
const displayLineCount = Number.isInteger(diff.displayLineCount) ? diff.displayLineCount : diff.lines.length;
|
|
150
|
+
if (displayLineCount > maxShown) {
|
|
151
|
+
body.push(theme.fg("dim", ` │ … ${displayLineCount - maxShown} more lines`));
|
|
152
|
+
}
|
|
153
|
+
return body;
|
|
154
|
+
}
|
|
155
|
+
|
|
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
|
+
}
|
|
266
167
|
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
168
|
+
function stripUnsafeControls(value) {
|
|
169
|
+
let clean = "";
|
|
170
|
+
for (const character of value) {
|
|
171
|
+
const codePoint = character.codePointAt(0);
|
|
172
|
+
const isC0 = codePoint <= 0x08 || codePoint === 0x0b || codePoint === 0x0c || (codePoint >= 0x0e && codePoint <= 0x1f);
|
|
173
|
+
const isDeleteOrC1 = codePoint >= 0x7f && codePoint <= 0x9f;
|
|
174
|
+
if (!isC0 && !isDeleteOrC1) clean += character;
|
|
270
175
|
}
|
|
176
|
+
return clean;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function cleanBlockText(value) {
|
|
180
|
+
const normalized = stripVTControlCharacters(String(value ?? "")).replace(/\r\n?/g, "\n");
|
|
181
|
+
return stripUnsafeControls(normalized).replace(/[\u061c\u200e\u200f\u202a-\u202e\u2066-\u2069]/g, "");
|
|
182
|
+
}
|
|
271
183
|
|
|
272
|
-
|
|
184
|
+
function cleanInlineText(value) {
|
|
185
|
+
return cleanBlockText(value).replace(/\s*\n\s*/g, " ").trim();
|
|
273
186
|
}
|
|
274
187
|
|
|
275
|
-
function displayOperation(tool, target) {
|
|
276
|
-
|
|
277
|
-
if (
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
return null;
|
|
188
|
+
function displayOperation(tool, target, diff, ok) {
|
|
189
|
+
const rawName = cleanInlineText(tool);
|
|
190
|
+
if (!rawName) return null;
|
|
191
|
+
const normalized = rawName === "apply_patch" ? "patch" : rawName;
|
|
192
|
+
return { tool: normalized, target, diff, ok };
|
|
281
193
|
}
|
|
282
194
|
|
|
283
195
|
function formatOpTarget(raw, tool) {
|
|
284
|
-
const text =
|
|
196
|
+
const text = cleanInlineText(raw);
|
|
285
197
|
if (!text) return "";
|
|
286
198
|
if (tool === "bash") {
|
|
287
199
|
// Keep commands readable; wrap handles the rest at render time.
|
|
@@ -292,7 +204,7 @@ function formatOpTarget(raw, tool) {
|
|
|
292
204
|
}
|
|
293
205
|
|
|
294
206
|
function isTheme(value) {
|
|
295
|
-
return
|
|
207
|
+
return isObject(value) && isFunction(value.fg);
|
|
296
208
|
}
|
|
297
209
|
|
|
298
210
|
/**
|
|
@@ -302,13 +214,13 @@ function isTheme(value) {
|
|
|
302
214
|
*/
|
|
303
215
|
export function normalizeCallRenderArgs(a, b, c) {
|
|
304
216
|
if (isTheme(b)) {
|
|
305
|
-
const context = c
|
|
306
|
-
if (!context.state
|
|
217
|
+
const context = isObject(c) ? c : {};
|
|
218
|
+
if (!isObject(context.state)) context.state = {};
|
|
307
219
|
return { args: a, theme: b, context, host: "pi" };
|
|
308
220
|
}
|
|
309
221
|
if (isTheme(c)) {
|
|
310
|
-
const options = b
|
|
311
|
-
if (!options.state
|
|
222
|
+
const options = isObject(b) ? b : {};
|
|
223
|
+
if (!isObject(options.state)) options.state = {};
|
|
312
224
|
const context = {
|
|
313
225
|
...options,
|
|
314
226
|
state: options.state,
|
|
@@ -329,21 +241,19 @@ export function normalizeCallRenderArgs(a, b, c) {
|
|
|
329
241
|
* Pi: (result, {expanded,isPartial}, theme, context)
|
|
330
242
|
* OMP: (result, {expanded,isPartial}, theme, args) — 4th is args, not context
|
|
331
243
|
*
|
|
332
|
-
* Call shapes share the first three positions, so host is inferred from the
|
|
333
|
-
*
|
|
244
|
+
* Call shapes share the first three positions, so host is inferred from the
|
|
245
|
+
* fourth argument's context-versus-args shape.
|
|
334
246
|
*/
|
|
335
247
|
function detectResultHost(options, ctxOrArgs) {
|
|
336
248
|
if (isTheme(options)) return "pi";
|
|
337
249
|
if (
|
|
338
|
-
ctxOrArgs &&
|
|
339
|
-
typeof ctxOrArgs === "object" &&
|
|
250
|
+
isObject(ctxOrArgs) &&
|
|
340
251
|
("lastComponent" in ctxOrArgs || "invalidate" in ctxOrArgs)
|
|
341
252
|
) {
|
|
342
253
|
return "pi";
|
|
343
254
|
}
|
|
344
255
|
if (
|
|
345
|
-
ctxOrArgs &&
|
|
346
|
-
typeof ctxOrArgs === "object" &&
|
|
256
|
+
isObject(ctxOrArgs) &&
|
|
347
257
|
("code" in ctxOrArgs || "timeoutMs" in ctxOrArgs)
|
|
348
258
|
) {
|
|
349
259
|
return "omp";
|
|
@@ -353,39 +263,40 @@ function detectResultHost(options, ctxOrArgs) {
|
|
|
353
263
|
|
|
354
264
|
export function normalizeResultRenderArgs(result, options, themeOrCtx, ctxOrArgs) {
|
|
355
265
|
if (isTheme(themeOrCtx)) {
|
|
356
|
-
const opts = options
|
|
266
|
+
const opts = isObject(options) ? options : {};
|
|
357
267
|
let context;
|
|
358
268
|
if (
|
|
359
|
-
ctxOrArgs &&
|
|
360
|
-
typeof ctxOrArgs === "object" &&
|
|
269
|
+
isObject(ctxOrArgs) &&
|
|
361
270
|
!isTheme(ctxOrArgs) &&
|
|
362
271
|
("lastComponent" in ctxOrArgs || "state" in ctxOrArgs || "invalidate" in ctxOrArgs)
|
|
363
272
|
) {
|
|
364
273
|
context = ctxOrArgs;
|
|
365
274
|
} else {
|
|
366
|
-
context = {
|
|
275
|
+
context = { state: opts.state, lastComponent: opts.lastComponent };
|
|
367
276
|
}
|
|
368
|
-
if (!context.state
|
|
277
|
+
if (!isObject(context.state)) context.state = {};
|
|
369
278
|
return {
|
|
370
279
|
result,
|
|
371
280
|
expanded: !!opts.expanded,
|
|
372
281
|
isPartial: !!opts.isPartial,
|
|
373
282
|
theme: themeOrCtx,
|
|
374
283
|
context,
|
|
284
|
+
args: ctxOrArgs?.code ? ctxOrArgs : context.args,
|
|
375
285
|
host: detectResultHost(options, ctxOrArgs),
|
|
376
286
|
options: opts,
|
|
377
287
|
};
|
|
378
288
|
}
|
|
379
289
|
// Extremely defensive: (result, theme, context) oddball
|
|
380
290
|
if (isTheme(options)) {
|
|
381
|
-
const context = themeOrCtx
|
|
382
|
-
if (!context.state
|
|
291
|
+
const context = isObject(themeOrCtx) ? themeOrCtx : {};
|
|
292
|
+
if (!isObject(context.state)) context.state = {};
|
|
383
293
|
return {
|
|
384
294
|
result,
|
|
385
295
|
expanded: !!context.expanded,
|
|
386
296
|
isPartial: !!context.isPartial,
|
|
387
297
|
theme: options,
|
|
388
298
|
context,
|
|
299
|
+
args: context.args,
|
|
389
300
|
host: "pi",
|
|
390
301
|
options: {},
|
|
391
302
|
};
|
|
@@ -393,225 +304,157 @@ export function normalizeResultRenderArgs(result, options, themeOrCtx, ctxOrArgs
|
|
|
393
304
|
throw new Error("supernova renderResult: theme missing (expected Pi or OMP signature)");
|
|
394
305
|
}
|
|
395
306
|
|
|
396
|
-
function
|
|
397
|
-
const
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
return extractOperationsFromCode(args?.code)
|
|
411
|
-
.map((op) => displayOperation(op.tool, op.target))
|
|
412
|
-
.filter(Boolean);
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
function formatElapsed(state) {
|
|
416
|
-
if (state?.wallMs != null) return `${state.wallMs}ms`;
|
|
417
|
-
if (state?.startedAt != null) {
|
|
418
|
-
const elapsed = Math.round(performance.now() - state.startedAt);
|
|
419
|
-
// Suppress sub-100ms noise on the pending head (matches quiet Write/Edit calls).
|
|
420
|
-
if (elapsed < 100) return "";
|
|
421
|
-
return elapsed >= 1000 ? `${(elapsed / 1000).toFixed(1)}s` : `${elapsed}ms`;
|
|
422
|
-
}
|
|
307
|
+
function operationTarget(item) {
|
|
308
|
+
const args = item?.args || {};
|
|
309
|
+
const name = item?.name;
|
|
310
|
+
if (name === "snap") {
|
|
311
|
+
const query = args.query ? `"${args.query}"` : "";
|
|
312
|
+
return args.path ? `${query} → ${args.path}` : query;
|
|
313
|
+
}
|
|
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);
|
|
423
321
|
return "";
|
|
424
322
|
}
|
|
425
323
|
|
|
426
|
-
function
|
|
427
|
-
const
|
|
428
|
-
if (
|
|
429
|
-
if (
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
const rawTarget = formatOpTarget(op.target, op.tool);
|
|
442
|
-
const target = rawTarget ? " " + theme.fg("muted", rawTarget) : "";
|
|
443
|
-
return `${bullet}${toolName}${target}`;
|
|
444
|
-
}
|
|
445
|
-
|
|
446
|
-
function shouldUseOmpFrame(host) {
|
|
447
|
-
return host === "omp";
|
|
448
|
-
}
|
|
449
|
-
|
|
450
|
-
/**
|
|
451
|
-
* OMP write/edit look: rounded framedBlock + status-line header.
|
|
452
|
-
* Pending call has no hourglass on the head row (same as native Write/Edit).
|
|
453
|
-
*/
|
|
454
|
-
function renderOmpCallCard(theme, { ops, timeStr, expanded, code }) {
|
|
455
|
-
const opSummary =
|
|
456
|
-
ops.length === 0
|
|
457
|
-
? "composing"
|
|
458
|
-
: ops.map((op) => op.tool).join(theme.sep?.dot ? ` ${theme.sep.dot} ` : " · ");
|
|
459
|
-
const description = timeStr ? `${opSummary} · ${timeStr}` : opSummary;
|
|
460
|
-
// No pending icon on the framed head row — matches native Write/Edit.
|
|
461
|
-
const header = novaStatusLine(theme, {
|
|
462
|
-
title: "nova",
|
|
463
|
-
description,
|
|
464
|
-
});
|
|
465
|
-
return novaFramedBlock(theme, (width) => {
|
|
466
|
-
const bodyLines = ops.map((op) => formatOpBodyLine(theme, op));
|
|
467
|
-
if (expanded && code) {
|
|
468
|
-
bodyLines.push(theme.fg("dim", "── source ──"));
|
|
469
|
-
for (const line of String(code).trim().split("\n")) {
|
|
470
|
-
bodyLines.push(theme.fg("toolOutput", line));
|
|
471
|
-
}
|
|
472
|
-
}
|
|
473
|
-
return {
|
|
474
|
-
header,
|
|
475
|
-
sections: bodyLines.length > 0 ? [{ lines: bodyLines }] : [],
|
|
476
|
-
state: "pending",
|
|
477
|
-
borderColor: "borderMuted",
|
|
478
|
-
width,
|
|
479
|
-
};
|
|
480
|
-
});
|
|
481
|
-
}
|
|
482
|
-
|
|
483
|
-
function renderOmpResultCard(theme, { isErr, payload, expanded, bodyText, isPartial, spinnerFrame }) {
|
|
484
|
-
if (isErr) {
|
|
485
|
-
const errLines = [];
|
|
486
|
-
if (payload?.error) errLines.push(theme.fg("error", String(payload.error)));
|
|
487
|
-
if (expanded && payload?.logs?.length) {
|
|
488
|
-
errLines.push(theme.fg("dim", "── logs ──"));
|
|
489
|
-
for (const log of payload.logs) errLines.push(theme.fg("dim", String(log)));
|
|
324
|
+
function normalizeTraceDiff(item) {
|
|
325
|
+
const diff = item?.diff;
|
|
326
|
+
if (isObject(diff)) return diff;
|
|
327
|
+
if (!isString(diff) || !diff.trim()) return undefined;
|
|
328
|
+
const lines = [];
|
|
329
|
+
let added = 0;
|
|
330
|
+
let removed = 0;
|
|
331
|
+
for (const rawLine of cleanBlockText(diff).split("\n")) {
|
|
332
|
+
let match = /^([+-])\s*(\d+)\s?(.*)$/.exec(rawLine);
|
|
333
|
+
if (match) {
|
|
334
|
+
const type = match[1] === "+" ? "add" : "remove";
|
|
335
|
+
if (type === "add") added += 1;
|
|
336
|
+
else removed += 1;
|
|
337
|
+
lines.push({ type, lineNum: Number(match[2]), text: match[3] });
|
|
338
|
+
continue;
|
|
490
339
|
}
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
title: "nova",
|
|
494
|
-
description: payload?.error ? String(payload.error).split("\n")[0] : "error",
|
|
495
|
-
});
|
|
496
|
-
return novaFramedBlock(theme, (width) => ({
|
|
497
|
-
header,
|
|
498
|
-
sections: errLines.length > 0 ? [{ lines: errLines }] : [],
|
|
499
|
-
state: "error",
|
|
500
|
-
borderColor: "error",
|
|
501
|
-
width,
|
|
502
|
-
}));
|
|
340
|
+
match = /^\s+(\d+)\s?(.*)$/.exec(rawLine);
|
|
341
|
+
if (match) lines.push({ type: "context", lineNum: Number(match[1]), text: match[2] });
|
|
503
342
|
}
|
|
343
|
+
if (lines.length === 0) return undefined;
|
|
344
|
+
return { path: item?.args?.path || "", op: item?.name, added, removed, lines };
|
|
345
|
+
}
|
|
504
346
|
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
description: wall || undefined,
|
|
511
|
-
});
|
|
512
|
-
const bodyLines = String(bodyText || "").split("\n");
|
|
513
|
-
while (bodyLines.length > 0 && bodyLines[0].trim() === "") bodyLines.shift();
|
|
514
|
-
while (bodyLines.length > 0 && bodyLines[bodyLines.length - 1].trim() === "") bodyLines.pop();
|
|
515
|
-
return novaFramedBlock(theme, (width) => ({
|
|
516
|
-
header,
|
|
517
|
-
sections: bodyLines.length > 0 ? [{ lines: bodyLines }] : [],
|
|
518
|
-
state: isPartial ? "pending" : "success",
|
|
519
|
-
borderColor: "borderMuted",
|
|
520
|
-
width,
|
|
521
|
-
}));
|
|
347
|
+
function operationsFromTrace(trace) {
|
|
348
|
+
if (!Array.isArray(trace)) return [];
|
|
349
|
+
return trace
|
|
350
|
+
.map((item) => displayOperation(item?.name || "tool", operationTarget(item), normalizeTraceDiff(item), item?.ok))
|
|
351
|
+
.filter(Boolean);
|
|
522
352
|
}
|
|
523
353
|
|
|
524
354
|
export function renderSupernovaCall(a, b, c) {
|
|
525
|
-
const {
|
|
526
|
-
tickCallTimer(context);
|
|
527
|
-
const ops = collectCallOps(args, context);
|
|
528
|
-
const timeStr = formatElapsed(context?.state);
|
|
529
|
-
|
|
530
|
-
if (shouldUseOmpFrame(host)) {
|
|
531
|
-
return renderOmpCallCard(theme, {
|
|
532
|
-
ops,
|
|
533
|
-
timeStr,
|
|
534
|
-
expanded: !!context?.expanded,
|
|
535
|
-
code: args?.code,
|
|
536
|
-
});
|
|
537
|
-
}
|
|
538
|
-
|
|
355
|
+
const { context, options } = normalizeCallRenderArgs(a, b, c);
|
|
539
356
|
const comp = context?.lastComponent instanceof SafeText ? context.lastComponent : new SafeText();
|
|
540
357
|
if (options) options.lastComponent = comp;
|
|
541
358
|
else if (context) context.lastComponent = comp;
|
|
359
|
+
comp.setText("");
|
|
360
|
+
return comp;
|
|
361
|
+
}
|
|
542
362
|
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
}
|
|
548
|
-
|
|
549
|
-
if (ops.length === 0) {
|
|
550
|
-
out += " " + theme.fg("dim", "· composing");
|
|
551
|
-
} else {
|
|
552
|
-
for (const op of ops) {
|
|
553
|
-
out += `\n ${formatOpBodyLine(theme, op)}`;
|
|
554
|
-
}
|
|
555
|
-
}
|
|
363
|
+
function formatDiffStats(theme, diff) {
|
|
364
|
+
if (!diff || !isObject(diff)) return "";
|
|
365
|
+
return " " + theme.fg("toolDiffAdded", `+${diff.added || 0}`) + theme.fg("dim", "/") + theme.fg("toolDiffRemoved", `-${diff.removed || 0}`);
|
|
366
|
+
}
|
|
556
367
|
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
368
|
+
function formatResultOperation(theme, op, isPartial, isError) {
|
|
369
|
+
const marker = op.ok === false
|
|
370
|
+
? theme.fg("error", "× ")
|
|
371
|
+
: isPartial && op.ok !== true
|
|
372
|
+
? theme.fg("dim", "· ")
|
|
373
|
+
: isError && op.ok !== true
|
|
374
|
+
? theme.fg("error", "× ")
|
|
375
|
+
: theme.fg("success", "✓ ");
|
|
376
|
+
const tool = theme.fg("syntaxFunction", op.tool.padEnd(7, " "));
|
|
377
|
+
const targetText = formatOpTarget(op.target, op.tool);
|
|
378
|
+
const target = targetText ? theme.fg("muted", targetText) : theme.fg("dim", "done");
|
|
379
|
+
const stats = formatDiffStats(theme, op.diff);
|
|
380
|
+
return stats ? `${marker}${tool}${stats} ${target}` : `${marker}${tool} ${target}`;
|
|
381
|
+
}
|
|
561
382
|
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
383
|
+
function boundedResult(value) {
|
|
384
|
+
let text;
|
|
385
|
+
try {
|
|
386
|
+
text = isString(value) ? value : JSON.stringify(value, null, 2);
|
|
387
|
+
} catch {
|
|
388
|
+
text = String(value);
|
|
389
|
+
}
|
|
390
|
+
const lines = cleanBlockText(text).split("\n");
|
|
391
|
+
const clipped = lines.slice(0, 24).join("\n");
|
|
392
|
+
const suffix = lines.length > 24 ? `\n… ${lines.length - 24} more lines` : "";
|
|
393
|
+
return (clipped + suffix).slice(0, 4000);
|
|
568
394
|
}
|
|
569
395
|
|
|
570
|
-
function buildResultBody(theme, { payload, context, expanded }) {
|
|
396
|
+
function buildResultBody(theme, { payload, context, args, expanded, isPartial, isError }) {
|
|
571
397
|
let out = "";
|
|
572
398
|
const trace = payload?.trace || context?.state?.trace || [];
|
|
573
|
-
const
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
`… ${remaining} more file edit${remaining === 1 ? "" : "s"} (press Enter to expand)`,
|
|
589
|
-
);
|
|
399
|
+
const tracedOps = operationsFromTrace(trace);
|
|
400
|
+
const ops = tracedOps.length > 0
|
|
401
|
+
? tracedOps
|
|
402
|
+
: extractOperationsFromCode(args?.code).map((op) => displayOperation(op.tool, op.target)).filter(Boolean);
|
|
403
|
+
const maxOps = expanded ? 12 : 8;
|
|
404
|
+
const maxDiffLines = expanded ? 24 : 8;
|
|
405
|
+
const visibleOps = ops.slice(0, maxOps);
|
|
406
|
+
for (const [index, op] of visibleOps.entries()) {
|
|
407
|
+
if (index > 0) out += "\n│";
|
|
408
|
+
const isLast = index === visibleOps.length - 1 && ops.length <= maxOps;
|
|
409
|
+
const branch = isLast ? "└─" : "├─";
|
|
410
|
+
out += (out ? "\n" : "") + `${branch} ${formatResultOperation(theme, op, isPartial, isError)}`;
|
|
411
|
+
if (op.diff && isObject(op.diff)) {
|
|
412
|
+
const continuation = isLast ? " " : "│ ";
|
|
413
|
+
for (const row of formatDiffRows(op.diff, theme, maxDiffLines)) out += `\n${continuation}${row}`;
|
|
590
414
|
}
|
|
591
415
|
}
|
|
416
|
+
if (ops.length > maxOps) out += `\n│\n└─ ${theme.fg("dim", `… ${ops.length - maxOps} more calls`)}`;
|
|
592
417
|
|
|
593
418
|
if (expanded) {
|
|
594
|
-
|
|
595
|
-
if (resVal !== undefined) {
|
|
596
|
-
let formatted;
|
|
597
|
-
try {
|
|
598
|
-
formatted = isString(resVal) ? resVal : JSON.stringify(resVal, null, 2);
|
|
599
|
-
} catch {
|
|
600
|
-
formatted = String(resVal);
|
|
601
|
-
}
|
|
419
|
+
if (payload?.result !== undefined) {
|
|
602
420
|
out += (out ? "\n" : "") + theme.fg("dim", "── result ──");
|
|
603
|
-
out += "\n" + theme.fg("toolOutput",
|
|
421
|
+
out += "\n" + theme.fg("toolOutput", boundedResult(payload.result));
|
|
604
422
|
}
|
|
605
|
-
if (payload?.logs?.length) {
|
|
423
|
+
if (!isError && payload?.logs?.length) {
|
|
606
424
|
out += (out ? "\n" : "") + theme.fg("dim", "── logs ──");
|
|
607
|
-
for (const log of payload.logs) out += `\n ${theme.fg("dim",
|
|
425
|
+
for (const log of payload.logs.slice(0, 24)) out += `\n ${theme.fg("dim", cleanBlockText(log))}`;
|
|
608
426
|
}
|
|
609
427
|
}
|
|
610
|
-
return out;
|
|
428
|
+
return { body: out, opCount: ops.length };
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
class UnifiedResultCard {
|
|
432
|
+
set(theme, model) {
|
|
433
|
+
this.theme = theme;
|
|
434
|
+
this.model = model;
|
|
435
|
+
}
|
|
436
|
+
invalidate() {}
|
|
437
|
+
render(width = 80) {
|
|
438
|
+
const { theme, model } = this;
|
|
439
|
+
if (!theme || !model) return [];
|
|
440
|
+
const header = novaStatusLine(theme, {
|
|
441
|
+
icon: model.isError ? "error" : model.isPartial ? "running" : undefined,
|
|
442
|
+
title: "nova",
|
|
443
|
+
description: model.description,
|
|
444
|
+
});
|
|
445
|
+
const lines = model.body ? model.body.split("\n") : [];
|
|
446
|
+
return novaFramedBlock(theme, (frameWidth) => ({
|
|
447
|
+
header,
|
|
448
|
+
sections: lines.length > 0 ? [{ lines }] : [],
|
|
449
|
+
state: model.isError ? "error" : model.isPartial ? "pending" : "success",
|
|
450
|
+
borderColor: model.isError ? "error" : "borderMuted",
|
|
451
|
+
width: frameWidth,
|
|
452
|
+
})).render(width);
|
|
453
|
+
}
|
|
611
454
|
}
|
|
612
455
|
|
|
613
456
|
export function renderSupernovaResult(resultArg, optionsArg, themeArg, contextArg) {
|
|
614
|
-
const { result, expanded, isPartial, theme, context, options, host } = normalizeResultRenderArgs(
|
|
457
|
+
const { result, expanded, isPartial, theme, context, args, options, host } = normalizeResultRenderArgs(
|
|
615
458
|
resultArg,
|
|
616
459
|
optionsArg,
|
|
617
460
|
themeArg,
|
|
@@ -620,102 +463,33 @@ export function renderSupernovaResult(resultArg, optionsArg, themeArg, contextAr
|
|
|
620
463
|
|
|
621
464
|
const payload = result?.details;
|
|
622
465
|
if (context?.state && payload) {
|
|
623
|
-
let changed = false;
|
|
624
466
|
if (Array.isArray(payload.trace) && context.state.trace !== payload.trace) {
|
|
625
467
|
context.state.trace = payload.trace;
|
|
626
|
-
changed = true;
|
|
627
468
|
}
|
|
628
469
|
if (payload.wallMs != null && context.state.wallMs !== payload.wallMs) {
|
|
629
470
|
context.state.wallMs = payload.wallMs;
|
|
630
|
-
changed = true;
|
|
631
|
-
}
|
|
632
|
-
if (context.state.timer != null && !isPartial) {
|
|
633
|
-
clearTimeout(context.state.timer);
|
|
634
|
-
context.state.timer = null;
|
|
635
471
|
}
|
|
636
|
-
if (changed) context.invalidate?.();
|
|
637
472
|
}
|
|
638
473
|
|
|
639
474
|
const isErr = result?.isError || payload?.ok === false;
|
|
640
|
-
const
|
|
641
|
-
|
|
642
|
-
if (useOmp) {
|
|
643
|
-
if (isPartial && !isErr) {
|
|
644
|
-
// Streaming partials stay quiet until body content exists — same as Pi.
|
|
645
|
-
const partialBody = buildResultBody(theme, { payload, context, expanded });
|
|
646
|
-
if (!partialBody.trim()) {
|
|
647
|
-
return {
|
|
648
|
-
render: () => [],
|
|
649
|
-
invalidate() {},
|
|
650
|
-
};
|
|
651
|
-
}
|
|
652
|
-
return renderOmpResultCard(theme, {
|
|
653
|
-
isErr: false,
|
|
654
|
-
payload,
|
|
655
|
-
expanded,
|
|
656
|
-
bodyText: partialBody,
|
|
657
|
-
isPartial: true,
|
|
658
|
-
spinnerFrame: options?.spinnerFrame,
|
|
659
|
-
});
|
|
660
|
-
}
|
|
661
|
-
if (isErr) {
|
|
662
|
-
return renderOmpResultCard(theme, {
|
|
663
|
-
isErr: true,
|
|
664
|
-
payload,
|
|
665
|
-
expanded,
|
|
666
|
-
bodyText: "",
|
|
667
|
-
isPartial: false,
|
|
668
|
-
spinnerFrame: options?.spinnerFrame,
|
|
669
|
-
});
|
|
670
|
-
}
|
|
671
|
-
const out = buildResultBody(theme, { payload, context, expanded });
|
|
672
|
-
if (!out.trim()) {
|
|
673
|
-
return {
|
|
674
|
-
render: () => [],
|
|
675
|
-
invalidate() {},
|
|
676
|
-
};
|
|
677
|
-
}
|
|
678
|
-
return renderOmpResultCard(theme, {
|
|
679
|
-
isErr: false,
|
|
680
|
-
payload,
|
|
681
|
-
expanded,
|
|
682
|
-
bodyText: out,
|
|
683
|
-
isPartial: false,
|
|
684
|
-
spinnerFrame: options?.spinnerFrame,
|
|
685
|
-
});
|
|
686
|
-
}
|
|
687
|
-
|
|
688
|
-
const comp = context?.lastComponent instanceof SafeText ? context.lastComponent : new SafeText();
|
|
689
|
-
if (options) options.lastComponent = comp;
|
|
690
|
-
else if (context) context.lastComponent = comp;
|
|
691
|
-
|
|
692
|
-
if (isPartial) {
|
|
693
|
-
comp.setText("");
|
|
694
|
-
return comp;
|
|
695
|
-
}
|
|
475
|
+
const view = buildResultBody(theme, { payload, context, args, expanded, isPartial, isError: isErr });
|
|
696
476
|
|
|
477
|
+
let body = view.body;
|
|
697
478
|
if (isErr) {
|
|
698
|
-
|
|
699
|
-
if (payload?.error) {
|
|
700
|
-
out += `\n ${theme.fg("error", String(payload.error))}`;
|
|
701
|
-
}
|
|
479
|
+
body += (body ? "\n" : "") + theme.fg("error", payload?.error ? cleanBlockText(payload.error) : "error");
|
|
702
480
|
if (expanded && payload?.logs?.length) {
|
|
703
|
-
|
|
704
|
-
for (const log of payload.logs)
|
|
481
|
+
body += `\n${theme.fg("dim", "── logs ──")}`;
|
|
482
|
+
for (const log of payload.logs.slice(0, 24)) body += `\n${theme.fg("dim", cleanBlockText(log))}`;
|
|
705
483
|
}
|
|
706
|
-
comp.setTone("error");
|
|
707
|
-
comp.setFraming(true);
|
|
708
|
-
comp.setText(out);
|
|
709
|
-
return comp;
|
|
710
|
-
}
|
|
711
|
-
|
|
712
|
-
const out = buildResultBody(theme, { payload, context, expanded });
|
|
713
|
-
if (!out.trim()) {
|
|
714
|
-
comp.setText("");
|
|
715
|
-
return comp;
|
|
716
484
|
}
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
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(" · ");
|
|
489
|
+
const previous = host === "omp" ? options?.lastComponent : context?.lastComponent;
|
|
490
|
+
const comp = previous instanceof UnifiedResultCard ? previous : new UnifiedResultCard();
|
|
491
|
+
if (host === "omp" && options) options.lastComponent = comp;
|
|
492
|
+
else if (context) context.lastComponent = comp;
|
|
493
|
+
comp.set(theme, { body, description, isError: isErr, isPartial });
|
|
720
494
|
return comp;
|
|
721
495
|
}
|