pi-supernova 0.0.11 → 0.1.0

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
@@ -1,19 +1,14 @@
1
1
  import { createRequire } from "node:module";
2
- import { isString, isFunction, isObject } from "./decode.js";
2
+ import { isString, isFunction } 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
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,
@@ -120,56 +111,38 @@ export default function piSupernova(pi) {
120
111
  getCwd: () => cwd,
121
112
  });
122
113
 
123
- function refreshCatalog() {
124
- let tools = [];
125
- try {
126
- if (isFunction(pi.getAllTools)) {
127
- tools = pi.getAllTools() || [];
128
- }
129
- } catch {
130
- tools = [];
131
- }
132
- const discoverable = mergeNativeToolDefinitions(tools, bridge.executors.keys());
114
+ function refreshCatalog(target = bridge) {
115
+ const tools = target.refreshTools();
116
+ const discoverable = mergeNativeToolDefinitions(tools, target.externalNames())
117
+ .filter(tool => target.isCallable(tool.name))
118
+ .map(tool => {
119
+ const schema = tool.parameters;
120
+ try {
121
+ if (isFunction(schema?.toJsonSchema)) return { ...tool, parameters: schema.toJsonSchema() };
122
+ if (schema && !schema.type && pi.zod?.toJSONSchema && (schema._zod || schema._def)) {
123
+ return { ...tool, parameters: pi.zod.toJSONSchema(schema, { io: "input" }) };
124
+ }
125
+ return tool;
126
+ } catch (error) {
127
+ return { ...tool, parameters: undefined, schemaError: error.message };
128
+ }
129
+ });
133
130
  catalog = buildCatalog(discoverable, config.excludeTools || []);
134
131
  return catalog;
135
132
  }
136
133
 
137
- function makeNovaApi() {
134
+ function makeNovaApi(runBridge, runCatalog, cancel) {
138
135
  return {
139
- async search(query, limit) {
140
- const cat = catalog.length ? catalog : refreshCatalog();
141
- const lim = Number.isInteger(limit) ? limit : config.maxSearchResults;
142
- return searchCatalog(cat, query, lim);
143
- },
144
- async describe(name) {
145
- const cat = catalog.length ? catalog : refreshCatalog();
146
- return describeTool(cat, name);
147
- },
148
- async call(name, args) {
149
- return bridge.call(name, args);
150
- },
151
- async callMany(calls) {
152
- return bridge.callMany(calls);
153
- },
154
- speculateBegin() {
155
- bridge.beginSpeculation();
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)])];
166
- },
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
- },
136
+ search: async (query, limit) => searchCatalog(runCatalog, query, Number.isInteger(limit) ? limit : config.maxSearchResults),
137
+ describe: async (name) => describeTool(runCatalog, name),
138
+ call: (name, args) => runBridge.call(name, args),
139
+ callMany: (calls) => runBridge.callMany(calls),
140
+ speculateBegin: () => runBridge.beginSpeculation(),
141
+ speculateCommit: () => runBridge.commitSpeculation(),
142
+ speculateRollback: () => runBridge.rollbackSpeculation(),
143
+ names: () => runCatalog.map(tool => tool.name),
144
+ batchRead: runBridge.supportsBatchRead(),
145
+ cancel,
173
146
  };
174
147
  }
175
148
 
@@ -179,7 +152,7 @@ export default function piSupernova(pi) {
179
152
  description: TOOL_DESCRIPTION,
180
153
  promptSnippet: "Compose host tools in one JavaScript program",
181
154
  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.",
155
+ "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
156
  ],
