pi-supernova 0.0.5 → 0.0.7
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 +20 -0
- package/README.md +9 -7
- package/diff.js +39 -46
- package/host-bridge.js +44 -5
- package/index.js +28 -7
- package/omp-frame.js +11 -26
- package/package.json +2 -11
- package/render-measure.js +1 -1
- package/render.js +132 -366
- package/runtime.js +7 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,26 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [0.0.7] - 2026-09-03
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- `bash` / `exec` run through `bash -c` (and strip a wrapping quote pair), so `git status` and `echo $PATH` are not looked up as a single binary name.
|
|
10
|
+
- `nova.search` / `nova.describe` return promises, so `.catch()` in guest programs works.
|
|
11
|
+
|
|
12
|
+
## [0.0.6] - 2026-09-03
|
|
13
|
+
|
|
14
|
+
### Changed
|
|
15
|
+
|
|
16
|
+
- 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.
|
|
17
|
+
|
|
18
|
+
### Fixed
|
|
19
|
+
|
|
20
|
+
- 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.
|
|
21
|
+
|
|
22
|
+
- Direct `nova.snap()` and `nova.surface()` helpers now return structured objects instead of host result envelopes.
|
|
23
|
+
- 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.
|
|
24
|
+
|
|
5
25
|
## [0.0.5] - 2026-09-03
|
|
6
26
|
|
|
7
27
|
### Fixed
|
package/README.md
CHANGED
|
@@ -48,17 +48,19 @@ async () => {
|
|
|
48
48
|
|
|
49
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
|
-
╭─── nova:
|
|
55
|
-
│ ✓ read packages/pi-supernova/host-bridge.js
|
|
56
|
-
│
|
|
57
|
-
│ ✓
|
|
58
|
-
|
|
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
|
+
╰─────────────────────────────────────────────────────────╯
|
|
59
61
|
```
|
|
60
62
|
|
|
61
|
-
Collapsed cards
|
|
63
|
+
Collapsed cards show bounded edit/write/patch hunks immediately. Press Enter for a larger hunk budget, logs, and the returned value.
|
|
62
64
|
|
|
63
65
|
---
|
|
64
66
|
|
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 {
|
|
@@ -61,61 +61,53 @@ export function buildPatchDiff(filePath, patchText) {
|
|
|
61
61
|
const lines = [];
|
|
62
62
|
let added = 0;
|
|
63
63
|
let removed = 0;
|
|
64
|
-
let
|
|
64
|
+
let oldLineNum = 1;
|
|
65
|
+
let newLineNum = 1;
|
|
65
66
|
let inHunk = false;
|
|
66
67
|
|
|
67
|
-
for (const
|
|
68
|
-
const headerMatch = /^@@\s+-(\d+)/.exec(
|
|
68
|
+
for (const patchLine of patchLines) {
|
|
69
|
+
const headerMatch = /^@@\s+-(\d+)(?:,\d+)?\s+\+(\d+)/.exec(patchLine);
|
|
69
70
|
if (headerMatch) {
|
|
70
|
-
|
|
71
|
+
oldLineNum = Number(headerMatch[1]);
|
|
72
|
+
newLineNum = Number(headerMatch[2]);
|
|
71
73
|
inHunk = true;
|
|
72
74
|
continue;
|
|
73
75
|
}
|
|
74
|
-
if (!inHunk ||
|
|
75
|
-
if (
|
|
76
|
+
if (!inHunk || patchLine.startsWith("\\")) continue;
|
|
77
|
+
if (patchLine.startsWith("-")) {
|
|
76
78
|
removed += 1;
|
|
77
|
-
lines.push({ type: "remove", lineNum:
|
|
78
|
-
|
|
79
|
-
} else if (
|
|
79
|
+
lines.push({ type: "remove", lineNum: oldLineNum, text: patchLine.slice(1) });
|
|
80
|
+
oldLineNum += 1;
|
|
81
|
+
} else if (patchLine.startsWith("+")) {
|
|
80
82
|
added += 1;
|
|
81
|
-
lines.push({ type: "add", lineNum:
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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;
|
|
85
89
|
}
|
|
86
90
|
}
|
|
87
91
|
|
|
88
|
-
return {
|
|
89
|
-
path: filePath,
|
|
90
|
-
op: "apply_patch",
|
|
91
|
-
added,
|
|
92
|
-
removed,
|
|
93
|
-
lines,
|
|
94
|
-
};
|
|
92
|
+
return { path: filePath, op: "apply_patch", added, removed, lines };
|
|
95
93
|
}
|
|
96
94
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
const
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
const lines = [];
|
|
104
|
-
for (let i = 0; i < sampleCount; i++) {
|
|
105
|
-
lines.push({ type: "add", lineNum: i + 1, text: newLines[i] });
|
|
106
|
-
}
|
|
107
|
-
return {
|
|
108
|
-
path: filePath,
|
|
109
|
-
op: "write",
|
|
110
|
-
added: newLines.length,
|
|
111
|
-
removed: 0,
|
|
112
|
-
lines,
|
|
113
|
-
};
|
|
114
|
-
}
|
|
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
|
+
}
|
|
115
101
|
|
|
116
|
-
|
|
102
|
+
export function buildWriteDiff(filePath, previousText, newText) {
|
|
103
|
+
const newLines = contentLines(newText);
|
|
104
|
+
const oldLines = contentLines(previousText);
|
|
105
|
+
const maxStoredLines = 64;
|
|
117
106
|
const lines = [];
|
|
118
|
-
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++) {
|
|
119
111
|
lines.push({ type: "add", lineNum: i + 1, text: newLines[i] });
|
|
120
112
|
}
|
|
121
113
|
return {
|
|
@@ -123,6 +115,7 @@ export function buildWriteDiff(filePath, previousText, newText) {
|
|
|
123
115
|
op: "write",
|
|
124
116
|
added: newLines.length,
|
|
125
117
|
removed: oldLines.length,
|
|
118
|
+
displayLineCount: oldLines.length + newLines.length,
|
|
126
119
|
lines,
|
|
127
120
|
};
|
|
128
121
|
}
|
package/host-bridge.js
CHANGED
|
@@ -3,7 +3,7 @@ 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
9
|
import { buildEditDiff, buildMultiEditDiff, buildPatchDiff, buildWriteDiff } from "./diff.js";
|
|
@@ -16,6 +16,17 @@ function textResult(text, details) {
|
|
|
16
16
|
};
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
+
/** Unwrap a single matching quote pair around the whole string (`'git status'`). */
|
|
20
|
+
function unwrapIfFullyQuoted(s) {
|
|
21
|
+
if (s.length < 2) return s;
|
|
22
|
+
const q = s[0];
|
|
23
|
+
if (q !== "'" && q !== '"') return s;
|
|
24
|
+
if (s[s.length - 1] !== q) return s;
|
|
25
|
+
const inner = s.slice(1, -1);
|
|
26
|
+
if (inner.includes(q)) return s;
|
|
27
|
+
return inner;
|
|
28
|
+
}
|
|
29
|
+
|
|
19
30
|
let cachedCwd = null;
|
|
20
31
|
let cachedResolvedCwd = null;
|
|
21
32
|
|
|
@@ -576,12 +587,13 @@ function createNativeAdapters(getCwd, vfs, config) {
|
|
|
576
587
|
},
|
|
577
588
|
async bash(params, signal) {
|
|
578
589
|
const cwd = getCwd();
|
|
579
|
-
const command = String(params?.command ?? "").trim();
|
|
590
|
+
const command = unwrapIfFullyQuoted(String(params?.command ?? "").trim());
|
|
580
591
|
if (!command) throw new Error("bash requires command");
|
|
581
592
|
const targetCwd = params?.cwd ? await resolveWorkspacePath(cwd, params.cwd, "bash cwd", true) : cwd;
|
|
582
593
|
|
|
583
|
-
|
|
584
|
-
|
|
594
|
+
// Always shell. Splitting on spaces treated `git status` / quoted `exec("git status")`
|
|
595
|
+
// as a single binary name (`bash: git status: command not found`).
|
|
596
|
+
const argv = ["bash", "-c", command];
|
|
585
597
|
|
|
586
598
|
const transactionBarrier = await vfs.prepareExternalMutation("bash");
|
|
587
599
|
let res;
|
|
@@ -740,6 +752,18 @@ export function createHostBridge({ pi, config, getCwd }) {
|
|
|
740
752
|
vfs.clear();
|
|
741
753
|
}
|
|
742
754
|
|
|
755
|
+
function resultDiff(response) {
|
|
756
|
+
let details = response?.details;
|
|
757
|
+
if (isString(details)) {
|
|
758
|
+
try {
|
|
759
|
+
details = JSON.parse(details);
|
|
760
|
+
} catch {
|
|
761
|
+
return undefined;
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
return isObject(details) ? details.diff : undefined;
|
|
765
|
+
}
|
|
766
|
+
|
|
743
767
|
function notifyCall(record) {
|
|
744
768
|
if (!callListener) return;
|
|
745
769
|
try {
|
|
@@ -766,13 +790,27 @@ export function createHostBridge({ pi, config, getCwd }) {
|
|
|
766
790
|
|
|
767
791
|
const record = { name, args: args || {}, time: Date.now() };
|
|
768
792
|
trace.push(record);
|
|
793
|
+
notifyCall(record);
|
|
769
794
|
|
|
770
795
|
try {
|
|
771
796
|
const exec = executors.get(name);
|
|
772
797
|
if (exec) {
|
|
798
|
+
let fallbackDiff;
|
|
799
|
+
if (name === "write" && isString(args?.path) && isString(args?.content)) {
|
|
800
|
+
const target = await resolveWorkspacePath(getCwd(), args.path, "write", false);
|
|
801
|
+
let previous = "";
|
|
802
|
+
try {
|
|
803
|
+
previous = await vfs.read(target);
|
|
804
|
+
} catch (error) {
|
|
805
|
+
if (error?.code !== "ENOENT") throw error;
|
|
806
|
+
}
|
|
807
|
+
fallbackDiff = buildWriteDiff(target, previous, args.content);
|
|
808
|
+
}
|
|
773
809
|
if (isMutatingTool(name, config)) await vfs.prepareExternalMutation(name);
|
|
774
810
|
const res = await exec(`supernova:${name}:${callCount}`, args || {}, activeSignal, undefined, activeCtx);
|
|
811
|
+
const diff = resultDiff(res) || fallbackDiff;
|
|
775
812
|
record.ok = res?.isError !== true && res?.details?.ok !== false;
|
|
813
|
+
if (diff && record.ok) record.diff = diff;
|
|
776
814
|
notifyCall(record);
|
|
777
815
|
return res;
|
|
778
816
|
}
|
|
@@ -780,8 +818,9 @@ export function createHostBridge({ pi, config, getCwd }) {
|
|
|
780
818
|
const native = natives[name];
|
|
781
819
|
if (native) {
|
|
782
820
|
const res = await native(args || {}, activeSignal);
|
|
783
|
-
|
|
821
|
+
const diff = resultDiff(res);
|
|
784
822
|
record.ok = res?.isError !== true && res?.details?.ok !== false;
|
|
823
|
+
if (diff && record.ok) record.diff = diff;
|
|
785
824
|
notifyCall(record);
|
|
786
825
|
return res;
|
|
787
826
|
}
|
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
|
|
|
@@ -74,12 +86,12 @@ export default function piSupernova(pi) {
|
|
|
74
86
|
|
|
75
87
|
function makeNovaApi() {
|
|
76
88
|
return {
|
|
77
|
-
search(query, limit) {
|
|
89
|
+
async search(query, limit) {
|
|
78
90
|
const cat = catalog.length ? catalog : refreshCatalog();
|
|
79
91
|
const lim = Number.isInteger(limit) ? limit : config.maxSearchResults;
|
|
80
92
|
return searchCatalog(cat, query, lim);
|
|
81
93
|
},
|
|
82
|
-
describe(name) {
|
|
94
|
+
async describe(name) {
|
|
83
95
|
const cat = catalog.length ? catalog : refreshCatalog();
|
|
84
96
|
return describeTool(cat, name);
|
|
85
97
|
},
|
|
@@ -101,10 +113,10 @@ export default function piSupernova(pi) {
|
|
|
101
113
|
}
|
|
102
114
|
},
|
|
103
115
|
async surface(filePath) {
|
|
104
|
-
return bridge.call("surface", { path: filePath });
|
|
116
|
+
return unwrapStructuredResult(await bridge.call("surface", { path: filePath }), "surface");
|
|
105
117
|
},
|
|
106
118
|
async snap(query, targetPath) {
|
|
107
|
-
return bridge.call("snap", { query, path: targetPath });
|
|
119
|
+
return unwrapStructuredResult(await bridge.call("snap", { query, path: targetPath }), "snap");
|
|
108
120
|
},
|
|
109
121
|
has(name) {
|
|
110
122
|
return bridge.hasExecutor(name) || catalog.some((t) => t.name === name);
|
|
@@ -134,8 +146,8 @@ export default function piSupernova(pi) {
|
|
|
134
146
|
}),
|
|
135
147
|
),
|
|
136
148
|
}),
|
|
137
|
-
//
|
|
138
|
-
//
|
|
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.
|
|
139
151
|
renderShell: "self",
|
|
140
152
|
mergeCallAndResult: true,
|
|
141
153
|
renderCall: renderSupernovaCall,
|
|
@@ -163,6 +175,15 @@ export default function piSupernova(pi) {
|
|
|
163
175
|
}
|
|
164
176
|
});
|
|
165
177
|
|
|
178
|
+
if (isFunction(onUpdate)) {
|
|
179
|
+
try {
|
|
180
|
+
onUpdate({
|
|
181
|
+
content: [{ type: "text", text: "" }],
|
|
182
|
+
details: { trace: [], running: true },
|
|
183
|
+
});
|
|
184
|
+
} catch {}
|
|
185
|
+
}
|
|
186
|
+
|
|
166
187
|
const runConfig = {
|
|
167
188
|
...config,
|
|
168
189
|
timeoutMs: Number.isInteger(params?.timeoutMs) ? params.timeoutMs : config.timeoutMs,
|
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,7 +95,7 @@ function bgFnForState(theme, state) {
|
|
|
102
95
|
return undefined;
|
|
103
96
|
}
|
|
104
97
|
|
|
105
|
-
|
|
98
|
+
function renderPortableFrame(theme, { header, sections = [], state = "pending", borderColor, width }) {
|
|
106
99
|
const w = Math.max(1, width | 0);
|
|
107
100
|
if (w < 8) {
|
|
108
101
|
const rawLines = [header];
|
|
@@ -156,7 +149,7 @@ export function renderPortableFrame(theme, { header, sections = [], state = "pen
|
|
|
156
149
|
return lines;
|
|
157
150
|
}
|
|
158
151
|
|
|
159
|
-
|
|
152
|
+
function createPortableFramedComponent(theme, build) {
|
|
160
153
|
let cacheWidth;
|
|
161
154
|
let cacheKey;
|
|
162
155
|
let cacheLines;
|
|
@@ -188,11 +181,3 @@ export function novaStatusLine(theme, options) {
|
|
|
188
181
|
return statusHeader(theme, options);
|
|
189
182
|
}
|
|
190
183
|
|
|
191
|
-
export function wrapPlainLines(text, width) {
|
|
192
|
-
const w = Math.max(1, width | 0);
|
|
193
|
-
const out = [];
|
|
194
|
-
for (const line of String(text ?? "").split("\n")) {
|
|
195
|
-
for (const chunk of wrapPlainToWidth(line, w)) out.push(chunk);
|
|
196
|
-
}
|
|
197
|
-
return out.length ? out : [""];
|
|
198
|
-
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-supernova",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.7",
|
|
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"
|
package/render-measure.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Width / truncate primitives
|
|
2
|
+
* Width / truncate primitives used by the compact renderer.
|
|
3
3
|
* Self-contained so path-install never depends on a host truncate that can
|
|
4
4
|
* append ellipsis after cutting to maxWidth (Pi 92>91 crash class).
|
|
5
5
|
*/
|
package/render.js
CHANGED
|
@@ -6,12 +6,12 @@
|
|
|
6
6
|
* so every width/truncate path here is self-contained and must never trust a host
|
|
7
7
|
* truncate that appends ellipsis after cutting to maxWidth.
|
|
8
8
|
*
|
|
9
|
-
* OMP
|
|
10
|
-
*
|
|
9
|
+
* Pi and OMP share one self-owned result card. The call slot stays empty so the
|
|
10
|
+
* lifecycle never duplicates; mutating operations include bounded inline diffs.
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
import { stripVTControlCharacters } from "node:util";
|
|
14
|
-
import { isString, isObject } from "./decode.js";
|
|
14
|
+
import { isString, isObject, isFunction } from "./decode.js";
|
|
15
15
|
import {
|
|
16
16
|
measureWidth,
|
|
17
17
|
hardTruncate,
|
|
@@ -43,83 +43,18 @@ function fitOutputLines(text, width) {
|
|
|
43
43
|
return out.length > 0 ? out : [""];
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
-
/**
|
|
47
|
-
* Soft violet / grey-blue wash — same structure as Pi's standard tool Box
|
|
48
|
-
* (padding + background), just purple-tinted instead of green.
|
|
49
|
-
* Tuned for dark themes (tokyo-night and friends).
|
|
50
|
-
*/
|
|
51
|
-
export const NOVA_CHROME = {
|
|
52
|
-
pendingBg: [26, 28, 42], // deep grey-blue
|
|
53
|
-
successBg: [24, 30, 46], // muted blue-purple
|
|
54
|
-
errorBg: [40, 26, 34], // muted rose-purple
|
|
55
|
-
};
|
|
56
|
-
|
|
57
|
-
function bgRgb(rgb, text) {
|
|
58
|
-
const [r, g, b] = rgb;
|
|
59
|
-
return `\x1b[48;2;${r};${g};${b}m${text}\x1b[49m`;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
function chromeBg(tone) {
|
|
63
|
-
if (tone === "error") return NOVA_CHROME.errorBg;
|
|
64
|
-
if (tone === "success") return NOVA_CHROME.successBg;
|
|
65
|
-
return NOVA_CHROME.pendingBg;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
/**
|
|
69
|
-
* Paint one row like Pi's Box: 1-col pad + content, washed to full width.
|
|
70
|
-
* No side-rail characters — the background block is the "border".
|
|
71
|
-
* Final visible width is always ≤ `width` (Pi crash contract).
|
|
72
|
-
*/
|
|
73
|
-
export function paintNovaRow(content, width, tone = "pending") {
|
|
74
|
-
const w = Math.max(1, width | 0);
|
|
75
|
-
const bg = chromeBg(tone);
|
|
76
|
-
const padX = 1;
|
|
77
|
-
const inner = Math.max(1, w - padX * 2);
|
|
78
|
-
const body = clampLine(content, inner);
|
|
79
|
-
const row = `${" ".repeat(padX)}${body}`;
|
|
80
|
-
const pad = Math.max(0, w - measureWidth(row));
|
|
81
|
-
const painted = bgRgb(bg, row + " ".repeat(pad));
|
|
82
|
-
if (measureWidth(painted) <= w) return painted;
|
|
83
|
-
return clampLine(stripVTControlCharacters(painted), w);
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
/**
|
|
87
|
-
* Framed Text for supernova. Uses renderShell: "self" so we own the chrome
|
|
88
|
-
* color (muted purple/grey-blue) instead of the host's green tool panels.
|
|
89
|
-
* Structure matches Pi's standard Box (pad + bg), without side-rail characters.
|
|
90
|
-
*/
|
|
46
|
+
/** Compact bounded text; the host supplies the card background and borders. */
|
|
91
47
|
export class SafeText {
|
|
92
48
|
constructor(text = "") {
|
|
93
49
|
this.text = text;
|
|
94
|
-
this.tone = "pending";
|
|
95
|
-
this.framing = true;
|
|
96
50
|
}
|
|
97
51
|
setText(text) {
|
|
98
52
|
this.text = text;
|
|
99
53
|
}
|
|
100
|
-
setTone(tone) {
|
|
101
|
-
if (tone === "error" || tone === "success" || tone === "pending") this.tone = tone;
|
|
102
|
-
}
|
|
103
|
-
setFraming(enabled) {
|
|
104
|
-
this.framing = !!enabled;
|
|
105
|
-
}
|
|
106
54
|
invalidate() {}
|
|
107
55
|
render(width = 80) {
|
|
108
|
-
const w = Math.max(1, width | 0);
|
|
109
56
|
const raw = String(this.text ?? "");
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
if (!this.framing) {
|
|
113
|
-
return fitOutputLines(raw, w);
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
// Match Pi Box: 1-col horizontal pad, 1-row vertical pad, purple bg wash.
|
|
117
|
-
const padX = 1;
|
|
118
|
-
const inner = Math.max(1, w - padX * 2);
|
|
119
|
-
const bodyLines = fitOutputLines(raw, inner);
|
|
120
|
-
const empty = paintNovaRow("", w, this.tone);
|
|
121
|
-
const painted = bodyLines.map((line) => paintNovaRow(line, w, this.tone));
|
|
122
|
-
return [empty, ...painted, empty];
|
|
57
|
+
return raw.trim() ? fitOutputLines(raw, Math.max(1, width | 0)) : [];
|
|
123
58
|
}
|
|
124
59
|
}
|
|
125
60
|
|
|
@@ -127,24 +62,6 @@ export class SafeText {
|
|
|
127
62
|
export const visibleWidth = measureWidth;
|
|
128
63
|
export const truncateToWidth = hardTruncate;
|
|
129
64
|
|
|
130
|
-
const ACTION_ICONS = {
|
|
131
|
-
write: "✎ ",
|
|
132
|
-
edit: "✎ ",
|
|
133
|
-
apply_patch: "✎ ",
|
|
134
|
-
patch: "✎ ",
|
|
135
|
-
bash: "❯ ",
|
|
136
|
-
exec: "❯ ",
|
|
137
|
-
read: "▤ ",
|
|
138
|
-
surface: "▤ ",
|
|
139
|
-
search: "⌕ ",
|
|
140
|
-
grep: "⌕ ",
|
|
141
|
-
find: "⌕ ",
|
|
142
|
-
ls: "▤ ",
|
|
143
|
-
// Avoid double-width emoji (⚡) — measure disagreements with Pi caused 92>91 crashes.
|
|
144
|
-
speculate: "✶ ",
|
|
145
|
-
snap: "⌖ ",
|
|
146
|
-
};
|
|
147
|
-
|
|
148
65
|
export function extractOperationsFromCode(code) {
|
|
149
66
|
const trimmed = String(code || "").trim();
|
|
150
67
|
if (!trimmed) return [];
|
|
@@ -213,72 +130,55 @@ export function extractOperationsFromCode(code) {
|
|
|
213
130
|
return ops;
|
|
214
131
|
}
|
|
215
132
|
|
|
216
|
-
|
|
217
|
-
if (!diff || !Array.isArray(diff.lines) || diff.lines.length === 0) return
|
|
218
|
-
|
|
219
|
-
const w = Math.max(20, width | 0);
|
|
220
|
-
const cleanPath = String(diff.path || "").replace(/\\/g, "/");
|
|
221
|
-
const baseName = cleanPath.split("/").pop() || cleanPath;
|
|
222
|
-
const opLabel = diff.op === "edit" ? "Edit" : diff.op === "write" ? "Write" : "Patch";
|
|
223
|
-
|
|
224
|
-
// Stats BEFORE path so +N/-N survive narrow-terminal truncation (prior test/crash footgun).
|
|
225
|
-
const stats =
|
|
226
|
-
theme.fg("dim", "⟨") +
|
|
227
|
-
theme.fg("toolDiffAdded", `+${diff.added}`) +
|
|
228
|
-
theme.fg("dim", "/") +
|
|
229
|
-
theme.fg("toolDiffRemoved", `-${diff.removed}`) +
|
|
230
|
-
theme.fg("dim", "⟩");
|
|
231
|
-
// Do not clamp here — SafeText.render(terminalWidth) is the single choke point.
|
|
232
|
-
// Pre-clamping with a guessed width ate filenames under mock/ANSI-marker themes.
|
|
233
|
-
const header =
|
|
234
|
-
theme.fg("accent", "✎ ") +
|
|
235
|
-
theme.fg("toolTitle", theme.bold(`${opLabel} `)) +
|
|
236
|
-
stats +
|
|
237
|
-
" " +
|
|
238
|
-
theme.fg("muted", baseName);
|
|
239
|
-
|
|
240
|
-
const divWidth = Math.min(w, Math.max(20, Math.min(70, w)));
|
|
241
|
-
const divider = theme.fg("borderMuted", "─".repeat(divWidth));
|
|
242
|
-
|
|
243
|
-
const maxShown = 6;
|
|
244
|
-
const shownLines = diff.lines.slice(0, maxShown);
|
|
133
|
+
function formatDiffRows(diff, theme, maxShown = 6) {
|
|
134
|
+
if (!diff || !Array.isArray(diff.lines) || diff.lines.length === 0) return [];
|
|
245
135
|
const body = [];
|
|
246
|
-
|
|
247
|
-
for (const item of shownLines) {
|
|
136
|
+
for (const item of diff.lines.slice(0, maxShown)) {
|
|
248
137
|
const num = item.lineNum || 0;
|
|
249
|
-
let row;
|
|
250
138
|
if (item.type === "remove") {
|
|
251
139
|
const gut = theme.fg("toolDiffRemoved", `-${num}`.padStart(5));
|
|
252
|
-
|
|
253
|
-
const txt = theme.fg("toolDiffRemoved", `- ${cleanInlineText(item.text)}`);
|
|
254
|
-
row = `${gut}${sep}${txt}`;
|
|
140
|
+
body.push(`${gut}${theme.fg("borderMuted", " │ ")}${theme.fg("toolDiffRemoved", `- ${cleanInlineText(item.text)}`)}`);
|
|
255
141
|
} else if (item.type === "add") {
|
|
256
142
|
const gut = theme.fg("toolDiffAdded", `+${num}`.padStart(5));
|
|
257
|
-
|
|
258
|
-
const txt = theme.fg("toolDiffAdded", `+ ${cleanInlineText(item.text)}`);
|
|
259
|
-
row = `${gut}${sep}${txt}`;
|
|
143
|
+
body.push(`${gut}${theme.fg("borderMuted", " │ ")}${theme.fg("toolDiffAdded", `+ ${cleanInlineText(item.text)}`)}`);
|
|
260
144
|
} else {
|
|
261
145
|
const gut = theme.fg("dim", ` ${num}`.padStart(5));
|
|
262
|
-
|
|
263
|
-
const txt = theme.fg("toolDiffContext", ` ${cleanInlineText(item.text)}`);
|
|
264
|
-
row = `${gut}${sep}${txt}`;
|
|
146
|
+
body.push(`${gut}${theme.fg("borderMuted", " │ ")}${theme.fg("toolDiffContext", ` ${cleanInlineText(item.text)}`)}`);
|
|
265
147
|
}
|
|
266
|
-
body.push(row);
|
|
267
148
|
}
|
|
268
|
-
|
|
269
|
-
if (
|
|
270
|
-
|
|
271
|
-
body.push(theme.fg("dim", ` │ … ${remaining} more lines`));
|
|
149
|
+
const displayLineCount = Number.isInteger(diff.displayLineCount) ? diff.displayLineCount : diff.lines.length;
|
|
150
|
+
if (displayLineCount > maxShown) {
|
|
151
|
+
body.push(theme.fg("dim", ` │ … ${displayLineCount - maxShown} more lines`));
|
|
272
152
|
}
|
|
153
|
+
return body;
|
|
154
|
+
}
|
|
273
155
|
|
|
274
|
-
|
|
156
|
+
export function renderDiffBox(diff, theme, width = 60, maxShown = 6) {
|
|
157
|
+
if (!diff || !Array.isArray(diff.lines) || diff.lines.length === 0) return "";
|
|
158
|
+
const w = Math.max(20, width | 0);
|
|
159
|
+
const cleanPath = String(diff.path || "").replace(/\\/g, "/");
|
|
160
|
+
const baseName = cleanPath.split("/").pop() || cleanPath;
|
|
161
|
+
const opLabel = diff.op === "edit" ? "Edit" : diff.op === "write" ? "Write" : "Patch";
|
|
162
|
+
const stats = theme.fg("dim", "⟨") + theme.fg("toolDiffAdded", `+${diff.added}`) + theme.fg("dim", "/") + theme.fg("toolDiffRemoved", `-${diff.removed}`) + theme.fg("dim", "⟩");
|
|
163
|
+
const header = theme.fg("accent", "✎ ") + theme.fg("toolTitle", theme.bold(`${opLabel} `)) + stats + " " + theme.fg("muted", baseName);
|
|
164
|
+
const divider = theme.fg("borderMuted", "─".repeat(Math.min(70, w)));
|
|
165
|
+
return `${header}\n${divider}\n${formatDiffRows(diff, theme, maxShown).join("\n")}\n${divider}`;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function stripUnsafeControls(value) {
|
|
169
|
+
let clean = "";
|
|
170
|
+
for (const character of value) {
|
|
171
|
+
const codePoint = character.codePointAt(0);
|
|
172
|
+
const isC0 = codePoint <= 0x08 || codePoint === 0x0b || codePoint === 0x0c || (codePoint >= 0x0e && codePoint <= 0x1f);
|
|
173
|
+
const isDeleteOrC1 = codePoint >= 0x7f && codePoint <= 0x9f;
|
|
174
|
+
if (!isC0 && !isDeleteOrC1) clean += character;
|
|
175
|
+
}
|
|
176
|
+
return clean;
|
|
275
177
|
}
|
|
276
178
|
|
|
277
179
|
function cleanBlockText(value) {
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
.replace(/[\x00-\x08\x0b\x0c\x0e-\x1f\x7f-\x9f]/g, "")
|
|
281
|
-
.replace(/[\u061c\u200e\u200f\u202a-\u202e\u2066-\u2069]/g, "");
|
|
180
|
+
const normalized = stripVTControlCharacters(String(value ?? "")).replace(/\r\n?/g, "\n");
|
|
181
|
+
return stripUnsafeControls(normalized).replace(/[\u061c\u200e\u200f\u202a-\u202e\u2066-\u2069]/g, "");
|
|
282
182
|
}
|
|
283
183
|
|
|
284
184
|
function cleanInlineText(value) {
|
|
@@ -304,7 +204,7 @@ function formatOpTarget(raw, tool) {
|
|
|
304
204
|
}
|
|
305
205
|
|
|
306
206
|
function isTheme(value) {
|
|
307
|
-
return
|
|
207
|
+
return isObject(value) && isFunction(value.fg);
|
|
308
208
|
}
|
|
309
209
|
|
|
310
210
|
/**
|
|
@@ -314,13 +214,13 @@ function isTheme(value) {
|
|
|
314
214
|
*/
|
|
315
215
|
export function normalizeCallRenderArgs(a, b, c) {
|
|
316
216
|
if (isTheme(b)) {
|
|
317
|
-
const context = c
|
|
318
|
-
if (!context.state
|
|
217
|
+
const context = isObject(c) ? c : {};
|
|
218
|
+
if (!isObject(context.state)) context.state = {};
|
|
319
219
|
return { args: a, theme: b, context, host: "pi" };
|
|
320
220
|
}
|
|
321
221
|
if (isTheme(c)) {
|
|
322
|
-
const options = b
|
|
323
|
-
if (!options.state
|
|
222
|
+
const options = isObject(b) ? b : {};
|
|
223
|
+
if (!isObject(options.state)) options.state = {};
|
|
324
224
|
const context = {
|
|
325
225
|
...options,
|
|
326
226
|
state: options.state,
|
|
@@ -341,21 +241,19 @@ export function normalizeCallRenderArgs(a, b, c) {
|
|
|
341
241
|
* Pi: (result, {expanded,isPartial}, theme, context)
|
|
342
242
|
* OMP: (result, {expanded,isPartial}, theme, args) — 4th is args, not context
|
|
343
243
|
*
|
|
344
|
-
* Call shapes share the first three positions, so host is inferred from the
|
|
345
|
-
*
|
|
244
|
+
* Call shapes share the first three positions, so host is inferred from the
|
|
245
|
+
* fourth argument's context-versus-args shape.
|
|
346
246
|
*/
|
|
347
247
|
function detectResultHost(options, ctxOrArgs) {
|
|
348
248
|
if (isTheme(options)) return "pi";
|
|
349
249
|
if (
|
|
350
|
-
ctxOrArgs &&
|
|
351
|
-
typeof ctxOrArgs === "object" &&
|
|
250
|
+
isObject(ctxOrArgs) &&
|
|
352
251
|
("lastComponent" in ctxOrArgs || "invalidate" in ctxOrArgs)
|
|
353
252
|
) {
|
|
354
253
|
return "pi";
|
|
355
254
|
}
|
|
356
255
|
if (
|
|
357
|
-
ctxOrArgs &&
|
|
358
|
-
typeof ctxOrArgs === "object" &&
|
|
256
|
+
isObject(ctxOrArgs) &&
|
|
359
257
|
("code" in ctxOrArgs || "timeoutMs" in ctxOrArgs)
|
|
360
258
|
) {
|
|
361
259
|
return "omp";
|
|
@@ -365,19 +263,18 @@ function detectResultHost(options, ctxOrArgs) {
|
|
|
365
263
|
|
|
366
264
|
export function normalizeResultRenderArgs(result, options, themeOrCtx, ctxOrArgs) {
|
|
367
265
|
if (isTheme(themeOrCtx)) {
|
|
368
|
-
const opts = options
|
|
266
|
+
const opts = isObject(options) ? options : {};
|
|
369
267
|
let context;
|
|
370
268
|
if (
|
|
371
|
-
ctxOrArgs &&
|
|
372
|
-
typeof ctxOrArgs === "object" &&
|
|
269
|
+
isObject(ctxOrArgs) &&
|
|
373
270
|
!isTheme(ctxOrArgs) &&
|
|
374
271
|
("lastComponent" in ctxOrArgs || "state" in ctxOrArgs || "invalidate" in ctxOrArgs)
|
|
375
272
|
) {
|
|
376
273
|
context = ctxOrArgs;
|
|
377
274
|
} else {
|
|
378
|
-
context = {
|
|
275
|
+
context = { state: opts.state, lastComponent: opts.lastComponent };
|
|
379
276
|
}
|
|
380
|
-
if (!context.state
|
|
277
|
+
if (!isObject(context.state)) context.state = {};
|
|
381
278
|
return {
|
|
382
279
|
result,
|
|
383
280
|
expanded: !!opts.expanded,
|
|
@@ -391,8 +288,8 @@ export function normalizeResultRenderArgs(result, options, themeOrCtx, ctxOrArgs
|
|
|
391
288
|
}
|
|
392
289
|
// Extremely defensive: (result, theme, context) oddball
|
|
393
290
|
if (isTheme(options)) {
|
|
394
|
-
const context = themeOrCtx
|
|
395
|
-
if (!context.state
|
|
291
|
+
const context = isObject(themeOrCtx) ? themeOrCtx : {};
|
|
292
|
+
if (!isObject(context.state)) context.state = {};
|
|
396
293
|
return {
|
|
397
294
|
result,
|
|
398
295
|
expanded: !!context.expanded,
|
|
@@ -424,174 +321,42 @@ function operationTarget(item) {
|
|
|
424
321
|
return "";
|
|
425
322
|
}
|
|
426
323
|
|
|
324
|
+
function normalizeTraceDiff(item) {
|
|
325
|
+
const diff = item?.diff;
|
|
326
|
+
if (isObject(diff)) return diff;
|
|
327
|
+
if (!isString(diff) || !diff.trim()) return undefined;
|
|
328
|
+
const lines = [];
|
|
329
|
+
let added = 0;
|
|
330
|
+
let removed = 0;
|
|
331
|
+
for (const rawLine of cleanBlockText(diff).split("\n")) {
|
|
332
|
+
let match = /^([+-])\s*(\d+)\s?(.*)$/.exec(rawLine);
|
|
333
|
+
if (match) {
|
|
334
|
+
const type = match[1] === "+" ? "add" : "remove";
|
|
335
|
+
if (type === "add") added += 1;
|
|
336
|
+
else removed += 1;
|
|
337
|
+
lines.push({ type, lineNum: Number(match[2]), text: match[3] });
|
|
338
|
+
continue;
|
|
339
|
+
}
|
|
340
|
+
match = /^\s+(\d+)\s?(.*)$/.exec(rawLine);
|
|
341
|
+
if (match) lines.push({ type: "context", lineNum: Number(match[1]), text: match[2] });
|
|
342
|
+
}
|
|
343
|
+
if (lines.length === 0) return undefined;
|
|
344
|
+
return { path: item?.args?.path || "", op: item?.name, added, removed, lines };
|
|
345
|
+
}
|
|
346
|
+
|
|
427
347
|
function operationsFromTrace(trace) {
|
|
428
348
|
if (!Array.isArray(trace)) return [];
|
|
429
349
|
return trace
|
|
430
|
-
.map((item) => displayOperation(item?.name || "tool", operationTarget(item), item
|
|
431
|
-
.filter(Boolean);
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
function collectCallOps(args, context) {
|
|
435
|
-
const traced = operationsFromTrace(context?.state?.trace);
|
|
436
|
-
if (traced.length > 0) return traced;
|
|
437
|
-
return extractOperationsFromCode(args?.code)
|
|
438
|
-
.map((op) => displayOperation(op.tool, op.target))
|
|
350
|
+
.map((item) => displayOperation(item?.name || "tool", operationTarget(item), normalizeTraceDiff(item), item?.ok))
|
|
439
351
|
.filter(Boolean);
|
|
440
352
|
}
|
|
441
353
|
|
|
442
|
-
function formatElapsed(state) {
|
|
443
|
-
if (state?.wallMs != null) return `${state.wallMs}ms`;
|
|
444
|
-
if (state?.startedAt != null) {
|
|
445
|
-
const elapsed = Math.round(performance.now() - state.startedAt);
|
|
446
|
-
// Suppress sub-100ms noise on the pending head (matches quiet Write/Edit calls).
|
|
447
|
-
if (elapsed < 100) return "";
|
|
448
|
-
return elapsed >= 1000 ? `${(elapsed / 1000).toFixed(1)}s` : `${elapsed}ms`;
|
|
449
|
-
}
|
|
450
|
-
return "";
|
|
451
|
-
}
|
|
452
|
-
|
|
453
|
-
function tickCallTimer(context) {
|
|
454
|
-
const state = context?.state;
|
|
455
|
-
if (state && state.startedAt == null) state.startedAt = performance.now();
|
|
456
|
-
if (state && context?.executionStarted && state.wallMs == null && state.timer == null) {
|
|
457
|
-
state.timer = setTimeout(() => {
|
|
458
|
-
state.timer = null;
|
|
459
|
-
context.invalidate?.();
|
|
460
|
-
}, 100);
|
|
461
|
-
}
|
|
462
|
-
}
|
|
463
|
-
|
|
464
|
-
function formatOpBodyLine(theme, op) {
|
|
465
|
-
const icon = ACTION_ICONS[op.tool] || "✦ ";
|
|
466
|
-
const bullet = theme.fg("accent", icon);
|
|
467
|
-
const toolName = theme.fg("syntaxFunction", op.tool.padEnd(7, " "));
|
|
468
|
-
const rawTarget = formatOpTarget(op.target, op.tool);
|
|
469
|
-
const target = rawTarget ? " " + theme.fg("muted", rawTarget) : "";
|
|
470
|
-
return `${bullet}${toolName}${target}`;
|
|
471
|
-
}
|
|
472
|
-
|
|
473
|
-
function shouldUseOmpFrame(host) {
|
|
474
|
-
return host === "omp";
|
|
475
|
-
}
|
|
476
|
-
|
|
477
|
-
/**
|
|
478
|
-
* OMP write/edit look: rounded framedBlock + status-line header.
|
|
479
|
-
* Pending call has no hourglass on the head row (same as native Write/Edit).
|
|
480
|
-
*/
|
|
481
|
-
function renderOmpCallCard(theme, { ops, timeStr, expanded, code }) {
|
|
482
|
-
const opSummary = ops.length === 0 ? "composing" : `${ops.length} call${ops.length === 1 ? "" : "s"}`;
|
|
483
|
-
const description = timeStr ? `${opSummary} · ${timeStr}` : opSummary;
|
|
484
|
-
// No pending icon on the framed head row — matches native Write/Edit.
|
|
485
|
-
const header = novaStatusLine(theme, {
|
|
486
|
-
title: "nova",
|
|
487
|
-
description,
|
|
488
|
-
});
|
|
489
|
-
return novaFramedBlock(theme, (width) => {
|
|
490
|
-
const bodyLines = ops.map((op) => formatOpBodyLine(theme, op));
|
|
491
|
-
if (expanded && code) {
|
|
492
|
-
bodyLines.push(theme.fg("dim", "── source ──"));
|
|
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`));
|
|
496
|
-
}
|
|
497
|
-
return {
|
|
498
|
-
header,
|
|
499
|
-
sections: bodyLines.length > 0 ? [{ lines: bodyLines }] : [],
|
|
500
|
-
state: "pending",
|
|
501
|
-
borderColor: "borderMuted",
|
|
502
|
-
width,
|
|
503
|
-
};
|
|
504
|
-
});
|
|
505
|
-
}
|
|
506
|
-
|
|
507
|
-
function renderOmpResultCard(theme, { isErr, payload, expanded, bodyText, isPartial, spinnerFrame, opCount = 0 }) {
|
|
508
|
-
if (isErr) {
|
|
509
|
-
const errLines = String(bodyText || "").trim() ? String(bodyText).split("\n") : [];
|
|
510
|
-
if (payload?.error) errLines.push(theme.fg("error", cleanBlockText(payload.error)));
|
|
511
|
-
if (expanded && payload?.logs?.length) {
|
|
512
|
-
errLines.push(theme.fg("dim", "── logs ──"));
|
|
513
|
-
for (const log of payload.logs.slice(0, 24)) errLines.push(theme.fg("dim", cleanBlockText(log)));
|
|
514
|
-
}
|
|
515
|
-
const wall = payload?.wallMs != null ? `${payload.wallMs}ms` : "";
|
|
516
|
-
const calls = opCount > 0 ? `${opCount} call${opCount === 1 ? "" : "s"}` : "";
|
|
517
|
-
const header = novaStatusLine(theme, {
|
|
518
|
-
icon: "error",
|
|
519
|
-
title: "nova",
|
|
520
|
-
description: [calls, "failed", wall].filter(Boolean).join(" · "),
|
|
521
|
-
});
|
|
522
|
-
return novaFramedBlock(theme, (width) => ({
|
|
523
|
-
header,
|
|
524
|
-
sections: errLines.length > 0 ? [{ lines: errLines }] : [],
|
|
525
|
-
state: "error",
|
|
526
|
-
borderColor: "error",
|
|
527
|
-
width,
|
|
528
|
-
}));
|
|
529
|
-
}
|
|
530
|
-
|
|
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(" · ");
|
|
534
|
-
const header = novaStatusLine(theme, {
|
|
535
|
-
icon: isPartial ? "running" : undefined,
|
|
536
|
-
spinnerFrame,
|
|
537
|
-
title: "nova",
|
|
538
|
-
description: description || undefined,
|
|
539
|
-
});
|
|
540
|
-
const bodyLines = String(bodyText || "").split("\n");
|
|
541
|
-
while (bodyLines.length > 0 && bodyLines[0].trim() === "") bodyLines.shift();
|
|
542
|
-
while (bodyLines.length > 0 && bodyLines[bodyLines.length - 1].trim() === "") bodyLines.pop();
|
|
543
|
-
return novaFramedBlock(theme, (width) => ({
|
|
544
|
-
header,
|
|
545
|
-
sections: bodyLines.length > 0 ? [{ lines: bodyLines }] : [],
|
|
546
|
-
state: isPartial ? "pending" : "success",
|
|
547
|
-
borderColor: "borderMuted",
|
|
548
|
-
width,
|
|
549
|
-
}));
|
|
550
|
-
}
|
|
551
|
-
|
|
552
354
|
export function renderSupernovaCall(a, b, c) {
|
|
553
|
-
const {
|
|
554
|
-
tickCallTimer(context);
|
|
555
|
-
const ops = collectCallOps(args, context);
|
|
556
|
-
const timeStr = formatElapsed(context?.state);
|
|
557
|
-
|
|
558
|
-
if (shouldUseOmpFrame(host)) {
|
|
559
|
-
return renderOmpCallCard(theme, {
|
|
560
|
-
ops,
|
|
561
|
-
timeStr,
|
|
562
|
-
expanded: !!context?.expanded,
|
|
563
|
-
code: args?.code,
|
|
564
|
-
});
|
|
565
|
-
}
|
|
566
|
-
|
|
355
|
+
const { context, options } = normalizeCallRenderArgs(a, b, c);
|
|
567
356
|
const comp = context?.lastComponent instanceof SafeText ? context.lastComponent : new SafeText();
|
|
568
357
|
if (options) options.lastComponent = comp;
|
|
569
358
|
else if (context) context.lastComponent = comp;
|
|
570
|
-
|
|
571
|
-
// Compact aesthetic — never dump raw JSON args (the stock tool fallback).
|
|
572
|
-
let out = theme.fg("toolTitle", theme.bold("nova"));
|
|
573
|
-
if (timeStr) {
|
|
574
|
-
out += " " + theme.fg("dim", `· ${timeStr}`);
|
|
575
|
-
}
|
|
576
|
-
|
|
577
|
-
if (ops.length === 0) {
|
|
578
|
-
out += " " + theme.fg("dim", "· composing");
|
|
579
|
-
} else {
|
|
580
|
-
for (const op of ops) {
|
|
581
|
-
out += `\n ${formatOpBodyLine(theme, op)}`;
|
|
582
|
-
}
|
|
583
|
-
}
|
|
584
|
-
|
|
585
|
-
if (context?.expanded && args?.code) {
|
|
586
|
-
out += "\n" + theme.fg("dim", "── source ──");
|
|
587
|
-
out += "\n" + theme.fg("toolOutput", cleanBlockText(args.code).trim());
|
|
588
|
-
}
|
|
589
|
-
|
|
590
|
-
if (context?.isError) comp.setTone("error");
|
|
591
|
-
else if (context?.isPartial) comp.setTone("pending");
|
|
592
|
-
else comp.setTone("success");
|
|
593
|
-
comp.setFraming(true);
|
|
594
|
-
comp.setText(out);
|
|
359
|
+
comp.setText("");
|
|
595
360
|
return comp;
|
|
596
361
|
}
|
|
597
362
|
|
|
@@ -611,7 +376,8 @@ function formatResultOperation(theme, op, isPartial, isError) {
|
|
|
611
376
|
const tool = theme.fg("syntaxFunction", op.tool.padEnd(7, " "));
|
|
612
377
|
const targetText = formatOpTarget(op.target, op.tool);
|
|
613
378
|
const target = targetText ? theme.fg("muted", targetText) : theme.fg("dim", "done");
|
|
614
|
-
|
|
379
|
+
const stats = formatDiffStats(theme, op.diff);
|
|
380
|
+
return stats ? `${marker}${tool}${stats} ${target}` : `${marker}${tool} ${target}`;
|
|
615
381
|
}
|
|
616
382
|
|
|
617
383
|
function boundedResult(value) {
|
|
@@ -635,21 +401,21 @@ function buildResultBody(theme, { payload, context, args, expanded, isPartial, i
|
|
|
635
401
|
? tracedOps
|
|
636
402
|
: extractOperationsFromCode(args?.code).map((op) => displayOperation(op.tool, op.target)).filter(Boolean);
|
|
637
403
|
const maxOps = expanded ? 12 : 8;
|
|
638
|
-
|
|
639
|
-
|
|
404
|
+
const maxDiffLines = expanded ? 24 : 8;
|
|
405
|
+
const visibleOps = ops.slice(0, maxOps);
|
|
406
|
+
for (const [index, op] of visibleOps.entries()) {
|
|
407
|
+
if (index > 0) out += "\n│";
|
|
408
|
+
const isLast = index === visibleOps.length - 1 && ops.length <= maxOps;
|
|
409
|
+
const branch = isLast ? "└─" : "├─";
|
|
410
|
+
out += (out ? "\n" : "") + `${branch} ${formatResultOperation(theme, op, isPartial, isError)}`;
|
|
411
|
+
if (op.diff && isObject(op.diff)) {
|
|
412
|
+
const continuation = isLast ? " " : "│ ";
|
|
413
|
+
for (const row of formatDiffRows(op.diff, theme, maxDiffLines)) out += `\n${continuation}${row}`;
|
|
414
|
+
}
|
|
640
415
|
}
|
|
641
|
-
if (ops.length > maxOps) out += `\n${theme.fg("dim", `… ${ops.length - maxOps} more calls`)}`;
|
|
416
|
+
if (ops.length > maxOps) out += `\n│\n└─ ${theme.fg("dim", `… ${ops.length - maxOps} more calls`)}`;
|
|
642
417
|
|
|
643
418
|
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)) {
|
|
648
|
-
const box = renderDiffBox(diff, theme, 120);
|
|
649
|
-
if (box) out += "\n" + box;
|
|
650
|
-
}
|
|
651
|
-
if (diffs.length > maxDiffs) out += `\n${theme.fg("dim", `… ${diffs.length - maxDiffs} more changed files`)}`;
|
|
652
|
-
|
|
653
419
|
if (payload?.result !== undefined) {
|
|
654
420
|
out += (out ? "\n" : "") + theme.fg("dim", "── result ──");
|
|
655
421
|
out += "\n" + theme.fg("toolOutput", boundedResult(payload.result));
|
|
@@ -662,6 +428,31 @@ function buildResultBody(theme, { payload, context, args, expanded, isPartial, i
|
|
|
662
428
|
return { body: out, opCount: ops.length };
|
|
663
429
|
}
|
|
664
430
|
|
|
431
|
+
class UnifiedResultCard {
|
|
432
|
+
set(theme, model) {
|
|
433
|
+
this.theme = theme;
|
|
434
|
+
this.model = model;
|
|
435
|
+
}
|
|
436
|
+
invalidate() {}
|
|
437
|
+
render(width = 80) {
|
|
438
|
+
const { theme, model } = this;
|
|
439
|
+
if (!theme || !model) return [];
|
|
440
|
+
const header = novaStatusLine(theme, {
|
|
441
|
+
icon: model.isError ? "error" : model.isPartial ? "running" : undefined,
|
|
442
|
+
title: "nova",
|
|
443
|
+
description: model.description,
|
|
444
|
+
});
|
|
445
|
+
const lines = model.body ? model.body.split("\n") : [];
|
|
446
|
+
return novaFramedBlock(theme, (frameWidth) => ({
|
|
447
|
+
header,
|
|
448
|
+
sections: lines.length > 0 ? [{ lines }] : [],
|
|
449
|
+
state: model.isError ? "error" : model.isPartial ? "pending" : "success",
|
|
450
|
+
borderColor: model.isError ? "error" : "borderMuted",
|
|
451
|
+
width: frameWidth,
|
|
452
|
+
})).render(width);
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
665
456
|
export function renderSupernovaResult(resultArg, optionsArg, themeArg, contextArg) {
|
|
666
457
|
const { result, expanded, isPartial, theme, context, args, options, host } = normalizeResultRenderArgs(
|
|
667
458
|
resultArg,
|
|
@@ -672,58 +463,33 @@ export function renderSupernovaResult(resultArg, optionsArg, themeArg, contextAr
|
|
|
672
463
|
|
|
673
464
|
const payload = result?.details;
|
|
674
465
|
if (context?.state && payload) {
|
|
675
|
-
let changed = false;
|
|
676
466
|
if (Array.isArray(payload.trace) && context.state.trace !== payload.trace) {
|
|
677
467
|
context.state.trace = payload.trace;
|
|
678
|
-
changed = true;
|
|
679
468
|
}
|
|
680
469
|
if (payload.wallMs != null && context.state.wallMs !== payload.wallMs) {
|
|
681
470
|
context.state.wallMs = payload.wallMs;
|
|
682
|
-
changed = true;
|
|
683
|
-
}
|
|
684
|
-
if (context.state.timer != null && !isPartial) {
|
|
685
|
-
clearTimeout(context.state.timer);
|
|
686
|
-
context.state.timer = null;
|
|
687
471
|
}
|
|
688
|
-
if (changed) context.invalidate?.();
|
|
689
472
|
}
|
|
690
473
|
|
|
691
474
|
const isErr = result?.isError || payload?.ok === false;
|
|
692
475
|
const view = buildResultBody(theme, { payload, context, args, expanded, isPartial, isError: isErr });
|
|
693
476
|
|
|
694
|
-
|
|
695
|
-
return renderOmpResultCard(theme, {
|
|
696
|
-
isErr,
|
|
697
|
-
payload,
|
|
698
|
-
expanded,
|
|
699
|
-
bodyText: view.body,
|
|
700
|
-
isPartial: isPartial && !isErr,
|
|
701
|
-
spinnerFrame: options?.spinnerFrame,
|
|
702
|
-
opCount: view.opCount,
|
|
703
|
-
});
|
|
704
|
-
}
|
|
705
|
-
|
|
706
|
-
const comp = context?.lastComponent instanceof SafeText ? context.lastComponent : new SafeText();
|
|
707
|
-
if (options) options.lastComponent = comp;
|
|
708
|
-
else if (context) context.lastComponent = comp;
|
|
709
|
-
|
|
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 ")}`;
|
|
715
|
-
|
|
477
|
+
let body = view.body;
|
|
716
478
|
if (isErr) {
|
|
717
|
-
|
|
479
|
+
body += (body ? "\n" : "") + theme.fg("error", payload?.error ? cleanBlockText(payload.error) : "error");
|
|
718
480
|
if (expanded && payload?.logs?.length) {
|
|
719
|
-
|
|
720
|
-
for (const log of payload.logs.slice(0, 24))
|
|
481
|
+
body += `\n${theme.fg("dim", "── logs ──")}`;
|
|
482
|
+
for (const log of payload.logs.slice(0, 24)) body += `\n${theme.fg("dim", cleanBlockText(log))}`;
|
|
721
483
|
}
|
|
722
|
-
comp.setTone("error");
|
|
723
|
-
} else {
|
|
724
|
-
comp.setTone(isPartial ? "pending" : "success");
|
|
725
484
|
}
|
|
726
|
-
|
|
727
|
-
|
|
485
|
+
const wall = payload?.wallMs != null ? `${payload.wallMs}ms` : "";
|
|
486
|
+
const calls = view.opCount > 0 ? `${view.opCount} call${view.opCount === 1 ? "" : "s"}` : "";
|
|
487
|
+
const status = isErr ? "failed" : isPartial ? "running" : calls ? "" : "complete";
|
|
488
|
+
const description = [calls, status, wall].filter(Boolean).join(" · ");
|
|
489
|
+
const previous = host === "omp" ? options?.lastComponent : context?.lastComponent;
|
|
490
|
+
const comp = previous instanceof UnifiedResultCard ? previous : new UnifiedResultCard();
|
|
491
|
+
if (host === "omp" && options) options.lastComponent = comp;
|
|
492
|
+
else if (context) context.lastComponent = comp;
|
|
493
|
+
comp.set(theme, { body, description, isError: isErr, isPartial });
|
|
728
494
|
return comp;
|
|
729
495
|
}
|
package/runtime.js
CHANGED
|
@@ -158,7 +158,13 @@ export async function runGuestProgram(options) {
|
|
|
158
158
|
};
|
|
159
159
|
const quoteShellArg = (value) => `'${String(value).replaceAll("'", "'\\''")}'`;
|
|
160
160
|
const guestExec = async (cmd, args, opts) => {
|
|
161
|
-
const
|
|
161
|
+
const command = String(cmd ?? "").trim();
|
|
162
|
+
if (!command) throw new Error("exec requires command");
|
|
163
|
+
// exec("git status") is a shell line; exec("git", ["status"]) is argv.
|
|
164
|
+
if (!Array.isArray(args) || args.length === 0) {
|
|
165
|
+
return guestBash(command, opts);
|
|
166
|
+
}
|
|
167
|
+
const argv = [command, ...args].map(quoteShellArg).join(" ");
|
|
162
168
|
return guestBash(argv, opts);
|
|
163
169
|
};
|
|
164
170
|
const guestSpeculate = async (fn) => (isFunction(nova.speculate) ? nova.speculate(fn) : fn());
|