pi-hashline-edit-pro 4.3.2 → 4.3.3
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 +7 -4
- package/index.ts +8 -3
- package/package.json +1 -1
- package/prompts/grep-guidelines.md +0 -1
- package/prompts/grep.md +1 -1
- package/prompts/insert-guidelines.md +1 -2
- package/prompts/insert.md +1 -1
- package/prompts/read-guidelines.md +2 -3
- package/prompts/replace-guidelines.md +4 -4
- package/prompts/replace.md +1 -1
- package/src/auto-read-all-state.ts +13 -0
- package/src/auto-read-all.ts +49 -27
- package/src/batch.ts +23 -32
- package/src/config-ui.ts +79 -7
- package/src/config.ts +35 -0
- package/src/edit-common.ts +25 -7
- package/src/file-reader.ts +2 -3
- package/src/grep.ts +176 -20
- package/src/hash-store.ts +21 -7
- package/src/hashline/apply.ts +1 -1
- package/src/hashline/hash.ts +3 -15
- package/src/hashline/hasher.ts +15 -1
- package/src/insert.ts +1 -1
- package/src/paths.ts +5 -1
- package/src/payload-contract.ts +2 -2
- package/src/read.ts +20 -1
- package/src/replace-undo.ts +10 -4
- package/src/replace.ts +2 -3
- package/src/utils.ts +1 -1
package/src/config.ts
CHANGED
|
@@ -15,6 +15,7 @@ export interface Config {
|
|
|
15
15
|
autoRead: boolean;
|
|
16
16
|
anchorGrepEnabled: boolean;
|
|
17
17
|
autoReadAll?: AutoReadAllMode;
|
|
18
|
+
autoReadAllIgnore?: string[];
|
|
18
19
|
requirePath?: boolean;
|
|
19
20
|
strictInput?: boolean;
|
|
20
21
|
boundaryDedupMode?: BoundaryDedupMode;
|
|
@@ -25,6 +26,7 @@ const DEFAULT_CONFIG: Config = {
|
|
|
25
26
|
autoRead: true,
|
|
26
27
|
anchorGrepEnabled: true,
|
|
27
28
|
autoReadAll: "off",
|
|
29
|
+
autoReadAllIgnore: [],
|
|
28
30
|
requirePath: false,
|
|
29
31
|
strictInput: false,
|
|
30
32
|
boundaryDedupMode: "on",
|
|
@@ -47,6 +49,26 @@ function parseAutoReadAllMode(value: unknown): AutoReadAllMode {
|
|
|
47
49
|
return DEFAULT_CONFIG.autoReadAll ?? "off";
|
|
48
50
|
}
|
|
49
51
|
|
|
52
|
+
export function normalizeAutoReadAllIgnoreEntry(entry: string): string {
|
|
53
|
+
return entry.trim().replace(/\\/g, "/").replace(/^\/+|\/+$/g, "").replace(/\/{2,}/g, "/");
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function parseAutoReadAllIgnore(value: unknown): string[] {
|
|
57
|
+
const raw = typeof value === "string" ? value.split(",") : Array.isArray(value) ? value : [];
|
|
58
|
+
const seen = new Set<string>();
|
|
59
|
+
const out: string[] = [];
|
|
60
|
+
for (const item of raw) {
|
|
61
|
+
if (typeof item !== "string") continue;
|
|
62
|
+
const cleaned = normalizeAutoReadAllIgnoreEntry(item);
|
|
63
|
+
if (cleaned.length === 0) continue;
|
|
64
|
+
const lower = cleaned.toLowerCase();
|
|
65
|
+
if (seen.has(lower)) continue;
|
|
66
|
+
seen.add(lower);
|
|
67
|
+
out.push(cleaned);
|
|
68
|
+
}
|
|
69
|
+
return out;
|
|
70
|
+
}
|
|
71
|
+
|
|
50
72
|
export function normalizeDiffContextLines(value: unknown): number {
|
|
51
73
|
if (typeof value !== "number" || !Number.isFinite(value)) return DEFAULT_DIFF_CONTEXT_LINES;
|
|
52
74
|
const floored = Math.floor(value);
|
|
@@ -68,6 +90,7 @@ function parseConfig(content: string): Config {
|
|
|
68
90
|
const boundaryDedupMode = parsed.boundaryDedupMode;
|
|
69
91
|
const legacyBoundaryDedup = parsed.boundaryDedupEnabled;
|
|
70
92
|
const diffContextLines = parsed.diffContextLines;
|
|
93
|
+
const autoReadAllIgnore = parsed.autoReadAllIgnore;
|
|
71
94
|
return {
|
|
72
95
|
autoRead: typeof autoRead === "boolean" ? autoRead : DEFAULT_CONFIG.autoRead,
|
|
73
96
|
anchorGrepEnabled: typeof anchorGrepEnabled === "boolean" ? anchorGrepEnabled : DEFAULT_CONFIG.anchorGrepEnabled,
|
|
@@ -76,6 +99,7 @@ function parseConfig(content: string): Config {
|
|
|
76
99
|
strictInput: typeof strictInput === "boolean" ? strictInput : DEFAULT_CONFIG.strictInput,
|
|
77
100
|
boundaryDedupMode: parseBoundaryDedupMode(boundaryDedupMode, legacyBoundaryDedup),
|
|
78
101
|
diffContextLines: normalizeDiffContextLines(diffContextLines),
|
|
102
|
+
autoReadAllIgnore: parseAutoReadAllIgnore(autoReadAllIgnore),
|
|
79
103
|
};
|
|
80
104
|
}
|
|
81
105
|
|
|
@@ -222,3 +246,14 @@ export async function adjustDiffContextLines(delta: number): Promise<number> {
|
|
|
222
246
|
});
|
|
223
247
|
return next;
|
|
224
248
|
}
|
|
249
|
+
export async function setAutoReadAllIgnore(dirs: string[]): Promise<string[]> {
|
|
250
|
+
let next: string[] = [];
|
|
251
|
+
await updateConfig((c) => {
|
|
252
|
+
next = parseAutoReadAllIgnore(dirs);
|
|
253
|
+
c.autoReadAllIgnore = next;
|
|
254
|
+
});
|
|
255
|
+
return next;
|
|
256
|
+
}
|
|
257
|
+
export async function setAutoReadAllIgnoreFromText(text: string): Promise<string[]> {
|
|
258
|
+
return setAutoReadAllIgnore(text.split(","));
|
|
259
|
+
}
|
package/src/edit-common.ts
CHANGED
|
@@ -13,13 +13,15 @@ export interface EditToolFlags {
|
|
|
13
13
|
strictInput: boolean;
|
|
14
14
|
boundaryDedupMode: BoundaryDedupMode;
|
|
15
15
|
autoRead: boolean;
|
|
16
|
+
autoReadAllActive: boolean;
|
|
16
17
|
}
|
|
17
18
|
|
|
18
19
|
export const DEFAULT_EDIT_FLAGS: EditToolFlags = {
|
|
19
20
|
requirePath: false,
|
|
20
21
|
strictInput: false,
|
|
21
22
|
boundaryDedupMode: "on",
|
|
22
|
-
autoRead: true
|
|
23
|
+
autoRead: true,
|
|
24
|
+
autoReadAllActive: false
|
|
23
25
|
};
|
|
24
26
|
|
|
25
27
|
export async function currentEditFlags(): Promise<EditToolFlags> {
|
|
@@ -28,7 +30,8 @@ export async function currentEditFlags(): Promise<EditToolFlags> {
|
|
|
28
30
|
requirePath: config.requirePath === true,
|
|
29
31
|
strictInput: config.strictInput === true,
|
|
30
32
|
boundaryDedupMode: config.boundaryDedupMode ?? "on",
|
|
31
|
-
autoRead: config.autoRead !== false
|
|
33
|
+
autoRead: config.autoRead !== false,
|
|
34
|
+
autoReadAllActive: (config.autoReadAll ?? "off") !== "off"
|
|
32
35
|
};
|
|
33
36
|
}
|
|
34
37
|
|
|
@@ -38,7 +41,8 @@ export function withReplacePrompts(base: { description: string; snippet: string;
|
|
|
38
41
|
let guidelines = [...base.guidelines];
|
|
39
42
|
if (!flags.autoRead) {
|
|
40
43
|
description = description.replace(" Anchor follow-up edits on the `+anchor│` and ` anchor│` rows of the post-edit diff instead of re-reading.", "");
|
|
41
|
-
guidelines = guidelines.
|
|
44
|
+
guidelines = guidelines.filter((guideline) => !guideline.includes("post-edit diff"));
|
|
45
|
+
description = description.replace("and the last call shows the combined diff,", "and the last call shows the combined result,");
|
|
42
46
|
}
|
|
43
47
|
const descriptionParts = [description];
|
|
44
48
|
if (flags.requirePath) {
|
|
@@ -64,13 +68,21 @@ export function withReplacePrompts(base: { description: string; snippet: string;
|
|
|
64
68
|
}
|
|
65
69
|
|
|
66
70
|
export function withReadPrompts(base: { description: string; snippet: string; guidelines: string[] }, flags: EditToolFlags): { description: string; snippet: string; guidelines: string[] } {
|
|
67
|
-
if (flags.
|
|
68
|
-
|
|
69
|
-
|
|
71
|
+
if (flags.autoReadAllActive) {
|
|
72
|
+
const rewritten = base.guidelines
|
|
73
|
+
.filter((guideline) => !guideline.includes("call again after an edit"))
|
|
74
|
+
return { description: base.description, snippet: base.snippet, guidelines: [...rewritten] };
|
|
75
|
+
}
|
|
76
|
+
const withoutAutoReadAll = base.guidelines.filter((guideline) => !guideline.includes("E_AUTO_READ_ALL"))
|
|
77
|
+
if (flags.autoRead) return { description: base.description, snippet: base.snippet, guidelines: [...withoutAutoReadAll] };
|
|
78
|
+
const guidelines = [...withoutAutoReadAll];
|
|
79
|
+
const mapped = guidelines.map((guideline) => guideline.startsWith("`read`: call again after an edit") ? "`read`: call again after an edit when you need anchors you lack." : guideline);
|
|
80
|
+
return { description: base.description, snippet: base.snippet, guidelines: mapped };
|
|
70
81
|
}
|
|
71
82
|
|
|
72
83
|
export function withInsertPrompts(base: { description: string; snippet: string; guidelines: string[] }, flags: EditToolFlags): { description: string; snippet: string; guidelines: string[] } {
|
|
73
|
-
const
|
|
84
|
+
const baseDescription = flags.autoRead ? base.description : base.description.replace("and the last call shows the combined diff,", "and the last call shows the combined result,");
|
|
85
|
+
const descriptionParts = [baseDescription];
|
|
74
86
|
const snippetParts = [base.snippet];
|
|
75
87
|
const guidelines = [...base.guidelines];
|
|
76
88
|
if (flags.requirePath) {
|
|
@@ -87,6 +99,12 @@ export function withInsertPrompts(base: { description: string; snippet: string;
|
|
|
87
99
|
}
|
|
88
100
|
return { description: descriptionParts.join(" "), snippet: snippetParts.join(""), guidelines };
|
|
89
101
|
}
|
|
102
|
+
|
|
103
|
+
export function withUndoPrompts(base: { description: string; snippet: string; guidelines: string[] }, flags: EditToolFlags): { description: string; snippet: string; guidelines: string[] } {
|
|
104
|
+
if (flags.autoRead) return { description: base.description, snippet: base.snippet, guidelines: [...base.guidelines] };
|
|
105
|
+
const guidelines = base.guidelines.map((guideline) => guideline.includes("bad diff") ? "`undo_last_change`: only the last `replace`/`insert` per file is undoable; a `write` clears it, so undo right after a bad edit — review what you're restoring." : guideline);
|
|
106
|
+
return { description: base.description, snippet: base.snippet, guidelines };
|
|
107
|
+
}
|
|
90
108
|
export function resolveEditTarget(removeFrom: string, removeTo?: string): string {
|
|
91
109
|
const refs = [removeFrom, removeTo].filter((value): value is string => typeof value === "string");
|
|
92
110
|
const owners = refs.map((ref) => ownerOf(parseHashRef(stripAnchorRow(ref.trim(), "anchor entry")).hash));
|
package/src/file-reader.ts
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
import { constants } from "node:fs";
|
|
2
2
|
import { stat } from "node:fs/promises";
|
|
3
|
-
import { relative } from "node:path";
|
|
4
3
|
import { lineHashes } from "./hashline";
|
|
5
4
|
import { loadFileKindAndText, type LFile } from "./file-kind";
|
|
6
5
|
import { resolveTarget, type FileIdentity } from "./fs-write";
|
|
7
|
-
import { toCwd } from "./paths";
|
|
6
|
+
import { toCwd, toDisplayPath } from "./paths";
|
|
8
7
|
import { detectEnding, toLF, stripBOM, type LineEnding } from "./normalize";
|
|
9
8
|
import { abortIf, errCode, assertLineLimit } from "./utils";
|
|
10
9
|
import { ANCHOR_POOL_EXHAUSTED_PREFIX } from "./constants";
|
|
@@ -119,7 +118,7 @@ export async function tryReadNormFile(
|
|
|
119
118
|
options?: ReadNormOptions,
|
|
120
119
|
): Promise<NormFile | undefined> {
|
|
121
120
|
try {
|
|
122
|
-
const displayPath =
|
|
121
|
+
const displayPath = toDisplayPath(cwd, absPath);
|
|
123
122
|
const file = await loadFileKindAndText(absPath, { maxLines: options?.maxLines, displayPath });
|
|
124
123
|
if (file.kind !== "text") return undefined;
|
|
125
124
|
return await readNormFile(absPath, cwd, { ...options, preloadedFile: file });
|
package/src/grep.ts
CHANGED
|
@@ -2,16 +2,16 @@ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
|
2
2
|
import { formatSize, DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES, type TruncationResult } from "@earendil-works/pi-coding-agent";
|
|
3
3
|
import { Type } from "typebox";
|
|
4
4
|
import { stat } from "node:fs/promises";
|
|
5
|
-
import { dirname, isAbsolute, join,
|
|
5
|
+
import { dirname, isAbsolute, join, win32 } from "node:path";
|
|
6
6
|
import { spawn, spawnSync } from "node:child_process";
|
|
7
7
|
import { createInterface } from "node:readline";
|
|
8
8
|
import { tryReadNormFile } from "./file-reader";
|
|
9
9
|
import { MAX_HASH_LINES, fmtRow, HASH_LEN, HASH_SEP } from "./hashline";
|
|
10
10
|
import { ANCHOR_POOL_EXHAUSTED_PREFIX, MAX_GREP_LINE_BYTES } from "./constants";
|
|
11
|
-
import { toCwd } from "./paths";
|
|
11
|
+
import { toCwd, toDisplayPath } from "./paths";
|
|
12
12
|
import { loadP, loadGuide } from "./prompts";
|
|
13
13
|
import { normReq } from "./payload-contract";
|
|
14
|
-
import { abortIf, errCode, isRec, makePrepareArguments, rejectUnknownFields, truncateToBytes, visLines } from "./utils";
|
|
14
|
+
import { abortIf, errCode, gutterWidth, isRec, makePrepareArguments, rejectUnknownFields, truncateToBytes, visLines } from "./utils";
|
|
15
15
|
import { withAnchorSession } from "./anchor-registry";
|
|
16
16
|
import { serveRows } from "./served";
|
|
17
17
|
import { Text } from "@earendil-works/pi-tui";
|
|
@@ -152,12 +152,39 @@ function assertSafeRegex(pattern: string): void {
|
|
|
152
152
|
}
|
|
153
153
|
}
|
|
154
154
|
|
|
155
|
-
function
|
|
156
|
-
|
|
155
|
+
function bracketEnd(source: string, start: number): number {
|
|
156
|
+
let j = start + 1;
|
|
157
|
+
if (j < source.length && (source[j] === "!" || source[j] === "^")) j++;
|
|
158
|
+
if (j < source.length && source[j] === "]") j++;
|
|
159
|
+
let esc = false;
|
|
160
|
+
while (j < source.length) {
|
|
161
|
+
const c = source[j]!;
|
|
162
|
+
if (esc) {
|
|
163
|
+
esc = false;
|
|
164
|
+
j++;
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
if (c === "\\") {
|
|
168
|
+
esc = true;
|
|
169
|
+
j++;
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
if (c === "]") return j;
|
|
173
|
+
j++;
|
|
174
|
+
}
|
|
175
|
+
return -1;
|
|
176
|
+
}
|
|
177
|
+
function globPartToSource(glob: string): string {
|
|
157
178
|
let source = "";
|
|
158
179
|
let i = 0;
|
|
159
180
|
while (i < glob.length) {
|
|
160
181
|
const ch = glob[i]!;
|
|
182
|
+
if (ch === "\\" && i + 1 < glob.length) {
|
|
183
|
+
const next = glob[i + 1]!;
|
|
184
|
+
source += next.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
185
|
+
i += 2;
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
161
188
|
if (ch === "*") {
|
|
162
189
|
if (glob[i + 1] === "*") {
|
|
163
190
|
i += 2;
|
|
@@ -170,14 +197,146 @@ function globToRegex(glob: string): RegExp {
|
|
|
170
197
|
continue;
|
|
171
198
|
}
|
|
172
199
|
source += ".*";
|
|
173
|
-
|
|
200
|
+
i += 1;
|
|
201
|
+
continue;
|
|
202
|
+
}
|
|
203
|
+
if (ch === "?") {
|
|
174
204
|
source += "[^/]";
|
|
175
|
-
|
|
176
|
-
|
|
205
|
+
i += 1;
|
|
206
|
+
continue;
|
|
177
207
|
}
|
|
178
|
-
|
|
208
|
+
if (ch === "{") {
|
|
209
|
+
let depth = 1;
|
|
210
|
+
let j = i + 1;
|
|
211
|
+
let esc = false;
|
|
212
|
+
while (j < glob.length && depth > 0) {
|
|
213
|
+
const c = glob[j]!;
|
|
214
|
+
if (esc) {
|
|
215
|
+
esc = false;
|
|
216
|
+
j++;
|
|
217
|
+
continue;
|
|
218
|
+
}
|
|
219
|
+
if (c === "\\") {
|
|
220
|
+
esc = true;
|
|
221
|
+
j++;
|
|
222
|
+
continue;
|
|
223
|
+
}
|
|
224
|
+
if (c === "[") {
|
|
225
|
+
const e = bracketEnd(glob, j);
|
|
226
|
+
if (e >= 0) {
|
|
227
|
+
j = e + 1;
|
|
228
|
+
continue;
|
|
229
|
+
}
|
|
230
|
+
j++;
|
|
231
|
+
continue;
|
|
232
|
+
}
|
|
233
|
+
if (c === "{") depth++;
|
|
234
|
+
else if (c === "}") depth--;
|
|
235
|
+
if (depth === 0) break;
|
|
236
|
+
j++;
|
|
237
|
+
}
|
|
238
|
+
if (depth !== 0) {
|
|
239
|
+
source += "\\{";
|
|
240
|
+
i++;
|
|
241
|
+
continue;
|
|
242
|
+
}
|
|
243
|
+
const inner = glob.slice(i + 1, j);
|
|
244
|
+
const parts: string[] = [];
|
|
245
|
+
let cur = "";
|
|
246
|
+
let d2 = 0;
|
|
247
|
+
let esc2 = false;
|
|
248
|
+
for (let k = 0; k < inner.length; k++) {
|
|
249
|
+
const c = inner[k]!;
|
|
250
|
+
if (esc2) {
|
|
251
|
+
cur += c;
|
|
252
|
+
esc2 = false;
|
|
253
|
+
continue;
|
|
254
|
+
}
|
|
255
|
+
if (c === "\\") {
|
|
256
|
+
esc2 = true;
|
|
257
|
+
cur += c;
|
|
258
|
+
continue;
|
|
259
|
+
}
|
|
260
|
+
if (c === "[") {
|
|
261
|
+
const e = bracketEnd(inner, k);
|
|
262
|
+
if (e >= 0) {
|
|
263
|
+
cur += inner.slice(k, e + 1);
|
|
264
|
+
k = e;
|
|
265
|
+
continue;
|
|
266
|
+
}
|
|
267
|
+
cur += c;
|
|
268
|
+
continue;
|
|
269
|
+
}
|
|
270
|
+
if (c === "{") {
|
|
271
|
+
d2++;
|
|
272
|
+
cur += c;
|
|
273
|
+
continue;
|
|
274
|
+
}
|
|
275
|
+
if (c === "}") {
|
|
276
|
+
d2--;
|
|
277
|
+
cur += c;
|
|
278
|
+
continue;
|
|
279
|
+
}
|
|
280
|
+
if (c === "," && d2 === 0) {
|
|
281
|
+
parts.push(cur);
|
|
282
|
+
cur = "";
|
|
283
|
+
continue;
|
|
284
|
+
}
|
|
285
|
+
cur += c;
|
|
286
|
+
}
|
|
287
|
+
parts.push(cur);
|
|
288
|
+
if (parts.length <= 1) {
|
|
289
|
+
source += "\\{" + globPartToSource(inner) + "\\}";
|
|
290
|
+
} else {
|
|
291
|
+
source += "(?:" + parts.map((p) => globPartToSource(p)).join("|") + ")";
|
|
292
|
+
}
|
|
293
|
+
i = j + 1;
|
|
294
|
+
continue;
|
|
295
|
+
}
|
|
296
|
+
if (ch === "[") {
|
|
297
|
+
let j = i + 1;
|
|
298
|
+
if (j < glob.length && (glob[j] === "!" || glob[j] === "^")) j++;
|
|
299
|
+
if (j < glob.length && glob[j] === "]") j++;
|
|
300
|
+
let esc3 = false;
|
|
301
|
+
while (j < glob.length) {
|
|
302
|
+
const c = glob[j]!;
|
|
303
|
+
if (esc3) {
|
|
304
|
+
esc3 = false;
|
|
305
|
+
j++;
|
|
306
|
+
continue;
|
|
307
|
+
}
|
|
308
|
+
if (c === "\\") {
|
|
309
|
+
esc3 = true;
|
|
310
|
+
j++;
|
|
311
|
+
continue;
|
|
312
|
+
}
|
|
313
|
+
if (c === "]") break;
|
|
314
|
+
j++;
|
|
315
|
+
}
|
|
316
|
+
if (j >= glob.length) {
|
|
317
|
+
source += "\\[";
|
|
318
|
+
i++;
|
|
319
|
+
continue;
|
|
320
|
+
}
|
|
321
|
+
let content = glob.slice(i + 1, j);
|
|
322
|
+
if (content.startsWith("!")) content = "^" + content.slice(1);
|
|
323
|
+
if (content.startsWith("]") || content.startsWith("^]")) {
|
|
324
|
+
if (content.startsWith("^]")) content = "^\\]" + content.slice(2);
|
|
325
|
+
else content = "\\]" + content.slice(1);
|
|
326
|
+
}
|
|
327
|
+
if (content.startsWith("^") && !content.includes("/")) content = "^/" + content.slice(1);
|
|
328
|
+
source += "[" + content + "]";
|
|
329
|
+
i = j + 1;
|
|
330
|
+
continue;
|
|
331
|
+
}
|
|
332
|
+
source += ch.replace(/[.+^${}()|[\]\\]/g, "\\$&");
|
|
333
|
+
i++;
|
|
179
334
|
}
|
|
180
|
-
return
|
|
335
|
+
return source;
|
|
336
|
+
}
|
|
337
|
+
function globToRegex(glob: string): RegExp {
|
|
338
|
+
if (glob.startsWith("/")) glob = glob.slice(1);
|
|
339
|
+
return new RegExp(`^${globPartToSource(glob)}$`);
|
|
181
340
|
}
|
|
182
341
|
|
|
183
342
|
interface FileHit {
|
|
@@ -401,14 +560,11 @@ async function collectRgMatches(
|
|
|
401
560
|
});
|
|
402
561
|
}
|
|
403
562
|
|
|
404
|
-
function gutterWidthFor(numbers: number[]): number {
|
|
405
|
-
let max = 0;
|
|
406
|
-
for (const n of numbers) if (n > max) max = n;
|
|
407
|
-
return String(max || 1).length;
|
|
408
|
-
}
|
|
409
563
|
|
|
410
564
|
function displayRowsForHit(hit: FileHit): string[] {
|
|
411
|
-
|
|
565
|
+
let max = 0;
|
|
566
|
+
for (const n of hit.lineNumbers) if (n > max) max = n;
|
|
567
|
+
const width = gutterWidth(max, 1);
|
|
412
568
|
return hit.rows.map((row, i) => {
|
|
413
569
|
const n = hit.lineNumbers[i]!;
|
|
414
570
|
const padded = String(n).padStart(width, " ");
|
|
@@ -571,8 +727,8 @@ export function regGrep(pi: ExtensionAPI): void {
|
|
|
571
727
|
const globRegex = req.glob === undefined ? undefined : globToRegex(req.glob);
|
|
572
728
|
const matchesGlob = (absPath: string): boolean => {
|
|
573
729
|
if (globRegex === undefined) return true;
|
|
574
|
-
const displayPath =
|
|
575
|
-
const globPath =
|
|
730
|
+
const displayPath = toDisplayPath(ctx.cwd, absPath);
|
|
731
|
+
const globPath = toDisplayPath(globRoot, absPath);
|
|
576
732
|
return globRegex.test(globPath) || globRegex.test(displayPath);
|
|
577
733
|
};
|
|
578
734
|
const validatedRegex = buildRegex(req.pattern, req.literal === true, req.ignoreCase === true);
|
|
@@ -613,7 +769,7 @@ export function regGrep(pi: ExtensionAPI): void {
|
|
|
613
769
|
if (!matchesGlob(absPath)) continue;
|
|
614
770
|
const norm = await readGrepFileShadow(absPath);
|
|
615
771
|
if (!norm) continue;
|
|
616
|
-
const hit = makeHitFromIndices(norm,
|
|
772
|
+
const hit = makeHitFromIndices(norm, toDisplayPath(ctx.cwd, absPath), indices, context, validatedRegex, totalForFile, indices.length);
|
|
617
773
|
const display = displayRowsForHit(hit);
|
|
618
774
|
totalRows += display.length;
|
|
619
775
|
for (const r of display) totalBytes += Buffer.byteLength(r, "utf-8") + 1;
|
|
@@ -635,7 +791,7 @@ export function regGrep(pi: ExtensionAPI): void {
|
|
|
635
791
|
if (!matchesGlob(absPath)) continue;
|
|
636
792
|
const norm = await readGrepFile(absPath);
|
|
637
793
|
if (!norm) continue;
|
|
638
|
-
const hit = makeHitFromIndices(norm,
|
|
794
|
+
const hit = makeHitFromIndices(norm, toDisplayPath(ctx.cwd, absPath), indices, context, validatedRegex, totalForFile, Math.min(totalForFile, remaining));
|
|
639
795
|
const display = displayRowsForHit(hit);
|
|
640
796
|
const keptRows: string[] = [];
|
|
641
797
|
const keptHashes: string[] = [];
|
package/src/hash-store.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { existsSync } from "node:fs";
|
|
|
3
3
|
import { chmod, readFile, rename, mkdir, stat } from "node:fs/promises";
|
|
4
4
|
import { hashStorePath, hashStoreDir, legacyHashStorePath } from "./paths";
|
|
5
5
|
import { errCode, isRec, splitLines } from "./utils";
|
|
6
|
-
import { initHasher, contentChecksum } from "./hashline/hasher";
|
|
6
|
+
import { initHasher, contentChecksum, lineChecksum } from "./hashline/hasher";
|
|
7
7
|
import { HASH_STORE_VERSION, HASH_STORE_BUSY_TIMEOUT } from "./constants";
|
|
8
8
|
import {
|
|
9
9
|
isValidHashList,
|
|
@@ -22,6 +22,7 @@ import { snapshotCache, cacheSnapshot, SNAPSHOT_CACHE_LIMIT } from "./hash-store
|
|
|
22
22
|
export { isValidHashList, parseHashList, parseStoredHashes, isCorruptionError };
|
|
23
23
|
export { SNAPSHOT_CACHE_LIMIT };
|
|
24
24
|
export const STORE_NOT_OPEN_MESSAGE = "Hash store is not open; transactional update aborted";
|
|
25
|
+
export const STORE_SHUT_DOWN_MESSAGE = "Hash store was shut down while it was opening; call loadHashStore again.";
|
|
25
26
|
|
|
26
27
|
type SqlParams = (string | number | null)[];
|
|
27
28
|
|
|
@@ -143,15 +144,16 @@ export interface UndoRecord {
|
|
|
143
144
|
let cachedDb: { path: string; db: RawDb; stmts: Prepared } | null = null;
|
|
144
145
|
let opening: { path: string; promise: Promise<HashStore> } | null = null;
|
|
145
146
|
let exitHandlerRegistered = false;
|
|
147
|
+
let storeEpoch = 0;
|
|
148
|
+
const liveDbs = new Set<RawDb>();
|
|
146
149
|
|
|
147
150
|
function openDb(storePath: string): { db: RawDb; stmts: Prepared } {
|
|
148
151
|
const db = openDbFn(storePath);
|
|
152
|
+
liveDbs.add(db);
|
|
149
153
|
try {
|
|
150
154
|
return buildStore(db);
|
|
151
155
|
} catch (error) {
|
|
152
|
-
|
|
153
|
-
db.close();
|
|
154
|
-
} catch {}
|
|
156
|
+
shutdownDb(db);
|
|
155
157
|
throw error;
|
|
156
158
|
}
|
|
157
159
|
}
|
|
@@ -259,11 +261,15 @@ async function quarantineStore(storePath: string): Promise<void> {
|
|
|
259
261
|
}
|
|
260
262
|
|
|
261
263
|
function shutdownDb(db: RawDb): void {
|
|
264
|
+
if (!liveDbs.delete(db)) return;
|
|
262
265
|
try {
|
|
263
266
|
db.exec("PRAGMA wal_checkpoint(TRUNCATE)");
|
|
264
267
|
} catch {
|
|
265
268
|
}
|
|
266
|
-
|
|
269
|
+
try {
|
|
270
|
+
db.close();
|
|
271
|
+
} catch {
|
|
272
|
+
}
|
|
267
273
|
}
|
|
268
274
|
|
|
269
275
|
async function openStore(storePath: string): Promise<HashStore> {
|
|
@@ -271,6 +277,7 @@ async function openStore(storePath: string): Promise<HashStore> {
|
|
|
271
277
|
return { stmts: cachedDb.stmts, engine: sqliteEngine };
|
|
272
278
|
}
|
|
273
279
|
if (cachedDb) shutdownHashStore();
|
|
280
|
+
const epoch = storeEpoch;
|
|
274
281
|
await initHasher();
|
|
275
282
|
await mkdir(hashStoreDir(), { recursive: true, mode: 0o700 });
|
|
276
283
|
if (process.platform !== "win32") {
|
|
@@ -327,6 +334,10 @@ async function openStore(storePath: string): Promise<HashStore> {
|
|
|
327
334
|
console.error("Hash store migration failed; continuing without legacy import:", error);
|
|
328
335
|
}
|
|
329
336
|
}
|
|
337
|
+
if (storeEpoch !== epoch) {
|
|
338
|
+
shutdownDb(db);
|
|
339
|
+
throw new Error(STORE_SHUT_DOWN_MESSAGE);
|
|
340
|
+
}
|
|
330
341
|
cachedDb = { path: storePath, db, stmts };
|
|
331
342
|
|
|
332
343
|
if (!exitHandlerRegistered) {
|
|
@@ -352,17 +363,20 @@ export function loadHashStore(): Promise<HashStore> {
|
|
|
352
363
|
return opening.promise;
|
|
353
364
|
}
|
|
354
365
|
const promise = openStore(storePath).finally(() => {
|
|
355
|
-
if (opening?.
|
|
366
|
+
if (opening?.promise === promise) opening = null;
|
|
356
367
|
});
|
|
357
368
|
opening = { path: storePath, promise };
|
|
358
369
|
return promise;
|
|
359
370
|
}
|
|
360
371
|
|
|
361
372
|
export function shutdownHashStore(): void {
|
|
373
|
+
storeEpoch += 1;
|
|
362
374
|
if (cachedDb) {
|
|
363
375
|
shutdownDb(cachedDb.db);
|
|
364
376
|
cachedDb = null;
|
|
365
377
|
}
|
|
378
|
+
for (const db of [...liveDbs]) shutdownDb(db);
|
|
379
|
+
opening = null;
|
|
366
380
|
snapshotCache.clear();
|
|
367
381
|
}
|
|
368
382
|
|
|
@@ -426,7 +440,7 @@ async function migrateLegacy(db: RawDb): Promise<void> {
|
|
|
426
440
|
contentChecksum(value.content),
|
|
427
441
|
splitLines(value.content).length,
|
|
428
442
|
JSON.stringify(value.hashes),
|
|
429
|
-
|
|
443
|
+
JSON.stringify(splitLines(value.content).map(lineChecksum)),
|
|
430
444
|
Date.now(),
|
|
431
445
|
]);
|
|
432
446
|
}
|
package/src/hashline/apply.ts
CHANGED
|
@@ -179,7 +179,7 @@ export function planEdit(
|
|
|
179
179
|
): PlannedEdit {
|
|
180
180
|
const signal = options?.signal;
|
|
181
181
|
abortIf(signal);
|
|
182
|
-
const fileLines = options?.baseFileLines ??
|
|
182
|
+
const fileLines = options?.baseFileLines ?? splitLines(content);
|
|
183
183
|
const lineIndex = { fileLines };
|
|
184
184
|
const fileHashes = precomputedHashes;
|
|
185
185
|
const warnings: string[] = [];
|
package/src/hashline/hash.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { splitLines,
|
|
2
|
-
import { MAX_HASH_SOURCE_BYTES } from "../constants";
|
|
1
|
+
import { splitLines, getCached } from "../utils";
|
|
3
2
|
import { loadHashStore, type HashStore } from "../hash-store";
|
|
4
3
|
import { allocateFileAnchors } from "../anchor-registry";
|
|
5
|
-
import { xxh32, initHasher,
|
|
4
|
+
import { xxh32, initHasher, canon, hashSource, lineChecksum } from "./hasher";
|
|
5
|
+
export { canon, hashSource, lineChecksum };
|
|
6
6
|
import { HASH_LEN, ANCHOR_COUNT, anchorAt, HASH_CLASS, HASH_RUN } from "./alphabet";
|
|
7
7
|
export { initHasher, HASH_LEN, HASH_CLASS, HASH_RUN };
|
|
8
8
|
|
|
@@ -48,18 +48,6 @@ export function stripRowPrefix(line: string): StrippedRow {
|
|
|
48
48
|
return { text: line, kind: null, hash: undefined };
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
-
export function canon(line: string): string {
|
|
52
|
-
return line.replace(/\r/g, "").trimEnd();
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
export function hashSource(line: string): string {
|
|
56
|
-
return truncateToBytes(canon(line), MAX_HASH_SOURCE_BYTES);
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
export function lineChecksum(line: string): string {
|
|
60
|
-
return contentChecksum(hashSource(line));
|
|
61
|
-
}
|
|
62
|
-
|
|
63
51
|
const BITSET_WORDS = Math.ceil(HASH_SPACE / 32);
|
|
64
52
|
|
|
65
53
|
function getBit(bits: Uint32Array, idx: number): boolean {
|
package/src/hashline/hasher.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import xxhash from "xxhash-wasm";
|
|
2
|
+
import { truncateToBytes } from "../utils";
|
|
3
|
+
import { MAX_HASH_SOURCE_BYTES } from "../constants";
|
|
2
4
|
|
|
3
5
|
export type Hasher = {
|
|
4
6
|
h32(input: string, seed?: number): number;
|
|
@@ -30,4 +32,16 @@ export function xxh32(input: string, seed = 0): number {
|
|
|
30
32
|
|
|
31
33
|
export function contentChecksum(content: string): string {
|
|
32
34
|
return getH().h64ToString(content);
|
|
33
|
-
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function canon(line: string): string {
|
|
38
|
+
return line.replace(/\r/g, "").trimEnd();
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function hashSource(line: string): string {
|
|
42
|
+
return truncateToBytes(canon(line), MAX_HASH_SOURCE_BYTES);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function lineChecksum(line: string): string {
|
|
46
|
+
return contentChecksum(hashSource(line));
|
|
47
|
+
}
|
package/src/insert.ts
CHANGED
|
@@ -17,7 +17,7 @@ export { assertInsertReq, type InsertReq };
|
|
|
17
17
|
|
|
18
18
|
const insertAnchorSchema = Type.String({
|
|
19
19
|
description:
|
|
20
|
-
'Bare 4-char anchor from a
|
|
20
|
+
'Bare 4-char anchor from a served anchor│content row (the text before the `│` separator), never the row content. A pasted diff row or `anchor│` prefix is stripped with a warning. The anchor line is preserved; lines go after or before it.',
|
|
21
21
|
});
|
|
22
22
|
|
|
23
23
|
const insertDirectionSchema = Type.Union(
|
package/src/paths.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { homedir } from "node:os";
|
|
2
|
-
import { isAbsolute, resolve as resolvePath, join, dirname } from "node:path";
|
|
2
|
+
import { isAbsolute, relative, resolve as resolvePath, join, dirname } from "node:path";
|
|
3
3
|
|
|
4
4
|
|
|
5
5
|
function homeBase(): string {
|
|
@@ -50,3 +50,7 @@ export function toCwd(filePath: string, cwd: string): string {
|
|
|
50
50
|
const expanded = expand(filePath);
|
|
51
51
|
return isAbsolute(expanded) ? expanded : resolvePath(cwd, expanded);
|
|
52
52
|
}
|
|
53
|
+
|
|
54
|
+
export function toDisplayPath(cwd: string, absolutePath: string, fallback?: string): string {
|
|
55
|
+
return relative(cwd, absolutePath).replace(/\\/g, "/") || (fallback ?? absolutePath);
|
|
56
|
+
}
|
package/src/payload-contract.ts
CHANGED
|
@@ -14,12 +14,12 @@ const replacementLinesSchema = Type.Array(
|
|
|
14
14
|
|
|
15
15
|
const removeFromSchema = Type.String({
|
|
16
16
|
description:
|
|
17
|
-
"Bare 4-char anchor from a
|
|
17
|
+
"Bare 4-char anchor from a served anchor│content row (the text before the `│` separator), never the row content. Marks the FIRST line to remove (inclusive)",
|
|
18
18
|
});
|
|
19
19
|
|
|
20
20
|
const removeToSchema = Type.String({
|
|
21
21
|
description:
|
|
22
|
-
"Bare 4-char anchor from a
|
|
22
|
+
"Bare 4-char anchor from a served anchor│content row (the text before the `│` separator), never the row content. Marks the LAST line to remove (inclusive)",
|
|
23
23
|
});
|
|
24
24
|
const pathRequiredSchema = Type.String({
|
|
25
25
|
description:
|
package/src/read.ts
CHANGED
|
@@ -17,8 +17,11 @@ import { abortIf, makePrepareArguments, numberedRead, visLines, splitLines } fro
|
|
|
17
17
|
import { loadP, loadGuide } from "./prompts";
|
|
18
18
|
import { withReadPrompts, DEFAULT_EDIT_FLAGS, type EditToolFlags } from "./edit-common";
|
|
19
19
|
import { valAccess } from "./validation";
|
|
20
|
-
import {
|
|
20
|
+
import { readConfig } from "./config";
|
|
21
|
+
import { resolveTarget } from "./fs-write";
|
|
22
|
+
import { withAnchorSession, servedForPath } from "./anchor-registry";
|
|
21
23
|
import { serveRows } from "./served";
|
|
24
|
+
import { getAutoReadAllSnapshot } from "./auto-read-all-state";
|
|
22
25
|
import { Text } from "@earendil-works/pi-tui";
|
|
23
26
|
const R_DESC = loadP("../prompts/read.md");
|
|
24
27
|
const R_SNIPPET = loadP("../prompts/read-snippet.md");
|
|
@@ -210,6 +213,22 @@ export function regRead(pi: ExtensionAPI, flags: EditToolFlags = DEFAULT_EDIT_FL
|
|
|
210
213
|
|
|
211
214
|
abortIf(signal);
|
|
212
215
|
await valAccess(absolutePath, rawPath);
|
|
216
|
+
const autoReadAllMode = (await readConfig()).autoReadAll ?? "off";
|
|
217
|
+
if (autoReadAllMode !== "off") {
|
|
218
|
+
const canonical = await resolveTarget(absolutePath).catch(() => undefined);
|
|
219
|
+
if (canonical !== undefined) {
|
|
220
|
+
const stored = getAutoReadAllSnapshot(canonical);
|
|
221
|
+
if (stored !== undefined) {
|
|
222
|
+
const current = await safeSnapId(canonical, "auto-read-all guard");
|
|
223
|
+
if (current !== undefined && current === stored) {
|
|
224
|
+
const served = servedForPath(canonical);
|
|
225
|
+
if (served !== undefined && served.size > 0) {
|
|
226
|
+
throw new Error(`[E_AUTO_READ_ALL] ${rawPath} is unchanged since this session's start-of-session auto-read, so the attached content is still exact. Read succeeds on files that have changed since the full auto read.`);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
213
232
|
|
|
214
233
|
abortIf(signal);
|
|
215
234
|
const file = await loadFileKindAndText(absolutePath, { maxLines: MAX_HASH_LINES, displayPath: rawPath });
|