pi-supernova 0.8.2 → 0.9.1
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 +188 -51
- package/docs/CHANGELOG.md +86 -1
- package/docs/TOKEN_COSTS.md +38 -0
- package/index.js +10 -175
- package/package.json +2 -1
- package/src/adapters/bash.js +14 -30
- package/src/adapters/errors.js +1 -9
- package/src/adapters/read-focus.js +98 -0
- package/src/adapters/read-image.js +51 -0
- package/src/adapters/read-json.js +42 -0
- package/src/adapters/read-text.js +71 -0
- package/src/adapters/read.js +66 -635
- package/src/bridge/catalog.js +3 -2
- package/src/bridge/host-bridge.js +35 -167
- package/src/bridge/tool-registry.js +104 -0
- package/src/bridge/trace.js +41 -0
- package/src/context/evidence-graph.js +249 -0
- package/src/context/evidence-rank.js +153 -0
- package/src/context/evidence.js +10 -424
- package/src/context/fuzzy.js +116 -43
- package/src/context/query.js +80 -0
- package/src/context/repo-index.js +23 -166
- package/src/context/search-files.js +19 -0
- package/src/context/search.js +2 -24
- package/src/context/snap-search.js +203 -0
- package/src/context/snap.js +5 -266
- package/src/context/source-entry.js +112 -0
- package/src/contract/bash.js +6 -1
- package/src/contract/program.js +36 -0
- package/src/contract/read.js +8 -53
- package/src/fs/check.js +1 -1
- package/src/fs/commit.js +161 -0
- package/src/fs/diff.js +11 -15
- package/src/fs/directory.js +79 -0
- package/src/fs/file-io.js +100 -0
- package/src/fs/glob.js +54 -0
- package/src/fs/json-size.js +54 -0
- package/src/fs/lines.js +117 -0
- package/src/fs/read-window.js +74 -0
- package/src/fs/session-resource.js +50 -0
- package/src/fs/text-ops.js +7 -227
- package/src/fs/vfs.js +5 -239
- package/src/fs/workspace.js +2 -1
- package/src/output/bottleneck.js +13 -67
- package/src/output/final.js +114 -0
- package/src/output/format.js +94 -5
- package/src/output/outcome.js +91 -0
- package/src/runtime/batch-input.js +68 -0
- package/src/runtime/guest-api.js +281 -0
- package/src/runtime/guest-worker.js +62 -333
- package/src/runtime/parallel.js +41 -39
- package/src/runtime/program-batch.js +21 -75
- package/src/runtime/program-file.js +3 -11
- package/src/runtime/program.js +141 -0
- package/src/runtime/reference.js +6 -5
- package/src/runtime/runtime.js +77 -253
- package/src/runtime/worker-pool.js +91 -0
- package/src/shared/decode.js +22 -8
- package/src/shared/image-worker.js +30 -0
- package/src/shared/image.js +78 -0
- package/src/shared/png.js +57 -0
- package/src/shared/result.js +77 -0
- package/src/shared/syntax-context.js +61 -3
- package/src/ui/host-render.js +104 -0
- package/src/ui/progress.js +51 -0
- package/src/ui/render.js +21 -421
- package/src/ui/trace.js +277 -0
package/src/fs/lines.js
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import {isNumber} from '../shared/decode.js';
|
|
2
|
+
|
|
3
|
+
function totalContentLines(text) {
|
|
4
|
+
if (text === "") return 1;
|
|
5
|
+
|
|
6
|
+
return contentLineInfo(text).count + (text.endsWith("\n") ? 1 : 0);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function emptySliceInfo(text, totalLines) {
|
|
10
|
+
return { text: "", end: totalLines, total: totalLines, count: 0, eof: true, whole: totalLines === 1 && text === "" };
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function sliceWindow(text, startIndex, count, totalLines) {
|
|
14
|
+
const endExclusive = Math.min(totalLines, startIndex + count);
|
|
15
|
+
const start = lineStartIndex(text, startIndex + 1);
|
|
16
|
+
const end = lineEndIndex(text, start, endExclusive - startIndex);
|
|
17
|
+
let selected = text.slice(start, end);
|
|
18
|
+
const eof = endExclusive >= totalLines || (endExclusive === totalLines - 1 && text.endsWith("\n"));
|
|
19
|
+
|
|
20
|
+
if (endExclusive < totalLines && !selected.endsWith("\n")) selected += "\n";
|
|
21
|
+
|
|
22
|
+
return { text: selected, end: endExclusive, total: totalLines, count: endExclusive - startIndex, eof, whole: startIndex === 0 && eof };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function sliceLinesRawInfo(text, offset, limit) {
|
|
26
|
+
const totalLines = totalContentLines(text);
|
|
27
|
+
|
|
28
|
+
if (!isNumber(offset) && !isNumber(limit)) {
|
|
29
|
+
return { text, end: totalLines, total: totalLines, count: totalLines, eof: true, whole: true };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const startIndex = (isNumber(offset) ? Math.max(1, Math.floor(offset)) : 1) - 1;
|
|
33
|
+
const count = isNumber(limit) ? Math.max(0, Math.floor(limit)) : totalLines;
|
|
34
|
+
|
|
35
|
+
if (count === 0 || startIndex >= totalLines) return emptySliceInfo(text, totalLines);
|
|
36
|
+
|
|
37
|
+
return sliceWindow(text, startIndex, count, totalLines);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Read-window slicing preserves the selected lines' own line ending. */
|
|
41
|
+
export function sliceLinesRaw(text, offset, limit) {
|
|
42
|
+
return sliceLinesRawInfo(text, offset, limit).text;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function sourceLines(content) {
|
|
46
|
+
const raw = content.split("\n");
|
|
47
|
+
|
|
48
|
+
if (raw.at(-1) === "") raw.pop();
|
|
49
|
+
|
|
50
|
+
return raw;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function lineNumberAt(content, index) {
|
|
54
|
+
let line = 1;
|
|
55
|
+
|
|
56
|
+
for (let i = 0; i < index; i++) if (content.charCodeAt(i) === 10) line++;
|
|
57
|
+
|
|
58
|
+
return line;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function formatNumberedLine(n, text) {
|
|
62
|
+
return String(n).padStart(5) + " " + text;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function numberedPreview(content, cap = EDIT_PREVIEW_LINES) {
|
|
66
|
+
const { count, preview } = contentLineInfo(content, cap);
|
|
67
|
+
|
|
68
|
+
if (count === 0) return "0 lines";
|
|
69
|
+
const body = preview.map((line, i) => formatNumberedLine(i + 1, line)).join("\n");
|
|
70
|
+
const suffix = count + " lines total";
|
|
71
|
+
|
|
72
|
+
return body + "\n" + suffix;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function lineStartIndex(content, line) {
|
|
76
|
+
return lineEndIndex(content, 0, line - 1);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function lineEndIndex(content, startIndex, lineCount) {
|
|
80
|
+
let index = startIndex;
|
|
81
|
+
|
|
82
|
+
for (let i = 0; i < lineCount; i++) {
|
|
83
|
+
const next = content.indexOf("\n", index);
|
|
84
|
+
|
|
85
|
+
if (next < 0) return content.length;
|
|
86
|
+
index = next + 1;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return index;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function lineTextRange(content, line) {
|
|
93
|
+
const start = lineStartIndex(content, line);
|
|
94
|
+
|
|
95
|
+
return { start, end: lineEndIndex(content, start, 1) };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function contentLineInfo(text, previewLimit = 0) {
|
|
99
|
+
if (text === "") return { count: 0, preview: [], newlines: 0 };
|
|
100
|
+
const preview = [];
|
|
101
|
+
let count = 0;
|
|
102
|
+
let start = 0;
|
|
103
|
+
|
|
104
|
+
do {
|
|
105
|
+
const newline = text.indexOf("\n", start);
|
|
106
|
+
const end = newline < 0 ? text.length : newline;
|
|
107
|
+
|
|
108
|
+
if (preview.length < previewLimit) preview.push(text.slice(start, end).replace(/\r$/, ""));
|
|
109
|
+
count++;
|
|
110
|
+
if (newline < 0) break;
|
|
111
|
+
start = newline + 1;
|
|
112
|
+
} while (start < text.length);
|
|
113
|
+
|
|
114
|
+
return { count, preview, newlines: count - Number(!text.endsWith("\n")) };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export const EDIT_PREVIEW_LINES = 16;
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import * as fs from "node:fs/promises";
|
|
2
|
+
import { decodeUtf8Strict, decodeUtf8Window } from "../shared/utf8.js";
|
|
3
|
+
import { sliceLinesRawInfo } from "./lines.js";
|
|
4
|
+
import { fileChunks, remapReadError } from "./file-io.js";
|
|
5
|
+
|
|
6
|
+
/** Advance through newline-delimited bytes without decoding or retaining skipped data. */
|
|
7
|
+
function advanceLines(bytes, start, remaining) {
|
|
8
|
+
while (remaining > 0) {
|
|
9
|
+
const newline = bytes.indexOf(10, start);
|
|
10
|
+
if (newline < 0) return { offset: bytes.length, remaining };
|
|
11
|
+
start = newline + 1;
|
|
12
|
+
remaining--;
|
|
13
|
+
}
|
|
14
|
+
return { offset: start, remaining };
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
async function scanWindow(file, stat, startLine, lineCount, maxBytes, signal) {
|
|
18
|
+
const scan = { parts: [], collected: 0, startByte: undefined, endByte: undefined };
|
|
19
|
+
let skip = startLine - 1, take = lineCount ?? Infinity, position = 0;
|
|
20
|
+
for await (const chunk of fileChunks(file, signal, stat.size)) {
|
|
21
|
+
const head = advanceLines(chunk, 0, skip);
|
|
22
|
+
skip = head.remaining;
|
|
23
|
+
const start = position;
|
|
24
|
+
position += chunk.length;
|
|
25
|
+
if (skip) continue;
|
|
26
|
+
scan.startByte ??= start + head.offset;
|
|
27
|
+
const tail = advanceLines(chunk, head.offset, take);
|
|
28
|
+
take = tail.remaining;
|
|
29
|
+
if (take === 0) scan.endByte = start + tail.offset;
|
|
30
|
+
const end = Math.min(tail.offset, head.offset + maxBytes + 1 - scan.collected);
|
|
31
|
+
if (end > head.offset) {
|
|
32
|
+
scan.parts.push(Buffer.from(chunk.subarray(head.offset, end)));
|
|
33
|
+
scan.collected += end - head.offset;
|
|
34
|
+
}
|
|
35
|
+
if (take === 0 || scan.collected > maxBytes) break;
|
|
36
|
+
}
|
|
37
|
+
return scan;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function overlayWindow(overlay, startLine, lineCount) {
|
|
41
|
+
const window = sliceLinesRawInfo(overlay, startLine, lineCount);
|
|
42
|
+
const satisfied = lineCount === undefined || lineCount === 0 || window.text === "" || window.count >= lineCount || window.eof;
|
|
43
|
+
return { text: window.text, satisfied, whole: window.whole };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async function openReadFile(target) {
|
|
47
|
+
try { return await fs.open(target, fs.constants.O_RDONLY | (fs.constants.O_NONBLOCK ?? 0)); }
|
|
48
|
+
catch (error) { remapReadError(error, target); }
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function createWindowReader(vfs) {
|
|
52
|
+
return async function readWindow(target, startLine, lineCount, maxBytes, signal) {
|
|
53
|
+
const overlay = vfs.getOverlay(target);
|
|
54
|
+
if (overlay !== undefined) {
|
|
55
|
+
const window = overlayWindow(overlay,startLine,lineCount);
|
|
56
|
+
window.satisfied &&= Buffer.byteLength(window.text,"utf8") <= maxBytes;
|
|
57
|
+
return window;
|
|
58
|
+
}
|
|
59
|
+
const file = await openReadFile(target);
|
|
60
|
+
try {
|
|
61
|
+
const stat = await file.stat();
|
|
62
|
+
if (!stat.isFile()) throw new Error("read requires a regular file: " + target);
|
|
63
|
+
if (lineCount === 0) return { text: "", satisfied: true, whole: stat.size === 0 };
|
|
64
|
+
const scan = await scanWindow(file, stat, startLine, lineCount, maxBytes, signal);
|
|
65
|
+
if (scan.startByte === undefined) return { text: "", satisfied: true, whole: stat.size === 0 };
|
|
66
|
+
const bytes = Buffer.concat(scan.parts, scan.collected);
|
|
67
|
+
const end = scan.startByte + scan.collected;
|
|
68
|
+
const eof = end >= stat.size;
|
|
69
|
+
const text = eof ? decodeUtf8Strict(bytes, target) : decodeUtf8Window(bytes);
|
|
70
|
+
await vfs.recordExpected(target, stat);
|
|
71
|
+
return { text, satisfied: end >= scan.endByte || eof, whole: startLine === 1 && scan.startByte === 0 && eof };
|
|
72
|
+
} finally { await file.close(); }
|
|
73
|
+
};
|
|
74
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import * as fs from 'node:fs/promises';
|
|
2
|
+
import * as path from 'node:path';
|
|
3
|
+
import {isString} from '../shared/decode.js';
|
|
4
|
+
|
|
5
|
+
function sessionUriParts(uri) {
|
|
6
|
+
const match = /^(agent|artifact):\/\/([^/?#]+)$/i.exec(uri);
|
|
7
|
+
|
|
8
|
+
if (!match) throw new Error("session resource reads support bare agent://<id> and artifact://<number>; use offset/limit for pagination");
|
|
9
|
+
const kind = match[1].toLowerCase();
|
|
10
|
+
const id = decodeURIComponent(match[2]);
|
|
11
|
+
|
|
12
|
+
if (!id || id === "." || id === ".." || (/[/\\]/u.test(id) || Array.from(id).some(char => char.charCodeAt(0) < 32)) || (kind === "artifact" && !/^\d+$/.test(id))) throw new Error("invalid session resource ID");
|
|
13
|
+
|
|
14
|
+
return { kind, id };
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
async function findArtifactFile(root, id, uri, signal) {
|
|
18
|
+
const matches = [];
|
|
19
|
+
let count = 0;
|
|
20
|
+
|
|
21
|
+
for await (const entry of await fs.opendir(root)) {
|
|
22
|
+
signal?.throwIfAborted();
|
|
23
|
+
|
|
24
|
+
if (++count > 4096) throw new Error("session artifact lookup exceeded its directory budget");
|
|
25
|
+
|
|
26
|
+
if (entry.name.startsWith(id + ".") && !entry.isDirectory()) matches.push(entry.name);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (matches.length !== 1) throw new Error(matches.length ? "ambiguous session artifact: " + uri : "session artifact not found: " + uri);
|
|
30
|
+
|
|
31
|
+
return matches[0];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export async function resolveSessionResource(uri, signal, hooks) {
|
|
35
|
+
const { kind, id } = sessionUriParts(uri);
|
|
36
|
+
const dir = hooks.artifactsDir?.();
|
|
37
|
+
|
|
38
|
+
if (!isString(dir) || !dir) throw new Error("this host session does not expose an artifacts directory for " + uri);
|
|
39
|
+
signal?.throwIfAborted();
|
|
40
|
+
const root = await fs.realpath(dir);
|
|
41
|
+
const file = kind === "artifact" ? await findArtifactFile(root, id, uri, signal) : id + ".md";
|
|
42
|
+
const target = await fs.realpath(path.join(root, file));
|
|
43
|
+
|
|
44
|
+
if (!target.startsWith(root + path.sep)) throw new Error("session resource escapes its artifacts directory");
|
|
45
|
+
|
|
46
|
+
if (!(await fs.stat(target)).isFile()) throw new Error("session resource is not a file: " + uri);
|
|
47
|
+
signal?.throwIfAborted();
|
|
48
|
+
|
|
49
|
+
return target;
|
|
50
|
+
}
|
package/src/fs/text-ops.js
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
import { fileChunks } from "./file-io.js";
|
|
2
|
+
export {MAX_DIRECTORY_ENTRIES,formatDirectoryEntry,formatLsEntry} from './directory.js';
|
|
3
|
+
export {textResult,resultDiff} from '../shared/result.js';
|
|
4
|
+
import {sliceLinesRaw,lineNumberAt,formatNumberedLine,numberedPreview,lineStartIndex,lineEndIndex,lineTextRange,contentLineInfo} from './lines.js';
|
|
5
|
+
export * from './lines.js';
|
|
6
|
+
export {jsonStringLength,maxJsonStringPrefix} from './json-size.js';
|
|
1
7
|
import * as fs from "node:fs/promises";
|
|
2
8
|
import * as path from "node:path";
|
|
3
9
|
import { homedir } from "node:os";
|
|
@@ -5,124 +11,6 @@ import { isString, isNumber, isObject } from "../shared/decode.js";
|
|
|
5
11
|
import { assertFilesystemPath } from "./workspace.js";
|
|
6
12
|
import { MAX_DIFF_MATCHES } from "./diff.js";
|
|
7
13
|
|
|
8
|
-
const JSON_TWO_BYTE = new Set([0x22, 0x5c, 8, 9, 10, 12, 13]);
|
|
9
|
-
|
|
10
|
-
function jsonAsciiWidth(c) {
|
|
11
|
-
if (JSON_TWO_BYTE.has(c)) return 2;
|
|
12
|
-
|
|
13
|
-
if (c < 32) return 6;
|
|
14
|
-
|
|
15
|
-
return 1;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
function jsonUnitWidth(s, i) {
|
|
19
|
-
const c = s.charCodeAt(i);
|
|
20
|
-
|
|
21
|
-
if (c >= 0xD800 && c <= 0xDBFF && i + 1 < s.length) {
|
|
22
|
-
const d = s.charCodeAt(i + 1);
|
|
23
|
-
|
|
24
|
-
if (d >= 0xDC00 && d <= 0xDFFF) return { add: 2, skip: 2 };
|
|
25
|
-
|
|
26
|
-
return { add: 6, skip: 1 };
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
if (c >= 0xD800 && c <= 0xDFFF) return { add: 6, skip: 1 };
|
|
30
|
-
|
|
31
|
-
return { add: jsonAsciiWidth(c), skip: 1 };
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
/** UTF-16 length of JSON.stringify(s) for a string, without allocating the JSON. */
|
|
35
|
-
export function jsonStringLength(s) {
|
|
36
|
-
let n = 2;
|
|
37
|
-
|
|
38
|
-
for (let i = 0; i < s.length; ) {
|
|
39
|
-
const unit = jsonUnitWidth(s, i);
|
|
40
|
-
n += unit.add;
|
|
41
|
-
i += unit.skip;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
return n;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
/** Largest prefix whose JSON.stringify length is <= limit. */
|
|
48
|
-
export function maxJsonStringPrefix(s, limit) {
|
|
49
|
-
let used = 2;
|
|
50
|
-
let i = 0;
|
|
51
|
-
|
|
52
|
-
while (i < s.length) {
|
|
53
|
-
const unit = jsonUnitWidth(s, i);
|
|
54
|
-
|
|
55
|
-
if (used + unit.add > limit) break;
|
|
56
|
-
used += unit.add;
|
|
57
|
-
i += unit.skip;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
return i;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
export function textResult(text, details) {
|
|
64
|
-
return {
|
|
65
|
-
content: [{ type: "text", text: String(text ?? "") }],
|
|
66
|
-
details: details || {},
|
|
67
|
-
};
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
export function resultDiff(response) {
|
|
71
|
-
let details = response?.details;
|
|
72
|
-
|
|
73
|
-
if (isString(details)) {
|
|
74
|
-
try {
|
|
75
|
-
details = JSON.parse(details);
|
|
76
|
-
} catch {
|
|
77
|
-
return undefined;
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
return isObject(details) ? details.diff : undefined;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
function totalContentLines(text) {
|
|
85
|
-
if (text === "") return 1;
|
|
86
|
-
|
|
87
|
-
return contentLineInfo(text).count + (text.endsWith("\n") ? 1 : 0);
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
function emptySliceInfo(text, totalLines) {
|
|
91
|
-
return { text: "", end: totalLines, total: totalLines, count: 0, eof: true, whole: totalLines === 1 && text === "" };
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
function sliceWindow(text, startIndex, count, totalLines) {
|
|
95
|
-
const endExclusive = Math.min(totalLines, startIndex + count);
|
|
96
|
-
const start = lineStartIndex(text, startIndex + 1);
|
|
97
|
-
const end = lineEndIndex(text, start, endExclusive - startIndex);
|
|
98
|
-
let selected = text.slice(start, end);
|
|
99
|
-
const eof = endExclusive >= totalLines || (endExclusive === totalLines - 1 && text.endsWith("\n"));
|
|
100
|
-
|
|
101
|
-
if (endExclusive < totalLines && !selected.endsWith("\n")) selected += "\n";
|
|
102
|
-
|
|
103
|
-
return { text: selected, end: endExclusive, total: totalLines, count: endExclusive - startIndex, eof, whole: startIndex === 0 && eof };
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
export function sliceLinesRawInfo(text, offset, limit) {
|
|
107
|
-
const totalLines = totalContentLines(text);
|
|
108
|
-
|
|
109
|
-
if (!isNumber(offset) && !isNumber(limit)) {
|
|
110
|
-
return { text, end: totalLines, total: totalLines, count: totalLines, eof: true, whole: true };
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
const startIndex = (isNumber(offset) ? Math.max(1, Math.floor(offset)) : 1) - 1;
|
|
114
|
-
const count = isNumber(limit) ? Math.max(0, Math.floor(limit)) : totalLines;
|
|
115
|
-
|
|
116
|
-
if (count === 0 || startIndex >= totalLines) return emptySliceInfo(text, totalLines);
|
|
117
|
-
|
|
118
|
-
return sliceWindow(text, startIndex, count, totalLines);
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
/** Read-window slicing preserves the selected lines' own line ending. */
|
|
122
|
-
export function sliceLinesRaw(text, offset, limit) {
|
|
123
|
-
return sliceLinesRawInfo(text, offset, limit).text;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
14
|
export function readLineParam(value, name) {
|
|
127
15
|
if (value === undefined) return undefined;
|
|
128
16
|
const number = isNumber(value) ? value : isString(value) && value.trim() !== "" ? Number(value) : NaN;
|
|
@@ -173,40 +61,6 @@ export async function probeExistingPath(cwd, targetParam, vfs) {
|
|
|
173
61
|
}
|
|
174
62
|
}
|
|
175
63
|
|
|
176
|
-
export const EDIT_PREVIEW_LINES = 16;
|
|
177
|
-
|
|
178
|
-
export const MAX_DIRECTORY_ENTRIES = 10000;
|
|
179
|
-
|
|
180
|
-
export function sourceLines(content) {
|
|
181
|
-
const raw = content.split("\n");
|
|
182
|
-
|
|
183
|
-
if (raw.at(-1) === "") raw.pop();
|
|
184
|
-
|
|
185
|
-
return raw;
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
export function lineNumberAt(content, index) {
|
|
189
|
-
let line = 1;
|
|
190
|
-
|
|
191
|
-
for (let i = 0; i < index; i++) if (content.charCodeAt(i) === 10) line++;
|
|
192
|
-
|
|
193
|
-
return line;
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
export function formatNumberedLine(n, text) {
|
|
197
|
-
return String(n).padStart(5) + " " + text;
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
export function numberedPreview(content, cap = EDIT_PREVIEW_LINES) {
|
|
201
|
-
const { count, preview } = contentLineInfo(content, cap);
|
|
202
|
-
|
|
203
|
-
if (count === 0) return "0 lines";
|
|
204
|
-
const body = preview.map((line, i) => formatNumberedLine(i + 1, line)).join("\n");
|
|
205
|
-
const suffix = count + " lines total";
|
|
206
|
-
|
|
207
|
-
return body + "\n" + suffix;
|
|
208
|
-
}
|
|
209
|
-
|
|
210
64
|
function lineAt(content, n) {
|
|
211
65
|
const range = lineTextRange(content, n);
|
|
212
66
|
|
|
@@ -283,38 +137,6 @@ export function applyReplacements(target, content, requestedEdits) {
|
|
|
283
137
|
return { updated, matches };
|
|
284
138
|
}
|
|
285
139
|
|
|
286
|
-
export function lineStartIndex(content, line) {
|
|
287
|
-
let index = 0;
|
|
288
|
-
|
|
289
|
-
for (let current = 1; current < line; current++) {
|
|
290
|
-
const next = content.indexOf("\n", index);
|
|
291
|
-
|
|
292
|
-
if (next < 0) return content.length;
|
|
293
|
-
index = next + 1;
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
return Math.min(index, content.length);
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
export function lineEndIndex(content, startIndex, lineCount) {
|
|
300
|
-
let index = startIndex;
|
|
301
|
-
|
|
302
|
-
for (let i = 0; i < lineCount; i++) {
|
|
303
|
-
const next = content.indexOf("\n", index);
|
|
304
|
-
|
|
305
|
-
if (next < 0) return content.length;
|
|
306
|
-
index = next + 1;
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
return index;
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
export function lineTextRange(content, line) {
|
|
313
|
-
const start = lineStartIndex(content, line);
|
|
314
|
-
|
|
315
|
-
return { start, end: lineEndIndex(content, start, 1) };
|
|
316
|
-
}
|
|
317
|
-
|
|
318
140
|
export function shiftDiffLines(diff, delta) {
|
|
319
141
|
if (!diff || delta === 0) return diff;
|
|
320
142
|
|
|
@@ -429,7 +251,7 @@ export async function countContentLines(target, signal) {
|
|
|
429
251
|
let last = -1;
|
|
430
252
|
let total = 0;
|
|
431
253
|
|
|
432
|
-
for await (const chunk of file
|
|
254
|
+
for await (const chunk of fileChunks(file, signal)) {
|
|
433
255
|
for (let i = 0; i < chunk.length; i++) if (chunk[i] === 10) newlines++;
|
|
434
256
|
last = chunk.at(-1);
|
|
435
257
|
total += chunk.length;
|
|
@@ -439,26 +261,6 @@ export async function countContentLines(target, signal) {
|
|
|
439
261
|
} finally { await file.close(); }
|
|
440
262
|
}
|
|
441
263
|
|
|
442
|
-
export function contentLineInfo(text, previewLimit = 0) {
|
|
443
|
-
if (text === "") return { count: 0, preview: [], newlines: 0 };
|
|
444
|
-
const preview = [];
|
|
445
|
-
let count = 0;
|
|
446
|
-
let start = 0;
|
|
447
|
-
|
|
448
|
-
while (start <= text.length) {
|
|
449
|
-
const newline = text.indexOf("\n", start);
|
|
450
|
-
const end = newline < 0 ? text.length : newline;
|
|
451
|
-
|
|
452
|
-
if (end === text.length && end === start && text.endsWith("\n")) break;
|
|
453
|
-
if (preview.length < previewLimit) preview.push(text.slice(start, end).replace(/\r$/, ""));
|
|
454
|
-
count++;
|
|
455
|
-
if (newline < 0) break;
|
|
456
|
-
start = newline + 1;
|
|
457
|
-
}
|
|
458
|
-
|
|
459
|
-
return { count, preview, newlines: count - Number(!text.endsWith("\n")) };
|
|
460
|
-
}
|
|
461
|
-
|
|
462
264
|
export function boundedEditDiff(target, original, matches) {
|
|
463
265
|
const rendered = matches.slice(0, MAX_DIFF_MATCHES);
|
|
464
266
|
const lines = [];
|
|
@@ -498,25 +300,3 @@ export function boundedWriteDiff(target, content, removed) {
|
|
|
498
300
|
lines: added.preview.map((text, i) => ({ type: "add", lineNum: i + 1, text })),
|
|
499
301
|
};
|
|
500
302
|
}
|
|
501
|
-
|
|
502
|
-
export function formatDirectoryEntry(name, type, size = 0) {
|
|
503
|
-
const sizeSuffix = size ? `, ${size} bytes` : "";
|
|
504
|
-
|
|
505
|
-
return `${name}${type === "dir" ? "/" : ""} (${type}${sizeSuffix})`;
|
|
506
|
-
}
|
|
507
|
-
|
|
508
|
-
export async function formatLsEntry(dirPath, entry) {
|
|
509
|
-
const isDir = entry.isDirectory();
|
|
510
|
-
const isSym = entry.isSymbolicLink();
|
|
511
|
-
const typeLabel = isDir ? "dir" : isSym ? "sym" : "file";
|
|
512
|
-
let size = 0;
|
|
513
|
-
|
|
514
|
-
try {
|
|
515
|
-
if (!isDir && !isSym) {
|
|
516
|
-
const st = await fs.stat(path.join(dirPath, entry.name));
|
|
517
|
-
size = st.size;
|
|
518
|
-
}
|
|
519
|
-
} catch {}
|
|
520
|
-
|
|
521
|
-
return formatDirectoryEntry(entry.name, typeLabel, size);
|
|
522
|
-
}
|