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 CHANGED
@@ -2,9 +2,32 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [0.0.6] - 2026-09-03
6
+
7
+ ### Changed
8
+
9
+ - Pi and OMP now share one self-owned framed result card. The hidden call slot prevents duplicate lifecycle cards, and connected rows preserve visual separation between calls.
10
+
11
+ ### Fixed
12
+
13
+ - Edit, write, and patch calls now show bounded line-numbered removed/added hunks while collapsed, with a larger budget when expanded; captured host executors propagate their native diff metadata into the same UI.
14
+
15
+ - Direct `nova.snap()` and `nova.surface()` helpers now return structured objects instead of host result envelopes.
16
+ - Package metadata now matches the official Pi package contract: every `files` entry exists, and only the actually imported `typebox` host dependency remains declared as a peer.
17
+
18
+ ## [0.0.5] - 2026-09-03
19
+
5
20
  ### Fixed
6
21
 
7
22
  - Native adapters are now included in `nova.search` / `nova.describe` with schemas matching the callable adapter, even when the host catalog omits parameters or the adapter name.
23
+ - `snap` discovers explicitly targeted hidden paths outside `.git` and sees files written earlier in the same speculative invocation.
24
+ - Top-level `snap()` and `surface()` helpers return structured objects, enabling an immediate `edit(hit.path, ...)` handoff.
25
+ - Live command activity remains visible across partial updates and final merged cards.
26
+ - Collapsed cards show a compact, correctly labeled call ledger with paths and change counts; diff hunks are bounded and shown only when expanded.
27
+ - Multi-replacement edits report only the changed hunks instead of presenting the entire file as replaced.
28
+ - Syntax errors now roll back the outer VFS transaction instead of leaking speculative depth into the next Supernova call.
29
+ - Root-level Snap searches continue to ignore hidden files; hidden discovery is enabled only when the caller explicitly targets a hidden search root, while Git metadata remains excluded.
30
+ - Command cards cover custom tools, mark failed traces accurately, sanitize terminal controls, and remain width-safe for narrow terminals and wide emoji.
8
31
 
9
32
  ## [0.0.4] - 2026-09-03
10
33
 
package/README.md CHANGED
@@ -46,16 +46,22 @@ async () => {
46
46
  }
47
47
  ```
48
48
 
49
- Globals: `nova` / `tools`, `parallel`, `pipeline`, `console`, plus shorthand `read` / `write` / `edit` / `bash` /
49
+ Globals: `nova` / `tools`, `parallel`, `pipeline`, `console`, plus shorthand `read`, `write`, `edit`, `patch`, `surface`, `snap`, `bash`, and `exec`.
50
50
 
51
- Terminal card (muted violet / grey-blue not a raw JSON args dump):
51
+ Unified Pi/OMP card with bounded mutation diffs visible while collapsed:
52
52
 
53
53
  ```text
