pi-hashline-edit-pro 0.13.0 → 0.13.2
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 +21 -32
- package/package.json +1 -1
- package/src/constants.ts +0 -12
- package/src/hashline/apply.ts +2 -42
- package/src/hashline/hash.ts +3 -3
- package/src/hashline/index.ts +0 -2
- package/src/hashline/resolve.ts +29 -30
- package/src/replace-normalize.ts +27 -45
- package/src/replace-response.ts +3 -31
- package/src/replace.ts +46 -55
package/index.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
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,
|
|
7
|
-
import { toLF } from "./src/replace-diff";
|
|
6
|
+
import { regRead, fmtReadPreview } from "./src/read";
|
|
7
|
+
import { toLF, stripBOM } from "./src/replace-diff";
|
|
8
8
|
import { visLines } from "./src/utils";
|
|
9
9
|
import { AUTO_READ_MAX } from "./src/constants";
|
|
10
10
|
|
|
@@ -12,10 +12,15 @@ export default function (pi: ExtensionAPI): void {
|
|
|
12
12
|
regRead(pi);
|
|
13
13
|
regReplace(pi);
|
|
14
14
|
|
|
15
|
+
const debugValue = process.env.PI_HASHLINE_DEBUG;
|
|
16
|
+
|
|
15
17
|
pi.on("session_start", async (_event, ctx) => {
|
|
16
18
|
const active = pi.getActiveTools();
|
|
17
19
|
pi.setActiveTools(active.filter((t) => t !== "edit"));
|
|
18
20
|
await initHasher();
|
|
21
|
+
if (debugValue === "1" || debugValue === "true") {
|
|
22
|
+
ctx.ui.notify("Hashline Edit mode active", "info");
|
|
23
|
+
}
|
|
19
24
|
});
|
|
20
25
|
|
|
21
26
|
const autoReadValue = process.env.PI_HASHLINE_AUTO_READ;
|
|
@@ -40,39 +45,23 @@ export default function (pi: ExtensionAPI): void {
|
|
|
40
45
|
try {
|
|
41
46
|
const absolutePath = isAbsolute(filePath) ? filePath : join(ctx.cwd, filePath);
|
|
42
47
|
const content = await readFile(absolutePath, "utf-8");
|
|
48
|
+
const { text: rawContent } = stripBOM(content);
|
|
49
|
+
const normalized = toLF(rawContent);
|
|
43
50
|
|
|
44
|
-
|
|
45
|
-
const visibleLines = visLines(normalized);
|
|
46
|
-
|
|
47
|
-
if (visibleLines.length === 0) return;
|
|
51
|
+
if (visLines(normalized).length === 0) return;
|
|
48
52
|
|
|
49
|
-
const
|
|
50
|
-
|
|
53
|
+
const preview = fmtReadPreview(normalized, { limit: AUTO_READ_MAX });
|
|
54
|
+
if (!preview.text) return;
|
|
51
55
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
if (hashlineOutput) {
|
|
61
|
-
return {
|
|
62
|
-
content: [
|
|
63
|
-
...(event.content ?? []),
|
|
64
|
-
{ type: "text", text: `\n\n--- Auto-read (hashline anchors) ---\n${hashlineOutput}${paginationHint}` },
|
|
65
|
-
],
|
|
66
|
-
};
|
|
67
|
-
}
|
|
68
|
-
} catch {
|
|
56
|
+
return {
|
|
57
|
+
content: [
|
|
58
|
+
...(event.content ?? []),
|
|
59
|
+
{ type: "text", text: `\n\n--- Auto-read (hashline anchors) ---\n${preview.text}` },
|
|
60
|
+
],
|
|
61
|
+
};
|
|
62
|
+
} catch (error) {
|
|
63
|
+
console.error("Auto-read after write failed:", error);
|
|
69
64
|
}
|
|
70
65
|
});
|
|
71
66
|
|
|
72
|
-
const debugValue = process.env.PI_HASHLINE_DEBUG;
|
|
73
|
-
if (debugValue === "1" || debugValue === "true") {
|
|
74
|
-
pi.on("session_start", async (_event, ctx) => {
|
|
75
|
-
ctx.ui.notify("Hashline Edit mode active", "info");
|
|
76
|
-
});
|
|
77
|
-
}
|
|
78
67
|
}
|
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
|
@@ -55,6 +55,9 @@ function getH(): Hasher {
|
|
|
55
55
|
hasherP = xxhash().then((h) => {
|
|
56
56
|
hasher = h;
|
|
57
57
|
return h;
|
|
58
|
+
}).catch((err) => {
|
|
59
|
+
console.error("xxhash-wasm initialization failed:", err);
|
|
60
|
+
throw err;
|
|
58
61
|
});
|
|
59
62
|
|
|
60
63
|
export function initHasher(): Promise<Hasher> {
|
|
@@ -98,8 +101,5 @@ export function lineHashes(content: string): string[] {
|
|
|
98
101
|
return hashes;
|
|
99
102
|
}
|
|
100
103
|
|
|
101
|
-
export function lineHash(idx: number, line: string): string {
|
|
102
|
-
return h2s(xxh32(canon(line)));
|
|
103
|
-
}
|
|
104
104
|
|
|
105
105
|
export { ALPH_RE };
|
package/src/hashline/index.ts
CHANGED
package/src/hashline/resolve.ts
CHANGED
|
@@ -222,6 +222,31 @@ export function descEdit(edit: RHEdit): string {
|
|
|
222
222
|
return `replace ${edit.hash_range_inclusive[0].hash}-${edit.hash_range_inclusive[1].hash}`;
|
|
223
223
|
}
|
|
224
224
|
|
|
225
|
+
function checkBoundaryDup(
|
|
226
|
+
adjacentLine: string | undefined,
|
|
227
|
+
replacementEdge: string | undefined,
|
|
228
|
+
kind: "trailing" | "leading",
|
|
229
|
+
survivingLineIndex: number,
|
|
230
|
+
fileLines: string[],
|
|
231
|
+
editIndex: number,
|
|
232
|
+
): BDupWarn | null {
|
|
233
|
+
if (
|
|
234
|
+
adjacentLine === undefined ||
|
|
235
|
+
replacementEdge === undefined ||
|
|
236
|
+
replacementEdge.length === 0 ||
|
|
237
|
+
replacementEdge !== adjacentLine
|
|
238
|
+
) return null;
|
|
239
|
+
return {
|
|
240
|
+
kind,
|
|
241
|
+
survivingLineContent: adjacentLine,
|
|
242
|
+
survivingLineIndex,
|
|
243
|
+
occurrence: fileLines.slice(0, survivingLineIndex).filter((l) => l === adjacentLine).length,
|
|
244
|
+
replacementLineContent: replacementEdge,
|
|
245
|
+
editIndex,
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
|
|
225
250
|
export function valEdits(
|
|
226
251
|
edits: HEdit[],
|
|
227
252
|
fileLines: string[],
|
|
@@ -258,38 +283,12 @@ export function valEdits(
|
|
|
258
283
|
const endLine = endResolved.line;
|
|
259
284
|
const nextLine = fileLines[endLine];
|
|
260
285
|
const replacementLastLine = edit.content_lines.at(-1);
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
replacementLastLine !== undefined &&
|
|
264
|
-
replacementLastLine.length > 0 &&
|
|
265
|
-
replacementLastLine === nextLine
|
|
266
|
-
) {
|
|
267
|
-
boundaryWarnings.push({
|
|
268
|
-
kind: "trailing",
|
|
269
|
-
survivingLineContent: nextLine,
|
|
270
|
-
survivingLineIndex: endLine,
|
|
271
|
-
occurrence: fileLines.slice(0, endLine).filter(l => l === nextLine).length,
|
|
272
|
-
replacementLineContent: replacementLastLine,
|
|
273
|
-
editIndex: resolved.length,
|
|
274
|
-
});
|
|
275
|
-
}
|
|
286
|
+
const trailing = checkBoundaryDup(nextLine, replacementLastLine, "trailing", endLine, fileLines, resolved.length);
|
|
287
|
+
if (trailing) boundaryWarnings.push(trailing);
|
|
276
288
|
const prevLine = fileLines[startResolved.line - 2];
|
|
277
289
|
const replacementFirstLine = edit.content_lines[0];
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
replacementFirstLine !== undefined &&
|
|
281
|
-
replacementFirstLine.length > 0 &&
|
|
282
|
-
replacementFirstLine === prevLine
|
|
283
|
-
) {
|
|
284
|
-
boundaryWarnings.push({
|
|
285
|
-
kind: "leading",
|
|
286
|
-
survivingLineContent: prevLine,
|
|
287
|
-
survivingLineIndex: startResolved.line - 2,
|
|
288
|
-
occurrence: fileLines.slice(0, startResolved.line - 2).filter(l => l === prevLine).length,
|
|
289
|
-
replacementLineContent: replacementFirstLine,
|
|
290
|
-
editIndex: resolved.length,
|
|
291
|
-
});
|
|
292
|
-
}
|
|
290
|
+
const leading = checkBoundaryDup(prevLine, replacementFirstLine, "leading", startResolved.line - 2, fileLines, resolved.length);
|
|
291
|
+
if (leading) boundaryWarnings.push(leading);
|
|
293
292
|
resolved.push({
|
|
294
293
|
hash_range_inclusive: [startResolved, endResolved],
|
|
295
294
|
content_lines: edit.content_lines,
|
package/src/replace-normalize.ts
CHANGED
|
@@ -1,46 +1,30 @@
|
|
|
1
1
|
import { isRec, has } from "./utils";
|
|
2
2
|
|
|
3
|
-
function
|
|
4
|
-
if (typeof
|
|
5
|
-
return changes;
|
|
6
|
-
}
|
|
3
|
+
function tryParseJSON<T>(value: unknown, guard: (v: unknown) => v is T): T | undefined {
|
|
4
|
+
if (typeof value !== "string") return undefined;
|
|
7
5
|
try {
|
|
8
|
-
const parsed: unknown = JSON.parse(
|
|
9
|
-
|
|
10
|
-
} catch {
|
|
11
|
-
|
|
12
|
-
}
|
|
6
|
+
const parsed: unknown = JSON.parse(value);
|
|
7
|
+
if (guard(parsed)) return parsed;
|
|
8
|
+
} catch {}
|
|
9
|
+
return undefined;
|
|
13
10
|
}
|
|
14
11
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
function coerceChangeItems(changes: unknown): unknown {
|
|
33
|
-
if (!Array.isArray(changes)) return changes;
|
|
34
|
-
return changes.map((item: unknown) => {
|
|
35
|
-
if (typeof item !== "string") return item;
|
|
36
|
-
try {
|
|
37
|
-
const parsed: unknown = JSON.parse(item);
|
|
38
|
-
if (isRec(parsed)) return parsed;
|
|
39
|
-
} catch {
|
|
40
|
-
// not valid JSON, leave as-is for downstream validation
|
|
41
|
-
}
|
|
42
|
-
return item;
|
|
43
|
-
});
|
|
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
|
+
});
|
|
44
28
|
}
|
|
45
29
|
|
|
46
30
|
export function normReq(input: unknown): unknown {
|
|
@@ -59,16 +43,14 @@ export function normReq(input: unknown): unknown {
|
|
|
59
43
|
const hasEditsField = has(record, "edits");
|
|
60
44
|
|
|
61
45
|
if (hasChangesField) {
|
|
62
|
-
record.changes
|
|
63
|
-
record.changes =
|
|
64
|
-
record.changes = coerceContentLines(record.changes);
|
|
46
|
+
const raw = tryParseJSON(record.changes, Array.isArray) ?? record.changes;
|
|
47
|
+
if (Array.isArray(raw)) record.changes = coerceEditArray(raw);
|
|
65
48
|
} else if (hasEditsField) {
|
|
66
|
-
|
|
67
|
-
record.changes =
|
|
68
|
-
record.changes = coerceChangeItems(record.changes);
|
|
69
|
-
record.changes = coerceContentLines(record.changes);
|
|
49
|
+
const raw = tryParseJSON(record.edits, Array.isArray) ?? record.edits;
|
|
50
|
+
if (Array.isArray(raw)) record.changes = coerceEditArray(raw);
|
|
70
51
|
delete record.edits;
|
|
71
52
|
}
|
|
72
53
|
|
|
73
54
|
return record;
|
|
74
55
|
}
|
|
56
|
+
|
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
|
@@ -86,6 +86,22 @@ export type ReplaceDetails = {
|
|
|
86
86
|
metrics?: RMetrics;
|
|
87
87
|
};
|
|
88
88
|
|
|
89
|
+
interface PipelineResult {
|
|
90
|
+
path: string;
|
|
91
|
+
toolEdits: HTEdit[];
|
|
92
|
+
originalNormalized: string;
|
|
93
|
+
result: string;
|
|
94
|
+
bom: string;
|
|
95
|
+
originalEnding: "\r\n" | "\n";
|
|
96
|
+
hadUtf8DecodeErrors: boolean;
|
|
97
|
+
warnings: string[];
|
|
98
|
+
noopEdits?: { editIndex: number; loc: string; currentContent: string }[];
|
|
99
|
+
firstChangedLine?: number;
|
|
100
|
+
lastChangedLine?: number;
|
|
101
|
+
originalHashes?: string[];
|
|
102
|
+
resultHashes?: string[];
|
|
103
|
+
}
|
|
104
|
+
|
|
89
105
|
const E_DESC = loadP("../prompts/replace.md");
|
|
90
106
|
const E_SNIPPET = loadP("../prompts/replace-snippet.md");
|
|
91
107
|
const E_GUIDE = loadGuide("../prompts/replace-guidelines.md");
|
|
@@ -122,20 +138,7 @@ async function execPipeline(
|
|
|
122
138
|
cwd: string,
|
|
123
139
|
accessMode: number,
|
|
124
140
|
signal?: AbortSignal,
|
|
125
|
-
): Promise<{
|
|
126
|
-
path: string;
|
|
127
|
-
toolEdits: HTEdit[];
|
|
128
|
-
originalNormalized: string;
|
|
129
|
-
result: string;
|
|
130
|
-
bom: string;
|
|
131
|
-
originalEnding: "\r\n" | "\n";
|
|
132
|
-
hadUtf8DecodeErrors: boolean;
|
|
133
|
-
warnings: string[];
|
|
134
|
-
noopEdits?: { editIndex: number; loc: string; currentContent: string }[];
|
|
135
|
-
firstChangedLine?: number;
|
|
136
|
-
lastChangedLine?: number;
|
|
137
|
-
originalHashes?: string[];
|
|
138
|
-
}> {
|
|
141
|
+
): Promise<PipelineResult> {
|
|
139
142
|
|
|
140
143
|
const path = params.path;
|
|
141
144
|
const toolEdits = Array.isArray(params.changes)
|
|
@@ -171,6 +174,7 @@ async function execPipeline(
|
|
|
171
174
|
noopEdits: anchorResult.noopEdits,
|
|
172
175
|
firstChangedLine: anchorResult.firstChangedLine,
|
|
173
176
|
lastChangedLine: anchorResult.lastChangedLine,
|
|
177
|
+
resultHashes: anchorResult.resultHashes,
|
|
174
178
|
originalHashes,
|
|
175
179
|
};
|
|
176
180
|
}
|
|
@@ -182,7 +186,7 @@ export async function compPreview(
|
|
|
182
186
|
try {
|
|
183
187
|
const normalized = normReq(request);
|
|
184
188
|
assertReq(normalized);
|
|
185
|
-
const { path, originalNormalized, result } = await execPipeline(
|
|
189
|
+
const { path, originalNormalized, result, resultHashes } = await execPipeline(
|
|
186
190
|
normalized,
|
|
187
191
|
cwd,
|
|
188
192
|
constants.R_OK,
|
|
@@ -194,7 +198,7 @@ export async function compPreview(
|
|
|
194
198
|
};
|
|
195
199
|
}
|
|
196
200
|
|
|
197
|
-
return { diff: genDiff(originalNormalized, result, 4,
|
|
201
|
+
return { diff: genDiff(originalNormalized, result, 4, resultHashes).diff };
|
|
198
202
|
} catch (error: unknown) {
|
|
199
203
|
return { error: error instanceof Error ? error.message : String(error) };
|
|
200
204
|
}
|
|
@@ -205,6 +209,21 @@ type ToolDef = ToolDefinition<
|
|
|
205
209
|
ReplaceDetails,
|
|
206
210
|
RRState
|
|
207
211
|
> & { renderShell?: "default" | "self" };
|
|
212
|
+
function reuseText(context: any, content: string): Text {
|
|
213
|
+
const t = context.lastComponent instanceof Text
|
|
214
|
+
? context.lastComponent
|
|
215
|
+
: new Text("", 0, 0);
|
|
216
|
+
t.setText(content);
|
|
217
|
+
return t;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function reuseMarkdown(context: any, content: string, theme: any): Markdown {
|
|
221
|
+
const m = context.lastComponent instanceof Markdown
|
|
222
|
+
? context.lastComponent
|
|
223
|
+
: new Markdown("", 0, 0, mkMdTheme(theme));
|
|
224
|
+
m.setText(content);
|
|
225
|
+
return m;
|
|
226
|
+
}
|
|
208
227
|
|
|
209
228
|
const toolDef: ToolDef = {
|
|
210
229
|
name: "replace",
|
|
@@ -273,10 +292,7 @@ const toolDef: ToolDef = {
|
|
|
273
292
|
|
|
274
293
|
renderResult(result, { isPartial }, theme, context) {
|
|
275
294
|
if (isPartial) {
|
|
276
|
-
|
|
277
|
-
(context.lastComponent as Text | undefined) ?? new Text("", 0, 0);
|
|
278
|
-
text.setText(theme.fg("warning", "Editing..."));
|
|
279
|
-
return text;
|
|
295
|
+
return reuseText(context, theme.fg("warning", "Editing..."));
|
|
280
296
|
}
|
|
281
297
|
|
|
282
298
|
const typedResult = result as {
|
|
@@ -292,44 +308,18 @@ const toolDef: ToolDef = {
|
|
|
292
308
|
}
|
|
293
309
|
|
|
294
310
|
if (context.isError) {
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
const text =
|
|
299
|
-
context.lastComponent instanceof Text
|
|
300
|
-
? context.lastComponent
|
|
301
|
-
: new Text("", 0, 0);
|
|
302
|
-
text.setText(`\n${theme.fg("error", renderedText)}`);
|
|
303
|
-
return text;
|
|
311
|
+
return renderedText
|
|
312
|
+
? reuseText(context, `\n${theme.fg("error", renderedText)}`)
|
|
313
|
+
: new Text("", 0, 0);
|
|
304
314
|
}
|
|
305
315
|
|
|
306
316
|
if (isApplied(typedResult.details)) {
|
|
307
|
-
const
|
|
308
|
-
|
|
309
|
-
typedResult.details,
|
|
310
|
-
theme,
|
|
311
|
-
);
|
|
312
|
-
if (!appliedChangedText) {
|
|
313
|
-
return new Text("", 0, 0);
|
|
314
|
-
}
|
|
315
|
-
const text =
|
|
316
|
-
context.lastComponent instanceof Text
|
|
317
|
-
? context.lastComponent
|
|
318
|
-
: new Text("", 0, 0);
|
|
319
|
-
text.setText(appliedChangedText);
|
|
320
|
-
return text;
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
if (!renderedText) {
|
|
324
|
-
return new Text("", 0, 0);
|
|
317
|
+
const appliedText = buildAppliedText(renderedText, typedResult.details, theme);
|
|
318
|
+
return appliedText ? reuseText(context, appliedText) : new Text("", 0, 0);
|
|
325
319
|
}
|
|
326
320
|
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
? context.lastComponent
|
|
330
|
-
: new Markdown("", 0, 0, mkMdTheme(theme));
|
|
331
|
-
markdown.setText(fmtResultMd(renderedText));
|
|
332
|
-
return markdown;
|
|
321
|
+
if (!renderedText) return new Text("", 0, 0);
|
|
322
|
+
return reuseMarkdown(context, fmtResultMd(renderedText), theme);
|
|
333
323
|
},
|
|
334
324
|
|
|
335
325
|
async execute(_toolCallId, params, signal, _onUpdate, ctx) {
|
|
@@ -352,8 +342,9 @@ const toolDef: ToolDef = {
|
|
|
352
342
|
noopEdits,
|
|
353
343
|
firstChangedLine,
|
|
354
344
|
lastChangedLine,
|
|
345
|
+
resultHashes,
|
|
355
346
|
} = await execPipeline(
|
|
356
|
-
|
|
347
|
+
normalizedParams,
|
|
357
348
|
ctx.cwd,
|
|
358
349
|
constants.R_OK | constants.W_OK,
|
|
359
350
|
signal,
|
|
@@ -402,7 +393,7 @@ const toolDef: ToolDef = {
|
|
|
402
393
|
path,
|
|
403
394
|
originalNormalized,
|
|
404
395
|
result,
|
|
405
|
-
resultHashes
|
|
396
|
+
resultHashes,
|
|
406
397
|
warnings,
|
|
407
398
|
snapshotId: updatedSnapshotId,
|
|
408
399
|
editMeta,
|