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/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 { extractOperationsFromCode, renderSupernovaCall, renderSupernovaResult, SafeText };
9
+ export { renderSupernovaCall, renderSupernovaResult };
15
10
 
16
- // Sync only — never top-level await. Dynamic import of host/deps hung OMP plugin load.
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 — add \`return\` to get a value)" : "";
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 — use before read
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, 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.",
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 — load this package early)"}`,
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 — never dynamic-imports `@oh-my-pi/pi-coding-agent`
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
- 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
- }
46
+ const STATUS_PREFIX = { error: ["error", "✗ "], running: ["dim", "… "] };
62
47
 
63
- function statusHeader(theme, { title, description, state, spinnerFrame, icon, iconOverride }) {
64
- const resolvedIcon = resolveStatusIcon({ icon, iconOverride, state });
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
- const prefix = statusPrefix(theme, resolvedIcon, iconOverride, spinnerFrame);
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.11",
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 — ANSI/OSC stripped, tabs → 3 spaces.
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 — undercount ⇒ 92>91 crash).
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 };