@oh-my-pi/pi-coding-agent 13.0.0 → 13.0.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/CHANGELOG.md +7 -0
- package/package.json +7 -7
- package/scripts/format-prompts.ts +33 -3
- package/src/commit/prompts/analysis-system.md +3 -3
- package/src/commit/prompts/changelog-system.md +3 -3
- package/src/commit/prompts/summary-system.md +5 -5
- package/src/extensibility/custom-tools/wrapper.ts +1 -0
- package/src/extensibility/extensions/wrapper.ts +2 -0
- package/src/extensibility/hooks/tool-wrapper.ts +1 -0
- package/src/lsp/index.ts +1 -0
- package/src/patch/diff.ts +2 -2
- package/src/patch/hashline.ts +88 -119
- package/src/patch/index.ts +138 -224
- package/src/patch/shared.ts +21 -35
- package/src/prompts/compaction/compaction-short-summary.md +1 -1
- package/src/prompts/system/agent-creation-architect.md +6 -6
- package/src/prompts/system/system-prompt.md +1 -1
- package/src/prompts/tools/bash.md +1 -1
- package/src/prompts/tools/find.md +9 -0
- package/src/prompts/tools/hashline.md +130 -154
- package/src/prompts/tools/patch.md +1 -1
- package/src/prompts/tools/python.md +2 -2
- package/src/prompts/tools/replace.md +1 -1
- package/src/prompts/tools/task.md +18 -19
- package/src/task/index.ts +1 -0
- package/src/tools/ask.ts +1 -0
- package/src/tools/bash.ts +1 -0
- package/src/tools/browser.ts +1 -0
- package/src/tools/calculator.ts +1 -0
- package/src/tools/cancel-job.ts +1 -0
- package/src/tools/exit-plan-mode.ts +1 -0
- package/src/tools/fetch.ts +1 -0
- package/src/tools/find.ts +1 -0
- package/src/tools/grep.ts +1 -0
- package/src/tools/notebook.ts +1 -0
- package/src/tools/plan-mode-guard.ts +2 -2
- package/src/tools/poll-jobs.ts +1 -0
- package/src/tools/python.ts +1 -0
- package/src/tools/read.ts +1 -0
- package/src/tools/ssh.ts +1 -0
- package/src/tools/submit-result.ts +1 -0
- package/src/tools/todo-write.ts +1 -0
- package/src/tools/write.ts +1 -0
- package/src/web/search/index.ts +1 -0
package/src/patch/index.ts
CHANGED
|
@@ -34,9 +34,8 @@ import { enforcePlanModeWrite, resolvePlanPath } from "../tools/plan-mode-guard"
|
|
|
34
34
|
import { applyPatch } from "./applicator";
|
|
35
35
|
import { generateDiffString, generateUnifiedDiffString, replaceText } from "./diff";
|
|
36
36
|
import { findMatch } from "./fuzzy";
|
|
37
|
-
import { applyHashlineEdits, computeLineHash, type HashlineEdit,
|
|
37
|
+
import { type Anchor, applyHashlineEdits, computeLineHash, type HashlineEdit, parseTag } from "./hashline";
|
|
38
38
|
import { detectLineEnding, normalizeToLF, restoreLineEndings, stripBom } from "./normalize";
|
|
39
|
-
import { buildNormativeUpdateInput } from "./normative";
|
|
40
39
|
import { type EditToolDetails, getLspBatchRequest } from "./shared";
|
|
41
40
|
// Internal imports
|
|
42
41
|
import type { FileSystem, Operation, PatchInput } from "./types";
|
|
@@ -109,14 +108,14 @@ export { ApplyPatchError, EditMatchError, ParseError } from "./types";
|
|
|
109
108
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
110
109
|
|
|
111
110
|
const replaceEditSchema = Type.Object({
|
|
112
|
-
|
|
111
|
+
file: Type.String({ description: "File path (relative or absolute)" }),
|
|
113
112
|
old_text: Type.String({ description: "Text to find (fuzzy whitespace matching enabled)" }),
|
|
114
113
|
new_text: Type.String({ description: "Replacement text" }),
|
|
115
114
|
all: Type.Optional(Type.Boolean({ description: "Replace all occurrences (default: unique match required)" })),
|
|
116
115
|
});
|
|
117
116
|
|
|
118
117
|
const patchEditSchema = Type.Object({
|
|
119
|
-
|
|
118
|
+
file: Type.String({ description: "File path" }),
|
|
120
119
|
op: Type.Optional(
|
|
121
120
|
StringEnum(["create", "delete", "update"], {
|
|
122
121
|
description: "Operation (default: update)",
|
|
@@ -168,25 +167,7 @@ export function stripNewLinePrefixes(lines: string[]): string[] {
|
|
|
168
167
|
});
|
|
169
168
|
}
|
|
170
169
|
|
|
171
|
-
|
|
172
|
-
Type.Union([
|
|
173
|
-
Type.Null(),
|
|
174
|
-
Type.Array(Type.String(), { description: `${kind} lines` }),
|
|
175
|
-
Type.String({ description: `${kind} line` }),
|
|
176
|
-
]);
|
|
177
|
-
|
|
178
|
-
const hashlineInsertContentFormat = (kind: string) =>
|
|
179
|
-
Type.Union([
|
|
180
|
-
Type.Array(Type.String(), { description: `${kind} lines`, minItems: 1 }),
|
|
181
|
-
Type.String({ description: `${kind} line`, minLength: 1 }),
|
|
182
|
-
]);
|
|
183
|
-
|
|
184
|
-
const hashlineTagFormat = (what: string) =>
|
|
185
|
-
Type.String({
|
|
186
|
-
description: `Tag identifying the ${what} in "LINE#ID" format`,
|
|
187
|
-
});
|
|
188
|
-
|
|
189
|
-
export function hashlineParseContent(edit: string | string[] | null): string[] {
|
|
170
|
+
export function hashlineParseText(edit: string[] | string | null): string[] {
|
|
190
171
|
if (edit === null) return [];
|
|
191
172
|
if (Array.isArray(edit)) return edit;
|
|
192
173
|
const lines = stripNewLinePrefixes(edit.split("\n"));
|
|
@@ -194,76 +175,91 @@ export function hashlineParseContent(edit: string | string[] | null): string[] {
|
|
|
194
175
|
if (lines[lines.length - 1].trim() === "") return lines.slice(0, -1);
|
|
195
176
|
return lines;
|
|
196
177
|
}
|
|
197
|
-
const hashlineReplaceTagEditSchema = Type.Object(
|
|
198
|
-
{
|
|
199
|
-
op: Type.Literal("replace"),
|
|
200
|
-
tag: hashlineTagFormat("line being replaced"),
|
|
201
|
-
content: hashlineReplaceContentFormat("Replacement"),
|
|
202
|
-
},
|
|
203
|
-
{ additionalProperties: false },
|
|
204
|
-
);
|
|
205
178
|
|
|
206
|
-
const
|
|
207
|
-
{
|
|
208
|
-
op: Type.Literal("append"),
|
|
209
|
-
after: Type.Optional(hashlineTagFormat("line after which to append")),
|
|
210
|
-
content: hashlineInsertContentFormat("Appended"),
|
|
211
|
-
},
|
|
212
|
-
{ additionalProperties: false },
|
|
213
|
-
);
|
|
214
|
-
|
|
215
|
-
const hashlinePrependEditSchema = Type.Object(
|
|
179
|
+
const hashlineEditSchema = Type.Object(
|
|
216
180
|
{
|
|
217
|
-
op:
|
|
218
|
-
|
|
219
|
-
|
|
181
|
+
op: StringEnum(["replace", "append", "prepend"]),
|
|
182
|
+
pos: Type.Optional(Type.String({ description: "anchor" })),
|
|
183
|
+
end: Type.Optional(Type.String({ description: "limit position" })),
|
|
184
|
+
lines: Type.Union([
|
|
185
|
+
Type.Array(Type.String(), { description: "content (preferred format)" }),
|
|
186
|
+
Type.String(),
|
|
187
|
+
Type.Null(),
|
|
188
|
+
]),
|
|
220
189
|
},
|
|
221
190
|
{ additionalProperties: false },
|
|
222
191
|
);
|
|
223
192
|
|
|
224
|
-
const
|
|
193
|
+
const hashlineEditParamsSchema = Type.Object(
|
|
225
194
|
{
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
195
|
+
file: Type.String({ description: "path" }),
|
|
196
|
+
edits: Type.Array(hashlineEditSchema, { description: "edits over $file" }),
|
|
197
|
+
delete: Type.Optional(Type.Boolean({ description: "If true, delete $file" })),
|
|
198
|
+
move: Type.Optional(Type.String({ description: "If set, move $file to $move" })),
|
|
230
199
|
},
|
|
231
200
|
{ additionalProperties: false },
|
|
232
201
|
);
|
|
233
202
|
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
op: Type.Literal("insert"),
|
|
237
|
-
before: Type.Optional(hashlineTagFormat("line before which to insert")),
|
|
238
|
-
after: Type.Optional(hashlineTagFormat("line after which to insert")),
|
|
239
|
-
content: hashlineInsertContentFormat("Inserted"),
|
|
240
|
-
},
|
|
241
|
-
{ additionalProperties: false },
|
|
242
|
-
);
|
|
203
|
+
export type HashlineToolEdit = Static<typeof hashlineEditSchema>;
|
|
204
|
+
export type HashlineParams = Static<typeof hashlineEditParamsSchema>;
|
|
243
205
|
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
hashlineAppendEditSchema,
|
|
248
|
-
hashlinePrependEditSchema,
|
|
249
|
-
hashlineInsertEditSchema,
|
|
250
|
-
]);
|
|
206
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
207
|
+
// Resilient anchor resolution
|
|
208
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
251
209
|
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
210
|
+
/**
|
|
211
|
+
* Map flat tool-schema edits (tag/end) into typed HashlineEdit objects.
|
|
212
|
+
*
|
|
213
|
+
* Resilient: as long as at least one anchor exists, we execute.
|
|
214
|
+
* - replace + tag only → single-line replace
|
|
215
|
+
* - replace + tag + end → range replace
|
|
216
|
+
* - append + tag or end → append after that anchor
|
|
217
|
+
* - prepend + tag or end → prepend before that anchor
|
|
218
|
+
* - no anchors → file-level append/prepend (only for those ops)
|
|
219
|
+
*
|
|
220
|
+
* Unknown ops default to "replace".
|
|
221
|
+
*/
|
|
222
|
+
function resolveEditAnchors(edits: HashlineToolEdit[]): HashlineEdit[] {
|
|
223
|
+
const result: HashlineEdit[] = [];
|
|
224
|
+
for (const edit of edits) {
|
|
225
|
+
const lines = hashlineParseText(edit.lines);
|
|
226
|
+
const tag = edit.pos ? tryParseTag(edit.pos) : undefined;
|
|
227
|
+
const end = edit.end ? tryParseTag(edit.end) : undefined;
|
|
228
|
+
|
|
229
|
+
// Normalize op — default unknown values to "replace"
|
|
230
|
+
const op = edit.op === "append" || edit.op === "prepend" ? edit.op : "replace";
|
|
231
|
+
switch (op) {
|
|
232
|
+
case "replace": {
|
|
233
|
+
if (tag && end) {
|
|
234
|
+
result.push({ op: "replace", pos: tag, end, lines });
|
|
235
|
+
} else if (tag || end) {
|
|
236
|
+
result.push({ op: "replace", pos: tag || end!, lines });
|
|
237
|
+
} else {
|
|
238
|
+
throw new Error("Replace requires at least one anchor (tag or end).");
|
|
239
|
+
}
|
|
240
|
+
break;
|
|
241
|
+
}
|
|
242
|
+
case "append": {
|
|
243
|
+
result.push({ op: "append", pos: tag ?? end, lines });
|
|
244
|
+
break;
|
|
245
|
+
}
|
|
246
|
+
case "prepend": {
|
|
247
|
+
result.push({ op: "prepend", pos: end ?? tag, lines });
|
|
248
|
+
break;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
return result;
|
|
253
|
+
}
|
|
264
254
|
|
|
265
|
-
|
|
266
|
-
|
|
255
|
+
/** Parse a tag, returning undefined instead of throwing on garbage. */
|
|
256
|
+
function tryParseTag(raw: string): Anchor | undefined {
|
|
257
|
+
try {
|
|
258
|
+
return parseTag(raw);
|
|
259
|
+
} catch {
|
|
260
|
+
return undefined;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
267
263
|
|
|
268
264
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
269
265
|
// LSP FileSystem for patch mode
|
|
@@ -353,11 +349,11 @@ function mergeDiagnosticsWithWarnings(
|
|
|
353
349
|
// Tool Class
|
|
354
350
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
355
351
|
|
|
356
|
-
type TInput = typeof replaceEditSchema | typeof patchEditSchema | typeof
|
|
352
|
+
type TInput = typeof replaceEditSchema | typeof patchEditSchema | typeof hashlineEditParamsSchema;
|
|
357
353
|
|
|
358
354
|
export type EditMode = "replace" | "patch" | "hashline";
|
|
359
355
|
|
|
360
|
-
export const DEFAULT_EDIT_MODE: EditMode = "
|
|
356
|
+
export const DEFAULT_EDIT_MODE: EditMode = "hashline";
|
|
361
357
|
|
|
362
358
|
export function normalizeEditMode(mode?: string | null): EditMode | null {
|
|
363
359
|
switch (mode) {
|
|
@@ -382,6 +378,7 @@ export class EditTool implements AgentTool<TInput> {
|
|
|
382
378
|
readonly label = "Edit";
|
|
383
379
|
readonly nonAbortable = true;
|
|
384
380
|
readonly concurrency = "exclusive";
|
|
381
|
+
readonly strict = true;
|
|
385
382
|
|
|
386
383
|
readonly #allowFuzzy: boolean;
|
|
387
384
|
readonly #fuzzyThreshold: number;
|
|
@@ -473,7 +470,7 @@ export class EditTool implements AgentTool<TInput> {
|
|
|
473
470
|
case "patch":
|
|
474
471
|
return patchEditSchema;
|
|
475
472
|
case "hashline":
|
|
476
|
-
return
|
|
473
|
+
return hashlineEditParamsSchema;
|
|
477
474
|
default:
|
|
478
475
|
return replaceEditSchema;
|
|
479
476
|
}
|
|
@@ -492,21 +489,20 @@ export class EditTool implements AgentTool<TInput> {
|
|
|
492
489
|
// Hashline mode execution
|
|
493
490
|
// ─────────────────────────────────────────────────────────────────
|
|
494
491
|
if (this.mode === "hashline") {
|
|
495
|
-
const { path, edits, delete: deleteFile,
|
|
492
|
+
const { file: path, edits, delete: deleteFile, move } = params as HashlineParams;
|
|
496
493
|
|
|
497
|
-
enforcePlanModeWrite(this.session, path, { op: deleteFile ? "delete" : "update",
|
|
494
|
+
enforcePlanModeWrite(this.session, path, { op: deleteFile ? "delete" : "update", move });
|
|
498
495
|
|
|
499
496
|
if (path.endsWith(".ipynb") && edits?.length > 0) {
|
|
500
497
|
throw new Error("Cannot edit Jupyter notebooks with the Edit tool. Use the NotebookEdit tool instead.");
|
|
501
498
|
}
|
|
502
499
|
|
|
503
500
|
const absolutePath = resolvePlanPath(this.session, path);
|
|
504
|
-
const
|
|
505
|
-
const file = Bun.file(absolutePath);
|
|
501
|
+
const resolvedMove = move ? resolvePlanPath(this.session, move) : undefined;
|
|
506
502
|
|
|
507
503
|
if (deleteFile) {
|
|
508
|
-
if (await
|
|
509
|
-
await
|
|
504
|
+
if (await fs.exists(absolutePath)) {
|
|
505
|
+
await fs.unlink(absolutePath);
|
|
510
506
|
}
|
|
511
507
|
invalidateFsScanAfterDelete(absolutePath);
|
|
512
508
|
return {
|
|
@@ -519,30 +515,21 @@ export class EditTool implements AgentTool<TInput> {
|
|
|
519
515
|
};
|
|
520
516
|
}
|
|
521
517
|
|
|
522
|
-
if (!(await
|
|
523
|
-
const
|
|
518
|
+
if (!(await fs.exists(absolutePath))) {
|
|
519
|
+
const lines: string[] = [];
|
|
524
520
|
for (const edit of edits) {
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
break;
|
|
532
|
-
}
|
|
533
|
-
case "prepend": {
|
|
534
|
-
if (edit.before) {
|
|
535
|
-
throw new Error(`File not found: ${path}`);
|
|
536
|
-
}
|
|
537
|
-
content.unshift(...hashlineParseContent(edit.content));
|
|
538
|
-
break;
|
|
539
|
-
}
|
|
540
|
-
default: {
|
|
541
|
-
throw new Error(`File not found: ${path}`);
|
|
521
|
+
// For file creation, only anchorless appends/prepends are valid
|
|
522
|
+
if ((edit.op === "append" || edit.op === "prepend") && !edit.pos && !edit.end) {
|
|
523
|
+
if (edit.op === "prepend") {
|
|
524
|
+
lines.unshift(...hashlineParseText(edit.lines));
|
|
525
|
+
} else {
|
|
526
|
+
lines.push(...hashlineParseText(edit.lines));
|
|
542
527
|
}
|
|
528
|
+
} else {
|
|
529
|
+
throw new Error(`File not found: ${path}`);
|
|
543
530
|
}
|
|
544
531
|
}
|
|
545
|
-
await
|
|
532
|
+
await fs.writeFile(absolutePath, lines.join("\n"));
|
|
546
533
|
return {
|
|
547
534
|
content: [{ type: "text", text: `Created ${path}` }],
|
|
548
535
|
details: {
|
|
@@ -553,98 +540,31 @@ export class EditTool implements AgentTool<TInput> {
|
|
|
553
540
|
};
|
|
554
541
|
}
|
|
555
542
|
|
|
556
|
-
const anchorEdits
|
|
557
|
-
for (const edit of edits) {
|
|
558
|
-
switch (edit.op) {
|
|
559
|
-
case "replace": {
|
|
560
|
-
if ("tag" in edit) {
|
|
561
|
-
anchorEdits.push({
|
|
562
|
-
op: "replace",
|
|
563
|
-
tag: parseTag(edit.tag),
|
|
564
|
-
content: hashlineParseContent(edit.content),
|
|
565
|
-
});
|
|
566
|
-
} else {
|
|
567
|
-
anchorEdits.push({
|
|
568
|
-
op: "replace",
|
|
569
|
-
first: parseTag(edit.first),
|
|
570
|
-
last: parseTag(edit.last),
|
|
571
|
-
content: hashlineParseContent(edit.content),
|
|
572
|
-
});
|
|
573
|
-
}
|
|
574
|
-
break;
|
|
575
|
-
}
|
|
576
|
-
case "append": {
|
|
577
|
-
const { after, content } = edit;
|
|
578
|
-
anchorEdits.push({
|
|
579
|
-
op: "append",
|
|
580
|
-
...(after ? { after: parseTag(after) } : {}),
|
|
581
|
-
content: hashlineParseContent(content),
|
|
582
|
-
});
|
|
583
|
-
break;
|
|
584
|
-
}
|
|
585
|
-
case "prepend": {
|
|
586
|
-
const { before, content } = edit;
|
|
587
|
-
anchorEdits.push({
|
|
588
|
-
op: "prepend",
|
|
589
|
-
...(before ? { before: parseTag(before) } : {}),
|
|
590
|
-
content: hashlineParseContent(content),
|
|
591
|
-
});
|
|
592
|
-
break;
|
|
593
|
-
}
|
|
594
|
-
case "insert": {
|
|
595
|
-
const { before, after, content } = edit;
|
|
596
|
-
if (before && !after) {
|
|
597
|
-
anchorEdits.push({
|
|
598
|
-
op: "prepend",
|
|
599
|
-
before: parseTag(before),
|
|
600
|
-
content: hashlineParseContent(content),
|
|
601
|
-
});
|
|
602
|
-
} else if (after && !before) {
|
|
603
|
-
anchorEdits.push({
|
|
604
|
-
op: "append",
|
|
605
|
-
after: parseTag(after),
|
|
606
|
-
content: hashlineParseContent(content),
|
|
607
|
-
});
|
|
608
|
-
} else if (before && after) {
|
|
609
|
-
anchorEdits.push({
|
|
610
|
-
op: "insert",
|
|
611
|
-
before: parseTag(before),
|
|
612
|
-
after: parseTag(after),
|
|
613
|
-
content: hashlineParseContent(content),
|
|
614
|
-
});
|
|
615
|
-
} else {
|
|
616
|
-
throw new Error(`Insert must have both before and after tags.`);
|
|
617
|
-
}
|
|
618
|
-
break;
|
|
619
|
-
}
|
|
620
|
-
default:
|
|
621
|
-
throw new Error(`Invalid edit operation: ${JSON.stringify(edit)}`);
|
|
622
|
-
}
|
|
623
|
-
}
|
|
543
|
+
const anchorEdits = resolveEditAnchors(edits);
|
|
624
544
|
|
|
625
|
-
const rawContent = await
|
|
626
|
-
const { bom, text
|
|
627
|
-
const originalEnding = detectLineEnding(
|
|
628
|
-
const originalNormalized = normalizeToLF(
|
|
629
|
-
let
|
|
545
|
+
const rawContent = await fs.readFile(absolutePath, "utf-8");
|
|
546
|
+
const { bom, text } = stripBom(rawContent);
|
|
547
|
+
const originalEnding = detectLineEnding(text);
|
|
548
|
+
const originalNormalized = normalizeToLF(text);
|
|
549
|
+
let normalizedText = originalNormalized;
|
|
630
550
|
|
|
631
|
-
// Apply anchor-based edits first (
|
|
632
|
-
const anchorResult = applyHashlineEdits(
|
|
633
|
-
|
|
551
|
+
// Apply anchor-based edits first (replace, append, prepend)
|
|
552
|
+
const anchorResult = applyHashlineEdits(normalizedText, anchorEdits);
|
|
553
|
+
normalizedText = anchorResult.lines;
|
|
634
554
|
|
|
635
555
|
const result = {
|
|
636
|
-
|
|
556
|
+
text: normalizedText,
|
|
637
557
|
firstChangedLine: anchorResult.firstChangedLine,
|
|
638
558
|
warnings: anchorResult.warnings,
|
|
639
559
|
noopEdits: anchorResult.noopEdits,
|
|
640
560
|
};
|
|
641
|
-
if (originalNormalized === result.
|
|
561
|
+
if (originalNormalized === result.text && !move) {
|
|
642
562
|
let diagnostic = `No changes made to ${path}. The edits produced identical content.`;
|
|
643
563
|
if (result.noopEdits && result.noopEdits.length > 0) {
|
|
644
564
|
const details = result.noopEdits
|
|
645
565
|
.map(
|
|
646
566
|
e =>
|
|
647
|
-
`Edit ${e.editIndex}: replacement for ${e.loc} is identical to current content:\n ${e.loc}| ${e.
|
|
567
|
+
`Edit ${e.editIndex}: replacement for ${e.loc} is identical to current content:\n ${e.loc}| ${e.current}`,
|
|
648
568
|
)
|
|
649
569
|
.join("\n");
|
|
650
570
|
diagnostic += `\n${details}`;
|
|
@@ -652,27 +572,24 @@ export class EditTool implements AgentTool<TInput> {
|
|
|
652
572
|
"\nYour content must differ from what the file already contains. Re-read the file to see the current state.";
|
|
653
573
|
} else {
|
|
654
574
|
// Edits were not literally identical but heuristics normalized them back
|
|
655
|
-
const lines = result.
|
|
575
|
+
const lines = result.text.split("\n");
|
|
656
576
|
const targetLines: string[] = [];
|
|
657
|
-
const refs:
|
|
577
|
+
const refs: Anchor[] = [];
|
|
658
578
|
for (const edit of anchorEdits) {
|
|
659
579
|
refs.length = 0;
|
|
660
580
|
switch (edit.op) {
|
|
661
581
|
case "replace":
|
|
662
|
-
if (
|
|
663
|
-
refs.push(edit.
|
|
582
|
+
if (edit.end) {
|
|
583
|
+
refs.push(edit.end, edit.pos);
|
|
664
584
|
} else {
|
|
665
|
-
refs.push(edit.
|
|
585
|
+
refs.push(edit.pos);
|
|
666
586
|
}
|
|
667
587
|
break;
|
|
668
588
|
case "append":
|
|
669
|
-
if (edit.
|
|
589
|
+
if (edit.pos) refs.push(edit.pos);
|
|
670
590
|
break;
|
|
671
591
|
case "prepend":
|
|
672
|
-
if (edit.
|
|
673
|
-
break;
|
|
674
|
-
case "insert":
|
|
675
|
-
refs.push(edit.after, edit.before);
|
|
592
|
+
if (edit.pos) refs.push(edit.pos);
|
|
676
593
|
break;
|
|
677
594
|
default:
|
|
678
595
|
break;
|
|
@@ -681,9 +598,9 @@ export class EditTool implements AgentTool<TInput> {
|
|
|
681
598
|
for (const ref of refs) {
|
|
682
599
|
try {
|
|
683
600
|
if (ref.line >= 1 && ref.line <= lines.length) {
|
|
684
|
-
const
|
|
685
|
-
const hash = computeLineHash(ref.line,
|
|
686
|
-
targetLines.push(`${ref.line}#${hash}:${
|
|
601
|
+
const text = lines[ref.line - 1];
|
|
602
|
+
const hash = computeLineHash(ref.line, text);
|
|
603
|
+
targetLines.push(`${ref.line}#${hash}:${text}`);
|
|
687
604
|
}
|
|
688
605
|
} catch {
|
|
689
606
|
/* skip malformed refs */
|
|
@@ -698,8 +615,8 @@ export class EditTool implements AgentTool<TInput> {
|
|
|
698
615
|
throw new Error(diagnostic);
|
|
699
616
|
}
|
|
700
617
|
|
|
701
|
-
const finalContent = bom + restoreLineEndings(result.
|
|
702
|
-
const writePath =
|
|
618
|
+
const finalContent = bom + restoreLineEndings(result.text, originalEnding);
|
|
619
|
+
const writePath = resolvedMove ?? absolutePath;
|
|
703
620
|
const diagnostics = await this.#writethrough(
|
|
704
621
|
writePath,
|
|
705
622
|
finalContent,
|
|
@@ -707,26 +624,19 @@ export class EditTool implements AgentTool<TInput> {
|
|
|
707
624
|
Bun.file(writePath),
|
|
708
625
|
batchRequest,
|
|
709
626
|
);
|
|
710
|
-
if (
|
|
711
|
-
await
|
|
712
|
-
invalidateFsScanAfterRename(absolutePath,
|
|
627
|
+
if (resolvedMove && resolvedMove !== absolutePath) {
|
|
628
|
+
await fs.unlink(absolutePath);
|
|
629
|
+
invalidateFsScanAfterRename(absolutePath, resolvedMove);
|
|
713
630
|
} else {
|
|
714
631
|
invalidateFsScanAfterWrite(absolutePath);
|
|
715
632
|
}
|
|
716
|
-
const diffResult = generateDiffString(originalNormalized, result.
|
|
717
|
-
|
|
718
|
-
const normative = buildNormativeUpdateInput({
|
|
719
|
-
path,
|
|
720
|
-
...(rename ? { rename } : {}),
|
|
721
|
-
oldContent: rawContent,
|
|
722
|
-
newContent: finalContent,
|
|
723
|
-
});
|
|
633
|
+
const diffResult = generateDiffString(originalNormalized, result.text);
|
|
724
634
|
|
|
725
635
|
const meta = outputMeta()
|
|
726
636
|
.diagnostics(diagnostics?.summary ?? "", diagnostics?.messages ?? [])
|
|
727
637
|
.get();
|
|
728
638
|
|
|
729
|
-
const resultText =
|
|
639
|
+
const resultText = move ? `Moved ${path} to ${move}` : `Updated ${path}`;
|
|
730
640
|
return {
|
|
731
641
|
content: [
|
|
732
642
|
{
|
|
@@ -739,10 +649,9 @@ export class EditTool implements AgentTool<TInput> {
|
|
|
739
649
|
firstChangedLine: result.firstChangedLine ?? diffResult.firstChangedLine,
|
|
740
650
|
diagnostics,
|
|
741
651
|
op: "update",
|
|
742
|
-
|
|
652
|
+
move,
|
|
743
653
|
meta,
|
|
744
654
|
},
|
|
745
|
-
$normative: normative,
|
|
746
655
|
};
|
|
747
656
|
}
|
|
748
657
|
|
|
@@ -750,12 +659,12 @@ export class EditTool implements AgentTool<TInput> {
|
|
|
750
659
|
// Patch mode execution
|
|
751
660
|
// ─────────────────────────────────────────────────────────────────
|
|
752
661
|
if (this.mode === "patch") {
|
|
753
|
-
const { path, op: rawOp, rename, diff } = params as PatchParams;
|
|
662
|
+
const { file: path, op: rawOp, rename, diff } = params as PatchParams;
|
|
754
663
|
|
|
755
664
|
// Normalize unrecognized operations to "update"
|
|
756
665
|
const op: Operation = rawOp === "create" || rawOp === "delete" ? rawOp : "update";
|
|
757
666
|
|
|
758
|
-
enforcePlanModeWrite(this.session, path, { op, rename });
|
|
667
|
+
enforcePlanModeWrite(this.session, path, { op, move: rename });
|
|
759
668
|
const resolvedPath = resolvePlanPath(this.session, path);
|
|
760
669
|
const resolvedRename = rename ? resolvePlanPath(this.session, rename) : undefined;
|
|
761
670
|
|
|
@@ -823,7 +732,7 @@ export class EditTool implements AgentTool<TInput> {
|
|
|
823
732
|
firstChangedLine: diffResult.firstChangedLine,
|
|
824
733
|
diagnostics: mergedDiagnostics,
|
|
825
734
|
op,
|
|
826
|
-
|
|
735
|
+
move: effRename,
|
|
827
736
|
meta,
|
|
828
737
|
},
|
|
829
738
|
};
|
|
@@ -832,7 +741,7 @@ export class EditTool implements AgentTool<TInput> {
|
|
|
832
741
|
// ─────────────────────────────────────────────────────────────────
|
|
833
742
|
// Replace mode execution
|
|
834
743
|
// ─────────────────────────────────────────────────────────────────
|
|
835
|
-
const { path, old_text, new_text, all } = params as ReplaceParams;
|
|
744
|
+
const { file: path, old_text, new_text, all } = params as ReplaceParams;
|
|
836
745
|
|
|
837
746
|
enforcePlanModeWrite(this.session, path);
|
|
838
747
|
|
|
@@ -845,13 +754,12 @@ export class EditTool implements AgentTool<TInput> {
|
|
|
845
754
|
}
|
|
846
755
|
|
|
847
756
|
const absolutePath = resolvePlanPath(this.session, path);
|
|
848
|
-
const file = Bun.file(absolutePath);
|
|
849
757
|
|
|
850
|
-
if (!(await
|
|
758
|
+
if (!(await fs.exists(absolutePath))) {
|
|
851
759
|
throw new Error(`File not found: ${path}`);
|
|
852
760
|
}
|
|
853
761
|
|
|
854
|
-
const rawContent = await
|
|
762
|
+
const rawContent = await fs.readFile(absolutePath, "utf-8");
|
|
855
763
|
const { bom, text: content } = stripBom(rawContent);
|
|
856
764
|
const originalEnding = detectLineEnding(content);
|
|
857
765
|
const normalizedContent = normalizeToLF(content);
|
|
@@ -894,7 +802,13 @@ export class EditTool implements AgentTool<TInput> {
|
|
|
894
802
|
}
|
|
895
803
|
|
|
896
804
|
const finalContent = bom + restoreLineEndings(result.content, originalEnding);
|
|
897
|
-
const diagnostics = await this.#writethrough(
|
|
805
|
+
const diagnostics = await this.#writethrough(
|
|
806
|
+
absolutePath,
|
|
807
|
+
finalContent,
|
|
808
|
+
signal,
|
|
809
|
+
Bun.file(absolutePath),
|
|
810
|
+
batchRequest,
|
|
811
|
+
);
|
|
898
812
|
invalidateFsScanAfterWrite(absolutePath);
|
|
899
813
|
const diffResult = generateDiffString(normalizedContent, result.content);
|
|
900
814
|
|