pi-supernova 0.6.0 → 0.7.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.
Files changed (49) hide show
  1. package/README.md +27 -3
  2. package/docs/CHANGELOG.md +104 -0
  3. package/docs/TOKEN_COSTS.md +13 -5
  4. package/index.js +120 -79
  5. package/package.json +1 -1
  6. package/src/adapters/bash.js +73 -0
  7. package/src/adapters/edit.js +249 -0
  8. package/src/adapters/errors.js +31 -0
  9. package/src/adapters/index.js +31 -0
  10. package/src/adapters/list.js +102 -0
  11. package/src/adapters/read.js +805 -0
  12. package/src/adapters/refs.js +41 -0
  13. package/src/adapters/write.js +96 -0
  14. package/src/bridge/catalog.js +28 -222
  15. package/src/bridge/host-bridge.js +113 -1668
  16. package/src/bridge/invoke.js +35 -0
  17. package/src/bridge/native-tools.js +1 -198
  18. package/src/context/evidence.js +140 -76
  19. package/src/context/fuzzy.js +42 -24
  20. package/src/context/ledger.js +43 -24
  21. package/src/context/outline.js +23 -18
  22. package/src/context/repo-index.js +206 -170
  23. package/src/context/search.js +157 -77
  24. package/src/context/snap.js +240 -136
  25. package/src/context/spans.js +2 -1
  26. package/src/context/surface.js +14 -5
  27. package/src/contract/bash.js +31 -0
  28. package/src/contract/edit.js +95 -0
  29. package/src/contract/read.js +220 -0
  30. package/src/fs/check.js +12 -8
  31. package/src/fs/diff.js +18 -7
  32. package/src/fs/json-read.js +66 -35
  33. package/src/fs/patch.js +94 -50
  34. package/src/fs/source-window.js +82 -0
  35. package/src/fs/text-ops.js +512 -0
  36. package/src/fs/vfs.js +205 -175
  37. package/src/fs/workspace.js +119 -108
  38. package/src/output/bottleneck.js +195 -116
  39. package/src/output/format.js +101 -67
  40. package/src/runtime/guest-deny-imports.js +34 -0
  41. package/src/runtime/guest-worker.js +289 -292
  42. package/src/runtime/parallel.js +97 -64
  43. package/src/runtime/program-batch.js +178 -69
  44. package/src/runtime/reference.js +15 -14
  45. package/src/runtime/runtime.js +327 -187
  46. package/src/shared/decode.js +58 -36
  47. package/src/ui/omp-frame.js +59 -42
  48. package/src/ui/render-measure.js +51 -29
  49. package/src/ui/render.js +241 -145
package/src/ui/render.js CHANGED
@@ -18,25 +18,31 @@ import { formatValue } from "../output/format.js";
18
18
 
19
19
  export { measureWidth, hardTruncate, clampLine };
