pi-hashline-edit-pro 0.13.1 → 0.13.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/index.ts +11 -22
- package/package.json +1 -1
- package/src/constants.ts +0 -12
- package/src/hashline/apply.ts +2 -42
- package/src/hashline/hash.ts +3 -0
- package/src/hashline/index.ts +0 -1
- package/src/replace-normalize.ts +35 -23
- package/src/replace-response.ts +3 -31
- package/src/replace.ts +6 -3
package/index.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { readFile } from "fs/promises";
|
|
3
3
|
import { join, isAbsolute } from "path";
|
|
4
|
-
import {
|
|
4
|
+
import { initHasher } from "./src/hashline";
|
|
5
5
|
import { regReplace } from "./src/replace";
|
|
6
|
-
import { regRead,
|
|
6
|
+
import { regRead, fmtReadPreview } from "./src/read";
|
|
7
7
|
import { toLF, stripBOM } from "./src/replace-diff";
|
|
8
8
|
import { visLines } from "./src/utils";
|
|
9
9
|
import { AUTO_READ_MAX } from "./src/constants";
|
|
@@ -47,29 +47,18 @@ export default function (pi: ExtensionAPI): void {
|
|
|
47
47
|
const content = await readFile(absolutePath, "utf-8");
|
|
48
48
|
const { text: rawContent } = stripBOM(content);
|
|
49
49
|
const normalized = toLF(rawContent);
|
|
50
|
-
const visibleLines = visLines(normalized);
|
|
51
50
|
|
|
52
|
-
if (
|
|
51
|
+
if (visLines(normalized).length === 0) return;
|
|
53
52
|
|
|
54
|
-
const
|
|
55
|
-
|
|
53
|
+
const preview = fmtReadPreview(normalized, { limit: AUTO_READ_MAX });
|
|
54
|
+
if (!preview.text) return;
|
|
56
55
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
: "";
|
|
64
|
-
|
|
65
|
-
if (hashlineOutput) {
|
|
66
|
-
return {
|
|
67
|
-
content: [
|
|
68
|
-
...(event.content ?? []),
|
|
69
|
-
{ type: "text", text: `\n\n--- Auto-read (hashline anchors) ---\n${hashlineOutput}${paginationHint}` },
|
|
70
|
-
],
|
|
71
|
-
};
|
|
72
|
-
}
|
|
56
|
+
return {
|
|
57
|
+
content: [
|
|
58
|
+
...(event.content ?? []),
|
|
59
|
+
{ type: "text", text: `\n\n--- Auto-read (hashline anchors) ---\n${preview.text}` },
|
|
60
|
+
],
|
|
61
|
+
};
|
|
73
62
|
} catch (error) {
|
|
74
63
|
console.error("Auto-read after write failed:", error);
|
|
75
64
|
}
|
package/package.json
CHANGED
package/src/constants.ts
CHANGED
|
@@ -1,16 +1,4 @@
|
|
|
1
1
|
export const AUTO_READ_MAX = 2000;
|
|
2
|
-
export const ANCHOR_BUDGET = 50 * 1024;
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Context lines of post-edit anchor surfacing. Intentionally 0: after a
|
|
6
|
-
* successful `replace` the response text is empty by design (the model calls
|
|
7
|
-
* `read` for fresh anchors — see "Chained edits" in the README). With 0,
|
|
8
|
-
* `affRange` returns null, so the anchor-block branch in `buildChanged`
|
|
9
|
-
* (`src/replace-response.ts`) is dormant. Set this > 0 (and likely bump
|
|
10
|
-
* MAX_OUT) to revive surfacing a small anchor window after each edit.
|
|
11
|
-
*/
|
|
12
|
-
export const CTX_LINES = 0;
|
|
13
|
-
export const MAX_OUT = 12;
|
|
14
2
|
export const SNIFF_BYTES = 8192;
|
|
15
3
|
export const MAX_BYTES = 100 * 1024 * 1024;
|
|
16
4
|
|
package/src/hashline/apply.ts
CHANGED
|
@@ -12,7 +12,6 @@ import {
|
|
|
12
12
|
type BDupWarn,
|
|
13
13
|
} from "./resolve";
|
|
14
14
|
import { cntLines } from "../utils";
|
|
15
|
-
import { CTX_LINES, MAX_OUT } from "../constants";
|
|
16
15
|
|
|
17
16
|
type LIdx = {
|
|
18
17
|
fileLines: string[];
|
|
@@ -271,6 +270,7 @@ export function applyEdits(
|
|
|
271
270
|
content: string;
|
|
272
271
|
firstChangedLine: number | undefined;
|
|
273
272
|
lastChangedLine: number | undefined;
|
|
273
|
+
resultHashes?: string[];
|
|
274
274
|
warnings?: string[];
|
|
275
275
|
noopEdits?: NEdit[];
|
|
276
276
|
} {
|
|
@@ -350,52 +350,12 @@ export function applyEdits(
|
|
|
350
350
|
content: result,
|
|
351
351
|
firstChangedLine: range?.firstChangedLine,
|
|
352
352
|
lastChangedLine: range?.lastChangedLine,
|
|
353
|
+
resultHashes,
|
|
353
354
|
...(warnings.length ? { warnings } : {}),
|
|
354
355
|
...(noopEdits.length ? { noopEdits } : {}),
|
|
355
356
|
};
|
|
356
357
|
}
|
|
357
358
|
|
|
358
|
-
export function affRange(params: {
|
|
359
|
-
firstChangedLine: number | undefined;
|
|
360
|
-
lastChangedLine: number | undefined;
|
|
361
|
-
resultLineCount: number;
|
|
362
|
-
contextLines?: number;
|
|
363
|
-
maxOutputLines?: number;
|
|
364
|
-
}): { start: number; end: number } | null {
|
|
365
|
-
const {
|
|
366
|
-
firstChangedLine,
|
|
367
|
-
lastChangedLine,
|
|
368
|
-
resultLineCount,
|
|
369
|
-
contextLines = CTX_LINES,
|
|
370
|
-
maxOutputLines = MAX_OUT,
|
|
371
|
-
} = params;
|
|
372
|
-
|
|
373
|
-
if (firstChangedLine === undefined || lastChangedLine === undefined) {
|
|
374
|
-
return null;
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
if (contextLines === 0) {
|
|
378
|
-
return null;
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
if (resultLineCount === 0) {
|
|
382
|
-
return null;
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
const start = Math.max(1, firstChangedLine - contextLines);
|
|
386
|
-
const end = Math.min(resultLineCount, lastChangedLine + contextLines);
|
|
387
|
-
|
|
388
|
-
if (end < start) {
|
|
389
|
-
return null;
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
if (end - start + 1 > maxOutputLines) {
|
|
393
|
-
return null;
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
return { start, end };
|
|
397
|
-
}
|
|
398
|
-
|
|
399
359
|
export function fmtRegion(
|
|
400
360
|
hashes: string[],
|
|
401
361
|
lines: string[],
|
package/src/hashline/hash.ts
CHANGED
package/src/hashline/index.ts
CHANGED
package/src/replace-normalize.ts
CHANGED
|
@@ -9,6 +9,24 @@ function tryParseJSON<T>(value: unknown, guard: (v: unknown) => v is T): T | und
|
|
|
9
9
|
return undefined;
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
+
/**
|
|
13
|
+
* Coerces an array of edit items: JSON-string items → objects,
|
|
14
|
+
* JSON-string content_lines → string arrays. Shared by the `changes`
|
|
15
|
+
* and `edits` normalization branches.
|
|
16
|
+
*/
|
|
17
|
+
function coerceEditArray(items: unknown[]): unknown[] {
|
|
18
|
+
return items
|
|
19
|
+
.map((item: unknown) => tryParseJSON(item, isRec) ?? item)
|
|
20
|
+
.map((change: unknown) => {
|
|
21
|
+
if (!isRec(change)) return change;
|
|
22
|
+
if (typeof change.content_lines !== "string") return change;
|
|
23
|
+
const parsed = tryParseJSON(change.content_lines, (v): v is string[] =>
|
|
24
|
+
Array.isArray(v) && v.every((i) => typeof i === "string"),
|
|
25
|
+
);
|
|
26
|
+
return parsed ? { ...change, content_lines: parsed } : change;
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
|
|
12
30
|
export function normReq(input: unknown): unknown {
|
|
13
31
|
if (!isRec(input)) {
|
|
14
32
|
return input;
|
|
@@ -24,36 +42,30 @@ export function normReq(input: unknown): unknown {
|
|
|
24
42
|
const hasChangesField = has(record, "changes");
|
|
25
43
|
const hasEditsField = has(record, "edits");
|
|
26
44
|
|
|
27
|
-
|
|
45
|
+
if (hasChangesField) {
|
|
28
46
|
const raw = tryParseJSON(record.changes, Array.isArray) ?? record.changes;
|
|
29
47
|
if (Array.isArray(raw)) {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
);
|
|
38
|
-
return parsed ? { ...change, content_lines: parsed } : change;
|
|
39
|
-
});
|
|
48
|
+
record.changes = coerceEditArray(raw);
|
|
49
|
+
} else {
|
|
50
|
+
// Single object (JSON string or literal) → wrap in array
|
|
51
|
+
const single = typeof raw === "string"
|
|
52
|
+
? tryParseJSON(raw, isRec)
|
|
53
|
+
: isRec(raw) ? raw : undefined;
|
|
54
|
+
if (single) record.changes = coerceEditArray([single]);
|
|
40
55
|
}
|
|
41
|
-
|
|
56
|
+
} else if (hasEditsField) {
|
|
42
57
|
const raw = tryParseJSON(record.edits, Array.isArray) ?? record.edits;
|
|
43
58
|
if (Array.isArray(raw)) {
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
Array.isArray(v) && v.every((i) => typeof i === "string"),
|
|
51
|
-
);
|
|
52
|
-
return parsed ? { ...change, content_lines: parsed } : change;
|
|
53
|
-
});
|
|
59
|
+
record.changes = coerceEditArray(raw);
|
|
60
|
+
} else {
|
|
61
|
+
const single = typeof raw === "string"
|
|
62
|
+
? tryParseJSON(raw, isRec)
|
|
63
|
+
: isRec(raw) ? raw : undefined;
|
|
64
|
+
if (single) record.changes = coerceEditArray([single]);
|
|
54
65
|
}
|
|
55
66
|
delete record.edits;
|
|
56
|
-
|
|
67
|
+
}
|
|
57
68
|
|
|
58
69
|
return record;
|
|
59
70
|
}
|
|
71
|
+
|
package/src/replace-response.ts
CHANGED
|
@@ -1,12 +1,9 @@
|
|
|
1
1
|
import type { ReplaceDetails } from "./replace";
|
|
2
2
|
import { genDiff } from "./replace-diff";
|
|
3
3
|
import {
|
|
4
|
-
affRange,
|
|
5
4
|
lineHashes,
|
|
6
|
-
fmtRegion,
|
|
7
5
|
} from "./hashline";
|
|
8
6
|
import { visLines } from "./utils";
|
|
9
|
-
import { ANCHOR_BUDGET } from "./constants";
|
|
10
7
|
|
|
11
8
|
type TResult = {
|
|
12
9
|
content: Array<{ type: "text"; text: string }>;
|
|
@@ -153,34 +150,9 @@ export function buildChanged(input: SuccessInput): TResult {
|
|
|
153
150
|
const addedLines = cntDiff(diffResult.diff, "+");
|
|
154
151
|
const removedLines = cntDiff(diffResult.diff, "-");
|
|
155
152
|
const warningsBlock = warnBlock(warnings);
|
|
156
|
-
const
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
resultLineCount: resultLines.length,
|
|
160
|
-
});
|
|
161
|
-
const anchorsBlock = anchorRange
|
|
162
|
-
? (() => {
|
|
163
|
-
const region = resultLines.slice(
|
|
164
|
-
anchorRange.start - 1,
|
|
165
|
-
anchorRange.end,
|
|
166
|
-
);
|
|
167
|
-
const regionHashes = resultHashes.slice(
|
|
168
|
-
anchorRange.start - 1,
|
|
169
|
-
anchorRange.end,
|
|
170
|
-
);
|
|
171
|
-
const formatted = fmtRegion(regionHashes, region);
|
|
172
|
-
const block = `--- Anchors ---\n${formatted}`;
|
|
173
|
-
return Buffer.byteLength(block, "utf8") <=
|
|
174
|
-
ANCHOR_BUDGET
|
|
175
|
-
? block
|
|
176
|
-
: "Anchors omitted; use read for subsequent edits.";
|
|
177
|
-
})()
|
|
178
|
-
: resultLines.length === 0
|
|
179
|
-
? "File is empty. Use replace to insert content."
|
|
180
|
-
: "";
|
|
181
|
-
const text = [anchorsBlock, warningsBlock.trimStart()]
|
|
182
|
-
.filter((section) => section.length > 0)
|
|
183
|
-
.join("\n\n");
|
|
153
|
+
const text = resultLines.length === 0
|
|
154
|
+
? "File is empty. Use replace to insert content."
|
|
155
|
+
: warningsBlock.trimStart();
|
|
184
156
|
|
|
185
157
|
const metrics = buildM({
|
|
186
158
|
classification: "applied",
|
package/src/replace.ts
CHANGED
|
@@ -99,6 +99,7 @@ interface PipelineResult {
|
|
|
99
99
|
firstChangedLine?: number;
|
|
100
100
|
lastChangedLine?: number;
|
|
101
101
|
originalHashes?: string[];
|
|
102
|
+
resultHashes?: string[];
|
|
102
103
|
}
|
|
103
104
|
|
|
104
105
|
const E_DESC = loadP("../prompts/replace.md");
|
|
@@ -173,6 +174,7 @@ async function execPipeline(
|
|
|
173
174
|
noopEdits: anchorResult.noopEdits,
|
|
174
175
|
firstChangedLine: anchorResult.firstChangedLine,
|
|
175
176
|
lastChangedLine: anchorResult.lastChangedLine,
|
|
177
|
+
resultHashes: anchorResult.resultHashes,
|
|
176
178
|
originalHashes,
|
|
177
179
|
};
|
|
178
180
|
}
|
|
@@ -184,7 +186,7 @@ export async function compPreview(
|
|
|
184
186
|
try {
|
|
185
187
|
const normalized = normReq(request);
|
|
186
188
|
assertReq(normalized);
|
|
187
|
-
const { path, originalNormalized, result } = await execPipeline(
|
|
189
|
+
const { path, originalNormalized, result, resultHashes } = await execPipeline(
|
|
188
190
|
normalized,
|
|
189
191
|
cwd,
|
|
190
192
|
constants.R_OK,
|
|
@@ -196,7 +198,7 @@ export async function compPreview(
|
|
|
196
198
|
};
|
|
197
199
|
}
|
|
198
200
|
|
|
199
|
-
return { diff: genDiff(originalNormalized, result, 4,
|
|
201
|
+
return { diff: genDiff(originalNormalized, result, 4, resultHashes).diff };
|
|
200
202
|
} catch (error: unknown) {
|
|
201
203
|
return { error: error instanceof Error ? error.message : String(error) };
|
|
202
204
|
}
|
|
@@ -340,6 +342,7 @@ const toolDef: ToolDef = {
|
|
|
340
342
|
noopEdits,
|
|
341
343
|
firstChangedLine,
|
|
342
344
|
lastChangedLine,
|
|
345
|
+
resultHashes,
|
|
343
346
|
} = await execPipeline(
|
|
344
347
|
normalizedParams,
|
|
345
348
|
ctx.cwd,
|
|
@@ -390,7 +393,7 @@ const toolDef: ToolDef = {
|
|
|
390
393
|
path,
|
|
391
394
|
originalNormalized,
|
|
392
395
|
result,
|
|
393
|
-
resultHashes
|
|
396
|
+
resultHashes,
|
|
394
397
|
warnings,
|
|
395
398
|
snapshotId: updatedSnapshotId,
|
|
396
399
|
editMeta,
|