pi-supernova 0.8.2 → 0.9.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.
Files changed (66) hide show
  1. package/README.md +188 -51
  2. package/docs/CHANGELOG.md +86 -1
  3. package/docs/TOKEN_COSTS.md +38 -0
  4. package/index.js +10 -175
  5. package/package.json +2 -1
  6. package/src/adapters/bash.js +14 -30
  7. package/src/adapters/errors.js +1 -9
  8. package/src/adapters/read-focus.js +98 -0
  9. package/src/adapters/read-image.js +51 -0
  10. package/src/adapters/read-json.js +42 -0
  11. package/src/adapters/read-text.js +71 -0
  12. package/src/adapters/read.js +66 -635
  13. package/src/bridge/catalog.js +3 -2
  14. package/src/bridge/host-bridge.js +35 -167
  15. package/src/bridge/tool-registry.js +104 -0
  16. package/src/bridge/trace.js +41 -0
  17. package/src/context/evidence-graph.js +249 -0
  18. package/src/context/evidence-rank.js +153 -0
  19. package/src/context/evidence.js +10 -424
  20. package/src/context/query.js +71 -0
  21. package/src/context/repo-index.js +8 -162
  22. package/src/context/search-files.js +19 -0
  23. package/src/context/search.js +2 -24
  24. package/src/context/snap-search.js +202 -0
  25. package/src/context/snap.js +5 -266
  26. package/src/context/source-entry.js +112 -0
  27. package/src/contract/bash.js +6 -1
  28. package/src/contract/program.js +36 -0
  29. package/src/contract/read.js +8 -53
  30. package/src/fs/check.js +1 -1
  31. package/src/fs/commit.js +161 -0
  32. package/src/fs/diff.js +11 -15
  33. package/src/fs/directory.js +79 -0
  34. package/src/fs/file-io.js +100 -0
  35. package/src/fs/glob.js +54 -0
  36. package/src/fs/json-size.js +54 -0
  37. package/src/fs/lines.js +117 -0
  38. package/src/fs/read-window.js +74 -0
  39. package/src/fs/session-resource.js +50 -0
  40. package/src/fs/text-ops.js +7 -227
  41. package/src/fs/vfs.js +5 -239
  42. package/src/fs/workspace.js +2 -1
  43. package/src/output/bottleneck.js +13 -67
  44. package/src/output/final.js +114 -0
  45. package/src/output/format.js +94 -5
  46. package/src/output/outcome.js +91 -0
  47. package/src/runtime/batch-input.js +68 -0
  48. package/src/runtime/guest-api.js +281 -0
  49. package/src/runtime/guest-worker.js +62 -333
  50. package/src/runtime/parallel.js +41 -39
  51. package/src/runtime/program-batch.js +21 -75
  52. package/src/runtime/program-file.js +3 -11
  53. package/src/runtime/program.js +141 -0
  54. package/src/runtime/reference.js +6 -5
  55. package/src/runtime/runtime.js +77 -253
  56. package/src/runtime/worker-pool.js +91 -0
  57. package/src/shared/decode.js +22 -8
  58. package/src/shared/image-worker.js +30 -0
  59. package/src/shared/image.js +78 -0
  60. package/src/shared/png.js +57 -0
  61. package/src/shared/result.js +77 -0
  62. package/src/shared/syntax-context.js +61 -3
  63. package/src/ui/host-render.js +104 -0
  64. package/src/ui/progress.js +51 -0
  65. package/src/ui/render.js +21 -421
  66. package/src/ui/trace.js +277 -0