20
20
 
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
+
21
41
  function formatDiffRows(diff, theme, maxShown = 6) {
22
42
  if (!diff || !Array.isArray(diff.lines) || diff.lines.length === 0) return [];
23
43
  const body = [];
24
44
 
25
- for (const item of diff.lines.slice(0, maxShown)) {
26
- const num = item.lineNum || 0;
27
-
28
- if (item.type === "remove") {
29
- const gut = theme.fg("toolDiffRemoved", `-${num}`.padStart(5));
30
- body.push(`${gut}${theme.fg("borderMuted", " │ ")}${theme.fg("toolDiffRemoved", `- ${cleanInlineText(item.text)}`)}`);
31
- } else if (item.type === "add") {
32
- const gut = theme.fg("toolDiffAdded", `+${num}`.padStart(5));
33
- body.push(`${gut}${theme.fg("borderMuted", " │ ")}${theme.fg("toolDiffAdded", `+ ${cleanInlineText(item.text)}`)}`);
34
- } else {
35
- const gut = theme.fg("dim", ` ${num}`.padStart(5));
36
- body.push(`${gut}${theme.fg("borderMuted", " │ ")}${theme.fg("toolDiffContext", ` ${cleanInlineText(item.text)}`)}`);
37
- }
38
- }
39
-
45
+ for (const item of diff.lines.slice(0, maxShown)) body.push(diffGut(theme, item));
40
46
  const displayLineCount = Number.isInteger(diff.displayLineCount) ? diff.displayLineCount : diff.lines.length;
41
47
 
42
48
  if (displayLineCount > maxShown) {
@@ -128,54 +134,71 @@ function contextFrom(opts, ctxOrArgs) {
128
134
  return { state: opts.state, lastComponent: opts.lastComponent };
129
135
  }
130
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
+
131
145
  function detectResultHost(options, ctxOrArgs) {
132
146
  if (isTheme(options)) return "pi";
133
147
 
134
148
  if (!isObject(ctxOrArgs)) return "pi";
135
149
 
136
- if ("lastComponent" in ctxOrArgs || "invalidate" in ctxOrArgs) return "pi";
150
+ if (isRenderContext(ctxOrArgs)) return "pi";
137
151
 
138
- if ("code" in ctxOrArgs || "file" in ctxOrArgs || "programs" in ctxOrArgs || "timeoutMs" in ctxOrArgs) return "omp";
152
+ if (isToolArgs(ctxOrArgs)) return "omp";
139
153
 
140
154
  return "pi";
141
155
  }
142
156
 
143
- function normalizeResultRenderArgs(result, options, themeOrCtx, ctxOrArgs) {
144
- if (isTheme(themeOrCtx)) {
145
- const opts = isObject(options) ? options : {};
146
- const context = contextFrom(opts, ctxOrArgs);
157
+ function ensureState(context) {
158
+ if (!isObject(context.state)) context.state = {};
147
159
 
148
- if (!isObject(context.state)) context.state = {};
160
+ return context;
161
+ }
149
162
 
150
- return {
151
- result,
152
- expanded: !!opts.expanded,
153
- isPartial: !!opts.isPartial,
154
- theme: themeOrCtx,
155
- context,
156
- args: ctxOrArgs?.code || ctxOrArgs?.file || ctxOrArgs?.programs ? ctxOrArgs : context.args,
157
- host: detectResultHost(options, ctxOrArgs),
158
- options: opts,
159
- };
160
- }
163
+ function resultArgs(ctxOrArgs, context) {
164
+ return ctxOrArgs?.code || ctxOrArgs?.file || ctxOrArgs?.programs ? ctxOrArgs : context.args;
165
+ }
161
166
 
162
- // Extremely defensive: (result, theme, context) oddball
163
- if (isTheme(options)) {
164
- const context = isObject(themeOrCtx) ? themeOrCtx : {};
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
+ }
165
182
 
166
- if (!isObject(context.state)) context.state = {};
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
+ }
167
197
 
168
- return {
169
- result,
170
- expanded: !!context.expanded,
171
- isPartial: !!context.isPartial,
172
- theme: options,
173
- context,
174
- args: context.args,
175
- host: "pi",
176
- options: {},
177
- };
178
- }
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);
179
202
 
180
203
  throw new Error("supernova renderResult: theme missing (expected Pi or OMP signature)");
181
204
  }
@@ -233,47 +256,57 @@ let cachedDiffChars = 0;
233
256
 
234
257
  const MAX_CACHED_DIFF_CHARS = 1_000_000;
235
258
 
