pi-supernova 0.5.0 → 0.6.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.
@@ -47,7 +47,7 @@ function prepareProgram(code) {
47
47
 
48
48
  if (candidate && FUNCTION_TYPES.has(candidate.type)) {
49
49
  expression = candidate;
50
- expressionSource = code.slice(0, statement.end).replace(/;\s*$/, "");
50
+ expressionSource = code.slice(statement.start, statement.end).replace(/;\s*$/, "");
51
51
  }
52
52
  } catch (bodyError) {
53
53
  expressionSource = code.trimEnd().replace(/;+\s*$/, "");
@@ -185,7 +185,10 @@ export async function runGuestProgram({ code, file, cwd = process.cwd(), data, n
185
185
 
186
186
  if (signal?.aborted) return fail(ABORT_MESSAGE);
187
187
  const runId = ++runSeq;
188
- const timeoutMs = config.timeoutMs ?? 60000;
188
+ const requestedTimeout = Number(config.timeoutMs === undefined ? 60000 : config.timeoutMs);
189
+
190
+ if (!Number.isFinite(requestedTimeout) || requestedTimeout <= 0) return fail("timeoutMs must be a positive finite number");
191
+ const timeoutMs = Math.max(1, Math.min(2_147_483_647, Math.floor(requestedTimeout)));
189
192
  const rssLimit = rssBytes() + (config.maxHeapMb ?? 512) * MEMORY_SLACK * 1048576;
190
193
 
191
194
  return new Promise((resolve) => {
@@ -195,6 +198,7 @@ export async function runGuestProgram({ code, file, cwd = process.cwd(), data, n
195
198
  let completing = false;
196
199
  let hostError;
197
200
  let notifyingHost = false;
201
+ let aborting = false;
198
202
  const pending = new Set();
199
203
  const inputController = new AbortController();
200
204
 
@@ -224,11 +228,13 @@ export async function runGuestProgram({ code, file, cwd = process.cwd(), data, n
224
228
  };
225
229
 
226
230
  const abort = () => {
227
- if (finished) return;
231
+ if (finished || aborting) return;
232
+ aborting = true;
228
233
  cancelHost();
229
234
 
230
235
  try { onTimeout?.(); } catch {}
231
236
 
237
+ aborting = false;
232
238
  finish(fail(ABORT_MESSAGE));
233
239
  };
234
240
 
@@ -267,8 +273,11 @@ export async function runGuestProgram({ code, file, cwd = process.cwd(), data, n
267
273
  handle?.worker.off("exit", onExit);
268
274
  void killWorker(handle);
269
275
 
270
- if (!outcome.ok) cancelHost();
271
- await Promise.allSettled(pending);
276
+ if (pending.size || !outcome.ok) cancelHost();
277
+ // A host tool that ignores cancellation must not keep the result unsettled
278
+ // after the guest worker is gone. Give it a brief drain window only.
279
+ await Promise.race([Promise.allSettled(pending), new Promise(resolve => setTimeout(resolve, 250))]);
280
+ if (pending.size && outcome.ok) hostError ??= "program completed with a host call still running";
272
281
 
273
282
  if (finished) return;
274
283
  finish(outcome.ok && hostError ? fail(hostError) : outcome);
@@ -339,7 +348,9 @@ export async function runGuestProgram({ code, file, cwd = process.cwd(), data, n
339
348
  await handle.ready;
340
349
 
341
350
  if (finished || signal?.aborted) return abort();
342
- const available = isFunction(nova.names) ? await nova.names() : [];
351
+ let available = isFunction(nova.names) ? await nova.names() : [];
352
+
353
+ if (!Array.isArray(available)) available = [];
343
354
 
344
355
  if (finished || signal?.aborted) return abort();
345
356
  handle.worker.on("message", onMessage);
@@ -65,7 +65,9 @@ export function toPlain(value, seen = new Set(), depth = 0) {
65
65
  if (value === null || value === undefined) return value;
66
66
  const tag = toStr.call(value);
67
67
 
68
- if (tag === "[object String]" || tag === "[object Number]" || tag === "[object Boolean]") return value.valueOf();
68
+ if (tag === "[object String]") return value.valueOf();
69
+
70
+ if (tag === "[object Number]" || tag === "[object Boolean]") return value.valueOf();
69
71
 
70
72
  if (tag === "[object BigInt]") return value.toString() + "n";
71
73
 
@@ -84,7 +86,12 @@ export function toPlain(value, seen = new Set(), depth = 0) {
84
86
  if (value instanceof Error) {
85
87
  const out = { name: value.name, message: value.message };
86
88
 
87
- if (value.cause !== undefined) out.cause = toPlain(value.cause, seen, depth + 1);
89
+ if (value.cause !== undefined) {
90
+ seen.add(value);
91
+
92
+ try { out.cause = toPlain(value.cause, seen, depth + 1); }
93
+ finally { seen.delete(value); }
94
+ }
88
95
 
89
96
  return out;
90
97
  }
@@ -93,7 +100,12 @@ export function toPlain(value, seen = new Set(), depth = 0) {
93
100
 
94
101
  if (ArrayBuffer.isView(value) || value instanceof ArrayBuffer) return plainFromBinary(value);
95
102
 
96
- if (isFunction(value.toJSON)) return toPlain(value.toJSON(), seen, depth + 1);
103
+ if (isFunction(value.toJSON)) {
104
+ seen.add(value);
105
+
106
+ try { return toPlain(value.toJSON(), seen, depth + 1); }
107
+ finally { seen.delete(value); }
108
+ }
97
109
  seen.add(value);
98
110
 
99
111
  try {
@@ -38,7 +38,10 @@ function borderPaint(theme, state, borderColor) {
38
38
 
39
39
  if (theme && isFunction(theme.fg)) {
40
40
  try {
41
- return (text) => theme.fg(key, text);
41
+ return (text) => {
42
+ try { return theme.fg(key, text); }
43
+ catch { return text; }
44
+ };
42
45
  } catch {
43
46
  /* fall through */
44
47
  }
@@ -51,10 +54,14 @@ const STATUS_PREFIX = { error: ["error", "✗ "], running: ["dim", "… "] };
51
54
 
52
55
  function statusHeader(theme, { title, description, state, icon }) {
53
56
  const resolved = icon ?? (state === "error" ? "error" : undefined);
57
+ const fg = (key, text) => {
58
+ try { return isFunction(theme?.fg) ? theme.fg(key, text) : text; }
59
+ catch { return text; }
60
+ };
54
61
  const prefixSpec = STATUS_PREFIX[resolved];
55
- const prefix = prefixSpec ? (theme?.fg ? theme.fg(prefixSpec[0], prefixSpec[1]) : prefixSpec[1]) : "";
56
- const titleText = theme?.fg ? theme.fg("accent", title) : title;
57
- const descText = description ? (theme?.fg ? theme.fg("muted", description) : description) : "";
62
+ const prefix = prefixSpec ? fg(prefixSpec[0], prefixSpec[1]) : "";
63
+ const titleText = fg("accent", title);
64
+ const descText = description ? fg("muted", description) : "";
58
65
 
59
66
  return descText ? `${prefix}${titleText}: ${descText}` : `${prefix}${titleText}`;
60
67
  }
package/src/ui/render.js CHANGED
@@ -363,9 +363,9 @@ function opDuration(op, isPartial) {
363
363
  */
364
364
  function formatOpRow(theme, op, width, isPartial, isError) {
365
365
  const marker = opMarker(theme, op, isPartial, isError);
366
- const toolText = op.tool.padEnd(TOOL_COL);
366
+ const toolText = op.tool.slice(0, TOOL_COL).padEnd(TOOL_COL);
367
367
  const tool = theme.fg("syntaxFunction", toolText);
368
- const durationText = opDuration(op, isPartial);
368
+ const durationText = opDuration(op, isPartial).slice(-DURATION_COL);
369
369
  const duration = theme.fg("dim", durationText.padStart(DURATION_COL));
370
370
  let prefix = `${marker} ${tool} ${duration} `;
371
371
  let used = 2 + toolText.length + 1 + DURATION_COL + 2;