@@ -0,0 +1,277 @@
1
+ import {stripVTControlCharacters} from 'node:util';
2
+ import {isString,isObject} from '../shared/decode.js';
3
+ import {clampLine,fitPath} from './render-measure.js';
4
+
5
+ function diffGut(theme, item) {
6
+ const [sign, color, textColor] = DIFF_STYLES.get(item.type) ?? [" ", "dim", "toolDiffContext"];
7
+ const gutter = theme.fg(color, (sign + (item.lineNum || 0)).padStart(5));
8
+ return gutter + theme.fg("borderMuted", " │ ") + theme.fg(textColor, sign + " " + cleanInlineText(item.text));
9
+ }
10
+
11
+ function formatDiffRows(diff, theme, maxShown = 6) {
12
+ if (!diff || !Array.isArray(diff.lines) || diff.lines.length === 0) return [];
13
+ const body = [];
14
+
15
+ for (const item of diff.lines.slice(0, maxShown)) body.push(diffGut(theme, item));
16
+ const displayLineCount = Number.isInteger(diff.displayLineCount) ? diff.displayLineCount : diff.lines.length;
17
+
18
+ if (displayLineCount > maxShown) {
19
+ body.push(theme.fg("dim", ` │ … ${displayLineCount - maxShown} more lines`));
20
+ }
21
+
22
+ return body;
23
+ }
24
+
25
+ function stripUnsafeControls(value) {
26
+ // Exactly the C0/DEL/C1 ranges previously filtered code point by code point.
27
+ // Native replacement avoids rebuilding every already-clean Unicode string.
28
+ // eslint-disable-next-line no-control-regex -- intentional terminal-control filtering
29
+ return value.replace(/[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f-\u009f]/g, "");
30
+ }
31
+
32
+ function cleanBlockText(value) {
33
+ const normalized = stripVTControlCharacters(String(value ?? "")).replace(/\r\n?/g, "\n");
34
+
35
+ return stripUnsafeControls(normalized).replace(/[\u061c\u200e\u200f\u202a-\u202e\u2066-\u2069]/g, "");
36
+ }
37
+
38
+ function cleanInlineText(value) {
39
+ return cleanBlockText(value).replace(/\s*\n\s*/g, " ").trim();
40
+ }
41
+
42
+ function displayOperation(tool, target, diff, ok, item) {
43
+ const rawName = cleanInlineText(tool);
44
+
45
+ if (!rawName) return null;
46
+ const normalized = rawName === "apply_patch" ? "patch" : rawName;
47
+
48
+ return { tool: normalized, target, diff, ok, ms: item?.ms, exitCode: item?.exitCode, time: item?.time, error: item?.error };
49
+ }
50
+
51
+ function batchTarget(paths) {
52
+ const names = paths.map((p) => String(p).replace(/\\/g, "/").split("/").pop());
53
+
54
+ return `${paths.length} files: ${names.join(", ")}`;
55
+ }
56
+
57
+ const OPERATION_TARGETS = [
58
+ [(item) => item?.name === "snap", (item, args) => {
59
+ const query = args.query ? `"${args.query}"` : "";
60
+
61
+ if (!args.path) return query;
62
+
63
+ return `${query} → ${args.path}`;
64
+ }],
65
+ [(item) => item?.name === "search", (item, args) => (args.query ? `"${args.query}"` : "")],
66
+ [(item, args) => Array.isArray(args.path), (item, args) => batchTarget(args.path)],
67
+ [(item, args) => args.path, (item, args) => String(args.path)],
68
+ [(item) => item?.diff?.path, (item) => String(item.diff.path)],
69
+ [(item, args) => args.target && isString(args.target), (item, args) => args.target],
70
+ [(item, args) => args.command, (item, args) => String(args.command)],
71
+ [(item, args) => args.pattern, (item, args) => String(args.pattern)],
72
+ [(item, args) => args.query, (item, args) => String(args.query)],
73
+ ];
74
+
75
+ function operationTarget(item) {
76
+ const args = item?.args || {};
77
+
78
+ for (const [predicate, formatter] of OPERATION_TARGETS) {
79
+ if (predicate(item, args)) return formatter(item, args);
80
+ }
81
+
82
+ return "";
83
+ }
84
+
85
+ function parseDiffLine(rawLine) {
86
+ const signed = /^([+-])\s*(\d+)\s?(.*)$/.exec(rawLine);
87
+
88
+ if (signed) return { type: signed[1] === "+" ? "add" : "remove", lineNum: Number(signed[2]), text: signed[3] };
89
+ const contextual = /^\s+(\d+)\s?(.*)$/.exec(rawLine);
90
+
91
+ if (contextual) return { type: "context", lineNum: Number(contextual[1]), text: contextual[2] };
92
+
93
+ return null;
94
+ }
95
+
96
+ // Text diffs are immutable, even when hosts replace trace snapshots on each frame.
97
+ // Cache only parsed data, not theme, paths or mutable result objects.
98
+ const textDiffCache = new Map();
99
+
100
+ let cachedDiffChars = 0;
101
+
102
+ const MAX_CACHED_DIFF_CHARS = 1_000_000;
103
+
104
+ function rememberDiff(diff, parsed) {
105
+ if (diff.length > MAX_CACHED_DIFF_CHARS) return parsed;
106
+
107
+ while (textDiffCache.size >= 24 || cachedDiffChars + diff.length > MAX_CACHED_DIFF_CHARS) {
108
+ const oldest = textDiffCache.keys().next().value;
109
+ textDiffCache.delete(oldest);
110
+ cachedDiffChars -= oldest.length;
111
+ }
112
+
113
+ textDiffCache.set(diff, parsed);
114
+ cachedDiffChars += diff.length;
115
+
116
+ return parsed;
117
+ }
118
+
119
+ function tallyDiffLine(parsed, counts) {
120
+ if (parsed.type === "add") counts.added += 1;
121
+ else if (parsed.type === "remove") counts.removed += 1;
122
+ }
123
+
124
+ function parseTraceDiffText(diff) {
125
+ const lines = [];
126
+ const counts = { added: 0, removed: 0, displayLineCount: 0 };
127
+
128
+ for (const rawLine of cleanBlockText(diff).split("\n")) {
129
+ const parsed = parseDiffLine(rawLine);
130
+
131
+ if (!parsed) continue;
132
+ tallyDiffLine(parsed, counts);
133
+ counts.displayLineCount++;
134
+
135
+ if (lines.length < 24) lines.push(parsed);
136
+ }
137
+
138
+ if (lines.length === 0) return undefined;
139
+
140
+ return { added: counts.added, removed: counts.removed, lines, displayLineCount: counts.displayLineCount };
141
+ }
142
+
143
+ function normalizeTraceDiff(item) {
144
+ const diff = item?.diff;
145
+
146
+ if (isObject(diff)) return diff;
147
+
148
+ if (!isString(diff) || !diff.trim()) return undefined;
149
+ const cached = textDiffCache.get(diff);
150
+
151
+ if (cached) return cached;
152
+ const parsed = parseTraceDiffText(diff);
153
+
154
+ return parsed ? rememberDiff(diff, parsed) : undefined;
155
+ }
156
+
157
+ function operationsFromTrace(trace) {
158
+ if (!Array.isArray(trace)) return [];
159
+
160
+ return trace
161
+ .map((item) => displayOperation(item?.name || "tool", operationTarget(item), normalizeTraceDiff(item), item?.ok, item))
162
+ .filter(Boolean);
163
+ }
164
+
165
+ const TOOL_COL = 7;
166
+
167
+ const DURATION_COL = 6;
168
+
169
+ function formatDuration(ms) {
170
+ if (!Number.isFinite(ms) || ms < 0) return "";
171
+
172
+ if (ms < 1000) return `${Math.round(ms)}ms`;
173
+
174
+ if (ms < 60000) return `${(ms / 1000).toFixed(1)}s`;
175
+ const minutes = Math.floor(ms / 60000);
176
+ const seconds = Math.round((ms % 60000) / 1000);
177
+
178
+ return `${minutes}m${String(seconds).padStart(2, "0")}s`;
179
+ }
180
+
181
+ /** First meaningful line of a shell command plus a count of the hidden remainder. */
182
+ function summarizeCommand(raw) {
183
+ const lines = cleanBlockText(raw).split("\n").map((line) => line.trim()).filter(Boolean);
184
+
185
+ if (lines.length === 0) return "";
186
+ const first = lines[0].replace(/\s+/g, " ");
187
+
188
+ return lines.length > 1 ? `${first} …+${lines.length - 1} lines` : first;
189
+ }
190
+
191
+ const SHELL_TOOLS = ["bash", "exec"];
192
+
193
+ const SEARCH_TOOLS = ["snap", "search"];
194
+
195
+ function formatTarget(op, budget) {
196
+ if (SHELL_TOOLS.includes(op.tool)) return clampLine(summarizeCommand(op.target), budget);
197
+ const text = cleanInlineText(op.target);
198
+
199
+ if (!text) return "";
200
+
201
+ if (SEARCH_TOOLS.includes(op.tool)) return clampLine(text, budget);
202
+
203
+ return fitPath(text, budget);
204
+ }
205
+
206
+ function opMarker(theme, op, isPartial, isError) {
207
+ const status = [
208
+ [op.ok === false, "error", "×"],
209
+ [op.mutationAttempt, "warning", "·"],
210
+ [op.ok === true, "success", "✓"],
211
+ [isPartial, "dim", "·"],
212
+ [isError, "error", "×"],
213
+ [true, "success", "✓"],
214
+ ].find(([matches]) => matches);
215
+ return theme.fg(status[1], status[2]);
216
+ }
217
+
218
+ function opDuration(op, isPartial) {
219
+ if (Number.isFinite(op.ms)) return formatDuration(op.ms);
220
+
221
+ if (isPartial && op.ok === undefined && Number.isFinite(op.time)) return formatDuration(Date.now() - op.time) + "…";
222
+
223
+ return "";
224
+ }
225
+
226
+ function appendExit(theme, op) {
227
+ if (!Number.isInteger(op.exitCode)) return { text: "", width: 0 };
228
+ const exit = `exit ${op.exitCode}`;
229
+
230
+ return { text: theme.fg("error", exit) + " ", width: exit.length + 2 };
231
+ }
232
+
233
+ function appendDiffCounts(theme, op) {
234
+ if (!(op.diff && isObject(op.diff))) return { text: "", width: 0 };
235
+ const added = `+${op.diff.added || 0}`;
236
+ const removed = `-${op.diff.removed || 0}`;
237
+
238
+ return {
239
+ text: theme.fg("toolDiffAdded", added) + theme.fg("dim", "/") + theme.fg("toolDiffRemoved", removed) + " ",
240
+ width: added.length + 1 + removed.length + 1,
241
+ };
242
+ }
243
+
244
+ function opRowSuffix(theme, op, prefix, budget) {
245
+ const target = formatTarget(op, budget);
246
+
247
+ if (target) return prefix + theme.fg("muted", target);
248
+
249
+ if (op.ok === false && op.error) return prefix + theme.fg("error", clampLine(cleanInlineText(op.error), budget));
250
+
251
+ return prefix.trimEnd();
252
+ }
253
+
254
+ /**
255
+ * One aligned row: marker · tool · duration · [exit N] · [+a/-r] · target.
256
+ * Fixed columns keep a ledger of mixed calls scannable at a glance.
257
+ */
258
+ function formatOpRow(theme, op, width, isPartial, isError) {
259
+ const marker = opMarker(theme, op, isPartial, isError);
260
+ const toolText = op.tool.slice(0, TOOL_COL).padEnd(TOOL_COL);
261
+ const tool = theme.fg("syntaxFunction", toolText);
262
+ const durationText = opDuration(op, isPartial).slice(-DURATION_COL);
263
+ const duration = theme.fg("dim", durationText.padStart(DURATION_COL));
264
+ const exit = appendExit(theme, op);
265
+ const counts = appendDiffCounts(theme, op);
266
+ const outcome = op.mutationAttempt ? "attempted " : "";
267
+ const prefix = `${marker} ${tool} ${duration} ` + exit.text + counts.text + theme.fg("warning", outcome);
268
+ const used = 2 + toolText.length + 1 + DURATION_COL + 2 + exit.width + counts.width + outcome.length;
269
+
270
+ return opRowSuffix(theme, op, prefix, Math.max(1, width - used));
271
+ }
272
+ export { formatDiffRows, operationsFromTrace, formatDuration, formatOpRow, cleanBlockText };
273
+
274
+ const DIFF_STYLES = new Map([
275
+ ["remove", ["-", "toolDiffRemoved", "toolDiffRemoved"]],
276
+ ["add", ["+", "toolDiffAdded", "toolDiffAdded"]],
277
+ ]);