pi-supernova 0.0.4 → 0.0.5
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 +10 -0
- package/README.md +14 -5
- package/catalog.js +4 -1
- package/diff.js +21 -0
- package/host-bridge.js +48 -27
- package/index.js +13 -0
- package/omp-frame.js +9 -1
- package/package.json +1 -1
- package/render-measure.js +14 -8
- package/render.js +144 -136
- package/runtime.js +38 -19
- package/snap.js +29 -3
package/CHANGELOG.md
CHANGED
|
@@ -2,9 +2,19 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [0.0.5] - 2026-09-03
|
|
6
|
+
|
|
5
7
|
### Fixed
|
|
6
8
|
|
|
7
9
|
- 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.
|
|
10
|
+
- `snap` discovers explicitly targeted hidden paths outside `.git` and sees files written earlier in the same speculative invocation.
|
|
11
|
+
- Top-level `snap()` and `surface()` helpers return structured objects, enabling an immediate `edit(hit.path, ...)` handoff.
|
|
12
|
+
- Live command activity remains visible across partial updates and final merged cards.
|
|
13
|
+
- Collapsed cards show a compact, correctly labeled call ledger with paths and change counts; diff hunks are bounded and shown only when expanded.
|
|
14
|
+
- Multi-replacement edits report only the changed hunks instead of presenting the entire file as replaced.
|
|
15
|
+
- Syntax errors now roll back the outer VFS transaction instead of leaking speculative depth into the next Supernova call.
|
|
16
|
+
- 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.
|
|
17
|
+
- Command cards cover custom tools, mark failed traces accurately, sanitize terminal controls, and remain width-safe for narrow terminals and wide emoji.
|
|
8
18
|
|
|
9
19
|
## [0.0.4] - 2026-09-03
|
|
10
20
|
|
package/README.md
CHANGED
|
@@ -46,16 +46,20 @@ 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
|
Terminal card (muted violet / grey-blue — not a raw JSON args dump):
|
|
52
52
|
|
|
53
53
|
```text
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
54
|
+
╭─── nova: 3 calls · 84ms ───────────────────────╮
|
|
55
|
+
│ ✓ read packages/pi-supernova/host-bridge.js │
|
|
56
|
+
│ ✓ edit packages/pi-supernova/diff.js +2/-1 │
|
|
57
|
+
│ ✓ snap "render lifecycle" → packages │
|
|
58
|
+
╰────────────────────────────────────────────────╯
|
|
57
59
|
```
|
|
58
60
|
|
|
61
|
+
Collapsed cards keep this command ledger visible. Press Enter to inspect bounded diff hunks, logs, and the returned value.
|
|
62
|
+
|
|
59
63
|
---
|
|
60
64
|
|
|
61
65
|
## API
|
|
@@ -66,9 +70,14 @@ Terminal card (muted violet / grey-blue — not a raw JSON args dump):
|
|
|
66
70
|
| `nova.describe(name)` | Parameter summary on demand |
|
|
67
71
|
| `nova.call(name, args)` | Host tool or native adapter |
|
|
68
72
|
| `nova.callMany([{name,args}])` | Auto parallel wave — iterable array with `.mode` / `.results` |
|
|
73
|
+
| `nova.surface(path)` | Structural outline for a source file |
|
|
74
|
+
| `nova.snap(query, searchRoot?)` | Most relevant source path, line, signature, confidence, and context |
|
|
75
|
+
| `nova.has(name)` | Whether a catalog or native tool is callable |
|
|
69
76
|
| `parallel(thunks)` / `pipeline(items, …stages)` | Raw `Promise.all` helpers |
|
|
70
77
|
| `nova.speculate(fn)` | Counterfactual branch (rollback / commit) |
|
|
71
78
|
|
|
79
|
+
Root Snap searches ignore hidden files. Passing a hidden search root includes hidden files beneath that root; Git metadata is always excluded.
|
|
80
|
+
|
|
72
81
|
---
|
|
73
82
|
|
|
74
83
|
## Configuration
|
|
@@ -126,7 +135,7 @@ Pair with DCE last if you use it: `omp install npm:pi-deferred-context-engine`.
|
|
|
126
135
|
|
|
127
136
|
- Guest JS is **unsandboxed**. Adapter path jails are not a boundary against `import("node:fs")`.
|
|
128
137
|
- `bash` / mutating tools flush speculative writes (transaction barrier); error rollback cannot undo that.
|
|
129
|
-
-
|
|
138
|
+
- Pre-1.0 package — APIs and TUI may still evolve between minor releases.
|
|
130
139
|
|
|
131
140
|
## License
|
|
132
141
|
|
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
|
@@ -35,6 +35,27 @@ 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 = [];
|
package/host-bridge.js
CHANGED
|
@@ -6,7 +6,7 @@ import { packageHostResult } from "./bottleneck.js";
|
|
|
6
6
|
import { isString, isNumber, isFunction } 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,13 @@ export function createHostBridge({ pi, config, getCwd }) {
|
|
|
726
740
|
vfs.clear();
|
|
727
741
|
}
|
|
728
742
|
|
|
743
|
+
function notifyCall(record) {
|
|
744
|
+
if (!callListener) return;
|
|
745
|
+
try {
|
|
746
|
+
callListener(record, [...trace]);
|
|
747
|
+
} catch {}
|
|
748
|
+
}
|
|
749
|
+
|
|
729
750
|
async function invokeRaw(name, args) {
|
|
730
751
|
const maxCalls = config.maxBridgeCalls ?? 256;
|
|
731
752
|
callCount += 1;
|
|
@@ -746,33 +767,33 @@ export function createHostBridge({ pi, config, getCwd }) {
|
|
|
746
767
|
const record = { name, args: args || {}, time: Date.now() };
|
|
747
768
|
trace.push(record);
|
|
748
769
|
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
if (
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
770
|
+
try {
|
|
771
|
+
const exec = executors.get(name);
|
|
772
|
+
if (exec) {
|
|
773
|
+
if (isMutatingTool(name, config)) await vfs.prepareExternalMutation(name);
|
|
774
|
+
const res = await exec(`supernova:${name}:${callCount}`, args || {}, activeSignal, undefined, activeCtx);
|
|
775
|
+
record.ok = res?.isError !== true && res?.details?.ok !== false;
|
|
776
|
+
notifyCall(record);
|
|
777
|
+
return res;
|
|
757
778
|
}
|
|
758
|
-
return res;
|
|
759
|
-
}
|
|
760
779
|
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
} catch {}
|
|
780
|
+
const native = natives[name];
|
|
781
|
+
if (native) {
|
|
782
|
+
const res = await native(args || {}, activeSignal);
|
|
783
|
+
if (res?.details?.diff) record.diff = res.details.diff;
|
|
784
|
+
record.ok = res?.isError !== true && res?.details?.ok !== false;
|
|
785
|
+
notifyCall(record);
|
|
786
|
+
return res;
|
|
769
787
|
}
|
|
770
|
-
return res;
|
|
771
|
-
}
|
|
772
788
|
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
789
|
+
throw new Error(
|
|
790
|
+
`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(", ")}`,
|
|
791
|
+
);
|
|
792
|
+
} catch (error) {
|
|
793
|
+
record.ok = false;
|
|
794
|
+
notifyCall(record);
|
|
795
|
+
throw error;
|
|
796
|
+
}
|
|
776
797
|
}
|
|
777
798
|
|
|
778
799
|
async function call(name, args) {
|
package/index.js
CHANGED
|
@@ -38,8 +38,12 @@ Inside the program you get:
|
|
|
38
38
|
nova.describe(name) — full parameter summary on demand
|
|
39
39
|
nova.call(name, args) — invoke a host tool (or native adapter)
|
|
40
40
|
nova.callMany([{name,args}]) — Auto parallel wave (serial if any mutating)
|
|
41
|
+
nova.snap(query, root?) — resolve a concept to a source location
|
|
42
|
+
nova.surface(path) — structural source outline
|
|
43
|
+
nova.has(name) — test host-tool availability
|
|
41
44
|
parallel(thunks) / pipeline(items, ...stages)
|
|
42
45
|
|
|
46
|
+
Shorthand globals: read, write, edit, patch, exec, snap, surface.
|
|
43
47
|
Prefer search→describe→call. Keep intermediates in the program; return a shaped value.
|
|
44
48
|
Schemas are NOT dumped into the system prompt — discover them inside the runtime.`;
|
|
45
49
|
|
|
@@ -164,6 +168,7 @@ export default function piSupernova(pi) {
|
|
|
164
168
|
timeoutMs: Number.isInteger(params?.timeoutMs) ? params.timeoutMs : config.timeoutMs,
|
|
165
169
|
};
|
|
166
170
|
|
|
171
|
+
const runStartedAt = performance.now();
|
|
167
172
|
let outcome;
|
|
168
173
|
try {
|
|
169
174
|
outcome = await runGuestProgram({
|
|
@@ -173,7 +178,15 @@ export default function piSupernova(pi) {
|
|
|
173
178
|
signal: runController.signal,
|
|
174
179
|
onTimeout: abortRun,
|
|
175
180
|
});
|
|
181
|
+
} catch (error) {
|
|
182
|
+
outcome = {
|
|
183
|
+
ok: false,
|
|
184
|
+
error: error instanceof Error ? error.message : String(error),
|
|
185
|
+
logs: [],
|
|
186
|
+
wallMs: Math.round(performance.now() - runStartedAt),
|
|
187
|
+
};
|
|
176
188
|
} finally {
|
|
189
|
+
bridge.setCallListener(null);
|
|
177
190
|
signal?.removeEventListener("abort", abortRun);
|
|
178
191
|
}
|
|
179
192
|
|
package/omp-frame.js
CHANGED
|
@@ -103,7 +103,15 @@ function bgFnForState(theme, state) {
|
|
|
103
103
|
}
|
|
104
104
|
|
|
105
105
|
export function renderPortableFrame(theme, { header, sections = [], state = "pending", borderColor, width }) {
|
|
106
|
-
const w = Math.max(
|
|
106
|
+
const w = Math.max(1, width | 0);
|
|
107
|
+
if (w < 8) {
|
|
108
|
+
const rawLines = [header];
|
|
109
|
+
for (const section of sections) {
|
|
110
|
+
if (section.label) rawLines.push(section.label);
|
|
111
|
+
rawLines.push(...(section.lines || []));
|
|
112
|
+
}
|
|
113
|
+
return rawLines.filter(Boolean).map((line) => clampLine(line, w));
|
|
114
|
+
}
|
|
107
115
|
const box = boxOf(theme);
|
|
108
116
|
const border = borderPaint(theme, state, borderColor);
|
|
109
117
|
const bgFn = bgFnForState(theme, state);
|
package/package.json
CHANGED
package/render-measure.js
CHANGED
|
@@ -27,6 +27,10 @@ export function measureWidth(text) {
|
|
|
27
27
|
|
|
28
28
|
function codePointWidth(cp) {
|
|
29
29
|
if (cp <= 0x1f || (cp >= 0x7f && cp <= 0x9f)) return 0;
|
|
30
|
+
if (cp === 0xfe0f) return 1; // Emoji presentation can widen an otherwise narrow symbol.
|
|
31
|
+
if (cp === 0x200d || (cp >= 0x0300 && cp <= 0x036f) || (cp >= 0x1ab0 && cp <= 0x1aff) ||
|
|
32
|
+
(cp >= 0x1dc0 && cp <= 0x1dff) || (cp >= 0x20d0 && cp <= 0x20ff) ||
|
|
33
|
+
(cp >= 0xfe00 && cp <= 0xfe0e) || (cp >= 0xfe20 && cp <= 0xfe2f)) return 0;
|
|
30
34
|
// Fullwidth / wide ranges (CJK, Hangul, emoji blocks we actually emit).
|
|
31
35
|
if (cp >= 0x1100 && cp <= 0x115f) return 2;
|
|
32
36
|
if (cp === 0x2329 || cp === 0x232a) return 2;
|
|
@@ -37,8 +41,7 @@ function codePointWidth(cp) {
|
|
|
37
41
|
if (cp >= 0xfe30 && cp <= 0xfe6f) return 2;
|
|
38
42
|
if (cp >= 0xff00 && cp <= 0xff60) return 2;
|
|
39
43
|
if (cp >= 0xffe0 && cp <= 0xffe6) return 2;
|
|
40
|
-
if (cp >=
|
|
41
|
-
if (cp >= 0x1f900 && cp <= 0x1f9ff) return 2;
|
|
44
|
+
if (cp >= 0x1f000 && cp <= 0x1faff) return 2;
|
|
42
45
|
if (cp >= 0x20000 && cp <= 0x3fffd) return 2;
|
|
43
46
|
// Ambiguous emoji/symbols pi-tui treats as wide (⚡ U+26A1 was the 92>91 footgun).
|
|
44
47
|
if (cp === 0x26a1 || cp === 0x2b50 || cp === 0x2728) return 2;
|
|
@@ -107,18 +110,21 @@ export function wrapPlainToWidth(plain, width) {
|
|
|
107
110
|
let visible = 0;
|
|
108
111
|
let lastBreak = -1;
|
|
109
112
|
while (end < text.length) {
|
|
110
|
-
const
|
|
113
|
+
const cp = text.codePointAt(end);
|
|
114
|
+
const ch = String.fromCodePoint(cp);
|
|
111
115
|
const cw = measureWidth(ch);
|
|
112
116
|
if (visible + cw > w) break;
|
|
113
117
|
visible += cw;
|
|
114
|
-
|
|
115
|
-
end
|
|
118
|
+
end += ch.length;
|
|
119
|
+
if (ch === "/" || ch === " ") lastBreak = end;
|
|
116
120
|
}
|
|
117
121
|
if (end === i) {
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
122
|
+
const ch = String.fromCodePoint(text.codePointAt(i));
|
|
123
|
+
lines.push(hardTruncate(ch, w));
|
|
124
|
+
i += ch.length;
|
|
125
|
+
continue;
|
|
121
126
|
}
|
|
127
|
+
if (end < text.length && lastBreak > i + Math.floor(w * 0.35)) end = lastBreak;
|
|
122
128
|
lines.push(text.slice(i, end));
|
|
123
129
|
i = end;
|
|
124
130
|
}
|
package/render.js
CHANGED
|
@@ -185,18 +185,20 @@ export function extractOperationsFromCode(code) {
|
|
|
185
185
|
|
|
186
186
|
const namedCalls = [
|
|
187
187
|
{ regex: /(?:^|[^\w$.])(?:nova\.)?read\s*\(\s*["'`]([^"'`]+)["'`]/gm, tool: "read", wrap: (p) => p },
|
|
188
|
-
{ regex: /(?:^|[^\w$.])(?:nova\.)?write\s*\(\s*["'`]([^"'`]+)["'`]/gm, tool: "
|
|
188
|
+
{ regex: /(?:^|[^\w$.])(?:nova\.)?write\s*\(\s*["'`]([^"'`]+)["'`]/gm, tool: "write", wrap: (p) => p },
|
|
189
189
|
{ regex: /(?:^|[^\w$.])(?:nova\.)?edit\s*\(\s*["'`]([^"'`]+)["'`]/gm, tool: "edit", wrap: (p) => p },
|
|
190
|
-
{ regex: /(?:^|[^\w$.])(?:nova\.)?patch\s*\(\s*["'`]([^"'`]+)["'`]/gm, tool: "
|
|
190
|
+
{ regex: /(?:^|[^\w$.])(?:nova\.)?patch\s*\(\s*["'`]([^"'`]+)["'`]/gm, tool: "patch", wrap: (p) => p },
|
|
191
191
|
{
|
|
192
192
|
regex: /(?:^|[^\w$.])(?:nova\.)?bash\s*\(\s*["'`]([^"'`]+)["'`]/gm,
|
|
193
193
|
tool: "bash",
|
|
194
194
|
wrap: (c) => (c.length > 32 ? c.slice(0, 29) + "…" : c),
|
|
195
195
|
},
|
|
196
|
-
{ regex: /(?:^|[^\w$.])(?:nova\.)?exec\s*\(\s*["'`]([^"'`]+)["'`]/gm, tool: "
|
|
197
|
-
{ regex: /(?:nova\.)?search\s*\(\s*["'`]([^"'`]+)["'`]/
|
|
198
|
-
{ regex: /(
|
|
199
|
-
{ regex: /(
|
|
196
|
+
{ regex: /(?:^|[^\w$.])(?:nova\.)?exec\s*\(\s*["'`]([^"'`]+)["'`]/gm, tool: "exec", wrap: (c) => c },
|
|
197
|
+
{ regex: /(?:^|[^\w$.])(?:nova\.)?search\s*\(\s*["'`]([^"'`]+)["'`]/gm, tool: "search", wrap: (q) => `"${q}"` },
|
|
198
|
+
{ regex: /(?:^|[^\w$.])nova\.describe\s*\(\s*["'`]([^"'`]+)["'`]/gm, tool: "describe", wrap: (name) => name },
|
|
199
|
+
{ regex: /(?:^|[^\w$.])nova\.has\s*\(\s*["'`]([^"'`]+)["'`]/gm, tool: "has", wrap: (name) => name },
|
|
200
|
+
{ regex: /(?:^|[^\w$.])(?:nova\.)?surface\s*\(\s*["'`]([^"'`]+)["'`]/gm, tool: "surface", wrap: (p) => p },
|
|
201
|
+
{ regex: /(?:^|[^\w$.])(?:nova\.)?snap\s*\(\s*["'`]([^"'`]+)["'`]/gm, tool: "snap", wrap: (q) => `"${q}"` },
|
|
200
202
|
];
|
|
201
203
|
for (const item of namedCalls) {
|
|
202
204
|
while ((match = item.regex.exec(trimmed)) !== null) {
|
|
@@ -238,7 +240,7 @@ export function renderDiffBox(diff, theme, width = 60) {
|
|
|
238
240
|
const divWidth = Math.min(w, Math.max(20, Math.min(70, w)));
|
|
239
241
|
const divider = theme.fg("borderMuted", "─".repeat(divWidth));
|
|
240
242
|
|
|
241
|
-
const maxShown =
|
|
243
|
+
const maxShown = 6;
|
|
242
244
|
const shownLines = diff.lines.slice(0, maxShown);
|
|
243
245
|
const body = [];
|
|
244
246
|
|
|
@@ -248,17 +250,17 @@ export function renderDiffBox(diff, theme, width = 60) {
|
|
|
248
250
|
if (item.type === "remove") {
|
|
249
251
|
const gut = theme.fg("toolDiffRemoved", `-${num}`.padStart(5));
|
|
250
252
|
const sep = theme.fg("borderMuted", " │ ");
|
|
251
|
-
const txt = theme.fg("toolDiffRemoved", `- ${item.text}`);
|
|
253
|
+
const txt = theme.fg("toolDiffRemoved", `- ${cleanInlineText(item.text)}`);
|
|
252
254
|
row = `${gut}${sep}${txt}`;
|
|
253
255
|
} else if (item.type === "add") {
|
|
254
256
|
const gut = theme.fg("toolDiffAdded", `+${num}`.padStart(5));
|
|
255
257
|
const sep = theme.fg("borderMuted", " │ ");
|
|
256
|
-
const txt = theme.fg("toolDiffAdded", `+ ${item.text}`);
|
|
258
|
+
const txt = theme.fg("toolDiffAdded", `+ ${cleanInlineText(item.text)}`);
|
|
257
259
|
row = `${gut}${sep}${txt}`;
|
|
258
260
|
} else {
|
|
259
261
|
const gut = theme.fg("dim", ` ${num}`.padStart(5));
|
|
260
262
|
const sep = theme.fg("borderMuted", " │ ");
|
|
261
|
-
const txt = theme.fg("toolDiffContext", ` ${item.text}`);
|
|
263
|
+
const txt = theme.fg("toolDiffContext", ` ${cleanInlineText(item.text)}`);
|
|
262
264
|
row = `${gut}${sep}${txt}`;
|
|
263
265
|
}
|
|
264
266
|
body.push(row);
|
|
@@ -272,16 +274,26 @@ export function renderDiffBox(diff, theme, width = 60) {
|
|
|
272
274
|
return `${header}\n${divider}\n${body.join("\n")}\n${divider}`;
|
|
273
275
|
}
|
|
274
276
|
|
|
275
|
-
function
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
277
|
+
function cleanBlockText(value) {
|
|
278
|
+
return stripVTControlCharacters(String(value ?? ""))
|
|
279
|
+
.replace(/\r\n?/g, "\n")
|
|
280
|
+
.replace(/[\x00-\x08\x0b\x0c\x0e-\x1f\x7f-\x9f]/g, "")
|
|
281
|
+
.replace(/[\u061c\u200e\u200f\u202a-\u202e\u2066-\u2069]/g, "");
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function cleanInlineText(value) {
|
|
285
|
+
return cleanBlockText(value).replace(/\s*\n\s*/g, " ").trim();
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function displayOperation(tool, target, diff, ok) {
|
|
289
|
+
const rawName = cleanInlineText(tool);
|
|
290
|
+
if (!rawName) return null;
|
|
291
|
+
const normalized = rawName === "apply_patch" ? "patch" : rawName;
|
|
292
|
+
return { tool: normalized, target, diff, ok };
|
|
281
293
|
}
|
|
282
294
|
|
|
283
295
|
function formatOpTarget(raw, tool) {
|
|
284
|
-
const text =
|
|
296
|
+
const text = cleanInlineText(raw);
|
|
285
297
|
if (!text) return "";
|
|
286
298
|
if (tool === "bash") {
|
|
287
299
|
// Keep commands readable; wrap handles the rest at render time.
|
|
@@ -372,6 +384,7 @@ export function normalizeResultRenderArgs(result, options, themeOrCtx, ctxOrArgs
|
|
|
372
384
|
isPartial: !!opts.isPartial,
|
|
373
385
|
theme: themeOrCtx,
|
|
374
386
|
context,
|
|
387
|
+
args: ctxOrArgs?.code ? ctxOrArgs : context.args,
|
|
375
388
|
host: detectResultHost(options, ctxOrArgs),
|
|
376
389
|
options: opts,
|
|
377
390
|
};
|
|
@@ -386,6 +399,7 @@ export function normalizeResultRenderArgs(result, options, themeOrCtx, ctxOrArgs
|
|
|
386
399
|
isPartial: !!context.isPartial,
|
|
387
400
|
theme: options,
|
|
388
401
|
context,
|
|
402
|
+
args: context.args,
|
|
389
403
|
host: "pi",
|
|
390
404
|
options: {},
|
|
391
405
|
};
|
|
@@ -393,20 +407,33 @@ export function normalizeResultRenderArgs(result, options, themeOrCtx, ctxOrArgs
|
|
|
393
407
|
throw new Error("supernova renderResult: theme missing (expected Pi or OMP signature)");
|
|
394
408
|
}
|
|
395
409
|
|
|
410
|
+
function operationTarget(item) {
|
|
411
|
+
const args = item?.args || {};
|
|
412
|
+
const name = item?.name;
|
|
413
|
+
if (name === "snap") {
|
|
414
|
+
const query = args.query ? `"${args.query}"` : "";
|
|
415
|
+
return args.path ? `${query} → ${args.path}` : query;
|
|
416
|
+
}
|
|
417
|
+
if (name === "search") return args.query ? `"${args.query}"` : "";
|
|
418
|
+
if (args.path) return String(args.path);
|
|
419
|
+
if (item?.diff?.path) return String(item.diff.path);
|
|
420
|
+
if (args.target && isString(args.target)) return args.target;
|
|
421
|
+
if (args.command) return String(args.command);
|
|
422
|
+
if (args.pattern) return String(args.pattern);
|
|
423
|
+
if (args.query) return String(args.query);
|
|
424
|
+
return "";
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
function operationsFromTrace(trace) {
|
|
428
|
+
if (!Array.isArray(trace)) return [];
|
|
429
|
+
return trace
|
|
430
|
+
.map((item) => displayOperation(item?.name || "tool", operationTarget(item), item?.diff, item?.ok))
|
|
431
|
+
.filter(Boolean);
|
|
432
|
+
}
|
|
433
|
+
|
|
396
434
|
function collectCallOps(args, context) {
|
|
397
|
-
const
|
|
398
|
-
if (
|
|
399
|
-
return stateTrace
|
|
400
|
-
.map((item) => {
|
|
401
|
-
const tool = item?.name || "tool";
|
|
402
|
-
let target = "";
|
|
403
|
-
if (item?.args?.path) target = String(item.args.path);
|
|
404
|
-
else if (item?.args?.command) target = String(item.args.command);
|
|
405
|
-
else if (item?.args?.pattern) target = String(item.args.pattern);
|
|
406
|
-
return displayOperation(tool, target);
|
|
407
|
-
})
|
|
408
|
-
.filter(Boolean);
|
|
409
|
-
}
|
|
435
|
+
const traced = operationsFromTrace(context?.state?.trace);
|
|
436
|
+
if (traced.length > 0) return traced;
|
|
410
437
|
return extractOperationsFromCode(args?.code)
|
|
411
438
|
.map((op) => displayOperation(op.tool, op.target))
|
|
412
439
|
.filter(Boolean);
|
|
@@ -437,7 +464,7 @@ function tickCallTimer(context) {
|
|
|
437
464
|
function formatOpBodyLine(theme, op) {
|
|
438
465
|
const icon = ACTION_ICONS[op.tool] || "✦ ";
|
|
439
466
|
const bullet = theme.fg("accent", icon);
|
|
440
|
-
const toolName = theme.fg("syntaxFunction", op.tool.padEnd(
|
|
467
|
+
const toolName = theme.fg("syntaxFunction", op.tool.padEnd(7, " "));
|
|
441
468
|
const rawTarget = formatOpTarget(op.target, op.tool);
|
|
442
469
|
const target = rawTarget ? " " + theme.fg("muted", rawTarget) : "";
|
|
443
470
|
return `${bullet}${toolName}${target}`;
|
|
@@ -452,10 +479,7 @@ function shouldUseOmpFrame(host) {
|
|
|
452
479
|
* Pending call has no hourglass on the head row (same as native Write/Edit).
|
|
453
480
|
*/
|
|
454
481
|
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} ` : " · ");
|
|
482
|
+
const opSummary = ops.length === 0 ? "composing" : `${ops.length} call${ops.length === 1 ? "" : "s"}`;
|
|
459
483
|
const description = timeStr ? `${opSummary} · ${timeStr}` : opSummary;
|
|
460
484
|
// No pending icon on the framed head row — matches native Write/Edit.
|
|
461
485
|
const header = novaStatusLine(theme, {
|
|
@@ -466,9 +490,9 @@ function renderOmpCallCard(theme, { ops, timeStr, expanded, code }) {
|
|
|
466
490
|
const bodyLines = ops.map((op) => formatOpBodyLine(theme, op));
|
|
467
491
|
if (expanded && code) {
|
|
468
492
|
bodyLines.push(theme.fg("dim", "── source ──"));
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
}
|
|
493
|
+
const sourceLines = cleanBlockText(code).trim().split("\n");
|
|
494
|
+
for (const line of sourceLines.slice(0, 24)) bodyLines.push(theme.fg("toolOutput", line));
|
|
495
|
+
if (sourceLines.length > 24) bodyLines.push(theme.fg("dim", `… ${sourceLines.length - 24} more lines`));
|
|
472
496
|
}
|
|
473
497
|
return {
|
|
474
498
|
header,
|
|
@@ -480,18 +504,20 @@ function renderOmpCallCard(theme, { ops, timeStr, expanded, code }) {
|
|
|
480
504
|
});
|
|
481
505
|
}
|
|
482
506
|
|
|
483
|
-
function renderOmpResultCard(theme, { isErr, payload, expanded, bodyText, isPartial, spinnerFrame }) {
|
|
507
|
+
function renderOmpResultCard(theme, { isErr, payload, expanded, bodyText, isPartial, spinnerFrame, opCount = 0 }) {
|
|
484
508
|
if (isErr) {
|
|
485
|
-
const errLines = [];
|
|
486
|
-
if (payload?.error) errLines.push(theme.fg("error",
|
|
509
|
+
const errLines = String(bodyText || "").trim() ? String(bodyText).split("\n") : [];
|
|
510
|
+
if (payload?.error) errLines.push(theme.fg("error", cleanBlockText(payload.error)));
|
|
487
511
|
if (expanded && payload?.logs?.length) {
|
|
488
512
|
errLines.push(theme.fg("dim", "── logs ──"));
|
|
489
|
-
for (const log of payload.logs) errLines.push(theme.fg("dim",
|
|
513
|
+
for (const log of payload.logs.slice(0, 24)) errLines.push(theme.fg("dim", cleanBlockText(log)));
|
|
490
514
|
}
|
|
515
|
+
const wall = payload?.wallMs != null ? `${payload.wallMs}ms` : "";
|
|
516
|
+
const calls = opCount > 0 ? `${opCount} call${opCount === 1 ? "" : "s"}` : "";
|
|
491
517
|
const header = novaStatusLine(theme, {
|
|
492
518
|
icon: "error",
|
|
493
519
|
title: "nova",
|
|
494
|
-
description:
|
|
520
|
+
description: [calls, "failed", wall].filter(Boolean).join(" · "),
|
|
495
521
|
});
|
|
496
522
|
return novaFramedBlock(theme, (width) => ({
|
|
497
523
|
header,
|
|
@@ -503,11 +529,13 @@ function renderOmpResultCard(theme, { isErr, payload, expanded, bodyText, isPart
|
|
|
503
529
|
}
|
|
504
530
|
|
|
505
531
|
const wall = payload?.wallMs != null ? `${payload.wallMs}ms` : "";
|
|
532
|
+
const calls = opCount > 0 ? `${opCount} call${opCount === 1 ? "" : "s"}` : "";
|
|
533
|
+
const description = [calls, isPartial ? "running" : "", wall].filter(Boolean).join(" · ");
|
|
506
534
|
const header = novaStatusLine(theme, {
|
|
507
535
|
icon: isPartial ? "running" : undefined,
|
|
508
536
|
spinnerFrame,
|
|
509
537
|
title: "nova",
|
|
510
|
-
description:
|
|
538
|
+
description: description || undefined,
|
|
511
539
|
});
|
|
512
540
|
const bodyLines = String(bodyText || "").split("\n");
|
|
513
541
|
while (bodyLines.length > 0 && bodyLines[0].trim() === "") bodyLines.shift();
|
|
@@ -556,7 +584,7 @@ export function renderSupernovaCall(a, b, c) {
|
|
|
556
584
|
|
|
557
585
|
if (context?.expanded && args?.code) {
|
|
558
586
|
out += "\n" + theme.fg("dim", "── source ──");
|
|
559
|
-
out += "\n" + theme.fg("toolOutput",
|
|
587
|
+
out += "\n" + theme.fg("toolOutput", cleanBlockText(args.code).trim());
|
|
560
588
|
}
|
|
561
589
|
|
|
562
590
|
if (context?.isError) comp.setTone("error");
|
|
@@ -567,51 +595,75 @@ export function renderSupernovaCall(a, b, c) {
|
|
|
567
595
|
return comp;
|
|
568
596
|
}
|
|
569
597
|
|
|
570
|
-
function
|
|
598
|
+
function formatDiffStats(theme, diff) {
|
|
599
|
+
if (!diff || !isObject(diff)) return "";
|
|
600
|
+
return " " + theme.fg("toolDiffAdded", `+${diff.added || 0}`) + theme.fg("dim", "/") + theme.fg("toolDiffRemoved", `-${diff.removed || 0}`);
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
function formatResultOperation(theme, op, isPartial, isError) {
|
|
604
|
+
const marker = op.ok === false
|
|
605
|
+
? theme.fg("error", "× ")
|
|
606
|
+
: isPartial && op.ok !== true
|
|
607
|
+
? theme.fg("dim", "· ")
|
|
608
|
+
: isError && op.ok !== true
|
|
609
|
+
? theme.fg("error", "× ")
|
|
610
|
+
: theme.fg("success", "✓ ");
|
|
611
|
+
const tool = theme.fg("syntaxFunction", op.tool.padEnd(7, " "));
|
|
612
|
+
const targetText = formatOpTarget(op.target, op.tool);
|
|
613
|
+
const target = targetText ? theme.fg("muted", targetText) : theme.fg("dim", "done");
|
|
614
|
+
return `${marker}${tool} ${target}${formatDiffStats(theme, op.diff)}`;
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
function boundedResult(value) {
|
|
618
|
+
let text;
|
|
619
|
+
try {
|
|
620
|
+
text = isString(value) ? value : JSON.stringify(value, null, 2);
|
|
621
|
+
} catch {
|
|
622
|
+
text = String(value);
|
|
623
|
+
}
|
|
624
|
+
const lines = cleanBlockText(text).split("\n");
|
|
625
|
+
const clipped = lines.slice(0, 24).join("\n");
|
|
626
|
+
const suffix = lines.length > 24 ? `\n… ${lines.length - 24} more lines` : "";
|
|
627
|
+
return (clipped + suffix).slice(0, 4000);
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
function buildResultBody(theme, { payload, context, args, expanded, isPartial, isError }) {
|
|
571
631
|
let out = "";
|
|
572
632
|
const trace = payload?.trace || context?.state?.trace || [];
|
|
573
|
-
const
|
|
633
|
+
const tracedOps = operationsFromTrace(trace);
|
|
634
|
+
const ops = tracedOps.length > 0
|
|
635
|
+
? tracedOps
|
|
636
|
+
: extractOperationsFromCode(args?.code).map((op) => displayOperation(op.tool, op.target)).filter(Boolean);
|
|
637
|
+
const maxOps = expanded ? 12 : 8;
|
|
638
|
+
for (const op of ops.slice(0, maxOps)) {
|
|
639
|
+
out += (out ? "\n" : "") + formatResultOperation(theme, op, isPartial, isError);
|
|
640
|
+
}
|
|
641
|
+
if (ops.length > maxOps) out += `\n${theme.fg("dim", `… ${ops.length - maxOps} more calls`)}`;
|
|
574
642
|
|
|
575
|
-
if (
|
|
576
|
-
const
|
|
577
|
-
const
|
|
578
|
-
|
|
643
|
+
if (expanded) {
|
|
644
|
+
const diffs = ops.filter((op) => op.diff && isObject(op.diff)).map((op) => op.diff);
|
|
645
|
+
const maxDiffs = 4;
|
|
646
|
+
if (diffs.length > 0) out += (out ? "\n" : "") + theme.fg("dim", "── changes ──");
|
|
647
|
+
for (const diff of diffs.slice(0, maxDiffs)) {
|
|
579
648
|
const box = renderDiffBox(diff, theme, 120);
|
|
580
|
-
if (box) out +=
|
|
581
|
-
}
|
|
582
|
-
if (!expanded && diffs.length > maxDiffsShown) {
|
|
583
|
-
const remaining = diffs.length - maxDiffsShown;
|
|
584
|
-
out +=
|
|
585
|
-
"\n\n" +
|
|
586
|
-
theme.fg(
|
|
587
|
-
"dim",
|
|
588
|
-
`… ${remaining} more file edit${remaining === 1 ? "" : "s"} (press Enter to expand)`,
|
|
589
|
-
);
|
|
649
|
+
if (box) out += "\n" + box;
|
|
590
650
|
}
|
|
591
|
-
|
|
651
|
+
if (diffs.length > maxDiffs) out += `\n${theme.fg("dim", `… ${diffs.length - maxDiffs} more changed files`)}`;
|
|
592
652
|
|
|
593
|
-
|
|
594
|
-
const resVal = payload?.result;
|
|
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
|
-
}
|
|
653
|
+
if (payload?.result !== undefined) {
|
|
602
654
|
out += (out ? "\n" : "") + theme.fg("dim", "── result ──");
|
|
603
|
-
out += "\n" + theme.fg("toolOutput",
|
|
655
|
+
out += "\n" + theme.fg("toolOutput", boundedResult(payload.result));
|
|
604
656
|
}
|
|
605
|
-
if (payload?.logs?.length) {
|
|
657
|
+
if (!isError && payload?.logs?.length) {
|
|
606
658
|
out += (out ? "\n" : "") + theme.fg("dim", "── logs ──");
|
|
607
|
-
for (const log of payload.logs) out += `\n ${theme.fg("dim",
|
|
659
|
+
for (const log of payload.logs.slice(0, 24)) out += `\n ${theme.fg("dim", cleanBlockText(log))}`;
|
|
608
660
|
}
|
|
609
661
|
}
|
|
610
|
-
return out;
|
|
662
|
+
return { body: out, opCount: ops.length };
|
|
611
663
|
}
|
|
612
664
|
|
|
613
665
|
export function renderSupernovaResult(resultArg, optionsArg, themeArg, contextArg) {
|
|
614
|
-
const { result, expanded, isPartial, theme, context, options, host } = normalizeResultRenderArgs(
|
|
666
|
+
const { result, expanded, isPartial, theme, context, args, options, host } = normalizeResultRenderArgs(
|
|
615
667
|
resultArg,
|
|
616
668
|
optionsArg,
|
|
617
669
|
themeArg,
|
|
@@ -637,51 +689,17 @@ export function renderSupernovaResult(resultArg, optionsArg, themeArg, contextAr
|
|
|
637
689
|
}
|
|
638
690
|
|
|
639
691
|
const isErr = result?.isError || payload?.ok === false;
|
|
640
|
-
const
|
|
641
|
-
|
|
642
|
-
if (
|
|
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
|
-
}
|
|
692
|
+
const view = buildResultBody(theme, { payload, context, args, expanded, isPartial, isError: isErr });
|
|
693
|
+
|
|
694
|
+
if (shouldUseOmpFrame(host)) {
|
|
678
695
|
return renderOmpResultCard(theme, {
|
|
679
|
-
isErr
|
|
696
|
+
isErr,
|
|
680
697
|
payload,
|
|
681
698
|
expanded,
|
|
682
|
-
bodyText:
|
|
683
|
-
isPartial:
|
|
699
|
+
bodyText: view.body,
|
|
700
|
+
isPartial: isPartial && !isErr,
|
|
684
701
|
spinnerFrame: options?.spinnerFrame,
|
|
702
|
+
opCount: view.opCount,
|
|
685
703
|
});
|
|
686
704
|
}
|
|
687
705
|
|
|
@@ -689,32 +707,22 @@ export function renderSupernovaResult(resultArg, optionsArg, themeArg, contextAr
|
|
|
689
707
|
if (options) options.lastComponent = comp;
|
|
690
708
|
else if (context) context.lastComponent = comp;
|
|
691
709
|
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
}
|
|
710
|
+
const wall = payload?.wallMs != null ? `${payload.wallMs}ms` : "";
|
|
711
|
+
const calls = view.opCount > 0 ? `${view.opCount} call${view.opCount === 1 ? "" : "s"}` : "complete";
|
|
712
|
+
let out = theme.fg("toolTitle", theme.bold("nova"));
|
|
713
|
+
out += " " + theme.fg("dim", `· ${[calls, isErr ? "failed" : isPartial ? "running" : "", wall].filter(Boolean).join(" · ")}`);
|
|
714
|
+
if (view.body) out += `\n ${view.body.replaceAll("\n", "\n ")}`;
|
|
696
715
|
|
|
697
716
|
if (isErr) {
|
|
698
|
-
|
|
699
|
-
if (payload?.error) {
|
|
700
|
-
out += `\n ${theme.fg("error", String(payload.error))}`;
|
|
701
|
-
}
|
|
717
|
+
out += `\n ${theme.fg("error", payload?.error ? cleanBlockText(payload.error) : "error")}`;
|
|
702
718
|
if (expanded && payload?.logs?.length) {
|
|
703
719
|
out += `\n${theme.fg("dim", "── logs ──")}`;
|
|
704
|
-
for (const log of payload.logs) out += `\n ${theme.fg("dim",
|
|
720
|
+
for (const log of payload.logs.slice(0, 24)) out += `\n ${theme.fg("dim", cleanBlockText(log))}`;
|
|
705
721
|
}
|
|
706
722
|
comp.setTone("error");
|
|
707
|
-
|
|
708
|
-
comp.
|
|
709
|
-
return comp;
|
|
710
|
-
}
|
|
711
|
-
|
|
712
|
-
const out = buildResultBody(theme, { payload, context, expanded });
|
|
713
|
-
if (!out.trim()) {
|
|
714
|
-
comp.setText("");
|
|
715
|
-
return comp;
|
|
723
|
+
} else {
|
|
724
|
+
comp.setTone(isPartial ? "pending" : "success");
|
|
716
725
|
}
|
|
717
|
-
comp.setTone("success");
|
|
718
726
|
comp.setFraming(true);
|
|
719
727
|
comp.setText(out);
|
|
720
728
|
return comp;
|
package/runtime.js
CHANGED
|
@@ -59,23 +59,32 @@ export async function runGuestProgram(options) {
|
|
|
59
59
|
|
|
60
60
|
let compiled = compiledCache.get(body);
|
|
61
61
|
if (!compiled) {
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
62
|
+
try {
|
|
63
|
+
compiled = new AsyncFunction(
|
|
64
|
+
"nova",
|
|
65
|
+
"tools",
|
|
66
|
+
"console",
|
|
67
|
+
"parallel",
|
|
68
|
+
"pipeline",
|
|
69
|
+
"read",
|
|
70
|
+
"write",
|
|
71
|
+
"edit",
|
|
72
|
+
"patch",
|
|
73
|
+
"surface",
|
|
74
|
+
"snap",
|
|
75
|
+
"bash",
|
|
76
|
+
"exec",
|
|
77
|
+
"speculate",
|
|
78
|
+
body,
|
|
79
|
+
);
|
|
80
|
+
} catch (err) {
|
|
81
|
+
return {
|
|
82
|
+
ok: false,
|
|
83
|
+
error: err instanceof Error ? err.message : String(err),
|
|
84
|
+
logs,
|
|
85
|
+
wallMs: Math.round(performance.now() - started),
|
|
86
|
+
};
|
|
87
|
+
}
|
|
79
88
|
if (compiledCache.size >= COMPILED_CACHE_MAX) {
|
|
80
89
|
const first = compiledCache.keys().next().value;
|
|
81
90
|
if (first !== undefined) compiledCache.delete(first);
|
|
@@ -108,6 +117,16 @@ export async function runGuestProgram(options) {
|
|
|
108
117
|
return res;
|
|
109
118
|
};
|
|
110
119
|
|
|
120
|
+
const unwrapJsonValue = (res) => {
|
|
121
|
+
const value = unwrapValue(res);
|
|
122
|
+
if (!isString(value)) return value;
|
|
123
|
+
try {
|
|
124
|
+
return JSON.parse(value);
|
|
125
|
+
} catch {
|
|
126
|
+
return value;
|
|
127
|
+
}
|
|
128
|
+
};
|
|
129
|
+
|
|
111
130
|
const guestRead = async (p, off, lim) => {
|
|
112
131
|
if (Array.isArray(p)) {
|
|
113
132
|
return await Promise.all(p.map((item) => guestRead(item, off, lim)));
|
|
@@ -123,11 +142,11 @@ export async function runGuestProgram(options) {
|
|
|
123
142
|
const guestPatch = async (p, d) => unwrapValue(await nova.call("apply_patch", { path: p, patch: d }));
|
|
124
143
|
const guestSurface = async (p) => {
|
|
125
144
|
const res = await (isFunction(nova.surface) ? nova.surface(p) : nova.call("surface", { path: p }));
|
|
126
|
-
return
|
|
145
|
+
return unwrapJsonValue(res);
|
|
127
146
|
};
|
|
128
147
|
const guestSnap = async (q, p) => {
|
|
129
148
|
const res = await (isFunction(nova.snap) ? nova.snap(q, p) : nova.call("snap", { query: q, path: p }));
|
|
130
|
-
return
|
|
149
|
+
return unwrapJsonValue(res);
|
|
131
150
|
};
|
|
132
151
|
const guestBash = async (cmd, opts) => {
|
|
133
152
|
const res = await nova.call("bash", { command: cmd, ...opts });
|
package/snap.js
CHANGED
|
@@ -94,21 +94,46 @@ function scoreContentDefinitions(content, tokens) {
|
|
|
94
94
|
return { totalScore: score, bestLine, bestLineScore };
|
|
95
95
|
}
|
|
96
96
|
|
|
97
|
-
|
|
97
|
+
function relativeHasSegment(relativePath, segmentName) {
|
|
98
|
+
return relativePath.split(path.sep).includes(segmentName);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function relativeHasHiddenSegment(relativePath) {
|
|
102
|
+
return relativePath.split(path.sep).some((segment) => segment.startsWith(".") && segment.length > 1);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export async function executeSnap({ query, searchDir, includeHidden = false, vfs, runCommand, pendingPaths = [] }) {
|
|
98
106
|
const { tokens, wantsTest, wantsType, wantsDoc } = tokenizeQuery(query);
|
|
99
107
|
if (tokens.length === 0) {
|
|
100
108
|
throw new Error("snap requires at least one searchable concept keyword");
|
|
101
109
|
}
|
|
102
110
|
|
|
103
111
|
const dir = searchDir || process.cwd();
|
|
112
|
+
if (path.resolve(dir).split(path.sep).includes(".git")) {
|
|
113
|
+
throw new Error("snap cannot search Git metadata");
|
|
114
|
+
}
|
|
104
115
|
let fileList = [];
|
|
105
116
|
try {
|
|
106
|
-
const
|
|
117
|
+
const rgArgs = ["rg", "--files"];
|
|
118
|
+
if (includeHidden) rgArgs.push("--hidden");
|
|
119
|
+
rgArgs.push("-g", "!.git/**", "-g", "!**/.git/**", dir);
|
|
120
|
+
const res = await runCommand(rgArgs, { timeoutMs: 15_000 });
|
|
107
121
|
fileList = res.stdout.split("\n").map((f) => f.trim()).filter(Boolean);
|
|
108
122
|
} catch {
|
|
109
123
|
fileList = [];
|
|
110
124
|
}
|
|
111
125
|
|
|
126
|
+
const seenPaths = new Set(fileList.map((filePath) => path.resolve(filePath)));
|
|
127
|
+
for (const pendingPath of pendingPaths) {
|
|
128
|
+
const absolutePath = path.resolve(pendingPath);
|
|
129
|
+
const relativePath = path.relative(path.resolve(dir), absolutePath);
|
|
130
|
+
const escapesDir = relativePath === ".." || relativePath.startsWith(`..${path.sep}`) || path.isAbsolute(relativePath);
|
|
131
|
+
const hiddenRelativePath = relativeHasHiddenSegment(relativePath);
|
|
132
|
+
if (escapesDir || relativeHasSegment(relativePath, ".git") || (!includeHidden && hiddenRelativePath) || seenPaths.has(absolutePath)) continue;
|
|
133
|
+
seenPaths.add(absolutePath);
|
|
134
|
+
fileList.push(absolutePath);
|
|
135
|
+
}
|
|
136
|
+
|
|
112
137
|
if (fileList.length === 0) {
|
|
113
138
|
throw new Error(`no files found to search in ${dir}`);
|
|
114
139
|
}
|
|
@@ -125,7 +150,8 @@ export async function executeSnap({ query, searchDir, vfs, runCommand }) {
|
|
|
125
150
|
|
|
126
151
|
if (candidates.length < 5) {
|
|
127
152
|
try {
|
|
128
|
-
const grepArgs = ["-l", "--max-count=1"];
|
|
153
|
+
const grepArgs = ["-l", "--max-count=1", "-g", "!.git/**", "-g", "!**/.git/**"];
|
|
154
|
+
if (includeHidden) grepArgs.push("--hidden");
|
|
129
155
|
if (!wantsTest) {
|
|
130
156
|
grepArgs.push("-g", "!test/**", "-g", "!tests/**", "-g", "!*.test.*", "-g", "!*.spec.*");
|
|
131
157
|
}
|