pi-supernova 0.0.11 → 0.0.15
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 -3
- package/README.md +33 -13
- package/bottleneck.js +1 -1
- package/catalog.js +8 -7
- package/check.js +166 -0
- package/config.default.json +1 -0
- package/config.js +1 -1
- package/decode.js +0 -10
- package/evidence.js +13 -30
- package/fuzzy.js +182 -0
- package/guest-worker.js +10 -8
- package/host-bridge.js +152 -103
- package/index.js +31 -40
- package/ledger.js +178 -0
- package/omp-frame.js +7 -22
- package/outline.js +80 -0
- package/package.json +6 -1
- package/render-measure.js +2 -30
- package/render.js +32 -146
- package/repo-index.js +105 -11
- package/runtime.js +3 -20
- package/search.js +141 -0
- package/snap.js +1 -6
- package/surface.js +22 -13
- package/vfs.js +0 -5
- package/workspace.js +14 -0
package/index.js
CHANGED
|
@@ -4,16 +4,11 @@ import { buildCatalog, searchCatalog, describeTool, mergeNativeToolDefinitions }
|
|
|
4
4
|
import { loadConfig } from "./config.js";
|
|
5
5
|
import { createHostBridge } from "./host-bridge.js";
|
|
6
6
|
import { runGuestProgram, warmGuestWorker } from "./runtime.js";
|
|
7
|
-
import {
|
|
8
|
-
extractOperationsFromCode,
|
|
9
|
-
renderSupernovaCall,
|
|
10
|
-
renderSupernovaResult,
|
|
11
|
-
SafeText,
|
|
12
|
-
} from "./render.js";
|
|
7
|
+
import { renderSupernovaCall, renderSupernovaResult } from "./render.js";
|
|
13
8
|
|
|
14
|
-
export {
|
|
9
|
+
export { renderSupernovaCall, renderSupernovaResult };
|
|
15
10
|
|
|
16
|
-
// Sync only
|
|
11
|
+
// Sync only, never top-level await. Dynamic import of host/deps hung OMP plugin load.
|
|
17
12
|
const require = createRequire(import.meta.url);
|
|
18
13
|
let Type;
|
|
19
14
|
try {
|
|
@@ -71,48 +66,44 @@ function progressEmitter(onUpdate) {
|
|
|
71
66
|
return emit;
|
|
72
67
|
}
|
|
73
68
|
|
|
69
|
+
function sessionStats({ programs, returnedChars, collapsedChars, collapsedRuns }) {
|
|
70
|
+
const total = returnedChars + collapsedChars;
|
|
71
|
+
const pct = total ? Math.round((collapsedChars / total) * 100) : 0;
|
|
72
|
+
return `this session: ${programs} programs · ~${Math.round(returnedChars / 4)} tokens returned · ~${Math.round(collapsedChars / 4)} already-seen tokens not re-sent (${pct}%, ${collapsedRuns} runs)`;
|
|
73
|
+
}
|
|
74
|
+
|
|
74
75
|
function logsBlock(outcome, tail = "") {
|
|
75
76
|
return outcome.logs?.length ? `\n--- logs\n${outcome.logs.join("\n")}${tail}` : "";
|
|
76
77
|
}
|
|
77
78
|
|
|
78
|
-
function errorText(outcome) {
|
|
79
|
-
return `error ${outcome.wallMs}ms: ${outcome.error}${logsBlock(outcome)}`;
|
|
79
|
+
function errorText(outcome, call) {
|
|
80
|
+
return `error #${call} ${outcome.wallMs}ms: ${outcome.error}${logsBlock(outcome)}`;
|
|
80
81
|
}
|
|
81
82
|
|
|
82
|
-
function successText(outcome) {
|
|
83
|
+
function successText(outcome, call) {
|
|
83
84
|
const truncated = outcome.returnTruncated ? " [return truncated]" : "";
|
|
84
|
-
const hint = outcome.undefinedReturn ? " (no return statement
|
|
85
|
-
return `ok ${outcome.wallMs}ms${truncated}${logsBlock(outcome, "\n--- result")}\n${outcome.resultText}${hint}`;
|
|
86
|
-
}
|
|
87
|
-
function unwrapStructuredResult(response, operation) {
|
|
88
|
-
if (response?.ok === false) {
|
|
89
|
-
throw new Error(response.value || response.error || `${operation} failed`);
|
|
90
|
-
}
|
|
91
|
-
const value = isObject(response) && "value" in response ? response.value : response;
|
|
92
|
-
if (!isString(value)) return value;
|
|
93
|
-
try {
|
|
94
|
-
return JSON.parse(value);
|
|
95
|
-
} catch {
|
|
96
|
-
return value;
|
|
97
|
-
}
|
|
85
|
+
const hint = outcome.undefinedReturn ? " (no return statement; add \`return\` to get a value)" : "";
|
|
86
|
+
return `ok #${call} ${outcome.wallMs}ms${truncated}${logsBlock(outcome, "\n--- result")}\n${outcome.resultText}${hint}`;
|
|
98
87
|
}
|
|
99
|
-
|
|
100
88
|
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).
|
|
101
89
|
|
|
102
90
|
Globals (async):
|
|
103
|
-
read(path|paths, offset?, limit?) → text | text[]
|
|
104
|
-
write(path, text) · edit(path, oldText, newText) · patch(path, unifiedDiff)
|
|
91
|
+
read(path|paths, offset?, limit?) → text | text[] · read(path, {about}) → whole-file outline, only relevant bodies expanded
|
|
92
|
+
write(path, text) · edit(path, oldText, newText) → post-edit lines (no re-read needed) · patch(path, unifiedDiff)
|
|
105
93
|
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
|
|
94
|
+
evidence(query, {k?}) → {spans: [{path, lines, name, text}]} top-K spans that answer a question; use before read
|
|
107
95
|
snap(query, root?) → {path, line, signature, context} · surface(path) → {items: [{name, kind, line}]}
|
|
108
96
|
nova.call(name, args) → {ok, value} for any host tool · nova.callMany([{name, args}]) parallel when read-only
|
|
109
97
|
nova.search(query) → [{name, description}] · nova.describe(name) → parameters · nova.has(name) sync
|
|
110
|
-
parallel(thunks) · pipeline(items, ...stages)
|
|
98
|
+
parallel(thunks) · pipeline(items, ...stages)
|
|
99
|
+
|
|
100
|
+
Already-seen lines collapse to "⋯ N lines same as #12 · path:a–b ⋯"; read(path, a, n) re-shows them.`;
|
|
111
101
|
|
|
112
102
|
export default function piSupernova(pi) {
|
|
113
103
|
const config = loadConfig();
|
|
114
104
|
let cwd = process.cwd();
|
|
115
105
|
let catalog = [];
|
|
106
|
+
let programSeq = 0;
|
|
116
107
|
|
|
117
108
|
const bridge = createHostBridge({
|
|
118
109
|
pi,
|
|
@@ -164,12 +155,6 @@ export default function piSupernova(pi) {
|
|
|
164
155
|
const cat = catalog.length ? catalog : refreshCatalog();
|
|
165
156
|
return [...new Set([...cat.map((t) => t.name), ...bridge.executors.keys(), ...Object.keys(bridge.natives)])];
|
|
166
157
|
},
|
|
167
|
-
async surface(filePath) {
|
|
168
|
-
return unwrapStructuredResult(await bridge.call("surface", { path: filePath }), "surface");
|
|
169
|
-
},
|
|
170
|
-
async snap(query, targetPath) {
|
|
171
|
-
return unwrapStructuredResult(await bridge.call("snap", { query, path: targetPath }), "snap");
|
|
172
|
-
},
|
|
173
158
|
};
|
|
174
159
|
}
|
|
175
160
|
|
|
@@ -179,7 +164,7 @@ export default function piSupernova(pi) {
|
|
|
179
164
|
description: TOOL_DESCRIPTION,
|
|
180
165
|
promptSnippet: "Compose host tools in one JavaScript program",
|
|
181
166
|
promptGuidelines: [
|
|
182
|
-
"Use supernova for multi-step tool work: loops, filtering, parallel reads, read→edit chains. To understand code
|
|
167
|
+
"Use supernova for multi-step tool work: loops, filtering, parallel reads, read→edit chains. To understand code: evidence(question) across the repo, or read(path, {about: question}) for one file: full structure, only relevant bodies expanded. Plain read(path) only for lines you will edit. Return a compact shaped value; keep raw tool output inside the program.",
|
|
183
168
|
],
|
|
184
169
|
parameters: Type.Object({
|
|
185
170
|
code: Type.String({ description: "JavaScript program: async body or arrow function." }),
|
|
@@ -202,6 +187,8 @@ export default function piSupernova(pi) {
|
|
|
202
187
|
bridge.resetCallBudget();
|
|
203
188
|
refreshCatalog();
|
|
204
189
|
bridge.beginSpeculation();
|
|
190
|
+
const call = ++programSeq;
|
|
191
|
+
bridge.ledger.beginProgram(call);
|
|
205
192
|
const emitProgress = progressEmitter(onUpdate);
|
|
206
193
|
bridge.setCallListener((_record, allTrace) => emitProgress(allTrace));
|
|
207
194
|
emitProgress([]);
|
|
@@ -218,10 +205,10 @@ export default function piSupernova(pi) {
|
|
|
218
205
|
const trace = bridge.getTrace();
|
|
219
206
|
if (!outcome.ok) {
|
|
220
207
|
bridge.rollbackSpeculation();
|
|
221
|
-
return result(errorText(outcome), { ok: false, error: outcome.error, wallMs: outcome.wallMs, logs: outcome.logs, trace });
|
|
208
|
+
return result(bridge.ledger.dedupe(errorText(outcome, call), call), { ok: false, error: outcome.error, wallMs: outcome.wallMs, logs: outcome.logs, trace });
|
|
222
209
|
}
|
|
223
210
|
await bridge.commitSpeculation();
|
|
224
|
-
return result(successText(outcome), {
|
|
211
|
+
return result(bridge.ledger.dedupe(successText(outcome, call), call), {
|
|
225
212
|
ok: true,
|
|
226
213
|
wallMs: outcome.wallMs,
|
|
227
214
|
returnTruncated: outcome.returnTruncated,
|
|
@@ -253,6 +240,9 @@ export default function piSupernova(pi) {
|
|
|
253
240
|
|
|
254
241
|
pi.on("session_start", (_event, ctx) => {
|
|
255
242
|
if (ctx && isString(ctx.cwd) && ctx.cwd) cwd = ctx.cwd;
|
|
243
|
+
// A new session is a new model context: nothing has been seen yet.
|
|
244
|
+
bridge.ledger.reset();
|
|
245
|
+
programSeq = 0;
|
|
256
246
|
refreshCatalog();
|
|
257
247
|
warmGuestWorker(config).catch(() => {});
|
|
258
248
|
});
|
|
@@ -265,9 +255,10 @@ export default function piSupernova(pi) {
|
|
|
265
255
|
const natives = Object.keys(bridge.natives).sort();
|
|
266
256
|
const lines = [
|
|
267
257
|
`pi-supernova catalog: ${catalog.length} tools`,
|
|
268
|
-
`captured executors: ${captured.length ? captured.join(", ") : "(none yet
|
|
258
|
+
`captured executors: ${captured.length ? captured.join(", ") : "(none yet; load this package early)"}`,
|
|
269
259
|
`native adapters: ${natives.join(", ")}`,
|
|
270
260
|
`timeoutMs=${config.timeoutMs} maxCallResultChars=${config.maxCallResultChars} maxReturnChars=${config.maxReturnChars} maxBridgeCalls=${config.maxBridgeCalls} maxHeapMb=${config.maxHeapMb}`,
|
|
261
|
+
sessionStats(bridge.ledger.stats),
|
|
271
262
|
];
|
|
272
263
|
ctx.ui.notify(lines.join("\n"), "info");
|
|
273
264
|
},
|
package/ledger.js
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
// Seen-ledger: the model's context window is a memory. Nothing that already reached the
|
|
2
|
+
// model in this session is sent again verbatim. A run of identical lines (≥ MIN_RUN, with
|
|
3
|
+
// enough substantive lines) collapses to one marker that cites the earlier program and,
|
|
4
|
+
// when the lines came from a file, path:a–b, so one read(path, a, n) recovers them.
|
|
5
|
+
//
|
|
6
|
+
// This is not compression: every collapsed line already exists, verbatim, in the model's
|
|
7
|
+
// context. Changed lines are never collapsed, so a re-read after an edit shows exactly the
|
|
8
|
+
// delta. Lines the current program read with an explicit offset/limit are pinned and always
|
|
9
|
+
// shown: that is the model asking for a specific window on purpose.
|
|
10
|
+
|
|
11
|
+
const MIN_RUN = 6;
|
|
12
|
+
const MIN_SUBSTANTIVE = 4;
|
|
13
|
+
const MAX_CANDIDATES = 8;
|
|
14
|
+
const DEFAULT_WINDOW = 40;
|
|
15
|
+
const MAX_STORED_LINES = 200_000;
|
|
16
|
+
|
|
17
|
+
function hashLine(line) {
|
|
18
|
+
let h = 0x811c9dc5;
|
|
19
|
+
for (let i = 0; i < line.length; i++) {
|
|
20
|
+
h ^= line.charCodeAt(i);
|
|
21
|
+
h = Math.imul(h, 0x01000193);
|
|
22
|
+
}
|
|
23
|
+
return h >>> 0;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function substantive(line) {
|
|
27
|
+
return line.trim().length >= 8;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export class SeenLedger {
|
|
31
|
+
constructor({ window = DEFAULT_WINDOW } = {}) {
|
|
32
|
+
this.window = window;
|
|
33
|
+
this.results = new Map(); // call → { hashes: Uint32Array, lines: string[] }
|
|
34
|
+
this.occurrences = new Map(); // hash → [{ call, index }]
|
|
35
|
+
this.origins = new Map(); // hash → { path, line } (provenance recorded by the bridge)
|
|
36
|
+
this.pinned = new Set(); // "path:line" pinned by the current program
|
|
37
|
+
this.storedLines = 0;
|
|
38
|
+
this.stats = { programs: 0, returnedChars: 0, collapsedChars: 0, collapsedRuns: 0 };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
reset() {
|
|
42
|
+
this.results.clear();
|
|
43
|
+
this.occurrences.clear();
|
|
44
|
+
this.origins.clear();
|
|
45
|
+
this.pinned.clear();
|
|
46
|
+
this.storedLines = 0;
|
|
47
|
+
this.stats = { programs: 0, returnedChars: 0, collapsedChars: 0, collapsedRuns: 0 };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
beginProgram(call) {
|
|
51
|
+
this.pinned.clear();
|
|
52
|
+
this.stats.programs++;
|
|
53
|
+
for (const old of [...this.results.keys()]) {
|
|
54
|
+
if (old <= call - this.window) this.forget(old);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
forget(call) {
|
|
59
|
+
const entry = this.results.get(call);
|
|
60
|
+
if (!entry) return;
|
|
61
|
+
for (let i = 0; i < entry.hashes.length; i++) {
|
|
62
|
+
const list = this.occurrences.get(entry.hashes[i]);
|
|
63
|
+
if (!list) continue;
|
|
64
|
+
const kept = list.filter((o) => o.call !== call);
|
|
65
|
+
if (kept.length) this.occurrences.set(entry.hashes[i], kept);
|
|
66
|
+
else this.occurrences.delete(entry.hashes[i]);
|
|
67
|
+
}
|
|
68
|
+
this.storedLines -= entry.hashes.length;
|
|
69
|
+
this.results.delete(call);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Provenance for lines the bridge is about to hand to the program: file text, outlines, evidence spans. */
|
|
73
|
+
recordOrigin(path, firstLine, lines, pin = false) {
|
|
74
|
+
for (let i = 0; i < lines.length; i++) {
|
|
75
|
+
if (!substantive(lines[i])) continue;
|
|
76
|
+
this.origins.set(hashLine(lines[i]), { path, line: firstLine + i });
|
|
77
|
+
if (pin) this.pinned.add(path + ":" + (firstLine + i));
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
isPinned(hash) {
|
|
82
|
+
const o = this.origins.get(hash);
|
|
83
|
+
return o !== undefined && this.pinned.has(o.path + ":" + o.line);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** Length of the identical run between lines[i…] and earlier result `cand`, stopping at pinned lines. */
|
|
87
|
+
runLength(hashes, cand, i) {
|
|
88
|
+
const earlier = this.results.get(cand.call);
|
|
89
|
+
if (!earlier) return 0;
|
|
90
|
+
let k = 0;
|
|
91
|
+
while (i + k < hashes.length && cand.index + k < earlier.hashes.length && earlier.hashes[cand.index + k] === hashes[i + k] && !this.isPinned(hashes[i + k])) k++;
|
|
92
|
+
return k;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Longest earlier run starting at lines[i]; null when shorter than MIN_RUN or not substantive enough. */
|
|
96
|
+
longestRun(hashes, lines, i) {
|
|
97
|
+
const candidates = this.occurrences.get(hashes[i]);
|
|
98
|
+
if (!candidates) return null;
|
|
99
|
+
let best = null;
|
|
100
|
+
for (const cand of candidates.slice(-MAX_CANDIDATES)) {
|
|
101
|
+
const length = this.runLength(hashes, cand, i);
|
|
102
|
+
if (length >= MIN_RUN && (!best || length > best.length)) best = { call: cand.call, index: cand.index, length };
|
|
103
|
+
}
|
|
104
|
+
if (!best) return null;
|
|
105
|
+
const substantiveCount = lines.slice(i, i + best.length).filter(substantive).length;
|
|
106
|
+
return substantiveCount >= MIN_SUBSTANTIVE ? best : null;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** "path:a–b" when every line of the run has consecutive provenance in one file, else "". */
|
|
110
|
+
citation(hashes, i, length) {
|
|
111
|
+
const first = this.origins.get(hashes[i]);
|
|
112
|
+
if (!first) return "";
|
|
113
|
+
let expectLine = first.line;
|
|
114
|
+
for (let k = 0; k < length; k++) {
|
|
115
|
+
const o = this.origins.get(hashes[i + k]);
|
|
116
|
+
if (o) {
|
|
117
|
+
if (o.path !== first.path || o.line < expectLine) return "";
|
|
118
|
+
expectLine = o.line + 1;
|
|
119
|
+
} else {
|
|
120
|
+
expectLine++;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return first.path + ":" + first.line + "–" + (expectLine - 1);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Collapse runs already shown; returns the text to send and remembers exactly that text as call N.
|
|
128
|
+
*/
|
|
129
|
+
dedupe(text, call) {
|
|
130
|
+
const lines = text.split("\n");
|
|
131
|
+
if (lines.length < MIN_RUN) {
|
|
132
|
+
this.stats.returnedChars += text.length;
|
|
133
|
+
this.remember(call, lines);
|
|
134
|
+
return text;
|
|
135
|
+
}
|
|
136
|
+
const hashes = new Uint32Array(lines.length);
|
|
137
|
+
for (let i = 0; i < lines.length; i++) hashes[i] = hashLine(lines[i]);
|
|
138
|
+
const out = [];
|
|
139
|
+
let collapsedChars = 0;
|
|
140
|
+
for (let i = 0; i < lines.length; ) {
|
|
141
|
+
const run = substantive(lines[i]) ? this.longestRun(hashes, lines, i) : null;
|
|
142
|
+
if (!run) {
|
|
143
|
+
out.push(lines[i]);
|
|
144
|
+
i++;
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
const cite = this.citation(hashes, i, run.length);
|
|
148
|
+
out.push("⋯ " + run.length + " lines same as #" + run.call + (cite ? " · " + cite : "") + " ⋯");
|
|
149
|
+
for (let k = 0; k < run.length; k++) collapsedChars += lines[i + k].length + 1;
|
|
150
|
+
this.stats.collapsedRuns++;
|
|
151
|
+
i += run.length;
|
|
152
|
+
}
|
|
153
|
+
const sent = out.join("\n");
|
|
154
|
+
this.stats.returnedChars += sent.length;
|
|
155
|
+
this.stats.collapsedChars += collapsedChars;
|
|
156
|
+
this.remember(call, out);
|
|
157
|
+
return sent;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
remember(call, lines) {
|
|
161
|
+
if (this.storedLines + lines.length > MAX_STORED_LINES) {
|
|
162
|
+
for (const old of [...this.results.keys()].sort((a, b) => a - b)) {
|
|
163
|
+
this.forget(old);
|
|
164
|
+
if (this.storedLines + lines.length <= MAX_STORED_LINES) break;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
const hashes = new Uint32Array(lines.length);
|
|
168
|
+
for (let i = 0; i < lines.length; i++) {
|
|
169
|
+
hashes[i] = hashLine(lines[i]);
|
|
170
|
+
if (!substantive(lines[i])) continue;
|
|
171
|
+
let list = this.occurrences.get(hashes[i]);
|
|
172
|
+
if (!list) this.occurrences.set(hashes[i], (list = []));
|
|
173
|
+
list.push({ call, index: i });
|
|
174
|
+
}
|
|
175
|
+
this.results.set(call, { hashes });
|
|
176
|
+
this.storedLines += lines.length;
|
|
177
|
+
}
|
|
178
|
+
}
|
package/omp-frame.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Shared Pi/OMP rounded tool chrome for supernova.
|
|
3
3
|
*
|
|
4
|
-
* Intentionally self-contained
|
|
4
|
+
* Intentionally self-contained: never dynamic-imports `@oh-my-pi/pi-coding-agent`
|
|
5
5
|
* (that hung OMP plugin load). Portable geometry matches native edit/write cards.
|
|
6
6
|
*/
|
|
7
7
|
|
|
@@ -43,30 +43,15 @@ function borderPaint(theme, state, borderColor) {
|
|
|
43
43
|
return (text) => text;
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
-
|
|
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
|
-
}
|
|
46
|
+
const STATUS_PREFIX = { error: ["error", "✗ "], running: ["dim", "… "] };
|
|
62
47
|
|
|
63
|
-
function statusHeader(theme, { title, description, state,
|
|
64
|
-
const
|
|
48
|
+
function statusHeader(theme, { title, description, state, icon }) {
|
|
49
|
+
const resolved = icon ?? (state === "error" ? "error" : undefined);
|
|
50
|
+
const prefixSpec = STATUS_PREFIX[resolved];
|
|
51
|
+
const prefix = prefixSpec ? (theme?.fg ? theme.fg(prefixSpec[0], prefixSpec[1]) : prefixSpec[1]) : "";
|
|
65
52
|
const titleText = theme?.fg ? theme.fg("accent", title) : title;
|
|
66
53
|
const descText = description ? (theme?.fg ? theme.fg("muted", description) : description) : "";
|
|
67
|
-
|
|
68
|
-
if (!descText) return `${prefix}${titleText}`;
|
|
69
|
-
return `${prefix}${titleText}: ${descText}`;
|
|
54
|
+
return descText ? `${prefix}${titleText}: ${descText}` : `${prefix}${titleText}`;
|
|
70
55
|
}
|
|
71
56
|
|
|
72
57
|
function padLine(line, width, bgFn) {
|
package/outline.js
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { WorkspaceIndex } from "./repo-index.js";
|
|
2
|
+
import { tokenizeQuery } from "./snap.js";
|
|
3
|
+
import { stem } from "./evidence.js";
|
|
4
|
+
|
|
5
|
+
// read(path, { about }): one call, whole-file structure, only the relevant bodies expanded.
|
|
6
|
+
// L0 (names) and L1 (signatures + line ranges) for every declaration; L2 (full text) for the
|
|
7
|
+
// spans that match the question, within a character budget. The model reads a 600-line file
|
|
8
|
+
// in ~15% of its tokens and knows the exact read(path, offset, limit) to issue for anything folded.
|
|
9
|
+
|
|
10
|
+
const OUTLINE_DEFAULTS = { maxChars: 8000, maxExpanded: 6, headerLines: 30, maxRefs: 5, references: null };
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
function relevance(span, lower, stems) {
|
|
14
|
+
if (stems.length === 0) return 0;
|
|
15
|
+
const nameLower = span.name.toLowerCase();
|
|
16
|
+
let score = 0;
|
|
17
|
+
for (const s of stems) if (nameLower.includes(s)) score += 40;
|
|
18
|
+
for (let i = span.start - 1; i < span.end; i++) {
|
|
19
|
+
let hits = 0;
|
|
20
|
+
for (const s of stems) if (lower[i].includes(s)) hits++;
|
|
21
|
+
score += hits * hits * 5;
|
|
22
|
+
}
|
|
23
|
+
return score;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function chooseExpanded(spans, lower, stems, raw, opts) {
|
|
27
|
+
const scored = spans.map((s, i) => ({ i, r: relevance(s, lower, stems), chars: raw.slice(s.start - 1, s.end).join("\n").length }));
|
|
28
|
+
scored.sort((a, b) => b.r - a.r || a.i - b.i);
|
|
29
|
+
const expanded = new Set();
|
|
30
|
+
let budget = opts.maxChars;
|
|
31
|
+
// Weak-match cutoff (as fff's weak-match detector): stop once relevance falls below 40% of the best span.
|
|
32
|
+
const floor = stems.length > 0 ? Math.max(1, scored[0].r * 0.4) : 0;
|
|
33
|
+
for (const { i, r, chars } of scored) {
|
|
34
|
+
if (expanded.size >= opts.maxExpanded || r < floor) break;
|
|
35
|
+
if (chars > budget) continue;
|
|
36
|
+
expanded.add(i);
|
|
37
|
+
budget -= chars;
|
|
38
|
+
}
|
|
39
|
+
return expanded;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function foldedLine(span) {
|
|
43
|
+
const body = span.end - span.start;
|
|
44
|
+
const sig = span.signature.replace(/\s*\{\s*$/, "");
|
|
45
|
+
return String(span.start).padStart(5) + " " + sig + (body > 0 ? " … " + body + " lines" : "");
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function expandedBlock(span, raw, opts) {
|
|
49
|
+
const out = [];
|
|
50
|
+
for (let l = span.start; l <= span.end; l++) out.push(String(l).padStart(5) + " " + raw[l - 1]);
|
|
51
|
+
const refs = opts.references ? opts.references(span.name, span.start) : [];
|
|
52
|
+
// Who uses this declaration: the relation a reader would otherwise grep for next.
|
|
53
|
+
if (refs.length) out.push(" // used by: " + refs.slice(0, opts.maxRefs).join(", ") + (refs.length > opts.maxRefs ? " (+" + (refs.length - opts.maxRefs) + ")" : ""));
|
|
54
|
+
return out.join("\n");
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* @param entry index entry (text + cached lines/surface)
|
|
59
|
+
* @param about question or symbol; empty ⇒ pure skeleton (every body folded)
|
|
60
|
+
*/
|
|
61
|
+
export function outlineFile(entry, relPath, about, options = {}) {
|
|
62
|
+
const opts = { ...OUTLINE_DEFAULTS, ...options };
|
|
63
|
+
const { raw, lower } = WorkspaceIndex.linesOf(entry);
|
|
64
|
+
const lineCount = raw.length;
|
|
65
|
+
const spans = WorkspaceIndex.spansOf(entry).map((s) => ({ ...s, signature: raw[s.start - 1].trim() }));
|
|
66
|
+
if (spans.length === 0) return null; // no structure: caller falls back to plain text
|
|
67
|
+
const stems = [...new Set(tokenizeQuery(about || "").tokens.map(stem))];
|
|
68
|
+
const expanded = chooseExpanded(spans, lower, stems, raw, opts);
|
|
69
|
+
|
|
70
|
+
const parts = [];
|
|
71
|
+
const headerEnd = Math.min(spans[0].start - 1, opts.headerLines);
|
|
72
|
+
if (headerEnd > 0) {
|
|
73
|
+
const header = raw.slice(0, headerEnd).filter((l) => l.trim());
|
|
74
|
+
if (header.length) parts.push(header.map((l, i) => String(i + 1).padStart(5) + " " + l).join("\n"));
|
|
75
|
+
if (spans[0].start - 1 > opts.headerLines) parts.push(" … " + (spans[0].start - 1 - opts.headerLines) + " more header lines");
|
|
76
|
+
}
|
|
77
|
+
for (let i = 0; i < spans.length; i++) parts.push(expanded.has(i) ? expandedBlock(spans[i], raw, opts) : foldedLine(spans[i]));
|
|
78
|
+
const title = "// " + relPath + " · " + lineCount + " lines · " + spans.length + " declarations · " + expanded.size + " expanded" + (about ? " for \"" + about + "\"" : "") + " · read(path, line, count) for a folded body";
|
|
79
|
+
return { text: title + "\n" + parts.join("\n"), expanded: expanded.size, declarations: spans.length };
|
|
80
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-supernova",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.15",
|
|
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",
|
|
@@ -32,6 +32,11 @@
|
|
|
32
32
|
"guest-worker.js",
|
|
33
33
|
"repo-index.js",
|
|
34
34
|
"evidence.js",
|
|
35
|
+
"fuzzy.js",
|
|
36
|
+
"outline.js",
|
|
37
|
+
"ledger.js",
|
|
38
|
+
"check.js",
|
|
39
|
+
"search.js",
|
|
35
40
|
"format.js",
|
|
36
41
|
"patch.js",
|
|
37
42
|
"vfs.js",
|
package/render-measure.js
CHANGED
|
@@ -9,9 +9,9 @@ import { stripVTControlCharacters } from "node:util";
|
|
|
9
9
|
const ELLIPSIS = "…";
|
|
10
10
|
|
|
11
11
|
/**
|
|
12
|
-
* Visible columns
|
|
12
|
+
* Visible columns: ANSI/OSC stripped, tabs → 3 spaces.
|
|
13
13
|
* ASCII-fast; non-ASCII uses a wide-char heuristic aligned with typical terminal
|
|
14
|
-
* / pi-tui behavior (emoji & symbols like ⚡ are 2 cols
|
|
14
|
+
* / pi-tui behavior (emoji & symbols like ⚡ are 2 cols; an undercount is the 92>91 crash).
|
|
15
15
|
*/
|
|
16
16
|
export function measureWidth(text) {
|
|
17
17
|
const raw = String(text ?? "").replace(/\t/g, " ");
|
|
@@ -119,33 +119,6 @@ export function clampLine(line, width) {
|
|
|
119
119
|
return plain.slice(0, Math.max(0, w - 1)) + ELLIPSIS;
|
|
120
120
|
}
|
|
121
121
|
|
|
122
|
-
/**
|
|
123
|
-
* Wrap plain text to width, preferring breaks after `/` or space so paths stay readable.
|
|
124
|
-
* Every returned chunk is ≤ width (no ellipsis — caller clamps if needed).
|
|
125
|
-
*/
|
|
126
|
-
export function wrapPlainToWidth(plain, width) {
|
|
127
|
-
const w = Math.max(1, width | 0);
|
|
128
|
-
const text = String(plain ?? "");
|
|
129
|
-
if (text.length === 0) return [""];
|
|
130
|
-
if (measureWidth(text) <= w) return [text];
|
|
131
|
-
const lines = [];
|
|
132
|
-
let i = 0;
|
|
133
|
-
while (i < text.length) {
|
|
134
|
-
const { end, lastBreak } = takeChunk(text, i, w);
|
|
135
|
-
if (end === i) {
|
|
136
|
-
const ch = text.codePointAt(i) > 0xffff ? text.slice(i, i + 2) : text[i];
|
|
137
|
-
lines.push(hardTruncate(ch, w));
|
|
138
|
-
i += ch.length;
|
|
139
|
-
continue;
|
|
140
|
-
}
|
|
141
|
-
let cut = end;
|
|
142
|
-
if (end < text.length && lastBreak > i + Math.floor(w * 0.35)) cut = lastBreak;
|
|
143
|
-
lines.push(text.slice(i, cut));
|
|
144
|
-
i = cut;
|
|
145
|
-
}
|
|
146
|
-
return lines;
|
|
147
|
-
}
|
|
148
|
-
|
|
149
122
|
/**
|
|
150
123
|
* Fit a filesystem path into `budget` columns, keeping the basename visible.
|
|
151
124
|
*/
|
|
@@ -160,4 +133,3 @@ export function fitPath(pathText, budget) {
|
|
|
160
133
|
return hardTruncate(base, w);
|
|
161
134
|
}
|
|
162
135
|
|
|
163
|
-
export { ELLIPSIS };
|