184
157
  parameters: Type.Object({
185
158
  code: Type.String({ description: "JavaScript program: async body or arrow function." }),
@@ -192,82 +165,81 @@ export default function piSupernova(pi) {
192
165
  renderCall: renderSupernovaCall,
193
166
  renderResult: renderSupernovaResult,
194
167
  async execute(_id, params, signal, onUpdate, ctx) {
195
- if (ctx && isString(ctx.cwd) && ctx.cwd) cwd = ctx.cwd;
168
+ const runCwd = ctx?.cwd || cwd;
196
169
  const runController = new AbortController();
197
170
  const abortRun = () => runController.abort(signal?.reason);
198
171
  if (signal?.aborted) abortRun();
199
172
  else signal?.addEventListener("abort", abortRun, { once: true });
173
+ const runBridge = bridge.fork({ getCwd: () => runCwd });
174
+ runBridge.bindCallContext(ctx, runController.signal);
175
+ runBridge.resetCallBudget();
200
176
 
201
- bridge.bindCallContext(ctx, runController.signal);
202
- bridge.resetCallBudget();
203
- refreshCatalog();
204
- bridge.beginSpeculation();
177
+ const call = ++programSeq;
178
+ runBridge.ledger.beginProgram(call);
205
179
  const emitProgress = progressEmitter(onUpdate);
206
- bridge.setCallListener((_record, allTrace) => emitProgress(allTrace));
180
+ runBridge.setCallListener((_record, trace) => emitProgress(trace));
207
181
  emitProgress([]);
208
-
182
+ const started = performance.now();
209
183
  let outcome;
210
184
  try {
211
- outcome = await runProgram(params, runController.signal, abortRun);
185
+ const runCatalog = refreshCatalog(runBridge);
186
+ runBridge.beginSpeculation();
187
+ outcome = await runGuestProgram({
188
+ code: params?.code,
189
+ nova: makeNovaApi(runBridge, runCatalog, abortRun),
190
+ config: { ...config, timeoutMs: Number.isInteger(params?.timeoutMs) ? params.timeoutMs : config.timeoutMs },
191
+ signal: runController.signal,
192
+ onTimeout: abortRun,
193
+ });
194
+ runBridge.close();
195
+ if (outcome.ok) {
196
+ if (runBridge.getOverlayDepth() !== 1) throw new Error("program ended with an unfinished nova.speculate branch; await it before returning");
197
+ await runBridge.commitSpeculation();
198
+ }
199
+ else runBridge.rollbackSpeculation();
200
+ } catch (error) {
201
+ abortRun();
202
+ runBridge.close();
203
+ runBridge.rollbackSpeculation();
204
+ outcome = { ok: false, error: error instanceof Error ? error.message : String(error), logs: outcome?.logs ?? [], wallMs: Math.round(performance.now() - started) };
212
205
  } finally {
213
- bridge.setCallListener(null);
206
+ runBridge.setCallListener(null);
214
207
  emitProgress.flush();
215
208
  signal?.removeEventListener("abort", abortRun);
216
209
  }
217
-
218
- const trace = bridge.getTrace();
219
- if (!outcome.ok) {
220
- bridge.rollbackSpeculation();
221
- return result(errorText(outcome), { ok: false, error: outcome.error, wallMs: outcome.wallMs, logs: outcome.logs, trace });
222
- }
223
- await bridge.commitSpeculation();
224
- return result(successText(outcome), {
225
- ok: true,
226
- wallMs: outcome.wallMs,
227
- returnTruncated: outcome.returnTruncated,
228
- logTruncated: outcome.logTruncated,
229
- logs: outcome.logs,
230
- result: outcome.result,
231
- trace,
210
+ const trace = runBridge.getTrace();
211
+ const text = outcome.ok ? successText(outcome, call) : errorText(outcome, call);
212
+ return result(runBridge.ledger.dedupe(text, call), {
213
+ ok: outcome.ok, error: outcome.error, wallMs: outcome.wallMs,
214
+ returnTruncated: outcome.returnTruncated, logTruncated: outcome.logTruncated,
215
+ logs: outcome.logs, result: outcome.result, trace,
232
216
  });
233
217
  },
234
218
  });
235
219
 
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
-
254
220
  pi.on("session_start", (_event, ctx) => {
255
221
  if (ctx && isString(ctx.cwd) && ctx.cwd) cwd = ctx.cwd;
222
+ // A new session is a new model context: nothing has been seen yet.
223
+ bridge.bindCallContext(ctx);
224
+ bridge.ledger.reset();
225
+ programSeq = 0;
256
226
  refreshCatalog();
257
227
  warmGuestWorker(config).catch(() => {});
258
228
  });
259
229
 
260
230
  pi.registerCommand("supernova", {
261
- description: "Show pi-supernova status (catalog size, captured executors)",
231
+ description: "Show pi-supernova status (callable tools and session statistics)",
262
232
  handler: async (_args, ctx) => {
233
+ bridge.bindCallContext(ctx);
263
234
  refreshCatalog();
264
- const captured = [...bridge.executors.keys()].sort();
265
- const natives = Object.keys(bridge.natives).sort();
235
+ const external = bridge.externalNames().filter(bridge.isCallable).sort();
236
+ const natives = Object.keys(bridge.natives).filter(name => bridge.isCallable(name) && !external.includes(name)).sort();
266
237
  const lines = [
267
238
  `pi-supernova catalog: ${catalog.length} tools`,
268
- `captured executors: ${captured.length ? captured.join(", ") : "(none yet — load this package early)"}`,
239
+ `external tools: ${external.length ? external.join(", ") : "(none)"}`,
269
240
  `native adapters: ${natives.join(", ")}`,
270
241
  `timeoutMs=${config.timeoutMs} maxCallResultChars=${config.maxCallResultChars} maxReturnChars=${config.maxReturnChars} maxBridgeCalls=${config.maxBridgeCalls} maxHeapMb=${config.maxHeapMb}`,
242
+ sessionStats(bridge.ledger.stats),
271
243
  ];
272
244
  ctx.ui.notify(lines.join("\n"), "info");
273
245
  },
package/ledger.js ADDED
@@ -0,0 +1,150 @@
1
+ const MIN_RUN = 6;
2
+ const MIN_SUBSTANTIVE = 4;
3
+ const MAX_CANDIDATES = 8;
4
+ const DEFAULT_WINDOW = 40;
5
+ const MAX_STORED_LINES = 200_000;
6
+
7
+ function hashLine(line) {
8
+ let hash = 0x811c9dc5;
9
+ for (let i = 0; i < line.length; i++) hash = Math.imul(hash ^ line.charCodeAt(i), 0x01000193);
10
+ return hash >>> 0;
11
+ }
12
+ function substantive(line) { return line.trim().length >= 8; }
13
+ function newHistory() {
14
+ return { results: new Map(), occurrences: new Map(), storedLines: 0, latestCall: 0,
15
+ stats: { programs: 0, returnedChars: 0, collapsedChars: 0, collapsedRuns: 0 } };
16
+ }
17
+
18
+ export class SeenLedger {
19
+ constructor({ window = DEFAULT_WINDOW, history } = {}) {
20
+ this.window = window;
21
+ this.history = history ?? newHistory();
22
+ this.origins = new Map();
23
+ this.pinned = new Set();
24
+ }
25
+ get results() { return this.history.results; }
26
+ get occurrences() { return this.history.occurrences; }
27
+ get storedLines() { return this.history.storedLines; }
28
+ get stats() { return this.history.stats; }
29
+ fork() { return new SeenLedger({ window: this.window, history: this.history }); }
30
+
31
+ reset() {
32
+ Object.assign(this.history, newHistory());
33
+ this.origins.clear();
34
+ this.pinned.clear();
35
+ }
36
+
37
+ beginProgram(call) {
38
+ this.origins.clear();
39
+ this.pinned.clear();
40
+ this.stats.programs++;
41
+ this.history.latestCall = Math.max(this.history.latestCall, call);
42
+ for (const old of this.results.keys()) if (old <= this.history.latestCall - this.window) this.forget(old);
43
+ }
44
+
45
+ forget(call) {
46
+ const entry = this.results.get(call);
47
+ if (!entry) return;
48
+ for (const hash of new Set(entry.hashes)) {
49
+ const kept = (this.occurrences.get(hash) ?? []).filter(item => item.call !== call);
50
+ if (kept.length) this.occurrences.set(hash, kept);
51
+ else this.occurrences.delete(hash);
52
+ }
53
+ this.history.storedLines -= entry.lines.length;
54
+ this.results.delete(call);
55
+ }
56
+
57
+ recordOrigin(path, firstLine, lines, pin = false) {
58
+ if (this.window === 0) return;
59
+ for (let i = 0; i < lines.length; i++) {
60
+ const text = lines[i];
61
+ if (!substantive(text)) continue;
62
+ if (this.origins.size >= MAX_STORED_LINES && !this.origins.has(text)) this.origins.delete(this.origins.keys().next().value);
63
+ this.origins.set(text, { path, line: firstLine + i });
64
+ if (pin) this.pinned.add(text);
65
+ }
66
+ }
67
+
68
+ runLength(lines, hashes, candidate, index) {
69
+ const earlier = this.results.get(candidate.call);
70
+ if (!earlier) return 0;
71
+ let count = 0;
72
+ while (index + count < lines.length && candidate.index + count < earlier.lines.length
73
+ && hashes[index + count] === earlier.hashes[candidate.index + count]
74
+ && lines[index + count] === earlier.lines[candidate.index + count]
75
+ && !this.pinned.has(lines[index + count])) count++;
76
+ return count;
77
+ }
78
+
79
+ longestRun(lines, hashes, index, call) {
80
+ const candidates = this.occurrences.get(hashes[index]);
81
+ if (!candidates) return null;
82
+ let best = null;
83
+ for (const candidate of candidates.filter(item => item.call < call).slice(-MAX_CANDIDATES)) {
84
+ const length = this.runLength(lines, hashes, candidate, index);
85
+ if (length >= MIN_RUN && (!best || length > best.length)) best = { ...candidate, length };
86
+ }
87
+ if (!best) return null;
88
+ let count = 0;
89
+ for (let i = index; i < index + best.length; i++) if (substantive(lines[i])) count++;
90
+ return count >= MIN_SUBSTANTIVE ? best : null;
91
+ }
92
+
93
+ citation(lines, index, run) {
94
+ const earlier = this.results.get(run.call);
95
+ const origin = offset => this.origins.get(lines[index + offset]) ?? earlier?.origins[run.index + offset];
96
+ const first = origin(0);
97
+ if (!first) return "";
98
+ let expected = first.line;
99
+ for (let i = 0; i < run.length; i++) {
100
+ const item = origin(i);
101
+ if (item && (item.path !== first.path || item.line !== expected)) return "";
102
+ expected++;
103
+ }
104
+ return first.path + ":" + first.line + "–" + (expected - 1);
105
+ }
106
+
107
+ dedupe(text, call) {
108
+ if (this.window === 0) {
109
+ this.stats.returnedChars += text.length;
110
+ return text;
111
+ }
112
+ const lines = text.split("\n");
113
+ const hashes = Uint32Array.from(lines, hashLine);
114
+ const out = [];
115
+ let collapsedChars = 0;
116
+ for (let i = 0; i < lines.length;) {
117
+ const run = substantive(lines[i]) ? this.longestRun(lines, hashes, i, call) : null;
118
+ if (!run) { out.push(lines[i++]); continue; }
119
+ const cite = this.citation(lines, i, run);
120
+ out.push("⋯ " + run.length + " lines same as #" + run.call + (cite ? " · " + cite : "") + " ⋯");
121
+ for (let k = 0; k < run.length; k++) collapsedChars += lines[i + k].length + 1;
122
+ this.stats.collapsedRuns++;
123
+ i += run.length;
124
+ }
125
+ const sent = out.join("\n");
126
+ this.stats.returnedChars += sent.length;
127
+ this.stats.collapsedChars += collapsedChars;
128
+ this.remember(call, out);
129
+ return sent;
130
+ }
131
+
132
+ remember(call, lines) {
133
+ if (this.window === 0 || call <= this.history.latestCall - this.window || lines.length > MAX_STORED_LINES) return;
134
+ if (this.results.has(call)) this.forget(call);
135
+ for (const old of [...this.results.keys()].sort((a, b) => a - b)) {
136
+ if (this.storedLines + lines.length <= MAX_STORED_LINES) break;
137
+ this.forget(old);
138
+ }
139
+ const hashes = Uint32Array.from(lines, hashLine);
140
+ const origins = lines.map(line => this.origins.get(line));
141
+ for (let i = 0; i < lines.length; i++) {
142
+ if (!substantive(lines[i])) continue;
143
+ let list = this.occurrences.get(hashes[i]);
144
+ if (!list) this.occurrences.set(hashes[i], (list = []));
145
+ list.push({ call, index: i });
146
+ }
147
+ this.results.set(call, { hashes, lines, origins });
148
+ this.history.storedLines += lines.length;
149
+ }
150
+ }
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" };
46
+ const STATUS_PREFIX = { error: ["error", "✗ "], running: ["dim", "… "] };
54
47
 
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
-
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) {
@@ -142,7 +127,6 @@ function renderPortableFrame(theme, { header, sections = [], state = "pending",
142
127
  const border = borderPaint(theme, state, borderColor);
143
128
  const bgFn = bgFnForState(theme, state);
144
129
  const h = box.horizontal;
145
- const v = box.vertical;
146
130
  const cap = h.repeat(3);
147
131
 
148
132
  const paintBar = (leftChar, rightChar, label) => {
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.1.0",
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",
@@ -85,5 +90,9 @@
85
90
  "bugs": {
86
91
  "url": "https://github.com/AdityaVG13/pi-stack/issues"
87
92
  },
88
- "homepage": "https://github.com/AdityaVG13/pi-stack/tree/main/packages/pi-supernova#readme"
93
+ "homepage": "https://github.com/AdityaVG13/pi-stack/tree/main/packages/pi-supernova#readme",
94
+ "dependencies": {
95
+ "acorn": "^8.18.0",
96
+ "string-width": "^8.2.2"
97
+ }
89
98
  }