54
- nova · 84ms
55
- read packages/pi-supernova/host-bridge.js
56
- edit packages/pi-supernova/diff.js
54
+ ╭─── nova: 2 calls · 84ms ───────────────────────────────╮
55
+ ├─ ✓ read packages/pi-supernova/host-bridge.js
56
+ │ │
57
+ │ └─ ✓ edit +1/-1 packages/pi-supernova/diff.js │
58
+ │ -143 │ - const oldValue = before; │
59
+ │ +143 │ + const newValue = after; │
60
+ ╰─────────────────────────────────────────────────────────╯
57
61
  ```
58
62
 
63
+ Collapsed cards show bounded edit/write/patch hunks immediately. Press Enter for a larger hunk budget, logs, and the returned value.
64
+
59
65
  ---
60
66
 
61
67
  ## API
@@ -66,9 +72,14 @@ Terminal card (muted violet / grey-blue — not a raw JSON args dump):
66
72
  | `nova.describe(name)` | Parameter summary on demand |
67
73
  | `nova.call(name, args)` | Host tool or native adapter |
68
74
  | `nova.callMany([{name,args}])` | Auto parallel wave — iterable array with `.mode` / `.results` |
75
+ | `nova.surface(path)` | Structural outline for a source file |
76
+ | `nova.snap(query, searchRoot?)` | Most relevant source path, line, signature, confidence, and context |
77
+ | `nova.has(name)` | Whether a catalog or native tool is callable |
69
78
  | `parallel(thunks)` / `pipeline(items, …stages)` | Raw `Promise.all` helpers |
70
79
  | `nova.speculate(fn)` | Counterfactual branch (rollback / commit) |
71
80
 
81
+ Root Snap searches ignore hidden files. Passing a hidden search root includes hidden files beneath that root; Git metadata is always excluded.
82
+
72
83
  ---
73
84
 
74
85
  ## Configuration
@@ -126,7 +137,7 @@ Pair with DCE last if you use it: `omp install npm:pi-deferred-context-engine`.
126
137
 
127
138
  - Guest JS is **unsandboxed**. Adapter path jails are not a boundary against `import("node:fs")`.
128
139
  - `bash` / mutating tools flush speculative writes (transaction barrier); error rollback cannot undo that.
129
- - First public cut (`0.0.1`) — APIs and TUI will iterate.
140
+ - Pre-1.0 package — APIs and TUI may still evolve between minor releases.
130
141
 
131
142
  ## License
132
143
 
package/catalog.js CHANGED
@@ -28,7 +28,10 @@ export const NATIVE_TOOL_DEFINITIONS = [
28
28
  },
29
29
  {
30
30
  name: "snap", description: "Resolve a concept query to the most relevant workspace source location.",
31
- parameters: { type: "object", properties: { query: { type: "string" }, path: { type: "string" } }, required: ["query"] },
31
+ parameters: { type: "object", properties: {
32
+ query: { type: "string", description: "Source concept to resolve" },
33
+ path: { type: "string", description: "Optional workspace search root; explicitly targeting a hidden directory includes its hidden files, but Git metadata is always excluded" },
34
+ }, required: ["query"] },
32
35
  },
33
36
  {
34
37
  name: "surface", description: "Extract a structural outline from a workspace source file.",
package/diff.js CHANGED
@@ -2,12 +2,12 @@
2
2
  import { isString } from "./decode.js";
3
3
 
4
4
  export function buildEditDiff(filePath, originalText, oldText, newText) {
5
- const fileLines = isString(originalText) ? originalText.split("\n") : [];
5
+ const fileLines = contentLines(originalText);
6
6
  const idx = isString(originalText) ? originalText.indexOf(oldText) : -1;
7
7
 
8
8
  const startLine = idx >= 0 ? originalText.slice(0, idx).split("\n").length : 1;
9
- const oldLines = oldText.split("\n");
10
- const newLines = newText.split("\n");
9
+ const oldLines = contentLines(oldText);
10
+ const newLines = contentLines(newText);
11
11
 
12
12
  const lines = [];
13
13
  if (startLine > 1 && fileLines.length >= startLine - 1) {
@@ -21,9 +21,9 @@ export function buildEditDiff(filePath, originalText, oldText, newText) {
21
21
  lines.push({ type: "add", lineNum: startLine + i, text: newLines[i] });
22
22
  }
23
23
 
24
- const afterLine = startLine + oldLines.length;
25
- if (fileLines.length >= afterLine) {
26
- lines.push({ type: "context", lineNum: afterLine, text: fileLines[afterLine - 1] });
24
+ const afterSourceLine = startLine + oldLines.length;
25
+ if (fileLines.length >= afterSourceLine) {
26
+ lines.push({ type: "context", lineNum: startLine + newLines.length, text: fileLines[afterSourceLine - 1] });
27
27
  }
28
28
 
29
29
  return {
@@ -35,66 +35,79 @@ export function buildEditDiff(filePath, originalText, oldText, newText) {
35
35
  };
36
36
  }
37
37
 
38
+ export function buildMultiEditDiff(filePath, originalText, replacements) {
39
+ const parts = replacements.map(({ oldText, newText }) =>
40
+ buildEditDiff(filePath, originalText, oldText, newText),
41
+ );
42
+ const lines = [];
43
+ for (const part of parts) {
44
+ for (const line of part.lines) {
45
+ const previous = lines.at(-1);
46
+ if (previous?.type === "context" && line.type === "context" && previous.lineNum === line.lineNum) continue;
47
+ lines.push(line);
48
+ }
49
+ }
50
+ return {
51
+ path: filePath,
52
+ op: "edit",
53
+ added: parts.reduce((sum, part) => sum + part.added, 0),
54
+ removed: parts.reduce((sum, part) => sum + part.removed, 0),
55
+ lines,
56
+ };
57
+ }
58
+
38
59
  export function buildPatchDiff(filePath, patchText) {
39
60
  const patchLines = isString(patchText) ? patchText.replace(/\r\n/g, "\n").split("\n") : [];
40
61
  const lines = [];
41
62
  let added = 0;
42
63
  let removed = 0;
43
- let currentLineNum = 1;
64
+ let oldLineNum = 1;
65
+ let newLineNum = 1;
44
66
  let inHunk = false;
45
67
 
46
- for (const pLine of patchLines) {
47
- const headerMatch = /^@@\s+-(\d+)/.exec(pLine);
68
+ for (const patchLine of patchLines) {
69
+ const headerMatch = /^@@\s+-(\d+)(?:,\d+)?\s+\+(\d+)/.exec(patchLine);
48
70
  if (headerMatch) {
49
- currentLineNum = parseInt(headerMatch[1], 10);
71
+ oldLineNum = Number(headerMatch[1]);
72
+ newLineNum = Number(headerMatch[2]);
50
73
  inHunk = true;
51
74
  continue;
52
75
  }
53
- if (!inHunk || pLine.startsWith("\\")) continue;
54
- if (pLine.startsWith("-")) {
76
+ if (!inHunk || patchLine.startsWith("\\")) continue;
77
+ if (patchLine.startsWith("-")) {
55
78
  removed += 1;
56
- lines.push({ type: "remove", lineNum: currentLineNum, text: pLine.slice(1) });
57
- currentLineNum += 1;
58
- } else if (pLine.startsWith("+")) {
79
+ lines.push({ type: "remove", lineNum: oldLineNum, text: patchLine.slice(1) });
80
+ oldLineNum += 1;
81
+ } else if (patchLine.startsWith("+")) {
59
82
  added += 1;
60
- lines.push({ type: "add", lineNum: currentLineNum, text: pLine.slice(1) });
61
- } else if (pLine.startsWith(" ")) {
62
- lines.push({ type: "context", lineNum: currentLineNum, text: pLine.slice(1) });
63
- currentLineNum += 1;
83
+ lines.push({ type: "add", lineNum: newLineNum, text: patchLine.slice(1) });
84
+ newLineNum += 1;
85
+ } else if (patchLine.startsWith(" ")) {
86
+ lines.push({ type: "context", lineNum: newLineNum, text: patchLine.slice(1) });
87
+ oldLineNum += 1;
88
+ newLineNum += 1;
64
89
  }
65
90
  }
66
91
 
67
- return {
68
- path: filePath,
69
- op: "apply_patch",
70
- added,
71
- removed,
72
- lines,
73
- };
92
+ return { path: filePath, op: "apply_patch", added, removed, lines };
74
93
  }
75
94
 
76
- export function buildWriteDiff(filePath, previousText, newText) {
77
- const newLines = isString(newText) ? newText.split("\n") : [];
78
- const oldLines = isString(previousText) ? previousText.split("\n") : [];
79
-
80
- if (oldLines.length === 0 || (oldLines.length === 1 && oldLines[0] === "")) {
81
- const sampleCount = Math.min(newLines.length, 6);
82
- const lines = [];
83
- for (let i = 0; i < sampleCount; i++) {
84
- lines.push({ type: "add", lineNum: i + 1, text: newLines[i] });
85
- }
86
- return {
87
- path: filePath,
88
- op: "write",
89
- added: newLines.length,
90
- removed: 0,
91
- lines,
92
- };
93
- }
95
+ function contentLines(text) {
96
+ if (!isString(text) || text.length === 0) return [];
97
+ const lines = text.replace(/\r\n/g, "\n").split("\n");
98
+ if (lines.at(-1) === "") lines.pop();
99
+ return lines;
100
+ }
94
101
 
95
- const sampleCount = Math.min(newLines.length, 6);
102
+ export function buildWriteDiff(filePath, previousText, newText) {
103
+ const newLines = contentLines(newText);
104
+ const oldLines = contentLines(previousText);
105
+ const maxStoredLines = 64;
96
106
  const lines = [];
97
- for (let i = 0; i < sampleCount; i++) {
107
+ for (let i = 0; i < oldLines.length && lines.length < maxStoredLines; i++) {
108
+ lines.push({ type: "remove", lineNum: i + 1, text: oldLines[i] });
109
+ }
110
+ for (let i = 0; i < newLines.length && lines.length < maxStoredLines; i++) {
98
111
  lines.push({ type: "add", lineNum: i + 1, text: newLines[i] });
99
112
  }
100
113
  return {
@@ -102,6 +115,7 @@ export function buildWriteDiff(filePath, previousText, newText) {
102
115
  op: "write",
103
116
  added: newLines.length,
104
117
  removed: oldLines.length,
118
+ displayLineCount: oldLines.length + newLines.length,
105
119
  lines,
106
120
  };
107
121
  }
package/host-bridge.js CHANGED
@@ -3,10 +3,10 @@ import * as fs from "node:fs/promises";
3
3
  import * as path from "node:path";
4
4
  import { spawn } from "node:child_process";
5
5
  import { packageHostResult } from "./bottleneck.js";
6
- import { isString, isNumber, isFunction } from "./decode.js";
6
+ import { isString, isNumber, isFunction, isObject } from "./decode.js";
7
7
  import { isMutatingTool, runParallelWave } from "./parallel.js";
8
8
  import { extractStructuralSurface } from "./surface.js";
9
- import { buildEditDiff, buildPatchDiff, buildWriteDiff } from "./diff.js";
9
+ import { buildEditDiff, buildMultiEditDiff, buildPatchDiff, buildWriteDiff } from "./diff.js";
10
10
  import { executeSnap } from "./snap.js";
11
11
 
12
12
  function textResult(text, details) {
@@ -33,7 +33,7 @@ async function resolveWorkspacePath(cwd, inputPath, opName, allowRoot = false) {
33
33
  const resolvedCwd = getResolvedCwd(cwd);
34
34
  const target = path.resolve(resolvedCwd, inputPath.trim());
35
35
  const rel = path.relative(resolvedCwd, target);
36
- if (rel.startsWith("..") || path.isAbsolute(rel)) {
36
+ if (rel === ".." || rel.startsWith(`..${path.sep}`) || path.isAbsolute(rel)) {
37
37
  throw new Error(`${opName} path escapes workspace`);
38
38
  }
39
39
  if (!allowRoot && target === resolvedCwd) {
@@ -54,7 +54,7 @@ async function resolveWorkspacePath(cwd, inputPath, opName, allowRoot = false) {
54
54
  }
55
55
  }
56
56
  const realRel = path.relative(realRoot, probe);
57
- if (realRel.startsWith("..") || path.isAbsolute(realRel)) {
57
+ if (realRel === ".." || realRel.startsWith(`..${path.sep}`) || path.isAbsolute(realRel)) {
58
58
  throw new Error(`${opName} path escapes workspace through symlink`);
59
59
  }
60
60
  return target;
@@ -242,6 +242,14 @@ class CausalVfs {
242
242
  return undefined;
243
243
  }
244
244
 
245
+ getOverlayPaths() {
246
+ const paths = new Set();
247
+ for (const overlay of this.overlays) {
248
+ for (const target of overlay.keys()) paths.add(target);
249
+ }
250
+ return [...paths];
251
+ }
252
+
245
253
  async read(target) {
246
254
  const overlay = this.getOverlay(target);
247
255
  if (overlay !== undefined) return overlay;
@@ -501,7 +509,7 @@ function createNativeAdapters(getCwd, vfs, config) {
501
509
  const diff =
502
510
  matches.length === 1
503
511
  ? buildEditDiff(target, content, matches[0].oldText, matches[0].newText)
504
- : { ...buildWriteDiff(target, content, updated), op: "edit" };
512
+ : buildMultiEditDiff(target, content, matches);
505
513
  const tag = speculative ? " (speculative)" : "";
506
514
  return textResult(`edited ${target}${tag}`, { path: target, speculative, diff });
507
515
  },
@@ -537,9 +545,14 @@ function createNativeAdapters(getCwd, vfs, config) {
537
545
  }
538
546
  if (signal?.aborted) throw new Error("aborted");
539
547
  const snapTarget = params?.path ? await resolveWorkspacePath(cwd, params.path, "snap", true) : cwd;
548
+ const relativeRoot = path.relative(cwd, snapTarget);
549
+ const includeHidden = Boolean(params?.path) && relativeRoot
550
+ .split(path.sep)
551
+ .some((segment) => segment.startsWith(".") && segment.length > 1);
540
552
  const res = await executeSnap({
541
553
  query: params.query,
542
554
  searchDir: snapTarget,
555
+ includeHidden,
543
556
  vfs: {
544
557
  read: async (candidate) => {
545
558
  // Jail each snap candidate (symlink files must not escape the workspace).
@@ -548,6 +561,7 @@ function createNativeAdapters(getCwd, vfs, config) {
548
561
  },
549
562
  },
550
563
  runCommand: (argv, opts) => runCommand(argv, { cwd: snapTarget, signal, ...opts }),
564
+ pendingPaths: vfs.getOverlayPaths(),
551
565
  });
552
566
  return textResult(JSON.stringify(res, null, 2), res);
553
567
  },
@@ -726,6 +740,25 @@ export function createHostBridge({ pi, config, getCwd }) {
726
740
  vfs.clear();
727
741
  }
728
742
 
743
+ function resultDiff(response) {
744
+ let details = response?.details;
745
+ if (isString(details)) {
746
+ try {
747
+ details = JSON.parse(details);
748
+ } catch {
749
+ return undefined;
750
+ }
751
+ }
752
+ return isObject(details) ? details.diff : undefined;
753
+ }
754
+
755
+ function notifyCall(record) {
756
+ if (!callListener) return;
757
+ try {
758
+ callListener(record, [...trace]);
759
+ } catch {}
760
+ }
761
+
729
762
  async function invokeRaw(name, args) {
730
763
  const maxCalls = config.maxBridgeCalls ?? 256;
731
764
  callCount += 1;
@@ -745,34 +778,49 @@ export function createHostBridge({ pi, config, getCwd }) {
745
778
 
746
779
  const record = { name, args: args || {}, time: Date.now() };
747
780
  trace.push(record);
781
+ notifyCall(record);
748
782
 
749
- const exec = executors.get(name);
750
- if (exec) {
751
- if (isMutatingTool(name, config)) await vfs.prepareExternalMutation(name);
752
- const res = await exec(`supernova:${name}:${callCount}`, args || {}, activeSignal, undefined, activeCtx);
753
- if (callListener) {
754
- try {
755
- callListener(record, [...trace]);
756
- } catch {}
783
+ try {
784
+ const exec = executors.get(name);
785
+ if (exec) {
786
+ let fallbackDiff;
787
+ if (name === "write" && isString(args?.path) && isString(args?.content)) {
788
+ const target = await resolveWorkspacePath(getCwd(), args.path, "write", false);
789
+ let previous = "";
790
+ try {
791
+ previous = await vfs.read(target);
792
+ } catch (error) {
793
+ if (error?.code !== "ENOENT") throw error;
794
+ }
795
+ fallbackDiff = buildWriteDiff(target, previous, args.content);
796
+ }
797
+ if (isMutatingTool(name, config)) await vfs.prepareExternalMutation(name);
798
+ const res = await exec(`supernova:${name}:${callCount}`, args || {}, activeSignal, undefined, activeCtx);
799
+ const diff = resultDiff(res) || fallbackDiff;
800
+ record.ok = res?.isError !== true && res?.details?.ok !== false;
801
+ if (diff && record.ok) record.diff = diff;
802
+ notifyCall(record);
803
+ return res;
757
804
  }
758
- return res;
759
- }
760
805
 
761
- const native = natives[name];
762
- if (native) {
763
- const res = await native(args || {}, activeSignal);
764
- if (res?.details?.diff) record.diff = res.details.diff;
765
- if (callListener) {
766
- try {
767
- callListener(record, [...trace]);
768
- } catch {}
806
+ const native = natives[name];
807
+ if (native) {
808
+ const res = await native(args || {}, activeSignal);
809
+ const diff = resultDiff(res);
810
+ record.ok = res?.isError !== true && res?.details?.ok !== false;
811
+ if (diff && record.ok) record.diff = diff;
812
+ notifyCall(record);
813
+ return res;
769
814
  }
770
- return res;
771
- }
772
815
 
773
- throw new Error(
774
- `no executor for tool "${name}" (not captured via registerTool and no native adapter). Use nova.describe to inspect; ensure pi-supernova loads before other extensions, or call a core adapter: ${Object.keys(natives).join(", ")}`,
775
- );
816
+ throw new Error(
817
+ `no executor for tool "${name}" (not captured via registerTool and no native adapter). Use nova.describe to inspect; ensure pi-supernova loads before other extensions, or call a core adapter: ${Object.keys(natives).join(", ")}`,
818
+ );
819
+ } catch (error) {
820
+ record.ok = false;
821
+ notifyCall(record);
822
+ throw error;
823
+ }
776
824
  }
777
825
 
778
826
  async function call(name, args) {
package/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { createRequire } from "node:module";
2
- import { isString, isFunction } from "./decode.js";
2
+ import { isString, isFunction, isObject } from "./decode.js";
3
3
  import { buildCatalog, searchCatalog, describeTool, mergeNativeToolDefinitions } from "./catalog.js";
4
4
  import { loadConfig } from "./config.js";
5
5
  import { createHostBridge } from "./host-bridge.js";
@@ -30,6 +30,18 @@ try {
30
30
  function result(text, details) {
31
31
  return { content: [{ type: "text", text }], details };
32
32
  }
33
+ function unwrapStructuredResult(response, operation) {
34
+ if (response?.ok === false) {
35
+ throw new Error(response.value || response.error || `${operation} failed`);
36
+ }
37
+ const value = isObject(response) && "value" in response ? response.value : response;
38
+ if (!isString(value)) return value;
39
+ try {
40
+ return JSON.parse(value);
41
+ } catch {
42
+ return value;
43
+ }
44
+ }
33
45
 
34
46
  const TOOL_DESCRIPTION = `Execute JavaScript that orchestrates host tools in one shot (Code Mode).
