pi-supernova 0.0.7 → 0.0.11
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 +56 -0
- package/README.md +27 -16
- package/bottleneck.js +32 -41
- package/catalog.js +72 -22
- package/config.default.json +2 -1
- package/config.js +19 -10
- package/diff.js +12 -4
- package/evidence.js +446 -0
- package/format.js +95 -0
- package/guest-worker.js +345 -0
- package/host-bridge.js +215 -463
- package/index.js +107 -97
- package/omp-frame.js +68 -51
- package/package.json +8 -1
- package/parallel.js +19 -20
- package/patch.js +106 -0
- package/render-measure.js +63 -49
- package/render.js +203 -166
- package/repo-index.js +179 -0
- package/runtime.js +266 -241
- package/snap.js +153 -151
- package/surface.js +16 -30
- package/vfs.js +144 -0
- package/workspace.js +129 -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,60 @@ try {
|
|
|
30
30
|
function result(text, details) {
|
|
31
31
|
return { content: [{ type: "text", text }], details };
|
|
32
32
|
}
|
|
33
|
+
|
|
34
|
+
const PROGRESS_FRAME_MS = 40;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Live trace updates for the card. The first update is immediate (seeds the result slot);
|
|
38
|
+
* later ones are coalesced to one host re-render per frame so a tight loop of nova.calls
|
|
39
|
+
* is not throttled by the TUI. A throwing host callback must never break the run.
|
|
40
|
+
*/
|
|
41
|
+
function progressEmitter(onUpdate) {
|
|
42
|
+
if (!isFunction(onUpdate)) return Object.assign(() => {}, { flush() {} });
|
|
43
|
+
let pending = null;
|
|
44
|
+
let timer = null;
|
|
45
|
+
const send = (trace) => {
|
|
46
|
+
try {
|
|
47
|
+
onUpdate({ content: [{ type: "text", text: "" }], details: { trace, running: true } });
|
|
48
|
+
} catch {}
|
|
49
|
+
};
|
|
50
|
+
const flush = () => {
|
|
51
|
+
timer = null;
|
|
52
|
+
if (pending === null) return;
|
|
53
|
+
const trace = pending;
|
|
54
|
+
pending = null;
|
|
55
|
+
send(trace);
|
|
56
|
+
};
|
|
57
|
+
const emit = (trace) => {
|
|
58
|
+
if (timer === null && pending === null) {
|
|
59
|
+
send(trace);
|
|
60
|
+
timer = setTimeout(flush, PROGRESS_FRAME_MS);
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
pending = trace;
|
|
64
|
+
if (timer === null) timer = setTimeout(flush, PROGRESS_FRAME_MS);
|
|
65
|
+
};
|
|
66
|
+
emit.flush = () => {
|
|
67
|
+
if (timer !== null) clearTimeout(timer);
|
|
68
|
+
pending = null;
|
|
69
|
+
timer = null;
|
|
70
|
+
};
|
|
71
|
+
return emit;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function logsBlock(outcome, tail = "") {
|
|
75
|
+
return outcome.logs?.length ? `\n--- logs\n${outcome.logs.join("\n")}${tail}` : "";
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function errorText(outcome) {
|
|
79
|
+
return `error ${outcome.wallMs}ms: ${outcome.error}${logsBlock(outcome)}`;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function successText(outcome) {
|
|
83
|
+
const truncated = outcome.returnTruncated ? " [return truncated]" : "";
|
|
84
|
+
const hint = outcome.undefinedReturn ? " (no return statement — add \`return\` to get a value)" : "";
|
|
85
|
+
return `ok ${outcome.wallMs}ms${truncated}${logsBlock(outcome, "\n--- result")}\n${outcome.resultText}${hint}`;
|
|
86
|
+
}
|
|
33
87
|
function unwrapStructuredResult(response, operation) {
|
|
34
88
|
if (response?.ok === false) {
|
|
35
89
|
throw new Error(response.value || response.error || `${operation} failed`);
|
|
@@ -43,21 +97,17 @@ function unwrapStructuredResult(response, operation) {
|
|
|
43
97
|
}
|
|
44
98
|
}
|
|
45
99
|
|
|
46
|
-
const TOOL_DESCRIPTION = `
|
|
47
|
-
|
|
48
|
-
Inside the program you get:
|
|
49
|
-
nova.search(query) — thin catalog hits (name + one-liner)
|
|
50
|
-
nova.describe(name) — full parameter summary on demand
|
|
51
|
-
nova.call(name, args) — invoke a host tool (or native adapter)
|
|
52
|
-
nova.callMany([{name,args}]) — Auto parallel wave (serial if any mutating)
|
|
53
|
-
nova.snap(query, root?) — resolve a concept to a source location
|
|
54
|
-
nova.surface(path) — structural source outline
|
|
55
|
-
nova.has(name) — test host-tool availability
|
|
56
|
-
parallel(thunks) / pipeline(items, ...stages)
|
|
100
|
+
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).
|
|
57
101
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
102
|
+
Globals (async):
|
|
103
|
+
read(path|paths, offset?, limit?) → text | text[]
|
|
104
|
+
write(path, text) · edit(path, oldText, newText) · patch(path, unifiedDiff)
|
|
105
|
+
bash(cmd, {cwd?, timeoutMs?}) → output, throws on non-zero exit · exec(cmd, argv?) quotes argv
|
|
106
|
+
evidence(query, {k?}) → {spans: [{path, lines, name, text}]} top-K spans that answer a question — use before read
|
|
107
|
+
snap(query, root?) → {path, line, signature, context} · surface(path) → {items: [{name, kind, line}]}
|
|
108
|
+
nova.call(name, args) → {ok, value} for any host tool · nova.callMany([{name, args}]) parallel when read-only
|
|
109
|
+
nova.search(query) → [{name, description}] · nova.describe(name) → parameters · nova.has(name) sync
|
|
110
|
+
parallel(thunks) · pipeline(items, ...stages)`;
|
|
61
111
|
|
|
62
112
|
export default function piSupernova(pi) {
|
|
63
113
|
const config = loadConfig();
|
|
@@ -101,16 +151,18 @@ export default function piSupernova(pi) {
|
|
|
101
151
|
async callMany(calls) {
|
|
102
152
|
return bridge.callMany(calls);
|
|
103
153
|
},
|
|
104
|
-
|
|
154
|
+
speculateBegin() {
|
|
105
155
|
bridge.beginSpeculation();
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
156
|
+
},
|
|
157
|
+
async speculateCommit() {
|
|
158
|
+
await bridge.commitSpeculation();
|
|
159
|
+
},
|
|
160
|
+
speculateRollback() {
|
|
161
|
+
bridge.rollbackSpeculation();
|
|
162
|
+
},
|
|
163
|
+
names() {
|
|
164
|
+
const cat = catalog.length ? catalog : refreshCatalog();
|
|
165
|
+
return [...new Set([...cat.map((t) => t.name), ...bridge.executors.keys(), ...Object.keys(bridge.natives)])];
|
|
114
166
|
},
|
|
115
167
|
async surface(filePath) {
|
|
116
168
|
return unwrapStructuredResult(await bridge.call("surface", { path: filePath }), "surface");
|
|
@@ -118,9 +170,6 @@ export default function piSupernova(pi) {
|
|
|
118
170
|
async snap(query, targetPath) {
|
|
119
171
|
return unwrapStructuredResult(await bridge.call("snap", { query, path: targetPath }), "snap");
|
|
120
172
|
},
|
|
121
|
-
has(name) {
|
|
122
|
-
return bridge.hasExecutor(name) || catalog.some((t) => t.name === name);
|
|
123
|
-
},
|
|
124
173
|
};
|
|
125
174
|
}
|
|
126
175
|
|
|
@@ -128,23 +177,13 @@ export default function piSupernova(pi) {
|
|
|
128
177
|
name: "supernova",
|
|
129
178
|
label: "Supernova",
|
|
130
179
|
description: TOOL_DESCRIPTION,
|
|
131
|
-
promptSnippet: "Compose
|
|
180
|
+
promptSnippet: "Compose host tools in one JavaScript program",
|
|
132
181
|
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.",
|
|
182
|
+
"Use supernova for multi-step tool work: loops, filtering, parallel reads, read→edit chains. To understand code, call evidence(question) and read only the returned spans; read whole files only to edit them. Return a compact shaped value; keep raw tool output inside the program.",
|
|
136
183
|
],
|
|
137
184
|
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
|
-
),
|
|
185
|
+
code: Type.String({ description: "JavaScript program: async body or arrow function." }),
|
|
186
|
+
timeoutMs: Type.Optional(Type.Integer({ minimum: 1000, description: "Hard timeout in ms." })),
|
|
148
187
|
}),
|
|
149
188
|
// One self-owned result frame is shared by Pi and OMP; renderCall stays empty
|
|
150
189
|
// so separate call/result slots cannot duplicate the lifecycle card.
|
|
@@ -163,74 +202,26 @@ export default function piSupernova(pi) {
|
|
|
163
202
|
bridge.resetCallBudget();
|
|
164
203
|
refreshCatalog();
|
|
165
204
|
bridge.beginSpeculation();
|
|
205
|
+
const emitProgress = progressEmitter(onUpdate);
|
|
206
|
+
bridge.setCallListener((_record, allTrace) => emitProgress(allTrace));
|
|
207
|
+
emitProgress([]);
|
|
166
208
|
|
|
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
209
|
let outcome;
|
|
194
210
|
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
|
-
};
|
|
211
|
+
outcome = await runProgram(params, runController.signal, abortRun);
|
|
209
212
|
} finally {
|
|
210
213
|
bridge.setCallListener(null);
|
|
214
|
+
emitProgress.flush();
|
|
211
215
|
signal?.removeEventListener("abort", abortRun);
|
|
212
216
|
}
|
|
213
217
|
|
|
214
218
|
const trace = bridge.getTrace();
|
|
215
219
|
if (!outcome.ok) {
|
|
216
220
|
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
|
-
});
|
|
221
|
+
return result(errorText(outcome), { ok: false, error: outcome.error, wallMs: outcome.wallMs, logs: outcome.logs, trace });
|
|
226
222
|
}
|
|
227
|
-
|
|
228
223
|
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, {
|
|
224
|
+
return result(successText(outcome), {
|
|
234
225
|
ok: true,
|
|
235
226
|
wallMs: outcome.wallMs,
|
|
236
227
|
returnTruncated: outcome.returnTruncated,
|
|
@@ -242,9 +233,28 @@ export default function piSupernova(pi) {
|
|
|
242
233
|
},
|
|
243
234
|
});
|
|
244
235
|
|
|
236
|
+
async function runProgram(params, signal, onTimeout) {
|
|
237
|
+
const runStartedAt = performance.now();
|
|
238
|
+
const runConfig = {
|
|
239
|
+
...config,
|
|
240
|
+
timeoutMs: Number.isInteger(params?.timeoutMs) ? params.timeoutMs : config.timeoutMs,
|
|
241
|
+
};
|
|
242
|
+
try {
|
|
243
|
+
return await runGuestProgram({ code: String(params?.code || ""), nova: makeNovaApi(), config: runConfig, signal, onTimeout });
|
|
244
|
+
} catch (error) {
|
|
245
|
+
return {
|
|
246
|
+
ok: false,
|
|
247
|
+
error: error instanceof Error ? error.message : String(error),
|
|
248
|
+
logs: [],
|
|
249
|
+
wallMs: Math.round(performance.now() - runStartedAt),
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
245
254
|
pi.on("session_start", (_event, ctx) => {
|
|
246
255
|
if (ctx && isString(ctx.cwd) && ctx.cwd) cwd = ctx.cwd;
|
|
247
256
|
refreshCatalog();
|
|
257
|
+
warmGuestWorker(config).catch(() => {});
|
|
248
258
|
});
|
|
249
259
|
|
|
250
260
|
pi.registerCommand("supernova", {
|
|
@@ -257,7 +267,7 @@ export default function piSupernova(pi) {
|
|
|
257
267
|
`pi-supernova catalog: ${catalog.length} tools`,
|
|
258
268
|
`captured executors: ${captured.length ? captured.join(", ") : "(none yet — load this package early)"}`,
|
|
259
269
|
`native adapters: ${natives.join(", ")}`,
|
|
260
|
-
`timeoutMs=${config.timeoutMs} maxCallResultChars=${config.maxCallResultChars} maxBridgeCalls=${config.maxBridgeCalls}`,
|
|
270
|
+
`timeoutMs=${config.timeoutMs} maxCallResultChars=${config.maxCallResultChars} maxReturnChars=${config.maxReturnChars} maxBridgeCalls=${config.maxBridgeCalls} maxHeapMb=${config.maxHeapMb}`,
|
|
261
271
|
];
|
|
262
272
|
ctx.ui.notify(lines.join("\n"), "info");
|
|
263
273
|
},
|
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.11",
|
|
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,13 @@
|
|
|
29
29
|
"bottleneck.js",
|
|
30
30
|
"parallel.js",
|
|
31
31
|
"runtime.js",
|
|
32
|
+
"guest-worker.js",
|
|
33
|
+
"repo-index.js",
|
|
34
|
+
"evidence.js",
|
|
35
|
+
"format.js",
|
|
36
|
+
"patch.js",
|
|
37
|
+
"vfs.js",
|
|
38
|
+
"workspace.js",
|
|
32
39
|
"config.js",
|
|
33
40
|
"config.default.json",
|
|
34
41
|
"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
|
+
}
|