pi-supernova 0.0.7 → 0.0.8
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 +26 -0
- package/README.md +22 -15
- package/bottleneck.js +32 -41
- package/catalog.js +61 -15
- package/config.default.json +2 -1
- package/config.js +19 -10
- package/diff.js +12 -4
- package/format.js +95 -0
- package/guest-worker.js +344 -0
- package/host-bridge.js +130 -439
- package/index.js +75 -97
- package/omp-frame.js +68 -51
- package/package.json +6 -1
- package/parallel.js +19 -20
- package/patch.js +106 -0
- package/render-measure.js +63 -49
- package/render.js +193 -166
- package/runtime.js +266 -241
- package/snap.js +120 -101
- package/surface.js +13 -30
- package/vfs.js +138 -0
- package/workspace.js +112 -0
package/index.js
CHANGED
|
@@ -3,7 +3,7 @@ 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";
|
|
6
|
-
import { runGuestProgram } from "./runtime.js";
|
|
6
|
+
import { runGuestProgram, warmGuestWorker } from "./runtime.js";
|
|
7
7
|
import {
|
|
8
8
|
extractOperationsFromCode,
|
|
9
9
|
renderSupernovaCall,
|
|
@@ -30,6 +30,30 @@ try {
|
|
|
30
30
|
function result(text, details) {
|
|
31
31
|
return { content: [{ type: "text", text }], details };
|
|
32
32
|
}
|
|
33
|
+
|
|
34
|
+
/** Live trace updates for the card; a throwing host callback must never break the run. */
|
|
35
|
+
function progressEmitter(onUpdate) {
|
|
36
|
+
if (!isFunction(onUpdate)) return () => {};
|
|
37
|
+
return (trace) => {
|
|
38
|
+
try {
|
|
39
|
+
onUpdate({ content: [{ type: "text", text: "" }], details: { trace, running: true } });
|
|
40
|
+
} catch {}
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function logsBlock(outcome, tail = "") {
|
|
45
|
+
return outcome.logs?.length ? `\n--- logs\n${outcome.logs.join("\n")}${tail}` : "";
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function errorText(outcome) {
|
|
49
|
+
return `error ${outcome.wallMs}ms: ${outcome.error}${logsBlock(outcome)}`;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function successText(outcome) {
|
|
53
|
+
const truncated = outcome.returnTruncated ? " [return truncated]" : "";
|
|
54
|
+
const hint = outcome.undefinedReturn ? " (no return statement — add \`return\` to get a value)" : "";
|
|
55
|
+
return `ok ${outcome.wallMs}ms${truncated}${logsBlock(outcome, "\n--- result")}\n${outcome.resultText}${hint}`;
|
|
56
|
+
}
|
|
33
57
|
function unwrapStructuredResult(response, operation) {
|
|
34
58
|
if (response?.ok === false) {
|
|
35
59
|
throw new Error(response.value || response.error || `${operation} failed`);
|
|
@@ -43,21 +67,16 @@ function unwrapStructuredResult(response, operation) {
|
|
|
43
67
|
}
|
|
44
68
|
}
|
|
45
69
|
|
|
46
|
-
const TOOL_DESCRIPTION = `
|
|
70
|
+
const TOOL_DESCRIPTION = `Run one JavaScript program that composes host tools. Async body or arrow; \`return\` a small shaped value (compact literal, capped; strings raw; console.log is captured).
|
|
47
71
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
parallel(thunks) / pipeline(items, ...stages)
|
|
57
|
-
|
|
58
|
-
Shorthand globals: read, write, edit, patch, exec, snap, surface.
|
|
59
|
-
Prefer search→describe→call. Keep intermediates in the program; return a shaped value.
|
|
60
|
-
Schemas are NOT dumped into the system prompt — discover them inside the runtime.`;
|
|
72
|
+
Globals (async):
|
|
73
|
+
read(path|paths, offset?, limit?) → text | text[]
|
|
74
|
+
write(path, text) · edit(path, oldText, newText) · patch(path, unifiedDiff)
|
|
75
|
+
bash(cmd, {cwd?, timeoutMs?}) → output, throws on non-zero exit · exec(cmd, argv?) quotes argv
|
|
76
|
+
snap(query, root?) → {path, line, signature, context} · surface(path) → {items: [{name, kind, line}]}
|
|
77
|
+
nova.call(name, args) → {ok, value} for any host tool · nova.callMany([{name, args}]) parallel when read-only
|
|
78
|
+
nova.search(query) → [{name, description}] · nova.describe(name) → parameters · nova.has(name) sync
|
|
79
|
+
parallel(thunks) · pipeline(items, ...stages)`;
|
|
61
80
|
|
|
62
81
|
export default function piSupernova(pi) {
|
|
63
82
|
const config = loadConfig();
|
|
@@ -101,16 +120,18 @@ export default function piSupernova(pi) {
|
|
|
101
120
|
async callMany(calls) {
|
|
102
121
|
return bridge.callMany(calls);
|
|
103
122
|
},
|
|
104
|
-
|
|
123
|
+
speculateBegin() {
|
|
105
124
|
bridge.beginSpeculation();
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
125
|
+
},
|
|
126
|
+
async speculateCommit() {
|
|
127
|
+
await bridge.commitSpeculation();
|
|
128
|
+
},
|
|
129
|
+
speculateRollback() {
|
|
130
|
+
bridge.rollbackSpeculation();
|
|
131
|
+
},
|
|
132
|
+
names() {
|
|
133
|
+
const cat = catalog.length ? catalog : refreshCatalog();
|
|
134
|
+
return [...new Set([...cat.map((t) => t.name), ...bridge.executors.keys(), ...Object.keys(bridge.natives)])];
|
|
114
135
|
},
|
|
115
136
|
async surface(filePath) {
|
|
116
137
|
return unwrapStructuredResult(await bridge.call("surface", { path: filePath }), "surface");
|
|
@@ -118,9 +139,6 @@ export default function piSupernova(pi) {
|
|
|
118
139
|
async snap(query, targetPath) {
|
|
119
140
|
return unwrapStructuredResult(await bridge.call("snap", { query, path: targetPath }), "snap");
|
|
120
141
|
},
|
|
121
|
-
has(name) {
|
|
122
|
-
return bridge.hasExecutor(name) || catalog.some((t) => t.name === name);
|
|
123
|
-
},
|
|
124
142
|
};
|
|
125
143
|
}
|
|
126
144
|
|
|
@@ -128,23 +146,13 @@ export default function piSupernova(pi) {
|
|
|
128
146
|
name: "supernova",
|
|
129
147
|
label: "Supernova",
|
|
130
148
|
description: TOOL_DESCRIPTION,
|
|
131
|
-
promptSnippet: "Compose
|
|
149
|
+
promptSnippet: "Compose host tools in one JavaScript program",
|
|
132
150
|
promptGuidelines: [
|
|
133
|
-
"Use supernova
|
|
134
|
-
"Discover tools with nova.search / nova.describe inside the program — do not guess full schemas.",
|
|
135
|
-
"Return a compact shaped value; intermediates stay in the runtime.",
|
|
151
|
+
"Use supernova for multi-step tool work: loops, filtering, parallel reads, read→edit chains. Return a compact shaped value; keep raw tool output inside the program.",
|
|
136
152
|
],
|
|
137
153
|
parameters: Type.Object({
|
|
138
|
-
code: Type.String({
|
|
139
|
-
|
|
140
|
-
"JavaScript async body or arrow. Globals: nova/tools, parallel, pipeline, console.",
|
|
141
|
-
}),
|
|
142
|
-
timeoutMs: Type.Optional(
|
|
143
|
-
Type.Integer({
|
|
144
|
-
minimum: 1000,
|
|
145
|
-
description: "Hard timeout in ms (default from supernova.json / package default)",
|
|
146
|
-
}),
|
|
147
|
-
),
|
|
154
|
+
code: Type.String({ description: "JavaScript program: async body or arrow function." }),
|
|
155
|
+
timeoutMs: Type.Optional(Type.Integer({ minimum: 1000, description: "Hard timeout in ms." })),
|
|
148
156
|
}),
|
|
149
157
|
// One self-owned result frame is shared by Pi and OMP; renderCall stays empty
|
|
150
158
|
// so separate call/result slots cannot duplicate the lifecycle card.
|
|
@@ -163,49 +171,13 @@ export default function piSupernova(pi) {
|
|
|
163
171
|
bridge.resetCallBudget();
|
|
164
172
|
refreshCatalog();
|
|
165
173
|
bridge.beginSpeculation();
|
|
174
|
+
const emitProgress = progressEmitter(onUpdate);
|
|
175
|
+
bridge.setCallListener((_record, allTrace) => emitProgress(allTrace));
|
|
176
|
+
emitProgress([]);
|
|
166
177
|
|
|
167
|
-
bridge.setCallListener((_record, allTrace) => {
|
|
168
|
-
if (isFunction(onUpdate)) {
|
|
169
|
-
try {
|
|
170
|
-
onUpdate({
|
|
171
|
-
content: [{ type: "text", text: "" }],
|
|
172
|
-
details: { trace: allTrace, running: true },
|
|
173
|
-
});
|
|
174
|
-
} catch {}
|
|
175
|
-
}
|
|
176
|
-
});
|
|
177
|
-
|
|
178
|
-
if (isFunction(onUpdate)) {
|
|
179
|
-
try {
|
|
180
|
-
onUpdate({
|
|
181
|
-
content: [{ type: "text", text: "" }],
|
|
182
|
-
details: { trace: [], running: true },
|
|
183
|
-
});
|
|
184
|
-
} catch {}
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
const runConfig = {
|
|
188
|
-
...config,
|
|
189
|
-
timeoutMs: Number.isInteger(params?.timeoutMs) ? params.timeoutMs : config.timeoutMs,
|
|
190
|
-
};
|
|
191
|
-
|
|
192
|
-
const runStartedAt = performance.now();
|
|
193
178
|
let outcome;
|
|
194
179
|
try {
|
|
195
|
-
outcome = await
|
|
196
|
-
code: String(params?.code || ""),
|
|
197
|
-
nova: makeNovaApi(),
|
|
198
|
-
config: runConfig,
|
|
199
|
-
signal: runController.signal,
|
|
200
|
-
onTimeout: abortRun,
|
|
201
|
-
});
|
|
202
|
-
} catch (error) {
|
|
203
|
-
outcome = {
|
|
204
|
-
ok: false,
|
|
205
|
-
error: error instanceof Error ? error.message : String(error),
|
|
206
|
-
logs: [],
|
|
207
|
-
wallMs: Math.round(performance.now() - runStartedAt),
|
|
208
|
-
};
|
|
180
|
+
outcome = await runProgram(params, runController.signal, abortRun);
|
|
209
181
|
} finally {
|
|
210
182
|
bridge.setCallListener(null);
|
|
211
183
|
signal?.removeEventListener("abort", abortRun);
|
|
@@ -214,23 +186,10 @@ export default function piSupernova(pi) {
|
|
|
214
186
|
const trace = bridge.getTrace();
|
|
215
187
|
if (!outcome.ok) {
|
|
216
188
|
bridge.rollbackSpeculation();
|
|
217
|
-
|
|
218
|
-
if (outcome.logs?.length) text += `\n\nLogs:\n${outcome.logs.join("\n")}`;
|
|
219
|
-
return result(text, {
|
|
220
|
-
ok: false,
|
|
221
|
-
error: outcome.error,
|
|
222
|
-
wallMs: outcome.wallMs,
|
|
223
|
-
logs: outcome.logs,
|
|
224
|
-
trace,
|
|
225
|
-
});
|
|
189
|
+
return result(errorText(outcome), { ok: false, error: outcome.error, wallMs: outcome.wallMs, logs: outcome.logs, trace });
|
|
226
190
|
}
|
|
227
|
-
|
|
228
191
|
await bridge.commitSpeculation();
|
|
229
|
-
|
|
230
|
-
if (outcome.returnTruncated) text += " [return truncated]";
|
|
231
|
-
if (outcome.logs?.length) text += `\n\nLogs:\n${outcome.logs.join("\n")}`;
|
|
232
|
-
text += `\n\nResult:\n${outcome.resultText}`;
|
|
233
|
-
return result(text, {
|
|
192
|
+
return result(successText(outcome), {
|
|
234
193
|
ok: true,
|
|
235
194
|
wallMs: outcome.wallMs,
|
|
236
195
|
returnTruncated: outcome.returnTruncated,
|
|
@@ -242,9 +201,28 @@ export default function piSupernova(pi) {
|
|
|
242
201
|
},
|
|
243
202
|
});
|
|
244
203
|
|
|
204
|
+
async function runProgram(params, signal, onTimeout) {
|
|
205
|
+
const runStartedAt = performance.now();
|
|
206
|
+
const runConfig = {
|
|
207
|
+
...config,
|
|
208
|
+
timeoutMs: Number.isInteger(params?.timeoutMs) ? params.timeoutMs : config.timeoutMs,
|
|
209
|
+
};
|
|
210
|
+
try {
|
|
211
|
+
return await runGuestProgram({ code: String(params?.code || ""), nova: makeNovaApi(), config: runConfig, signal, onTimeout });
|
|
212
|
+
} catch (error) {
|
|
213
|
+
return {
|
|
214
|
+
ok: false,
|
|
215
|
+
error: error instanceof Error ? error.message : String(error),
|
|
216
|
+
logs: [],
|
|
217
|
+
wallMs: Math.round(performance.now() - runStartedAt),
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
245
222
|
pi.on("session_start", (_event, ctx) => {
|
|
246
223
|
if (ctx && isString(ctx.cwd) && ctx.cwd) cwd = ctx.cwd;
|
|
247
224
|
refreshCatalog();
|
|
225
|
+
warmGuestWorker(config).catch(() => {});
|
|
248
226
|
});
|
|
249
227
|
|
|
250
228
|
pi.registerCommand("supernova", {
|
|
@@ -257,7 +235,7 @@ export default function piSupernova(pi) {
|
|
|
257
235
|
`pi-supernova catalog: ${catalog.length} tools`,
|
|
258
236
|
`captured executors: ${captured.length ? captured.join(", ") : "(none yet — load this package early)"}`,
|
|
259
237
|
`native adapters: ${natives.join(", ")}`,
|
|
260
|
-
`timeoutMs=${config.timeoutMs} maxCallResultChars=${config.maxCallResultChars} maxBridgeCalls=${config.maxBridgeCalls}`,
|
|
238
|
+
`timeoutMs=${config.timeoutMs} maxCallResultChars=${config.maxCallResultChars} maxReturnChars=${config.maxReturnChars} maxBridgeCalls=${config.maxBridgeCalls} maxHeapMb=${config.maxHeapMb}`,
|
|
261
239
|
];
|
|
262
240
|
ctx.ui.notify(lines.join("\n"), "info");
|
|
263
241
|
},
|
package/omp-frame.js
CHANGED
|
@@ -25,10 +25,14 @@ function boxOf(theme) {
|
|
|
25
25
|
return DEFAULT_BOX;
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
+
const BORDER_BY_STATE = { error: "error", warning: "warning", running: "accent", pending: "accent" };
|
|
29
|
+
|
|
30
|
+
function borderKeyFor(state) {
|
|
31
|
+
return BORDER_BY_STATE[state] || "dim";
|
|
32
|
+
}
|
|
33
|
+
|
|
28
34
|
function borderPaint(theme, state, borderColor) {
|
|
29
|
-
const key =
|
|
30
|
-
borderColor ||
|
|
31
|
-
(state === "error" ? "error" : state === "warning" ? "warning" : state === "running" || state === "pending" ? "accent" : "dim");
|
|
35
|
+
const key = borderColor || borderKeyFor(state);
|
|
32
36
|
if (theme && isFunction(theme.fg)) {
|
|
33
37
|
try {
|
|
34
38
|
return (text) => theme.fg(key, text);
|
|
@@ -39,22 +43,30 @@ function borderPaint(theme, state, borderColor) {
|
|
|
39
43
|
return (text) => text;
|
|
40
44
|
}
|
|
41
45
|
|
|
46
|
+
function resolveStatusIcon({ icon, iconOverride, state }) {
|
|
47
|
+
if (iconOverride !== undefined) return undefined;
|
|
48
|
+
if (icon !== undefined) return icon;
|
|
49
|
+
return state === "error" ? "error" : undefined;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const STATUS_GLYPH = { error: "✗ ", running: "… " };
|
|
53
|
+
const STATUS_COLOR = { error: "error", running: "dim" };
|
|
54
|
+
|
|
55
|
+
function statusPrefix(theme, resolvedIcon, iconOverride, spinnerFrame) {
|
|
56
|
+
if (iconOverride) return `${iconOverride} `;
|
|
57
|
+
const key = resolvedIcon === "error" ? "error" : resolvedIcon === "running" || spinnerFrame ? "running" : undefined;
|
|
58
|
+
const glyph = STATUS_GLYPH[key];
|
|
59
|
+
if (!glyph) return "";
|
|
60
|
+
return theme?.fg ? theme.fg(STATUS_COLOR[key], glyph) : glyph;
|
|
61
|
+
}
|
|
62
|
+
|
|
42
63
|
function statusHeader(theme, { title, description, state, spinnerFrame, icon, iconOverride }) {
|
|
43
|
-
const resolvedIcon =
|
|
44
|
-
iconOverride !== undefined
|
|
45
|
-
? undefined
|
|
46
|
-
: icon !== undefined
|
|
47
|
-
? icon
|
|
48
|
-
: state === "error"
|
|
49
|
-
? "error"
|
|
50
|
-
: undefined;
|
|
64
|
+
const resolvedIcon = resolveStatusIcon({ icon, iconOverride, state });
|
|
51
65
|
const titleText = theme?.fg ? theme.fg("accent", title) : title;
|
|
52
66
|
const descText = description ? (theme?.fg ? theme.fg("muted", description) : description) : "";
|
|
53
|
-
|
|
54
|
-
if (
|
|
55
|
-
|
|
56
|
-
else if (resolvedIcon === "running" || spinnerFrame) prefix = theme?.fg ? theme.fg("dim", "… ") : "… ";
|
|
57
|
-
return descText ? `${prefix}${titleText}: ${descText}` : `${prefix}${titleText}`;
|
|
67
|
+
const prefix = statusPrefix(theme, resolvedIcon, iconOverride, spinnerFrame);
|
|
68
|
+
if (!descText) return `${prefix}${titleText}`;
|
|
69
|
+
return `${prefix}${titleText}: ${descText}`;
|
|
58
70
|
}
|
|
59
71
|
|
|
60
72
|
function padLine(line, width, bgFn) {
|
|
@@ -65,34 +77,55 @@ function padLine(line, width, bgFn) {
|
|
|
65
77
|
return bgFn ? bgFn(padded) : padded;
|
|
66
78
|
}
|
|
67
79
|
|
|
80
|
+
const BG_BY_STATE = { error: "toolErrorBg", pending: "toolPendingBg", running: "toolPendingBg" };
|
|
81
|
+
|
|
82
|
+
function bgKeyFor(state) {
|
|
83
|
+
return BG_BY_STATE[state] || "toolSuccessBg";
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function wrapBg(paint) {
|
|
87
|
+
return (text) => {
|
|
88
|
+
const out = paint(text);
|
|
89
|
+
return isString(out) ? out : text;
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
68
93
|
function bgFnForState(theme, state) {
|
|
69
94
|
if (!state || !theme) return undefined;
|
|
95
|
+
const key = bgKeyFor(state);
|
|
70
96
|
if (isFunction(theme.bg)) {
|
|
71
|
-
const key =
|
|
72
|
-
state === "error" ? "toolErrorBg" : state === "pending" || state === "running" ? "toolPendingBg" : "toolSuccessBg";
|
|
73
97
|
try {
|
|
74
|
-
|
|
75
|
-
if (!isString(probe)) return undefined;
|
|
76
|
-
return (text) => {
|
|
77
|
-
const painted = theme.bg(key, text);
|
|
78
|
-
return isString(painted) ? painted : text;
|
|
79
|
-
};
|
|
98
|
+
if (!isString(theme.bg(key, "x"))) return undefined;
|
|
80
99
|
} catch {
|
|
81
100
|
return undefined;
|
|
82
101
|
}
|
|
102
|
+
return wrapBg((text) => theme.bg(key, text));
|
|
83
103
|
}
|
|
84
|
-
if (isFunction(theme.getBgAnsi))
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
104
|
+
if (!isFunction(theme.getBgAnsi)) return undefined;
|
|
105
|
+
try {
|
|
106
|
+
const ansi = theme.getBgAnsi(key);
|
|
107
|
+
if (!ansi) return undefined;
|
|
108
|
+
return (text) => `${ansi}${text}\x1b[49m`;
|
|
109
|
+
} catch {
|
|
110
|
+
return undefined;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function frameBodyLines(sections, contentWidth, box, border, bgFn, w, paintBar) {
|
|
115
|
+
const lines = [];
|
|
116
|
+
const normalized = sections.length > 0 ? sections : [{ lines: [] }];
|
|
117
|
+
const v = box.vertical;
|
|
118
|
+
for (const section of normalized) {
|
|
119
|
+
if (section.label) lines.push(paintBar(box.teeRight || "├", box.teeLeft || "┤", section.label));
|
|
120
|
+
for (const raw of section.lines || []) {
|
|
121
|
+
for (const piece of String(raw).split("\n")) {
|
|
122
|
+
const body = clampLine(piece, contentWidth);
|
|
123
|
+
const pad = Math.max(0, contentWidth - measureWidth(body));
|
|
124
|
+
lines.push(padLine(`${border(v)} ${body}${" ".repeat(pad)} ${border(v)}`, w, bgFn));
|
|
125
|
+
}
|
|
93
126
|
}
|
|
94
127
|
}
|
|
95
|
-
return
|
|
128
|
+
return lines;
|
|
96
129
|
}
|
|
97
130
|
|
|
98
131
|
function renderPortableFrame(theme, { header, sections = [], state = "pending", borderColor, width }) {
|
|
@@ -129,22 +162,7 @@ function renderPortableFrame(theme, { header, sections = [], state = "pending",
|
|
|
129
162
|
const contentWidth = Math.max(1, w - 2 - 2);
|
|
130
163
|
const lines = [];
|
|
131
164
|
lines.push(paintBar(box.topLeft, box.topRight, header));
|
|
132
|
-
|
|
133
|
-
const normalized = sections.length > 0 ? sections : [{ lines: [] }];
|
|
134
|
-
for (const section of normalized) {
|
|
135
|
-
if (section.label) {
|
|
136
|
-
lines.push(paintBar(box.teeRight || "├", box.teeLeft || "┤", section.label));
|
|
137
|
-
}
|
|
138
|
-
for (const raw of section.lines || []) {
|
|
139
|
-
for (const piece of String(raw).split("\n")) {
|
|
140
|
-
const body = clampLine(piece, contentWidth);
|
|
141
|
-
const pad = Math.max(0, contentWidth - measureWidth(body));
|
|
142
|
-
const inner = `${body}${" ".repeat(pad)}`;
|
|
143
|
-
lines.push(padLine(`${border(v)} ${inner} ${border(v)}`, w, bgFn));
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
|
|
165
|
+
lines.push(...frameBodyLines(sections, contentWidth, box, border, bgFn, w, paintBar));
|
|
148
166
|
lines.push(paintBar(box.bottomLeft, box.bottomRight, null));
|
|
149
167
|
return lines;
|
|
150
168
|
}
|
|
@@ -180,4 +198,3 @@ export function novaFramedBlock(theme, build) {
|
|
|
180
198
|
export function novaStatusLine(theme, options) {
|
|
181
199
|
return statusHeader(theme, options);
|
|
182
200
|
}
|
|
183
|
-
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-supernova",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.8",
|
|
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",
|
|
@@ -29,6 +29,11 @@
|
|
|
29
29
|
"bottleneck.js",
|
|
30
30
|
"parallel.js",
|
|
31
31
|
"runtime.js",
|
|
32
|
+
"guest-worker.js",
|
|
33
|
+
"format.js",
|
|
34
|
+
"patch.js",
|
|
35
|
+
"vfs.js",
|
|
36
|
+
"workspace.js",
|
|
32
37
|
"config.js",
|
|
33
38
|
"config.default.json",
|
|
34
39
|
"README.md",
|
package/parallel.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
|
|
2
2
|
import { isString } from "./decode.js";
|
|
3
|
-
|
|
4
3
|
export function isMutatingTool(name, config) {
|
|
5
4
|
const exact = new Set(config.mutatingTools || []);
|
|
6
5
|
if (exact.has(name)) return true;
|
|
@@ -10,35 +9,35 @@ export function isMutatingTool(name, config) {
|
|
|
10
9
|
}
|
|
11
10
|
return false;
|
|
12
11
|
}
|
|
13
|
-
|
|
12
|
+
async function runSerial(list) {
|
|
13
|
+
const out = [];
|
|
14
|
+
for (const thunk of list) out.push(await thunk());
|
|
15
|
+
return out;
|
|
16
|
+
}
|
|
17
|
+
function shouldParallelize(mode, anyMutating, count) {
|
|
18
|
+
if (mode === "parallel") return true;
|
|
19
|
+
if (mode !== "auto") return false;
|
|
20
|
+
if (anyMutating) return false;
|
|
21
|
+
return count > 1;
|
|
22
|
+
}
|
|
14
23
|
export async function runParallelWave(thunks, meta, options = {}) {
|
|
15
24
|
const list = Array.isArray(thunks) ? thunks : [];
|
|
16
|
-
if (list.length === 0) {
|
|
17
|
-
|
|
18
|
-
}
|
|
19
|
-
const mode = options.mode || "auto";
|
|
25
|
+
if (list.length === 0) return { results: [], mode: "serial", reason: "empty" };
|
|
26
|
+
const { mode = "auto", config = {} } = options;
|
|
20
27
|
const names = Array.isArray(meta?.names) ? meta.names : [];
|
|
21
|
-
const config = options.config || {};
|
|
22
28
|
const anyMutating = names.some((n) => isString(n) && isMutatingTool(n, config));
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
const out = [];
|
|
27
|
-
for (const thunk of list) {
|
|
28
|
-
out.push(await thunk());
|
|
29
|
-
}
|
|
30
|
-
return { results: out, mode: "serial", reason: anyMutating ? "mutating" : "single-or-forced" };
|
|
29
|
+
if (shouldParallelize(mode, anyMutating, list.length)) {
|
|
30
|
+
const results = await Promise.all(list.map((thunk) => thunk()));
|
|
31
|
+
return { results, mode: "parallel", reason: "independent-reads" };
|
|
31
32
|
}
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
return { results, mode: "
|
|
33
|
+
const results = await runSerial(list);
|
|
34
|
+
if (anyMutating) return { results, mode: "serial", reason: "mutating" };
|
|
35
|
+
return { results, mode: "serial", reason: "single-or-forced" };
|
|
35
36
|
}
|
|
36
|
-
|
|
37
37
|
export async function parallel(items) {
|
|
38
38
|
const list = Array.isArray(items) ? items : [];
|
|
39
39
|
return Promise.all(list.map((item) => (item instanceof Function ? item() : item)));
|
|
40
40
|
}
|
|
41
|
-
|
|
42
41
|
export async function pipeline(items, ...stages) {
|
|
43
42
|
let current = Array.isArray(items) ? items.slice() : [];
|
|
44
43
|
for (const stage of stages) {
|
package/patch.js
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { isString } from "./decode.js";
|
|
2
|
+
|
|
3
|
+
function parseHunkHeader(line) {
|
|
4
|
+
const match = /^@@\s+-(\d+)(?:,(\d+))?\s+\+(\d+)(?:,(\d+))?\s+@@/.exec(line);
|
|
5
|
+
if (!match) return null;
|
|
6
|
+
return {
|
|
7
|
+
oldStart: parseInt(match[1], 10),
|
|
8
|
+
oldLength: match[2] !== undefined ? parseInt(match[2], 10) : 1,
|
|
9
|
+
newStart: parseInt(match[3], 10),
|
|
10
|
+
newLength: match[4] !== undefined ? parseInt(match[4], 10) : 1,
|
|
11
|
+
lines: [],
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function isHunkLine(line) {
|
|
16
|
+
return line.startsWith("+") || line.startsWith("-") || line.startsWith(" ");
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function parsePatchHunks(patchText) {
|
|
20
|
+
const patchLines = patchText.replace(/\r\n/g, "\n").split("\n");
|
|
21
|
+
const hunks = [];
|
|
22
|
+
let current = null;
|
|
23
|
+
|
|
24
|
+
for (const line of patchLines) {
|
|
25
|
+
const header = parseHunkHeader(line);
|
|
26
|
+
if (header) {
|
|
27
|
+
if (current) hunks.push(current);
|
|
28
|
+
current = header;
|
|
29
|
+
} else if (current && isHunkLine(line)) {
|
|
30
|
+
current.lines.push(line);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
if (current) hunks.push(current);
|
|
34
|
+
if (hunks.length === 0) {
|
|
35
|
+
throw new Error("no valid patch hunks found (expected @@ -old,len +new,len @@)");
|
|
36
|
+
}
|
|
37
|
+
return hunks;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function findHunkMatch(fileLines, expectedOld, nominal) {
|
|
41
|
+
const matchAt = (idx) => {
|
|
42
|
+
if (idx < 0 || idx + expectedOld.length > fileLines.length) return false;
|
|
43
|
+
for (let j = 0; j < expectedOld.length; j++) {
|
|
44
|
+
if (fileLines[idx + j] !== expectedOld[j]) return false;
|
|
45
|
+
}
|
|
46
|
+
return true;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
if (matchAt(nominal)) return nominal;
|
|
50
|
+
const maxDelta = Math.max(fileLines.length, 100);
|
|
51
|
+
for (let delta = 1; delta <= maxDelta; delta++) {
|
|
52
|
+
if (matchAt(nominal + delta)) return nominal + delta;
|
|
53
|
+
if (matchAt(nominal - delta)) return nominal - delta;
|
|
54
|
+
}
|
|
55
|
+
return -1;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function splitHunkLines(hunk) {
|
|
59
|
+
const expectedOld = [];
|
|
60
|
+
const newLines = [];
|
|
61
|
+
for (const hLine of hunk.lines) {
|
|
62
|
+
if (hLine.startsWith("-")) {
|
|
63
|
+
expectedOld.push(hLine.slice(1));
|
|
64
|
+
} else if (hLine.startsWith("+")) {
|
|
65
|
+
newLines.push(hLine.slice(1));
|
|
66
|
+
} else {
|
|
67
|
+
const val = hLine.startsWith(" ") ? hLine.slice(1) : "";
|
|
68
|
+
expectedOld.push(val);
|
|
69
|
+
newLines.push(val);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return { expectedOld, newLines };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function applyPatchToText(originalText, patchText) {
|
|
76
|
+
if (!isString(patchText) || !patchText.trim()) {
|
|
77
|
+
throw new Error("apply_patch requires non-empty patch");
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const hunks = parsePatchHunks(patchText);
|
|
81
|
+
let fileLines = originalText.replace(/\r\n/g, "\n").split("\n");
|
|
82
|
+
const hasTrailingNewline = originalText.endsWith("\n");
|
|
83
|
+
let offsetShift = 0;
|
|
84
|
+
|
|
85
|
+
for (let h = 0; h < hunks.length; h++) {
|
|
86
|
+
const hunk = hunks[h];
|
|
87
|
+
const { expectedOld, newLines } = splitHunkLines(hunk);
|
|
88
|
+
|
|
89
|
+
if (expectedOld.length !== hunk.oldLength || newLines.length !== hunk.newLength) {
|
|
90
|
+
throw new Error(`patch hunk ${h + 1} length does not match its header`);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const nominal = Math.max(0, hunk.oldStart - 1 + offsetShift);
|
|
94
|
+
const matchIdx = findHunkMatch(fileLines, expectedOld, nominal);
|
|
95
|
+
if (matchIdx === -1) {
|
|
96
|
+
throw new Error(`patch hunk ${h + 1} rejected at line ${hunk.oldStart}: context did not match`);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
fileLines.splice(matchIdx, expectedOld.length, ...newLines);
|
|
100
|
+
offsetShift += (matchIdx - nominal) + (newLines.length - expectedOld.length);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
let resultText = fileLines.join("\n");
|
|
104
|
+
if (hasTrailingNewline && !resultText.endsWith("\n")) resultText += "\n";
|
|
105
|
+
return { resultText, hunkCount: hunks.length };
|
|
106
|
+
}
|