236
- function normalizeTraceDiff(item) {
237
- const diff = item?.diff;
259
+ function rememberDiff(diff, parsed) {
260
+ if (diff.length > MAX_CACHED_DIFF_CHARS) return parsed;
238
261
 
239
- if (isObject(diff)) return diff;
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
+ }
240
267
 
241
- if (!isString(diff) || !diff.trim()) return undefined;
242
- const cached = textDiffCache.get(diff);
268
+ textDiffCache.set(diff, parsed);
269
+ cachedDiffChars += diff.length;
243
270
 
244
- if (cached) return cached;
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) {
245
280
  const lines = [];
246
- let displayLineCount = 0;
247
- let added = 0;
248
- let removed = 0;
281
+ const counts = { added: 0, removed: 0, displayLineCount: 0 };
249
282
 
250
283
  for (const rawLine of cleanBlockText(diff).split("\n")) {
251
284
  const parsed = parseDiffLine(rawLine);
252
285
 
253
286
  if (!parsed) continue;
254
-
255
- if (parsed.type === "add") added += 1;
256
- else if (parsed.type === "remove") removed += 1;
257
- displayLineCount++;
287
+ tallyDiffLine(parsed, counts);
288
+ counts.displayLineCount++;
258
289
 
259
290
  if (lines.length < 24) lines.push(parsed);
260
291
  }
261
292
 
262
293
  if (lines.length === 0) return undefined;
263
- const parsed = { added, removed, lines, displayLineCount };
264
294
 
265
- if (diff.length <= MAX_CACHED_DIFF_CHARS) {
266
- while (textDiffCache.size >= 24 || cachedDiffChars + diff.length > MAX_CACHED_DIFF_CHARS) {
267
- const oldest = textDiffCache.keys().next().value;
268
- textDiffCache.delete(oldest);
269
- cachedDiffChars -= oldest.length;
270
- }
295
+ return { added: counts.added, removed: counts.removed, lines, displayLineCount: counts.displayLineCount };
296
+ }
271
297
 
272
- textDiffCache.set(diff, parsed);
273
- cachedDiffChars += diff.length;
274
- }
298
+ function normalizeTraceDiff(item) {
299
+ const diff = item?.diff;
275
300
 
276
- return parsed;
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;
277
310
  }
278
311
 
