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/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
|
|
49
|
+
Globals: `nova` / `tools`, `parallel`, `pipeline`, `console`, plus shorthand `read`, `write`, `edit`, `patch`, `surface`, `snap`, `bash`, and `exec`.
|
|
50
50
|
|
|
51
|
-
|
|
51
|
+
Unified Pi/OMP card with bounded mutation diffs visible while collapsed:
|
|
52
52
|
|
|
53
53
|
```text
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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
|
-
-
|
|
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: {
|
|
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 =
|
|
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
|
|
10
|
-
const newLines = newText
|
|
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
|
|
25
|
-
if (fileLines.length >=
|
|
26
|
-
lines.push({ type: "context", lineNum:
|
|
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
|
|
64
|
+
let oldLineNum = 1;
|
|
65
|
+
let newLineNum = 1;
|
|
44
66
|
let inHunk = false;
|
|
45
67
|
|
|
46
|
-
for (const
|
|
47
|
-
const headerMatch = /^@@\s+-(\d+)/.exec(
|
|
68
|
+
for (const patchLine of patchLines) {
|
|
69
|
+
const headerMatch = /^@@\s+-(\d+)(?:,\d+)?\s+\+(\d+)/.exec(patchLine);
|
|
48
70
|
if (headerMatch) {
|
|
49
|
-
|
|
71
|
+
oldLineNum = Number(headerMatch[1]);
|
|
72
|
+
newLineNum = Number(headerMatch[2]);
|
|
50
73
|
inHunk = true;
|
|
51
74
|
continue;
|
|
52
75
|
}
|
|
53
|
-
if (!inHunk ||
|
|
54
|
-
if (
|
|
76
|
+
if (!inHunk || patchLine.startsWith("\\")) continue;
|
|
77
|
+
if (patchLine.startsWith("-")) {
|
|
55
78
|
removed += 1;
|
|
56
|
-
lines.push({ type: "remove", lineNum:
|
|
57
|
-
|
|
58
|
-
} else if (
|
|
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:
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
-
|
|
77
|
-
|
|
78
|
-
const
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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
|
-
|
|
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 <
|
|
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(
|
|
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(
|
|
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
|
-
:
|
|
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
|
-
|
|
750
|
-
|
|
751
|
-
if (
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
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
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
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
|
-
|
|
774
|
-
|
|
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
|
-
//
|
|
134
|
-
//
|
|
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
|
|
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).
|
|
5
|
+
* (that hung OMP plugin load). Portable geometry matches native edit/write cards.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import { clampLine, measureWidth
|
|
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 &&
|
|
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 (
|
|
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 (
|
|
75
|
+
if (!isString(probe)) return undefined;
|
|
83
76
|
return (text) => {
|
|
84
77
|
const painted = theme.bg(key, text);
|
|
85
|
-
return
|
|
78
|
+
return isString(painted) ? painted : text;
|
|
86
79
|
};
|
|
87
80
|
} catch {
|
|
88
81
|
return undefined;
|
|
89
82
|
}
|
|
90
83
|
}
|
|
91
|
-
if (
|
|
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
|
-
|
|
106
|
-
const w = Math.max(
|
|
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
|
-
|
|
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.
|
|
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"
|