35
47
 
@@ -38,8 +50,12 @@ Inside the program you get:
38
50
  nova.describe(name) — full parameter summary on demand
39
51
  nova.call(name, args) — invoke a host tool (or native adapter)
40
52
  nova.callMany([{name,args}]) — Auto parallel wave (serial if any mutating)
53
+ nova.snap(query, root?) — resolve a concept to a source location
54
+ nova.surface(path) — structural source outline
55
+ nova.has(name) — test host-tool availability
41
56
  parallel(thunks) / pipeline(items, ...stages)
42
57
 
58
+ Shorthand globals: read, write, edit, patch, exec, snap, surface.
43
59
  Prefer search→describe→call. Keep intermediates in the program; return a shaped value.
44
60
  Schemas are NOT dumped into the system prompt — discover them inside the runtime.`;
45
61
 
@@ -97,10 +113,10 @@ export default function piSupernova(pi) {
97
113
  }
98
114
  },
99
115
  async surface(filePath) {
100
- return bridge.call("surface", { path: filePath });
116
+ return unwrapStructuredResult(await bridge.call("surface", { path: filePath }), "surface");
101
117
  },
102
118
  async snap(query, targetPath) {
103
- return bridge.call("snap", { query, path: targetPath });
119
+ return unwrapStructuredResult(await bridge.call("snap", { query, path: targetPath }), "snap");
104
120
  },
105
121
  has(name) {
106
122
  return bridge.hasExecutor(name) || catalog.some((t) => t.name === name);
@@ -130,8 +146,8 @@ export default function piSupernova(pi) {
130
146
  }),
131
147
  ),
132
148
  }),
133
- // "self" = we own chrome. OMP uses native framedBlock (write/edit look);
134
- // Pi keeps the muted violet SafeText wash. "default" falls back to raw JSON.
149
+ // One self-owned result frame is shared by Pi and OMP; renderCall stays empty
150
+ // so separate call/result slots cannot duplicate the lifecycle card.
135
151
  renderShell: "self",
136
152
  mergeCallAndResult: true,
137
153
  renderCall: renderSupernovaCall,
@@ -159,11 +175,21 @@ export default function piSupernova(pi) {
159
175
  }
160
176
  });
161
177
 
178
+ if (isFunction(onUpdate)) {
179
+ try {
180
+ onUpdate({
181
+ content: [{ type: "text", text: "" }],
182
+ details: { trace: [], running: true },
183
+ });
184
+ } catch {}
185
+ }
186
+
162
187
  const runConfig = {
163
188
  ...config,
164
189
  timeoutMs: Number.isInteger(params?.timeoutMs) ? params.timeoutMs : config.timeoutMs,
165
190
  };
166
191
 
192
+ const runStartedAt = performance.now();
167
193
  let outcome;
168
194
  try {
169
195
  outcome = await runGuestProgram({
@@ -173,7 +199,15 @@ export default function piSupernova(pi) {
173
199
  signal: runController.signal,
174
200
  onTimeout: abortRun,
175
201
  });
202
+ } catch (error) {
203
+ outcome = {
204
+ ok: false,
205
+ error: error instanceof Error ? error.message : String(error),
206
+ logs: [],
207
+ wallMs: Math.round(performance.now() - runStartedAt),
208
+ };
176
209
  } finally {
210
+ bridge.setCallListener(null);
177
211
  signal?.removeEventListener("abort", abortRun);
178
212
  }
179
213
 
package/omp-frame.js CHANGED
@@ -1,11 +1,12 @@
1
1
  /**
2
- * OMP-style rounded tool chrome for supernova.
2
+ * Shared Pi/OMP rounded tool chrome for supernova.
3
3
  *
4
4
  * Intentionally self-contained — never dynamic-imports `@oh-my-pi/pi-coding-agent`
5
- * (that hung OMP plugin load). Geometry matches native write/edit framedBlock.
5
+ * (that hung OMP plugin load). Portable geometry matches native edit/write cards.
6
6
  */
7
7
 
8
- import { clampLine, measureWidth, wrapPlainToWidth } from "./render-measure.js";
8
+ import { clampLine, measureWidth } from "./render-measure.js";
9
+ import { isFunction, isString } from "./decode.js";
9
10
 
10
11
  const DEFAULT_BOX = {
11
12
  topLeft: "╭",
@@ -18,14 +19,6 @@ const DEFAULT_BOX = {
18
19
  teeRight: "├",
19
20
  };
20
21
 
21
- export function hasHostFramedBlock() {
22
- return false;
23
- }
24
-
25
- export async function ensureOmpChrome() {
26
- // No-op: portable frame only. Kept so call sites stay stable.
27
- }
28
-
29
22
  function boxOf(theme) {
30
23
  const b = theme?.boxRound;
31
24
  if (b && b.topLeft && b.horizontal && b.vertical) return b;
@@ -36,7 +29,7 @@ function borderPaint(theme, state, borderColor) {
36
29
  const key =
37
30
  borderColor ||
38
31
  (state === "error" ? "error" : state === "warning" ? "warning" : state === "running" || state === "pending" ? "accent" : "dim");
39
- if (theme && typeof theme.fg === "function") {
32
+ if (theme && isFunction(theme.fg)) {
40
33
  try {
41
34
  return (text) => theme.fg(key, text);
42
35
  } catch {
@@ -74,21 +67,21 @@ function padLine(line, width, bgFn) {
74
67
 
75
68
  function bgFnForState(theme, state) {
76
69
  if (!state || !theme) return undefined;
77
- if (typeof theme.bg === "function") {
70
+ if (isFunction(theme.bg)) {
78
71
  const key =
79
72
  state === "error" ? "toolErrorBg" : state === "pending" || state === "running" ? "toolPendingBg" : "toolSuccessBg";
80
73
  try {
81
74
  const probe = theme.bg(key, "x");
82
- if (typeof probe !== "string") return undefined;
75
+ if (!isString(probe)) return undefined;
83
76
  return (text) => {
84
77
  const painted = theme.bg(key, text);
85
- return typeof painted === "string" ? painted : text;
78
+ return isString(painted) ? painted : text;
86
79
  };
87
80
  } catch {
88
81
  return undefined;
89
82
  }
90
83
  }
91
- if (typeof theme.getBgAnsi === "function") {
84
+ if (isFunction(theme.getBgAnsi)) {
92
85
  try {
93
86
  const key =
94
87
  state === "error" ? "toolErrorBg" : state === "pending" || state === "running" ? "toolPendingBg" : "toolSuccessBg";
@@ -102,8 +95,16 @@ function bgFnForState(theme, state) {
102
95
  return undefined;
103
96
  }
104
97
 
105
- export function renderPortableFrame(theme, { header, sections = [], state = "pending", borderColor, width }) {
106
- const w = Math.max(8, width | 0);
98
+ function renderPortableFrame(theme, { header, sections = [], state = "pending", borderColor, width }) {
99
+ const w = Math.max(1, width | 0);
100
+ if (w < 8) {
101
+ const rawLines = [header];
102
+ for (const section of sections) {
103
+ if (section.label) rawLines.push(section.label);
104
+ rawLines.push(...(section.lines || []));
105
+ }
106
+ return rawLines.filter(Boolean).map((line) => clampLine(line, w));
107
+ }
107
108
  const box = boxOf(theme);
108
109
  const border = borderPaint(theme, state, borderColor);
109
110
  const bgFn = bgFnForState(theme, state);
@@ -148,7 +149,7 @@ export function renderPortableFrame(theme, { header, sections = [], state = "pen
148
149
  return lines;
149
150
  }
150
151
 
151
- export function createPortableFramedComponent(theme, build) {
152
+ function createPortableFramedComponent(theme, build) {
152
153
  let cacheWidth;
153
154
  let cacheKey;
154
155
  let cacheLines;
@@ -180,11 +181,3 @@ export function novaStatusLine(theme, options) {
180
181
  return statusHeader(theme, options);
181
182
  }
182
183
 
183
- export function wrapPlainLines(text, width) {
184
- const w = Math.max(1, width | 0);
185
- const out = [];
186
- for (const line of String(text ?? "").split("\n")) {
187
- for (const chunk of wrapPlainToWidth(line, w)) out.push(chunk);
188
- }
189
- return out.length ? out : [""];
190
- }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-supernova",
3
- "version": "0.0.4",
3
+ "version": "0.0.6",
4
4
  "description": "Dual-host CodeMode for Pi/OMP: progressive tool discovery, result bottleneck, and Amdahl Auto parallel.",
5
5
  "type": "module",
6
6
  "author": "AdityaVG13",
@@ -57,24 +57,15 @@
57
57
  ]
58
58
  },
59
59
  "peerDependencies": {
60
- "@earendil-works/pi-coding-agent": "*",
61
- "@earendil-works/pi-tui": "*",
62
60
  "typebox": "*"
63
61
  },
64
62
  "peerDependenciesMeta": {
65
- "@earendil-works/pi-coding-agent": {
66
- "optional": true
67
- },
68
- "@earendil-works/pi-tui": {
69
- "optional": true
70
- },
71
63
  "typebox": {
72
64
  "optional": true
73
65
  }
74
66
  },
75
67
  "devDependencies": {
76
- "typebox": "^1.0.0",
77
- "@earendil-works/pi-tui": "^0.84.0"
68
+ "typebox": "^1.0.0"
78
69
  },
79
70
  "publishConfig": {
80
71
  "access": "public"