279
312
  function operationsFromTrace(trace) {
@@ -357,6 +390,34 @@ function opDuration(op, isPartial) {
357
390
  return "";
358
391
  }
359
392
 
393
+ function appendExit(theme, op) {
394
+ if (!Number.isInteger(op.exitCode)) return { text: "", width: 0 };
395
+ const exit = `exit ${op.exitCode}`;
396
+
397
+ return { text: theme.fg("error", exit) + " ", width: exit.length + 2 };
398
+ }
399
+
400
+ function appendDiffCounts(theme, op) {
401
+ if (!(op.diff && isObject(op.diff))) return { text: "", width: 0 };
402
+ const added = `+${op.diff.added || 0}`;
403
+ const removed = `-${op.diff.removed || 0}`;
404
+
405
+ return {
406
+ text: theme.fg("toolDiffAdded", added) + theme.fg("dim", "/") + theme.fg("toolDiffRemoved", removed) + " ",
407
+ width: added.length + 1 + removed.length + 1,
408
+ };
409
+ }
410
+
411
+ function opRowSuffix(theme, op, prefix, budget) {
412
+ const target = formatTarget(op, budget);
413
+
414
+ if (target) return prefix + theme.fg("muted", target);
415
+
416
+ if (op.ok === false && op.error) return prefix + theme.fg("error", clampLine(cleanInlineText(op.error), budget));
417
+
418
+ return prefix.trimEnd();
419
+ }
420
+
360
421
  /**
361
422
  * One aligned row: marker · tool · duration · [exit N] · [+a/-r] · target.
362
423
  * Fixed columns keep a ledger of mixed calls scannable at a glance.
@@ -367,30 +428,12 @@ function formatOpRow(theme, op, width, isPartial, isError) {
367
428
  const tool = theme.fg("syntaxFunction", toolText);
368
429
  const durationText = opDuration(op, isPartial).slice(-DURATION_COL);
369
430
  const duration = theme.fg("dim", durationText.padStart(DURATION_COL));
370
- let prefix = `${marker} ${tool} ${duration} `;
371
- let used = 2 + toolText.length + 1 + DURATION_COL + 2;
372
-
373
- if (Number.isInteger(op.exitCode)) {
374
- const exit = `exit ${op.exitCode}`;
375
- prefix += theme.fg("error", exit) + " ";
376
- used += exit.length + 2;
377
- }
378
-
379
- if (op.diff && isObject(op.diff)) {
380
- const added = `+${op.diff.added || 0}`;
381
- const removed = `-${op.diff.removed || 0}`;
382
- prefix += theme.fg("toolDiffAdded", added) + theme.fg("dim", "/") + theme.fg("toolDiffRemoved", removed) + " ";
383
- used += added.length + 1 + removed.length + 1;
384
- }
385
-
386
- const budget = Math.max(1, width - used);
387
- const target = formatTarget(op, budget);
431
+ const exit = appendExit(theme, op);
432
+ const counts = appendDiffCounts(theme, op);
433
+ const prefix = `${marker} ${tool} ${duration} ` + exit.text + counts.text;
434
+ const used = 2 + toolText.length + 1 + DURATION_COL + 2 + exit.width + counts.width;
388
435
 
389
- if (target) return prefix + theme.fg("muted", target);
390
-
391
- if (op.ok === false && op.error) return prefix + theme.fg("error", clampLine(cleanInlineText(op.error), budget));
392
-
393
- return prefix.trimEnd();
436
+ return opRowSuffix(theme, op, prefix, Math.max(1, width - used));
394
437
  }
395
438
 
396
439
  function traceFor(payload, context) {
@@ -417,38 +460,60 @@ function appendOps(lines, theme, ops, maxOps, maxDiffLines, width, isPartial, is
417
460
  if (ops.length > maxOps) lines.push(theme.fg("dim", ` … ${ops.length - maxOps} more calls`));
418
461
  }
419
462
 
420
- function appendTail(lines, theme, payload, expanded, isError, width) {
421
- if (isError) {
422
- const error = "✗ " + (payload?.error ? cleanBlockText(payload.error) : "error");
463
+ function appendError(lines, theme, payload, expanded, width) {
464
+ const error = "✗ " + (payload?.error ? cleanBlockText(payload.error) : "error");
423
465
 
424
- for (const line of expanded ? resultLines(error, width) : error.split("\n")) lines.push(theme.fg("error", line));
425
- }
426
- else if (expanded && payload?.result !== undefined) {
427
- lines.push(theme.fg("dim", "── result ──"));
466
+ for (const line of expanded ? resultLines(error, width) : error.split("\n")) lines.push(theme.fg("error", line));
467
+ }
428
468
 
429
- for (const line of resultLines(payload.result, width)) lines.push(theme.fg("toolOutput", line));
430
- }
469
+ function appendResult(lines, theme, payload, expanded, width) {
470
+ if (expanded) lines.push(theme.fg("dim", "── result ──"));
471
+ const wrapped = resultLines(payload.result, width);
472
+ const shown = expanded ? wrapped : wrapped.slice(0, 8);
431
473
 
432
- if (expanded && payload?.logs?.length) {
433
- lines.push(theme.fg("dim", "── logs ──"));
474
+ for (const line of shown) lines.push(theme.fg("toolOutput", line));
434
475
 
435
- for (const log of payload.logs) for (const line of resultLines(log, width)) lines.push(theme.fg("dim", line));
436
- }
476
+ if (!expanded && wrapped.length > shown.length) lines.push(theme.fg("dim", ` … ${wrapped.length - shown.length} more result lines`));
477
+ }
478
+
479
+ function appendLogs(lines, theme, payload, width) {
480
+ lines.push(theme.fg("dim", "── logs ──"));
481
+
482
+ for (const log of payload.logs) for (const line of resultLines(log, width)) lines.push(theme.fg("dim", line));
483
+ }
484
+
485
+ function appendTail(lines, theme, payload, expanded, isError, width, previewResult) {
486
+ if (isError) appendError(lines, theme, payload, expanded, width);
487
+ else if (payload?.result !== undefined && (expanded || previewResult)) appendResult(lines, theme, payload, expanded, width);
488
+
489
+ if (expanded && payload?.logs?.length) appendLogs(lines, theme, payload, width);
490
+ }
491
+
492
+ function bodyLimits(expanded, isPartial) {
493
+ return { maxOps: expanded ? 24 : 8, maxDiffLines: expanded ? 24 : isPartial ? 0 : 8 };
494
+ }
495
+
496
+ function visibleTrace(trace, maxOps, isPartial) {
497
+ return isPartial ? trace.slice(-maxOps) : trace.slice(0, maxOps);
498
+ }
499
+
500
+ function appendOverflow(lines, theme, trace, maxOps, isPartial) {
501
+ if (trace.length > maxOps) lines.push(theme.fg("dim", ` … ${trace.length - maxOps} ${isPartial ? "earlier" : "more"} calls`));
502
+ }
503
+
504
+ function appendEmptyOps(lines, theme, ops, isError, isPartial) {
505
+ if (ops.length === 0 && !isError && !isPartial) lines.push(theme.fg("dim", "no adapter calls"));
437
506
  }
438
507
 
439
508
  function buildBodyLines(theme, width, { payload, context, expanded, isPartial, isError }) {
440
509
  const trace = traceFor(payload, context);
441
- const maxOps = expanded ? 24 : 8;
442
- const maxDiffLines = expanded ? 24 : isPartial ? 0 : 8;
443
- // Select before parsing diffs: invisible history must not consume a frame.
444
- // While running, show current activity rather than the first completed calls.
445
- const visible = isPartial ? trace.slice(-maxOps) : trace.slice(0, maxOps);
446
- const ops = operationsFromTrace(visible);
510
+ const { maxOps, maxDiffLines } = bodyLimits(expanded, isPartial);
511
+ const ops = operationsFromTrace(visibleTrace(trace, maxOps, isPartial));
447
512
  const lines = [];
448
513
  appendOps(lines, theme, ops, maxOps, maxDiffLines, width, isPartial, isError);
449
-
450
- if (trace.length > maxOps) lines.push(theme.fg("dim", ` … ${trace.length - maxOps} ${isPartial ? "earlier" : "more"} calls`));
451
- appendTail(lines, theme, payload, expanded, isError, width);
514
+ appendOverflow(lines, theme, trace, maxOps, isPartial);
515
+ appendEmptyOps(lines, theme, ops, isError, isPartial);
516
+ appendTail(lines, theme, payload, expanded, isError, width, ops.length === 0 && !isPartial);
452
517
 
453
518
  return { lines, opCount: trace.length };
454
519
  }
@@ -461,6 +526,43 @@ function describeCard(model, opCount) {
461
526
  return [calls, status, wall].filter(Boolean).join(" · ");
462
527
  }
463
528
 
529
+ function cardIcon(model) {
530
+ if (model.isError) return "error";
531
+
532
+ if (model.isPartial) return "running";
533
+
534
+ return undefined;
535
+ }
536
+
537
+ function cardChrome(model) {
538
+ return {
539
+ state: model.isError ? "error" : model.isPartial ? "pending" : "success",
540
+ borderColor: model.isError ? "error" : "dim",
541
+ };
542
+ }
543
+
544
+ function renderCardLines(theme, model, width, view) {
545
+ const header = novaStatusLine(theme, {
546
+ icon: cardIcon(model),
547
+ title: "nova",
548
+ description: describeCard(model, view.opCount),
549
+ });
550
+
551
+ // Empty body: one status line, no framed box.
552
+ if (view.lines.length === 0) return [clampLine(header, width)];
553
+ const chrome = cardChrome(model);
554
+
555
+ return novaFramedBlock(theme, () => ({
556
+ header,
557
+ sections: [{ lines: view.lines }],
558
+ state: chrome.state,
559
+ // borderMuted is invisible on OMP's card background; dim matches the duration column.
560
+ borderColor: chrome.borderColor,
561
+ width,
562
+ paintBg: model.host !== "omp",
563
+ })).render(width);
564
+ }
565
+
464
566
  class UnifiedResultCard {
465
567
  set(theme, model) {
466
568
  this.theme = theme;
@@ -477,25 +579,7 @@ class UnifiedResultCard {
477
579
 
478
580
  if (this.cache?.width === width) return this.cache.lines;
479
581
  const view = buildBodyLines(theme, Math.max(1, width - 4), model);
480
-
481
- const header = novaStatusLine(theme, {
482
- icon: model.isError ? "error" : model.isPartial ? "running" : undefined,
483
- title: "nova",
484
- description: describeCard(model, view.opCount),
485
- });
486
-
487
- // A program with no host calls has nothing to frame: one status line, no empty box.
488
- const lines = view.lines.length === 0
489
- ? [clampLine(header, width)]
490
- : novaFramedBlock(theme, () => ({
491
- header,
492
- sections: [{ lines: view.lines }],
493
- state: model.isError ? "error" : model.isPartial ? "pending" : "success",
494
- // borderMuted is invisible on OMP's card background; dim matches the duration column.
495
- borderColor: model.isError ? "error" : "dim",
496
- width,
497
- })).render(width);
498
-
582
+ const lines = renderCardLines(theme, model, width, view);
499
583
  this.cache = { width, lines };
500
584
 
501
585
  return lines;
@@ -510,6 +594,24 @@ function syncState(context, payload) {
510
594
  if (payload.wallMs != null && context.state.wallMs !== payload.wallMs) context.state.wallMs = payload.wallMs;
511
595
  }
512
596
 
597
+ function errorPayload(result) {
598
+ return result?.isError ? { ok: false, error: result.content?.flatMap(block => block.type === "text" ? [block.text] : []).join("\n") } : undefined;
599
+ }
600
+
601
+ function payloadFromResult(result) {
602
+ return result?.details ?? errorPayload(result);
603
+ }
604
+
605
+ function bindResultCard(host, options, context) {
606
+ const previous = host === "omp" ? options?.lastComponent : context?.lastComponent;
607
+ const comp = previous instanceof UnifiedResultCard ? previous : new UnifiedResultCard();
608
+
609
+ if (host === "omp" && options) options.lastComponent = comp;
610
+ else if (context) context.lastComponent = comp;
611
+
612
+ return comp;
613
+ }
614
+
513
615
  export function renderSupernovaResult(resultArg, optionsArg, themeArg, contextArg) {
514
616
  const { result, expanded, isPartial, theme, context, args, options, host } = normalizeResultRenderArgs(
515
617
  resultArg,
@@ -517,17 +619,11 @@ export function renderSupernovaResult(resultArg, optionsArg, themeArg, contextAr
517
619
  themeArg,
518
620
  contextArg,
519
621
  );
520
-
521
- const payload = result?.details ?? (result?.isError ? { ok: false, error: result.content?.filter(block => block.type === "text").map(block => block.text).join("\n") } : undefined);
622
+ const payload = payloadFromResult(result);
522
623
  syncState(context, payload);
523
-
524
624
  const isError = result?.isError || payload?.ok === false;
525
- const previous = host === "omp" ? options?.lastComponent : context?.lastComponent;
526
- const comp = previous instanceof UnifiedResultCard ? previous : new UnifiedResultCard();
527
-
528
- if (host === "omp" && options) options.lastComponent = comp;
529
- else if (context) context.lastComponent = comp;
530
- comp.set(theme, { payload, context, args, expanded, isPartial, isError });
625
+ const comp = bindResultCard(host, options, context);
626
+ comp.set(theme, { payload, context, args, expanded, isPartial, isError, host });
531
627
 
532
628
  return comp;
533
629
  }