pi-supernova 0.10.2 → 0.11.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/README.md +365 -90
- package/docs/CHANGELOG.md +89 -5
- package/docs/TOKEN_COSTS.md +149 -324
- package/index.js +40 -11
- package/package.json +4 -3
- package/src/adapters/bash.js +40 -10
- package/src/adapters/edit.js +77 -23
- package/src/adapters/errors.js +4 -0
- package/src/adapters/index.js +1 -0
- package/src/adapters/list.js +6 -3
- package/src/adapters/read-focus.js +6 -0
- package/src/adapters/read-image.js +17 -9
- package/src/adapters/read-indexed.js +72 -0
- package/src/adapters/read-text.js +19 -2
- package/src/adapters/read.js +57 -13
- package/src/adapters/refs.js +17 -7
- package/src/adapters/write.js +14 -21
- package/src/bridge/host-bridge.js +152 -39
- package/src/bridge/invoke.js +3 -1
- package/src/bridge/tool-registry.js +132 -16
- package/src/context/evidence-graph.js +1 -0
- package/src/context/evidence-rank.js +1 -0
- package/src/context/evidence.js +4 -2
- package/src/context/fuzzy.js +31 -8
- package/src/context/outline.js +33 -9
- package/src/context/query.js +7 -2
- package/src/context/repo-index.js +8 -3
- package/src/context/search-files.js +20 -0
- package/src/context/search.js +25 -10
- package/src/context/snap-search.js +28 -6
- package/src/context/snap.js +8 -3
- package/src/context/source-entry.js +64 -28
- package/src/context/spans.js +1 -0
- package/src/contract/bash.js +16 -7
- package/src/contract/edit.js +26 -7
- package/src/contract/program.js +2 -1
- package/src/contract/read.js +46 -46
- package/src/fs/background.js +10 -2
- package/src/fs/check.js +844 -46
- package/src/fs/commit.js +89 -15
- package/src/fs/diff.js +12 -9
- package/src/fs/directory.js +14 -4
- package/src/fs/file-io.js +28 -7
- package/src/fs/glob.js +93 -11
- package/src/fs/json-read.js +3 -2
- package/src/fs/json-size.js +2 -1
- package/src/fs/lines.js +49 -2
- package/src/fs/patch.js +26 -10
- package/src/fs/process-tree.js +19 -1
- package/src/fs/read-window.js +36 -20
- package/src/fs/source-window.js +5 -10
- package/src/fs/text-ops.js +38 -18
- package/src/fs/vfs.js +296 -41
- package/src/fs/workspace.js +24 -6
- package/src/output/bottleneck.js +50 -25
- package/src/output/final.js +20 -0
- package/src/output/format.js +97 -36
- package/src/output/outcome.js +16 -4
- package/src/runtime/batch-input.js +17 -3
- package/src/runtime/guest-api.js +32 -7
- package/src/runtime/guest-worker.js +22 -4
- package/src/runtime/parallel.js +9 -49
- package/src/runtime/program-batch.js +53 -28
- package/src/runtime/program-file.js +2 -1
- package/src/runtime/program.js +94 -41
- package/src/runtime/reference.js +10 -9
- package/src/runtime/runtime.js +69 -18
- package/src/runtime/worker-pool.js +10 -1
- package/src/shared/decode.js +9 -8
- package/src/shared/image-worker.js +7 -0
- package/src/shared/image.js +58 -15
- package/src/shared/png.js +18 -0
- package/src/shared/result.js +30 -9
- package/src/shared/syntax-context.js +7 -1
- package/src/shared/utf8.js +11 -13
- package/src/ui/host-render.js +4 -0
- package/src/ui/omp-frame.js +2 -0
- package/src/ui/progress.js +2 -1
- package/src/ui/render-measure.js +11 -0
- package/src/ui/render.js +92 -31
- package/src/ui/trace.js +8 -5
package/index.js
CHANGED
|
@@ -10,6 +10,7 @@ import { REFERENCE } from "./src/runtime/reference.js";
|
|
|
10
10
|
import { errorMessage, isString } from "./src/shared/decode.js";
|
|
11
11
|
import { loadConfig } from "./src/config/config.js";
|
|
12
12
|
import { createHostBridge } from "./src/bridge/host-bridge.js";
|
|
13
|
+
import { NATIVE_NAMES } from "./src/bridge/native-tools.js";
|
|
13
14
|
|
|
14
15
|
import { runGuestProgram, warmGuestWorker, stopWarmGuestWorker } from "./src/runtime/runtime.js";
|
|
15
16
|
import { renderSupernovaCall, renderSupernovaResult } from "./src/ui/render.js";
|
|
@@ -65,14 +66,12 @@ export function registerCodeMode(pi) {
|
|
|
65
66
|
function makeNovaApi(runBridge, cancel) {
|
|
66
67
|
return {
|
|
67
68
|
call: (name, args, onItem) => runBridge.call(name, args, onItem),
|
|
68
|
-
callMany: (calls) => runBridge.callMany(calls),
|
|
69
69
|
speculateBegin: () => runBridge.barrier(() => runBridge.beginSpeculation()),
|
|
70
70
|
speculateCommit: () => runBridge.barrier(() => runBridge.commitSpeculation()),
|
|
71
71
|
speculateRollback: () => runBridge.barrier(() => runBridge.rollbackSpeculation()),
|
|
72
|
-
names: () =>
|
|
72
|
+
names: () => NATIVE_NAMES,
|
|
73
73
|
describeMemory: () => runBridge.describeMemory?.() ?? null,
|
|
74
74
|
batchRead: runBridge.supportsBatchRead(),
|
|
75
|
-
nativeArgv: runBridge.supportsNativeArgv?.() === true,
|
|
76
75
|
cancel,
|
|
77
76
|
};
|
|
78
77
|
}
|
|
@@ -102,16 +101,18 @@ export function registerCodeMode(pi) {
|
|
|
102
101
|
}
|
|
103
102
|
|
|
104
103
|
async function runAndCommit(params, runCwd, runBridge, abortRun, runController, budget) {
|
|
104
|
+
const started = performance.now();
|
|
105
|
+
const timeoutMs = params?.timeoutMs === undefined ? config.timeoutMs : Number(params.timeoutMs);
|
|
105
106
|
refreshCatalog(runBridge);
|
|
106
107
|
runBridge.beginSpeculation();
|
|
107
108
|
|
|
108
|
-
|
|
109
|
+
let outcome = await runGuestProgram({
|
|
109
110
|
code: params?.code,
|
|
110
111
|
file: params?.file,
|
|
111
112
|
cwd: runCwd,
|
|
112
113
|
data: params?.data,
|
|
113
114
|
nova: makeNovaApi(runBridge, abortRun),
|
|
114
|
-
config: { ...config, maxLogLines: Math.max(0,config.maxLogLines-(budget?.logLines ?? 0)), timeoutMs
|
|
115
|
+
config: { ...config, maxLogLines: Math.max(0,config.maxLogLines-(budget?.logLines ?? 0)), timeoutMs },
|
|
115
116
|
signal: runController.signal,
|
|
116
117
|
onTimeout: abortRun,
|
|
117
118
|
});
|
|
@@ -119,14 +120,35 @@ export function registerCodeMode(pi) {
|
|
|
119
120
|
runBridge.close();
|
|
120
121
|
|
|
121
122
|
if (outcome.ok) {
|
|
122
|
-
|
|
123
|
-
|
|
123
|
+
try {
|
|
124
|
+
if (runBridge.getOverlayDepth() !== 1) throw new Error("program ended with an unfinished edit checkpoint; await it before returning");
|
|
125
|
+
await commitBeforeDeadline(runBridge, runController, timeoutMs, started);
|
|
126
|
+
} catch (error) {
|
|
127
|
+
abortRun();
|
|
128
|
+
outcome = {ok:false,error:errorMessage(error),logs:outcome.logs,logTruncated:outcome.logTruncated};
|
|
129
|
+
}
|
|
124
130
|
}
|
|
125
|
-
|
|
131
|
+
|
|
132
|
+
if (!outcome.ok) while (runBridge.getOverlayDepth()) runBridge.rollbackSpeculation();
|
|
133
|
+
outcome.wallMs = Math.round(performance.now() - started);
|
|
126
134
|
|
|
127
135
|
return outcome;
|
|
128
136
|
}
|
|
129
137
|
|
|
138
|
+
async function commitBeforeDeadline(runBridge, controller, requestedTimeout, started) {
|
|
139
|
+
// The guest has finished, but its public invocation still owns the queued
|
|
140
|
+
// and on-disk commit. Carry the same admitted timeout through this phase.
|
|
141
|
+
const timeout = Math.max(1, Math.min(2147483647, Math.floor(requestedTimeout)));
|
|
142
|
+
const remaining = timeout - (performance.now() - started);
|
|
143
|
+
const expire = () => controller.abort(new Error("program timed out after " + Math.round(performance.now()-started) + "ms during final commit (timeoutMs=" + timeout + ")"));
|
|
144
|
+
const timer = remaining > 0 ? setTimeout(expire, remaining) : undefined;
|
|
145
|
+
|
|
146
|
+
if (remaining <= 0) expire();
|
|
147
|
+
|
|
148
|
+
try { await runBridge.commitSpeculation(); }
|
|
149
|
+
finally { clearTimeout(timer); }
|
|
150
|
+
}
|
|
151
|
+
|
|
130
152
|
function scheduleWarm(runController) {
|
|
131
153
|
cancelWarmTimer();
|
|
132
154
|
|
|
@@ -160,9 +182,9 @@ export function registerCodeMode(pi) {
|
|
|
160
182
|
const visible = runBridge.ledger.dedupe(bounded, call);
|
|
161
183
|
|
|
162
184
|
const response = result(visible, {
|
|
163
|
-
ok: outcome.ok, error: outcome.error, wallMs: outcome.wallMs,
|
|
185
|
+
ok: outcome.ok, partial: !outcome.ok && outcome.mutations.committed > 0, error: outcome.error, wallMs: outcome.wallMs,
|
|
164
186
|
returnTruncated: outcome.returnTruncated, logTruncated: outcome.logTruncated,
|
|
165
|
-
logs: outcome.logs, result: outcome.result, trace, mutations: outcome.mutations,
|
|
187
|
+
logs: outcome.logs, result: outcome.result, trace, mutations: outcome.mutations, savedPaths: outcome.savedPaths,
|
|
166
188
|
});
|
|
167
189
|
|
|
168
190
|
if (outcome.images?.length) response.content.push(...outcome.images);
|
|
@@ -172,6 +194,7 @@ export function registerCodeMode(pi) {
|
|
|
172
194
|
|
|
173
195
|
pi.registerTool({
|
|
174
196
|
name: "supernova",
|
|
197
|
+
exposure: "model-only",
|
|
175
198
|
label: "Supernova",
|
|
176
199
|
description: TOOL_DESCRIPTION,
|
|
177
200
|
promptSnippet: "read, write, edit, bash",
|
|
@@ -230,6 +253,12 @@ export function registerCodeMode(pi) {
|
|
|
230
253
|
},
|
|
231
254
|
});
|
|
232
255
|
|
|
256
|
+
// Pi treats resolved execute() calls as successful, ignoring result.isError.
|
|
257
|
+
// Keep partial batch results/images intact and correct the final host status.
|
|
258
|
+
pi.on("tool_result", event => {
|
|
259
|
+
if (event.toolName === "supernova" && event.details?.ok === false) return { isError: true };
|
|
260
|
+
});
|
|
261
|
+
|
|
233
262
|
// This is a pre-conversion observation, not a final-payload retention proof.
|
|
234
263
|
// Shipping seenWindow:0 must not subscribe: a no-op listener still runs on every
|
|
235
264
|
// provider context event. Opt-in windows register here.
|
|
@@ -263,7 +292,7 @@ export function registerCodeMode(pi) {
|
|
|
263
292
|
handler: async (_args, ctx) => {
|
|
264
293
|
bridge.bindCallContext(ctx);
|
|
265
294
|
refreshCatalog();
|
|
266
|
-
const commands =
|
|
295
|
+
const commands = NATIVE_NAMES.filter(bridge.isCallable);
|
|
267
296
|
|
|
268
297
|
const lines = [
|
|
269
298
|
`Supernova CodeMode: ${commands.join(", ")}`,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-supernova",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.0",
|
|
4
4
|
"description": "CodeMode for Pi and OMP: read, edit, write and bash, with transactional files, source views and shared-input program batches.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"author": "AdityaVG13",
|
|
@@ -25,7 +25,8 @@
|
|
|
25
25
|
"files": [
|
|
26
26
|
"index.js",
|
|
27
27
|
"src/",
|
|
28
|
-
"docs/",
|
|
28
|
+
"docs/CHANGELOG.md",
|
|
29
|
+
"docs/TOKEN_COSTS.md",
|
|
29
30
|
"README.md",
|
|
30
31
|
"LICENSE"
|
|
31
32
|
],
|
|
@@ -34,7 +35,7 @@
|
|
|
34
35
|
"test:hosts": "node tests/hosts/verify.mjs",
|
|
35
36
|
"measure": "node tests/efficiency/measure.mjs",
|
|
36
37
|
"test:tokens": "node tests/efficiency/tokens.mjs",
|
|
37
|
-
"prepublishOnly": "
|
|
38
|
+
"prepublishOnly": "node ../../scripts/preflight.mjs"
|
|
38
39
|
},
|
|
39
40
|
"pi": {
|
|
40
41
|
"extensions": [
|
package/src/adapters/bash.js
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import * as fs from "node:fs/promises";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import { canonicalNewPath } from "../fs/commit.js";
|
|
2
4
|
import { readResult } from "../shared/result.js";
|
|
5
|
+
import { truncateChars } from "../output/format.js";
|
|
6
|
+
|
|
7
|
+
const BASH_CAPTURE_CHARS = 2 * 1024 * 1024;
|
|
3
8
|
|
|
4
9
|
import { normalizeBash } from "../contract/bash.js";
|
|
5
10
|
import { sourceForReferences } from "../fs/source-window.js";
|
|
@@ -20,7 +25,7 @@ export function createBash(ctx) {
|
|
|
20
25
|
const command = String(params.command);
|
|
21
26
|
const literal = Array.isArray(params.args);
|
|
22
27
|
const argv = literal ? [command, ...params.args] : ["bash", "-c", command];
|
|
23
|
-
const targetCwd = await commandCwd(params, cwd);
|
|
28
|
+
const targetCwd = await commandCwd(params, cwd, vfs);
|
|
24
29
|
|
|
25
30
|
const transactionBarrier = await vfs.prepareExternalMutation("bash");
|
|
26
31
|
let res;
|
|
@@ -32,7 +37,7 @@ export function createBash(ctx) {
|
|
|
32
37
|
commandLabel: command,
|
|
33
38
|
timeoutMs: params?.timeoutMs === undefined ? config.timeoutMs : params.timeoutMs,
|
|
34
39
|
signal,
|
|
35
|
-
maxOutputChars:
|
|
40
|
+
maxOutputChars: BASH_CAPTURE_CHARS,
|
|
36
41
|
});
|
|
37
42
|
} catch (error) {
|
|
38
43
|
if (!signal?.aborted) error.message += await sourceForReferences(cwd, targetCwd, error.message, signal, ledger);
|
|
@@ -44,6 +49,8 @@ export function createBash(ctx) {
|
|
|
44
49
|
hooks.workspaceChanged();
|
|
45
50
|
}
|
|
46
51
|
|
|
52
|
+
if (res.outputTruncated) throw new Error("bash output exceeds " + BASH_CAPTURE_CHARS + " character capture limit; redirect output to a file and read it with complete:true or json");
|
|
53
|
+
|
|
47
54
|
const { stdout, stderr } = res;
|
|
48
55
|
let text = combineBashText(stdout, stderr);
|
|
49
56
|
|
|
@@ -55,11 +62,14 @@ export function createBash(ctx) {
|
|
|
55
62
|
text += await sourceForReferences(cwd, targetCwd, text, signal, ledger);
|
|
56
63
|
}
|
|
57
64
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
65
|
+
// Computation owns the complete captured value; traces/model output get
|
|
66
|
+
// only a bounded preview. Never feed a clipped JSON string to the guest.
|
|
67
|
+
const result = readResult(text, { exitCode: res.exitCode, signal: res.signal, outputTruncated: false, transactionBarrier },
|
|
68
|
+
truncateChars(text, config.maxCallResultChars, "command output").text);
|
|
69
|
+
|
|
70
|
+
result.isError = res.exitCode !== 0;
|
|
71
|
+
|
|
72
|
+
return result;
|
|
63
73
|
}
|
|
64
74
|
|
|
65
75
|
async function background(params, signal) {
|
|
@@ -71,7 +81,7 @@ export function createBash(ctx) {
|
|
|
71
81
|
|
|
72
82
|
if (params.background === true) {
|
|
73
83
|
await manager.validateStart(params, hooks.terminalGeneration);
|
|
74
|
-
targetCwd = await commandCwd(params, getCwd());
|
|
84
|
+
targetCwd = await commandCwd(params, getCwd(), vfs);
|
|
75
85
|
} else manager.validateControl(params, owner, hooks.terminalGeneration);
|
|
76
86
|
const transactionBarrier = mutating ? await vfs.prepareExternalMutation("bash") : false;
|
|
77
87
|
|
|
@@ -97,13 +107,33 @@ export function createBash(ctx) {
|
|
|
97
107
|
return { bash };
|
|
98
108
|
}
|
|
99
109
|
|
|
100
|
-
|
|
110
|
+
// Admit only directories that the pending file set will materialize. Checking
|
|
111
|
+
// before the barrier keeps invalid cwd requests from committing unrelated files.
|
|
112
|
+
async function stagedDirectory(target, vfs) {
|
|
113
|
+
const pending = await vfs.getOverlayPaths();
|
|
114
|
+
|
|
115
|
+
if (!pending.length) return false;
|
|
116
|
+
const directory = await canonicalNewPath(target);
|
|
117
|
+
let found = false;
|
|
118
|
+
|
|
119
|
+
for (const logicalPath of pending) {
|
|
120
|
+
const file = await canonicalNewPath(logicalPath);
|
|
121
|
+
|
|
122
|
+
if (file === directory) return false;
|
|
123
|
+
|
|
124
|
+
if (file.startsWith(directory + path.sep)) found = true;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return found;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
async function commandCwd(params, cwd, vfs) {
|
|
101
131
|
const target = params.cwd ? await resolveWorkspacePath(cwd, params.cwd, "bash cwd", true) : cwd;
|
|
102
132
|
|
|
103
133
|
if (params.cwd !== undefined) {
|
|
104
134
|
const stat = await fs.stat(target).catch(() => null);
|
|
105
135
|
|
|
106
|
-
if (!stat?.isDirectory()) throw new Error("bash cwd is not a directory: " + params.cwd);
|
|
136
|
+
if (!stat?.isDirectory() && (stat || !await stagedDirectory(target, vfs))) throw new Error("bash cwd is not a directory: " + params.cwd);
|
|
107
137
|
}
|
|
108
138
|
|
|
109
139
|
return target;
|
package/src/adapters/edit.js
CHANGED
|
@@ -7,17 +7,48 @@ import { applyPatchToText } from "../fs/patch.js";
|
|
|
7
7
|
import { resolveWorkspacePath, relativeSlash } from "../fs/workspace.js";
|
|
8
8
|
import { referencesForNames } from "../context/search.js";
|
|
9
9
|
import {
|
|
10
|
-
textResult, sourceLines,
|
|
11
|
-
shiftDiffLines, contentLineInfo, boundedEditDiff, QUICK_CHECK_MAX_CHARS,
|
|
10
|
+
textResult, sourceLines, applyReplacements, applyViewReplace,
|
|
11
|
+
shiftDiffLines, contentLineInfo, boundedEditDiff, QUICK_CHECK_MAX_CHARS, lineStartIndex, lineEndIndex,
|
|
12
12
|
} from "../fs/text-ops.js";
|
|
13
13
|
|
|
14
14
|
export function createEdit(ctx) {
|
|
15
15
|
const { getCwd, vfs, index, ledger } = ctx;
|
|
16
|
-
function lineAt(updated, newLines, n) {
|
|
17
|
-
if (newLines) return newLines[n - 1] ?? "";
|
|
18
|
-
const { start, end } = lineTextRange(updated, n);
|
|
19
16
|
|
|
20
|
-
|
|
17
|
+
async function editTargetPath(cwd, inputPath, command) {
|
|
18
|
+
try {
|
|
19
|
+
// Windows can report ENOENT for an oversized component instead of ENAMETOOLONG.
|
|
20
|
+
if (process.platform === "win32" && isString(inputPath) && inputPath.split(/[\\/]/).some(part => part.length > 255)) {
|
|
21
|
+
throw Object.assign(new Error("path component exceeds the filesystem limit"), { code: "ENAMETOOLONG" });
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return await resolveWorkspacePath(cwd, inputPath, command, false);
|
|
25
|
+
}
|
|
26
|
+
catch (error) {
|
|
27
|
+
if (error?.code !== "ENAMETOOLONG") throw error;
|
|
28
|
+
|
|
29
|
+
// A plain read value mistaken for a path can contain an entire source
|
|
30
|
+
// file. Keep the errno, but do not reflect that value in the diagnostic.
|
|
31
|
+
const failure = new Error(command + " path exceeds the filesystem limit; expected a filename. Ordinary read(path) returns text; use edit(path,oldText,newText), or read(path,{resolve:true}) for an editable view");
|
|
32
|
+
|
|
33
|
+
failure.code = error.code;
|
|
34
|
+
|
|
35
|
+
throw failure;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function editBlockLines(updated, newLines, start, last) {
|
|
40
|
+
if (newLines) return newLines.slice(start - 1, last);
|
|
41
|
+
const lines = [];
|
|
42
|
+
let from = lineStartIndex(updated, start);
|
|
43
|
+
|
|
44
|
+
// Consecutive preview lines share one prefix scan, even in large files.
|
|
45
|
+
for (let line = start; line <= last; line++) {
|
|
46
|
+
const end = lineEndIndex(updated, from, 1);
|
|
47
|
+
lines.push(updated.slice(from, end).replace(/\r?\n$/, ""));
|
|
48
|
+
from = end;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return lines;
|
|
21
52
|
}
|
|
22
53
|
|
|
23
54
|
function spanEditRange(span, lineCount) {
|
|
@@ -39,6 +70,7 @@ export function createEdit(ctx) {
|
|
|
39
70
|
|
|
40
71
|
if (explicit) return explicit;
|
|
41
72
|
const ranges = [];
|
|
73
|
+
|
|
42
74
|
const positions = diff.lines.filter(row => row.type !== "context")
|
|
43
75
|
.map(row => Math.min(lineCount, row.newLineNum ?? row.lineNum)).sort((a, b) => a - b);
|
|
44
76
|
|
|
@@ -53,7 +85,7 @@ export function createEdit(ctx) {
|
|
|
53
85
|
|
|
54
86
|
for (const { start, end } of ranges) {
|
|
55
87
|
const last = Math.min(end, start + perRange - 1);
|
|
56
|
-
const lines =
|
|
88
|
+
const lines = editBlockLines(updated, newLines, start, last);
|
|
57
89
|
ledger.recordOrigin(rel, start, lines);
|
|
58
90
|
blocks.push("edited " + rel + ":" + start + "-" + last + "\n" + lines.map((line, i) => String(start + i).padStart(5) + " " + line).join("\n"));
|
|
59
91
|
|
|
@@ -80,21 +112,34 @@ export function createEdit(ctx) {
|
|
|
80
112
|
return out;
|
|
81
113
|
}
|
|
82
114
|
|
|
83
|
-
function
|
|
84
|
-
|
|
85
|
-
|
|
115
|
+
function lineReader(text) {
|
|
116
|
+
let line = 1, start = 0;
|
|
117
|
+
|
|
118
|
+
// Diff rows share a forward cursor per version, not a fresh prefix scan
|
|
119
|
+
// per row or a second array of every line. Backward coordinates rewind it.
|
|
120
|
+
return number => {
|
|
121
|
+
const nextLine = Math.max(1, number);
|
|
122
|
+
|
|
123
|
+
if (nextLine < line) { line = 1; start = 0; }
|
|
86
124
|
|
|
87
|
-
|
|
125
|
+
start = lineEndIndex(text, start, nextLine - line);
|
|
126
|
+
line = nextLine;
|
|
127
|
+
const end = lineEndIndex(text, start, 1);
|
|
128
|
+
|
|
129
|
+
return text.slice(start, end).replace(/\r?\n$/, "");
|
|
130
|
+
};
|
|
88
131
|
}
|
|
89
132
|
|
|
90
|
-
function nameAtDiffLine(l, original, updated,
|
|
133
|
+
function nameAtDiffLine(l, original, updated, readOldLine, readNewLine, canMapOwners, target, spans) {
|
|
91
134
|
const number = l.type === "remove" ? l.lineNum : l.newLineNum ?? l.lineNum;
|
|
92
135
|
const source = l.type === "remove" ? original : updated;
|
|
93
|
-
const
|
|
94
|
-
const name = declaredName(
|
|
136
|
+
const readLine = l.type === "remove" ? readOldLine : readNewLine;
|
|
137
|
+
const name = declaredName(readLine(number));
|
|
95
138
|
|
|
96
139
|
if (name) return name;
|
|
140
|
+
|
|
97
141
|
if (!canMapOwners) return;
|
|
142
|
+
|
|
98
143
|
if (!spans.has(l.type)) spans.set(l.type, WorkspaceIndex.spansOf(WorkspaceIndex.fromText(target, source)));
|
|
99
144
|
|
|
100
145
|
return spans.get(l.type).find(span => span.start <= number && number <= span.end)?.name;
|
|
@@ -102,16 +147,19 @@ export function createEdit(ctx) {
|
|
|
102
147
|
|
|
103
148
|
function collectChangedNames(target, original, updated, diff) {
|
|
104
149
|
const canMapOwners = original.length <= 512 * 1024 && updated.length <= 512 * 1024;
|
|
105
|
-
const
|
|
106
|
-
const
|
|
150
|
+
const readOldLine = lineReader(original);
|
|
151
|
+
const readNewLine = lineReader(updated);
|
|
107
152
|
const names = new Set();
|
|
108
153
|
const spans = new Map();
|
|
109
154
|
|
|
110
155
|
for (const l of diff.lines) {
|
|
111
156
|
if (l.type === "context") continue;
|
|
112
|
-
const name = nameAtDiffLine(l, original, updated,
|
|
157
|
+
const name = nameAtDiffLine(l, original, updated, readOldLine, readNewLine, canMapOwners, target, spans);
|
|
158
|
+
|
|
159
|
+
// The generic declaration heuristic reads "const auto" as a binding.
|
|
160
|
+
// In C++ it is a type keyword, not a symbol to search across the repo.
|
|
161
|
+
if (name && !(name === "auto" && /\.(?:cc|cpp|h|hpp)$/i.test(target))) names.add(name);
|
|
113
162
|
|
|
114
|
-
if (name) names.add(name);
|
|
115
163
|
if (names.size >= 3) break;
|
|
116
164
|
}
|
|
117
165
|
|
|
@@ -137,8 +185,10 @@ export function createEdit(ctx) {
|
|
|
137
185
|
if (names.size === 0) return "";
|
|
138
186
|
|
|
139
187
|
try {
|
|
188
|
+
const excludePath = path.resolve(cwd, path.relative(await vfs.resolvePath(cwd), await vfs.resolvePath(target)));
|
|
189
|
+
|
|
140
190
|
const { references, incomplete } = await referencesForNames({ root: cwd, names: [...names].slice(0, 3),
|
|
141
|
-
excludePath
|
|
191
|
+
excludePath, overlayText: file => vfs.getOverlay(file), pendingPaths: await vfs.getOverlayPaths(cwd), signal });
|
|
142
192
|
|
|
143
193
|
return formatNameRefs(references, incomplete);
|
|
144
194
|
} catch (error) {
|
|
@@ -159,9 +209,11 @@ export function createEdit(ctx) {
|
|
|
159
209
|
async function applyViewEdit(cwd, target, content, params, signal) {
|
|
160
210
|
const viewText = String(params.viewText);
|
|
161
211
|
const nextText = String(params.newText);
|
|
212
|
+
|
|
162
213
|
const windowNext = isString(params.oldText)
|
|
163
214
|
? applyReplacements(target, viewText, [{ oldText: String(params.oldText), newText: nextText }]).updated
|
|
164
215
|
: nextText;
|
|
216
|
+
|
|
165
217
|
const { updated } = applyViewReplace(target, content, params.viewStart, params.viewEnd, viewText, windowNext);
|
|
166
218
|
const diffFrom = isString(params.oldText) ? String(params.oldText) : viewText;
|
|
167
219
|
const diff = shiftDiffLines(buildEditDiff(target, viewText, diffFrom, isString(params.oldText) ? nextText : windowNext), params.viewStart - 1);
|
|
@@ -172,6 +224,7 @@ export function createEdit(ctx) {
|
|
|
172
224
|
|
|
173
225
|
function diffForMatches(target, content, updated, matches) {
|
|
174
226
|
if (content.length > 512 * 1024 || updated.length > 512 * 1024) return boundedEditDiff(target, content, matches);
|
|
227
|
+
|
|
175
228
|
if (matches.length === 1) return buildEditDiff(target, content, matches[0].oldText, matches[0].newText);
|
|
176
229
|
|
|
177
230
|
return buildMultiEditDiff(target, content, matches);
|
|
@@ -179,10 +232,10 @@ export function createEdit(ctx) {
|
|
|
179
232
|
|
|
180
233
|
async function edit(params, signal) {
|
|
181
234
|
const cwd = getCwd();
|
|
182
|
-
const target = await
|
|
235
|
+
const target = await editTargetPath(cwd, params?.path, "edit");
|
|
183
236
|
|
|
184
237
|
if (signal?.aborted) throw new Error("aborted");
|
|
185
|
-
const content = await vfs.read(target, { maxBytes: 64 * 1024 * 1024 });
|
|
238
|
+
const content = await vfs.read(target, { maxBytes: 64 * 1024 * 1024, forWrite: true });
|
|
186
239
|
|
|
187
240
|
if (isNumber(params?.viewStart) && isNumber(params?.viewEnd) && isString(params?.viewText) && isString(params?.newText)) {
|
|
188
241
|
return applyViewEdit(cwd, target, content, params, signal);
|
|
@@ -210,7 +263,7 @@ export function createEdit(ctx) {
|
|
|
210
263
|
|
|
211
264
|
async function readPatchOriginal(target) {
|
|
212
265
|
try {
|
|
213
|
-
return await vfs.read(target, { maxBytes: 64 * 1024 * 1024,
|
|
266
|
+
return await vfs.read(target, { maxBytes: 64 * 1024 * 1024, forWrite: true });
|
|
214
267
|
} catch (error) {
|
|
215
268
|
if (error?.code !== "ENOENT") throw error;
|
|
216
269
|
|
|
@@ -220,7 +273,7 @@ export function createEdit(ctx) {
|
|
|
220
273
|
|
|
221
274
|
async function apply_patch(params, signal) {
|
|
222
275
|
const cwd = getCwd();
|
|
223
|
-
const target = await
|
|
276
|
+
const target = await editTargetPath(cwd, patchInputPath(params), "apply_patch");
|
|
224
277
|
|
|
225
278
|
if (!isString(params?.patch) || !params.patch.trim()) {
|
|
226
279
|
throw new Error("apply_patch requires patch");
|
|
@@ -228,6 +281,7 @@ export function createEdit(ctx) {
|
|
|
228
281
|
|
|
229
282
|
if (signal?.aborted) throw new Error("aborted");
|
|
230
283
|
const original = await readPatchOriginal(target);
|
|
284
|
+
|
|
231
285
|
if (original.length > 2 * 1024 * 1024) throw new Error("apply_patch input exceeds 2 MiB; use edit() for targeted replacements");
|
|
232
286
|
const { resultText, hunkCount, relocations } = applyPatchToText(original, params.patch);
|
|
233
287
|
const { speculative } = await vfs.write(target, resultText);
|
package/src/adapters/errors.js
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
export const IMAGE_MAX_BYTES = 20 * 1024 * 1024;
|
|
2
|
+
|
|
2
3
|
export const LARGE_FILE_BYTES = 512 * 1024;
|
|
4
|
+
|
|
3
5
|
export const TEXT_MAX_BYTES = 64 * 1024 * 1024;
|
|
4
6
|
|
|
5
7
|
export const ABOUT_TOKEN_MAX = 16;
|
|
8
|
+
|
|
6
9
|
export const IMAGE_MIME = {
|
|
7
10
|
".png": "image/png",
|
|
8
11
|
".jpg": "image/jpeg",
|
|
@@ -19,5 +22,6 @@ export function imageTooLarge(rel, size) {
|
|
|
19
22
|
export function missingFile(targetPath) {
|
|
20
23
|
const error = new Error("no such file: " + targetPath + " (locate it with read using a directory path or source question; use Promise.allSettled for optional reads to retain successful siblings)");
|
|
21
24
|
error.code = "ENOENT";
|
|
25
|
+
|
|
22
26
|
return error;
|
|
23
27
|
}
|
package/src/adapters/index.js
CHANGED
package/src/adapters/list.js
CHANGED
|
@@ -48,7 +48,7 @@ export function createList(ctx) {
|
|
|
48
48
|
if (signal?.aborted) throw new Error("aborted");
|
|
49
49
|
const searchDir = await resolveListDir(op, params, cwd);
|
|
50
50
|
const globPattern = globPatternOf(op, params);
|
|
51
|
-
const pending = vfs.getOverlayPaths();
|
|
51
|
+
const pending = await vfs.getOverlayPaths(searchDir);
|
|
52
52
|
const cached = await listFromCache(searchDir, cwd, globPattern, pending);
|
|
53
53
|
|
|
54
54
|
if (cached !== null) return cached;
|
|
@@ -72,9 +72,10 @@ export function createList(ctx) {
|
|
|
72
72
|
|
|
73
73
|
if (!pattern) throw new Error("grep requires pattern");
|
|
74
74
|
const searchPath = params?.path ? await resolveWorkspacePath(cwd, params.path, "grep", true) : cwd;
|
|
75
|
-
const indexed = await grepIndexed(index, pattern, params, searchPath, cwd, file => vfs.getOverlay(file), vfs.getOverlayPaths());
|
|
75
|
+
const indexed = await grepIndexed(index, pattern, params, searchPath, cwd, file => vfs.getOverlay(file), await vfs.getOverlayPaths(searchPath));
|
|
76
76
|
|
|
77
77
|
if (indexed !== null) return textResult(indexed, { exitCode: indexed ? 0 : 1, via: "index" });
|
|
78
|
+
|
|
78
79
|
// Large tree: real rg keeps its own output format.
|
|
79
80
|
return grepWithRg(pattern, params, searchPath, cwd, signal);
|
|
80
81
|
}
|
|
@@ -82,13 +83,15 @@ export function createList(ctx) {
|
|
|
82
83
|
async function glob(params, signal) {
|
|
83
84
|
return listFiles(params, signal, "glob", getCwd());
|
|
84
85
|
}
|
|
86
|
+
|
|
85
87
|
async function find(params, signal) {
|
|
86
88
|
return listFiles(params, signal, "find", getCwd());
|
|
87
89
|
}
|
|
90
|
+
|
|
88
91
|
async function ls(params, signal) {
|
|
89
92
|
const cwd = getCwd();
|
|
90
93
|
const dirPath = params?.path ? await resolveWorkspacePath(cwd, params.path, "ls", true) : cwd;
|
|
91
|
-
const pending = vfs.getOverlay(dirPath);
|
|
94
|
+
const pending = await vfs.getOverlay(dirPath);
|
|
92
95
|
|
|
93
96
|
if (pending !== undefined) return fileListing(dirPath, Buffer.byteLength(pending, "utf8"));
|
|
94
97
|
const stat = await fs.stat(dirPath).catch(() => null);
|
|
@@ -4,6 +4,7 @@ import {tokenizeQuery,stem} from '../context/snap.js';
|
|
|
4
4
|
import {runCommand} from '../fs/workspace.js';
|
|
5
5
|
import {lineStartIndex,lineTextRange} from '../fs/lines.js';
|
|
6
6
|
import {textResult} from '../shared/result.js';
|
|
7
|
+
|
|
7
8
|
export function createFocusedReader(vfs, readBudget) {
|
|
8
9
|
function aboutStems(about, requireStem) {
|
|
9
10
|
const tokens = tokenizeQuery(about).tokens;
|
|
@@ -29,6 +30,7 @@ export function createFocusedReader(vfs, readBudget) {
|
|
|
29
30
|
|
|
30
31
|
if (stems.some(st => row.includes(st))) {
|
|
31
32
|
hits.push(line);
|
|
33
|
+
|
|
32
34
|
if (hits.length >= 200) break;
|
|
33
35
|
}
|
|
34
36
|
|
|
@@ -53,6 +55,7 @@ export function createFocusedReader(vfs, readBudget) {
|
|
|
53
55
|
const body = overlay.slice(first, last);
|
|
54
56
|
|
|
55
57
|
if (used + body.length > budget) { truncated = true; break; }
|
|
58
|
+
|
|
56
59
|
if (out.length && from > cursor) out.push("...");
|
|
57
60
|
out.push(`// ${rel}:${from}\n${body}`);
|
|
58
61
|
used += body.length;
|
|
@@ -71,7 +74,9 @@ export function createFocusedReader(vfs, readBudget) {
|
|
|
71
74
|
const res = await runCommand(args, { cwd: path.dirname(targetPath), timeoutMs: 15000, maxOutputChars: budget, signal });
|
|
72
75
|
|
|
73
76
|
if (res.exitCode === 0 || res.exitCode === 1) await vfs.recordExpected(targetPath, observed);
|
|
77
|
+
|
|
74
78
|
if (res.exitCode === 1) return textResult("// " + rel + " · no matching text\n", { path: targetPath, outputTruncated: false, complete: false });
|
|
79
|
+
|
|
75
80
|
if (res.exitCode !== 0) throw new Error(res.stderr.trim() || `rg exited ${res.exitCode}`);
|
|
76
81
|
const marker = res.outputTruncated ? "\n[focused read truncated; narrow about or use read(path, line, count)]" : "";
|
|
77
82
|
|
|
@@ -94,5 +99,6 @@ export function createFocusedReader(vfs, readBudget) {
|
|
|
94
99
|
|
|
95
100
|
return textResult("// " + rel + " · focused staged text windows (not a complete file)\n" + out.join("\n") + marker, { path: targetPath, outputTruncated: truncated, complete: false });
|
|
96
101
|
}
|
|
102
|
+
|
|
97
103
|
return focusAbout;
|
|
98
104
|
}
|
|
@@ -2,10 +2,12 @@ import * as fs from 'node:fs/promises';
|
|
|
2
2
|
import * as path from 'node:path';
|
|
3
3
|
import {assertModelImageMime} from '../shared/decode.js';
|
|
4
4
|
import {validateImageBytes} from '../shared/image.js';
|
|
5
|
+
import {readLimitedBytes} from '../fs/file-io.js';
|
|
5
6
|
import {IMAGE_MIME,IMAGE_MAX_BYTES,imageTooLarge,missingFile} from './errors.js';
|
|
7
|
+
|
|
6
8
|
export function createImageReader(vfs) {
|
|
7
|
-
async function readImage(rel, targetPath,
|
|
8
|
-
const staged = vfs.getOverlay(targetPath);
|
|
9
|
+
async function readImage(rel, targetPath, signal) {
|
|
10
|
+
const staged = await vfs.getOverlay(targetPath);
|
|
9
11
|
|
|
10
12
|
if (staged !== undefined) {
|
|
11
13
|
const size = Buffer.byteLength(staged, "utf8");
|
|
@@ -23,16 +25,22 @@ export function createImageReader(vfs) {
|
|
|
23
25
|
throw error;
|
|
24
26
|
}
|
|
25
27
|
|
|
28
|
+
let stat, bytes;
|
|
29
|
+
|
|
26
30
|
try {
|
|
27
|
-
|
|
31
|
+
stat = await file.stat();
|
|
28
32
|
|
|
29
33
|
if (!stat.isFile()) throw new Error("image read requires a regular file: " + targetPath);
|
|
30
|
-
if (stat.size > IMAGE_MAX_BYTES) throw imageTooLarge(rel, stat.size);
|
|
31
|
-
const bytes = await file.readFile({ signal });
|
|
32
|
-
await vfs.recordExpected(targetPath, stat);
|
|
33
34
|
|
|
34
|
-
|
|
35
|
+
if (stat.size > IMAGE_MAX_BYTES) throw imageTooLarge(rel, stat.size);
|
|
36
|
+
bytes = await readLimitedBytes(file, stat, IMAGE_MAX_BYTES, "image", signal,
|
|
37
|
+
() => new Error("image read limit exceeded: " + rel + "; maximum " + IMAGE_MAX_BYTES + " bytes (20 MiB); resize or select fewer/smaller images"));
|
|
35
38
|
} finally { await file.close(); }
|
|
39
|
+
|
|
40
|
+
// Recovery must be able to replace the destination before we await it.
|
|
41
|
+
await vfs.recordExpected(targetPath, stat, bytes);
|
|
42
|
+
|
|
43
|
+
return bytes;
|
|
36
44
|
}
|
|
37
45
|
|
|
38
46
|
async function maybeImage(rel, targetPath, signal) {
|
|
@@ -40,12 +48,12 @@ export function createImageReader(vfs) {
|
|
|
40
48
|
|
|
41
49
|
if (!mime) return null;
|
|
42
50
|
assertModelImageMime(mime);
|
|
43
|
-
const bytes = await readImage(rel, targetPath,
|
|
51
|
+
const bytes = await readImage(rel, targetPath, signal);
|
|
44
52
|
|
|
45
|
-
if (bytes.length > IMAGE_MAX_BYTES) throw imageTooLarge(rel, bytes.length);
|
|
46
53
|
await validateImageBytes(bytes,mime,rel,signal);
|
|
47
54
|
|
|
48
55
|
return { content: [{ type: "image", mimeType: mime, data: bytes.toString("base64") }], details: { path: targetPath } };
|
|
49
56
|
}
|
|
57
|
+
|
|
50
58
|
return maybeImage;
|
|
51
59
|
}
|