@oh-my-pi/pi-coding-agent 12.13.0 → 12.14.0
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 +62 -0
- package/package.json +8 -7
- package/scripts/generate-docs-index.ts +56 -0
- package/src/config/prompt-templates.ts +2 -2
- package/src/config/settings-schema.ts +10 -1
- package/src/discovery/builtin.ts +14 -4
- package/src/extensibility/extensions/types.ts +1 -0
- package/src/internal-urls/docs-index.generated.ts +101 -0
- package/src/internal-urls/docs-protocol.ts +84 -0
- package/src/internal-urls/index.ts +1 -0
- package/src/internal-urls/router.ts +1 -1
- package/src/modes/controllers/event-controller.ts +20 -0
- package/src/patch/diff.ts +1 -1
- package/src/patch/hashline.ts +197 -328
- package/src/patch/index.ts +325 -102
- package/src/patch/shared.ts +23 -40
- package/src/prompts/system/system-prompt.md +13 -2
- package/src/prompts/tools/bash.md +1 -1
- package/src/prompts/tools/grep.md +1 -1
- package/src/prompts/tools/hashline.md +192 -90
- package/src/prompts/tools/read.md +3 -1
- package/src/sdk.ts +17 -0
- package/src/session/agent-session.ts +1 -0
- package/src/tools/fetch.ts +4 -3
- package/src/tools/grep.ts +13 -3
- package/src/tools/read.ts +2 -2
- package/src/web/search/render.ts +2 -2
package/src/patch/index.ts
CHANGED
|
@@ -34,7 +34,14 @@ 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 {
|
|
37
|
+
import {
|
|
38
|
+
applyHashlineEdits,
|
|
39
|
+
computeLineHash,
|
|
40
|
+
type HashlineEdit,
|
|
41
|
+
type LineTag,
|
|
42
|
+
parseTag,
|
|
43
|
+
type ReplaceTextEdit,
|
|
44
|
+
} from "./hashline";
|
|
38
45
|
import { detectLineEnding, normalizeToLF, restoreLineEndings, stripBom } from "./normalize";
|
|
39
46
|
import { buildNormativeUpdateInput } from "./normative";
|
|
40
47
|
import { type EditToolDetails, getLspBatchRequest } from "./shared";
|
|
@@ -66,7 +73,7 @@ export {
|
|
|
66
73
|
computeLineHash,
|
|
67
74
|
formatHashLines,
|
|
68
75
|
HashlineMismatchError,
|
|
69
|
-
|
|
76
|
+
parseTag,
|
|
70
77
|
streamHashLinesFromLines,
|
|
71
78
|
streamHashLinesFromUtf8,
|
|
72
79
|
validateLineRef,
|
|
@@ -129,63 +136,160 @@ const patchEditSchema = Type.Object({
|
|
|
129
136
|
export type ReplaceParams = Static<typeof replaceEditSchema>;
|
|
130
137
|
export type PatchParams = Static<typeof patchEditSchema>;
|
|
131
138
|
|
|
132
|
-
|
|
139
|
+
/** Pattern matching hashline display format: `LINE#ID:CONTENT` */
|
|
140
|
+
const HASHLINE_PREFIX_RE = /^\s*(?:>>>|>>)?\s*\d+#[0-9a-zA-Z]{1,16}:/;
|
|
141
|
+
|
|
142
|
+
/** Pattern matching a unified-diff `+` prefix (but not `++`) */
|
|
143
|
+
const DIFF_PLUS_RE = /^[+-](?![+-])/;
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Strip hashline display prefixes and diff `+` markers from replacement lines.
|
|
147
|
+
*
|
|
148
|
+
* Models frequently copy the `LINE#ID ` prefix from read output into their
|
|
149
|
+
* replacement content, or include unified-diff `+` prefixes. Both corrupt the
|
|
150
|
+
* output file. This strips them heuristically before application.
|
|
151
|
+
*/
|
|
152
|
+
function stripNewLinePrefixes(lines: string[]): string[] {
|
|
153
|
+
// Detect whether the *majority* of non-empty lines carry a prefix —
|
|
154
|
+
// if only one line out of many has a match it's likely real content.
|
|
155
|
+
let hashPrefixCount = 0;
|
|
156
|
+
let diffPlusCount = 0;
|
|
157
|
+
let nonEmpty = 0;
|
|
158
|
+
for (const l of lines) {
|
|
159
|
+
if (l.length === 0) continue;
|
|
160
|
+
nonEmpty++;
|
|
161
|
+
if (HASHLINE_PREFIX_RE.test(l)) hashPrefixCount++;
|
|
162
|
+
if (DIFF_PLUS_RE.test(l)) diffPlusCount++;
|
|
163
|
+
}
|
|
164
|
+
if (nonEmpty === 0) return lines;
|
|
165
|
+
|
|
166
|
+
const stripHash = hashPrefixCount > 0 && hashPrefixCount >= nonEmpty * 0.5;
|
|
167
|
+
const stripPlus = !stripHash && diffPlusCount > 0 && diffPlusCount >= nonEmpty * 0.5;
|
|
168
|
+
|
|
169
|
+
if (!stripHash && !stripPlus) return lines;
|
|
170
|
+
|
|
171
|
+
return lines.map(l => {
|
|
172
|
+
if (stripHash) return l.replace(HASHLINE_PREFIX_RE, "");
|
|
173
|
+
if (stripPlus) return l.replace(DIFF_PLUS_RE, "");
|
|
174
|
+
return l;
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const hashlineReplaceContentFormat = (kind: string) =>
|
|
179
|
+
Type.Union([
|
|
180
|
+
Type.Null(),
|
|
181
|
+
Type.Array(Type.String(), { description: `${kind} lines` }),
|
|
182
|
+
Type.String({ description: `${kind} line` }),
|
|
183
|
+
]);
|
|
184
|
+
|
|
185
|
+
const hashlineInsertContentFormat = (kind: string) =>
|
|
186
|
+
Type.Union([
|
|
187
|
+
Type.Array(Type.String(), { description: `${kind} lines`, minItems: 1 }),
|
|
188
|
+
Type.String({ description: `${kind} line`, minLength: 1 }),
|
|
189
|
+
]);
|
|
190
|
+
|
|
191
|
+
const hashlineTagFormat = (what: string) =>
|
|
192
|
+
Type.String({
|
|
193
|
+
description: `Tag identifying the ${what} in "LINE#ID" format`,
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
function hashlineParseContent(edit: string | string[] | null): string[] {
|
|
197
|
+
if (edit === null) return [];
|
|
198
|
+
if (Array.isArray(edit)) return edit;
|
|
199
|
+
const lines = stripNewLinePrefixes(edit.split("\n"));
|
|
200
|
+
if (lines.length === 0) return [];
|
|
201
|
+
if (lines[lines.length - 1].trim() === "") return lines.slice(0, -1);
|
|
202
|
+
return lines;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function hashlineParseContentString(edit: string | string[] | null): string {
|
|
206
|
+
if (edit === null) return "";
|
|
207
|
+
if (Array.isArray(edit)) return edit.join("\n");
|
|
208
|
+
return edit;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const hashlineTargetEditSchema = Type.Object(
|
|
133
212
|
{
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
}),
|
|
213
|
+
op: Type.Literal("set"),
|
|
214
|
+
tag: hashlineTagFormat("line being replaced"),
|
|
215
|
+
content: hashlineReplaceContentFormat("Replacement"),
|
|
138
216
|
},
|
|
139
|
-
{ additionalProperties:
|
|
217
|
+
{ additionalProperties: false },
|
|
140
218
|
);
|
|
141
219
|
|
|
142
|
-
const
|
|
220
|
+
const hashlineAppendEditSchema = Type.Object(
|
|
143
221
|
{
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
body: Type.Array(Type.String(), { description: "Replacement lines (empty array to delete)" }),
|
|
148
|
-
}),
|
|
222
|
+
op: Type.Literal("append"),
|
|
223
|
+
after: Type.Optional(hashlineTagFormat("line after which to append")),
|
|
224
|
+
content: hashlineInsertContentFormat("Appended"),
|
|
149
225
|
},
|
|
150
|
-
{ additionalProperties:
|
|
226
|
+
{ additionalProperties: false },
|
|
151
227
|
);
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
{
|
|
155
|
-
insert: Type.Object({
|
|
156
|
-
before: Type.Optional(Type.String({ minLength: 1, description: 'Insert before this line "LINE#ID"' })),
|
|
157
|
-
after: Type.Optional(Type.String({ minLength: 1, description: 'Insert after this line "LINE#ID"' })),
|
|
158
|
-
body: Type.Array(Type.String(), { description: "Lines to insert; must be non-empty" }),
|
|
159
|
-
}),
|
|
160
|
-
},
|
|
161
|
-
{ additionalProperties: true },
|
|
162
|
-
),
|
|
163
|
-
]);
|
|
164
|
-
const hashlineReplaceSchema = Type.Object(
|
|
228
|
+
|
|
229
|
+
const hashlinePrependEditSchema = Type.Object(
|
|
165
230
|
{
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
231
|
+
op: Type.Literal("prepend"),
|
|
232
|
+
before: Type.Optional(hashlineTagFormat("line before which to prepend")),
|
|
233
|
+
content: hashlineInsertContentFormat("Prepended"),
|
|
234
|
+
},
|
|
235
|
+
{ additionalProperties: false },
|
|
236
|
+
);
|
|
237
|
+
|
|
238
|
+
const hashlineRangeEditSchema = Type.Object(
|
|
239
|
+
{
|
|
240
|
+
op: Type.Literal("replace"),
|
|
241
|
+
first: hashlineTagFormat("first line"),
|
|
242
|
+
last: hashlineTagFormat("last line"),
|
|
243
|
+
content: hashlineReplaceContentFormat("Replacement"),
|
|
244
|
+
},
|
|
245
|
+
{ additionalProperties: false },
|
|
246
|
+
);
|
|
247
|
+
|
|
248
|
+
const hashlineInsertEditSchema = Type.Object(
|
|
249
|
+
{
|
|
250
|
+
op: Type.Literal("insert"),
|
|
251
|
+
before: Type.Optional(hashlineTagFormat("line before which to insert")),
|
|
252
|
+
after: Type.Optional(hashlineTagFormat("line after which to insert")),
|
|
253
|
+
content: hashlineInsertContentFormat("Inserted"),
|
|
254
|
+
},
|
|
255
|
+
{ additionalProperties: false },
|
|
256
|
+
);
|
|
257
|
+
|
|
258
|
+
const hashlineReplaceTextEditSchema = Type.Object(
|
|
259
|
+
{
|
|
260
|
+
op: Type.Literal("replaceText"),
|
|
261
|
+
old_text: Type.String({ description: "Text to find", minLength: 1 }),
|
|
262
|
+
new_text: hashlineReplaceContentFormat("Replacement"),
|
|
263
|
+
all: Type.Optional(Type.Boolean({ description: "Replace all occurrences" })),
|
|
171
264
|
},
|
|
172
|
-
{ additionalProperties:
|
|
265
|
+
{ additionalProperties: false },
|
|
173
266
|
);
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
267
|
+
|
|
268
|
+
const HL_REPLACE_ENABLED = Bun.env.PI_HL_REPLACETXT === "1";
|
|
269
|
+
|
|
270
|
+
const hashlineEditSpecSchema = Type.Union([
|
|
271
|
+
hashlineTargetEditSchema,
|
|
272
|
+
hashlineRangeEditSchema,
|
|
273
|
+
hashlineAppendEditSchema,
|
|
274
|
+
hashlinePrependEditSchema,
|
|
275
|
+
hashlineInsertEditSchema,
|
|
276
|
+
...(HL_REPLACE_ENABLED ? [hashlineReplaceTextEditSchema] : []),
|
|
179
277
|
]);
|
|
278
|
+
|
|
180
279
|
const hashlineEditSchema = Type.Object(
|
|
181
280
|
{
|
|
182
281
|
path: Type.String({ description: "File path (relative or absolute)" }),
|
|
183
|
-
edits: Type.Array(
|
|
282
|
+
edits: Type.Array(hashlineEditSpecSchema, {
|
|
283
|
+
description: "Changes to apply to the file at `path`",
|
|
284
|
+
minItems: 0,
|
|
285
|
+
}),
|
|
286
|
+
delete: Type.Optional(Type.Boolean({ description: "Delete the file when true" })),
|
|
287
|
+
rename: Type.Optional(Type.String({ description: "New path if moving" })),
|
|
184
288
|
},
|
|
185
|
-
{ additionalProperties:
|
|
289
|
+
{ additionalProperties: false },
|
|
186
290
|
);
|
|
187
291
|
|
|
188
|
-
export type
|
|
292
|
+
export type HashlineToolEdit = Static<typeof hashlineEditSpecSchema>;
|
|
189
293
|
export type HashlineParams = Static<typeof hashlineEditSchema>;
|
|
190
294
|
|
|
191
295
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
@@ -382,7 +486,7 @@ export class EditTool implements AgentTool<TInput> {
|
|
|
382
486
|
case "patch":
|
|
383
487
|
return renderPromptTemplate(patchDescription);
|
|
384
488
|
case "hashline":
|
|
385
|
-
return renderPromptTemplate(hashlineDescription);
|
|
489
|
+
return renderPromptTemplate(hashlineDescription, { allowReplaceText: HL_REPLACE_ENABLED });
|
|
386
490
|
default:
|
|
387
491
|
return renderPromptTemplate(replaceDescription);
|
|
388
492
|
}
|
|
@@ -415,46 +519,143 @@ export class EditTool implements AgentTool<TInput> {
|
|
|
415
519
|
// Hashline mode execution
|
|
416
520
|
// ─────────────────────────────────────────────────────────────────
|
|
417
521
|
if (this.mode === "hashline") {
|
|
418
|
-
const { path, edits } = params as HashlineParams;
|
|
522
|
+
const { path, edits, delete: deleteFile, rename } = params as HashlineParams;
|
|
419
523
|
|
|
420
|
-
enforcePlanModeWrite(this.session, path);
|
|
524
|
+
enforcePlanModeWrite(this.session, path, { op: deleteFile ? "delete" : "update", rename });
|
|
421
525
|
|
|
422
|
-
if (path.endsWith(".ipynb")) {
|
|
526
|
+
if (path.endsWith(".ipynb") && edits?.length > 0) {
|
|
423
527
|
throw new Error("Cannot edit Jupyter notebooks with the Edit tool. Use the NotebookEdit tool instead.");
|
|
424
528
|
}
|
|
425
529
|
|
|
426
|
-
// Detect wrong-format fields from models confusing edit modes
|
|
427
|
-
for (let i = 0; i < edits.length; i++) {
|
|
428
|
-
const edit = edits[i] as Record<string, unknown>;
|
|
429
|
-
if (("old_text" in edit || "new_text" in edit) && !("replace" in edit)) {
|
|
430
|
-
throw new Error(
|
|
431
|
-
`edits[${i}] contains 'old_text'/'new_text' at top level (replace mode). ` +
|
|
432
|
-
`Use {replace: {old_text, new_text}} for hashline content replace, or {set}, {set_range}, {insert}.`,
|
|
433
|
-
);
|
|
434
|
-
}
|
|
435
|
-
if ("diff" in edit) {
|
|
436
|
-
throw new Error(
|
|
437
|
-
`edits[${i}] contains 'diff' field from patch mode. ` +
|
|
438
|
-
`Hashline edits use: {set}, {set_range}, {insert}, or {replace}.`,
|
|
439
|
-
);
|
|
440
|
-
}
|
|
441
|
-
if (!("set" in edit) && !("set_range" in edit) && !("insert" in edit) && !("replace" in edit)) {
|
|
442
|
-
throw new Error(
|
|
443
|
-
`edits[${i}] must contain exactly one of: 'set', 'set_range', 'insert', or 'replace'. Got keys: [${Object.keys(edit).join(", ")}].`,
|
|
444
|
-
);
|
|
445
|
-
}
|
|
446
|
-
}
|
|
447
|
-
|
|
448
|
-
const anchorEdits = edits.filter((e): e is HashlineEdit => "set" in e || "set_range" in e || "insert" in e);
|
|
449
|
-
const replaceEdits = edits.filter(
|
|
450
|
-
(e): e is { replace: { old_text: string; new_text: string; all?: boolean } } => "replace" in e,
|
|
451
|
-
);
|
|
452
|
-
|
|
453
530
|
const absolutePath = resolvePlanPath(this.session, path);
|
|
531
|
+
const resolvedRename = rename ? resolvePlanPath(this.session, rename) : undefined;
|
|
454
532
|
const file = Bun.file(absolutePath);
|
|
455
533
|
|
|
534
|
+
if (deleteFile) {
|
|
535
|
+
if (await file.exists()) {
|
|
536
|
+
await file.unlink();
|
|
537
|
+
}
|
|
538
|
+
invalidateFsScanAfterDelete(absolutePath);
|
|
539
|
+
return {
|
|
540
|
+
content: [{ type: "text", text: `Deleted ${path}` }],
|
|
541
|
+
details: {
|
|
542
|
+
diff: "",
|
|
543
|
+
op: "delete",
|
|
544
|
+
meta: outputMeta().get(),
|
|
545
|
+
},
|
|
546
|
+
};
|
|
547
|
+
}
|
|
548
|
+
|
|
456
549
|
if (!(await file.exists())) {
|
|
457
|
-
|
|
550
|
+
const content: string[] = [];
|
|
551
|
+
for (const edit of edits) {
|
|
552
|
+
switch (edit.op) {
|
|
553
|
+
case "append": {
|
|
554
|
+
if (edit.after) {
|
|
555
|
+
throw new Error(`File not found: ${path}`);
|
|
556
|
+
}
|
|
557
|
+
content.push(...hashlineParseContent(edit.content));
|
|
558
|
+
break;
|
|
559
|
+
}
|
|
560
|
+
case "prepend": {
|
|
561
|
+
if (edit.before) {
|
|
562
|
+
throw new Error(`File not found: ${path}`);
|
|
563
|
+
}
|
|
564
|
+
content.unshift(...hashlineParseContent(edit.content));
|
|
565
|
+
break;
|
|
566
|
+
}
|
|
567
|
+
default: {
|
|
568
|
+
throw new Error(`File not found: ${path}`);
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
await file.write(content.join("\n"));
|
|
573
|
+
return {
|
|
574
|
+
content: [{ type: "text", text: `Created ${path}` }],
|
|
575
|
+
details: {
|
|
576
|
+
diff: "",
|
|
577
|
+
op: "create",
|
|
578
|
+
meta: outputMeta().get(),
|
|
579
|
+
},
|
|
580
|
+
};
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
const anchorEdits: HashlineEdit[] = [];
|
|
584
|
+
const replaceEdits: ReplaceTextEdit[] = [];
|
|
585
|
+
for (const edit of edits) {
|
|
586
|
+
switch (edit.op) {
|
|
587
|
+
case "set": {
|
|
588
|
+
const { tag, content } = edit;
|
|
589
|
+
anchorEdits.push({ op: "set", tag: parseTag(tag), content: hashlineParseContent(content) });
|
|
590
|
+
break;
|
|
591
|
+
}
|
|
592
|
+
case "replace": {
|
|
593
|
+
const { first, last, content } = edit;
|
|
594
|
+
anchorEdits.push({
|
|
595
|
+
op: "replace",
|
|
596
|
+
first: parseTag(first),
|
|
597
|
+
last: parseTag(last),
|
|
598
|
+
content: hashlineParseContent(content),
|
|
599
|
+
});
|
|
600
|
+
break;
|
|
601
|
+
}
|
|
602
|
+
case "append": {
|
|
603
|
+
const { after, content } = edit;
|
|
604
|
+
anchorEdits.push({
|
|
605
|
+
op: "append",
|
|
606
|
+
...(after ? { after: parseTag(after) } : {}),
|
|
607
|
+
content: hashlineParseContent(content),
|
|
608
|
+
});
|
|
609
|
+
break;
|
|
610
|
+
}
|
|
611
|
+
case "prepend": {
|
|
612
|
+
const { before, content } = edit;
|
|
613
|
+
anchorEdits.push({
|
|
614
|
+
op: "prepend",
|
|
615
|
+
...(before ? { before: parseTag(before) } : {}),
|
|
616
|
+
content: hashlineParseContent(content),
|
|
617
|
+
});
|
|
618
|
+
break;
|
|
619
|
+
}
|
|
620
|
+
case "insert": {
|
|
621
|
+
const { before, after, content } = edit;
|
|
622
|
+
if (before && !after) {
|
|
623
|
+
anchorEdits.push({
|
|
624
|
+
op: "prepend",
|
|
625
|
+
before: parseTag(before),
|
|
626
|
+
content: hashlineParseContent(content),
|
|
627
|
+
});
|
|
628
|
+
} else if (after && !before) {
|
|
629
|
+
anchorEdits.push({
|
|
630
|
+
op: "append",
|
|
631
|
+
after: parseTag(after),
|
|
632
|
+
content: hashlineParseContent(content),
|
|
633
|
+
});
|
|
634
|
+
} else if (before && after) {
|
|
635
|
+
anchorEdits.push({
|
|
636
|
+
op: "insert",
|
|
637
|
+
before: parseTag(before),
|
|
638
|
+
after: parseTag(after),
|
|
639
|
+
content: hashlineParseContent(content),
|
|
640
|
+
});
|
|
641
|
+
} else {
|
|
642
|
+
throw new Error(`Insert must have both before and after tags.`);
|
|
643
|
+
}
|
|
644
|
+
break;
|
|
645
|
+
}
|
|
646
|
+
case "replaceText": {
|
|
647
|
+
const { old_text, new_text, all } = edit;
|
|
648
|
+
replaceEdits.push({
|
|
649
|
+
op: "replaceText",
|
|
650
|
+
old_text: old_text,
|
|
651
|
+
new_text: hashlineParseContentString(new_text),
|
|
652
|
+
all: all ?? false,
|
|
653
|
+
});
|
|
654
|
+
break;
|
|
655
|
+
}
|
|
656
|
+
default:
|
|
657
|
+
throw new Error(`Invalid edit operation: ${JSON.stringify(edit)}`);
|
|
658
|
+
}
|
|
458
659
|
}
|
|
459
660
|
|
|
460
661
|
const rawContent = await file.text();
|
|
@@ -469,12 +670,12 @@ export class EditTool implements AgentTool<TInput> {
|
|
|
469
670
|
|
|
470
671
|
// Apply content-replace edits (substr-style fuzzy replace)
|
|
471
672
|
for (const r of replaceEdits) {
|
|
472
|
-
if (r.
|
|
473
|
-
throw new Error("
|
|
673
|
+
if (r.old_text.length === 0) {
|
|
674
|
+
throw new Error("old_text must not be empty.");
|
|
474
675
|
}
|
|
475
|
-
const rep = replaceText(normalizedContent, r.
|
|
676
|
+
const rep = replaceText(normalizedContent, r.old_text, r.new_text, {
|
|
476
677
|
fuzzy: this.#allowFuzzy,
|
|
477
|
-
all: r.
|
|
678
|
+
all: r.all ?? false,
|
|
478
679
|
threshold: this.#fuzzyThreshold,
|
|
479
680
|
});
|
|
480
681
|
normalizedContent = rep.content;
|
|
@@ -486,7 +687,7 @@ export class EditTool implements AgentTool<TInput> {
|
|
|
486
687
|
warnings: anchorResult.warnings,
|
|
487
688
|
noopEdits: anchorResult.noopEdits,
|
|
488
689
|
};
|
|
489
|
-
if (originalNormalized === result.content) {
|
|
690
|
+
if (originalNormalized === result.content && !rename) {
|
|
490
691
|
let diagnostic = `No changes made to ${path}. The edits produced identical content.`;
|
|
491
692
|
if (result.noopEdits && result.noopEdits.length > 0) {
|
|
492
693
|
const details = result.noopEdits
|
|
@@ -502,21 +703,35 @@ export class EditTool implements AgentTool<TInput> {
|
|
|
502
703
|
// Edits were not literally identical but heuristics normalized them back
|
|
503
704
|
const lines = result.content.split("\n");
|
|
504
705
|
const targetLines: string[] = [];
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
706
|
+
const refs: LineTag[] = [];
|
|
707
|
+
for (const edit of anchorEdits) {
|
|
708
|
+
refs.length = 0;
|
|
709
|
+
switch (edit.op) {
|
|
710
|
+
case "set":
|
|
711
|
+
refs.push(edit.tag);
|
|
712
|
+
break;
|
|
713
|
+
case "replace":
|
|
714
|
+
refs.push(edit.first, edit.last);
|
|
715
|
+
break;
|
|
716
|
+
case "append":
|
|
717
|
+
if (edit.after) refs.push(edit.after);
|
|
718
|
+
break;
|
|
719
|
+
case "prepend":
|
|
720
|
+
if (edit.before) refs.push(edit.before);
|
|
721
|
+
break;
|
|
722
|
+
case "insert":
|
|
723
|
+
refs.push(edit.after, edit.before);
|
|
724
|
+
break;
|
|
725
|
+
default:
|
|
726
|
+
break;
|
|
512
727
|
}
|
|
728
|
+
|
|
513
729
|
for (const ref of refs) {
|
|
514
730
|
try {
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
const
|
|
518
|
-
|
|
519
|
-
targetLines.push(`${parsed.line}#${hash}|${lineContent}`);
|
|
731
|
+
if (ref.line >= 1 && ref.line <= lines.length) {
|
|
732
|
+
const lineContent = lines[ref.line - 1];
|
|
733
|
+
const hash = computeLineHash(ref.line, lineContent);
|
|
734
|
+
targetLines.push(`${ref.line}#${hash}:${lineContent}`);
|
|
520
735
|
}
|
|
521
736
|
} catch {
|
|
522
737
|
/* skip malformed refs */
|
|
@@ -532,12 +747,25 @@ export class EditTool implements AgentTool<TInput> {
|
|
|
532
747
|
}
|
|
533
748
|
|
|
534
749
|
const finalContent = bom + restoreLineEndings(result.content, originalEnding);
|
|
535
|
-
const
|
|
536
|
-
|
|
750
|
+
const writePath = resolvedRename ?? absolutePath;
|
|
751
|
+
const diagnostics = await this.#writethrough(
|
|
752
|
+
writePath,
|
|
753
|
+
finalContent,
|
|
754
|
+
signal,
|
|
755
|
+
Bun.file(writePath),
|
|
756
|
+
batchRequest,
|
|
757
|
+
);
|
|
758
|
+
if (resolvedRename && resolvedRename !== absolutePath) {
|
|
759
|
+
await file.unlink();
|
|
760
|
+
invalidateFsScanAfterRename(absolutePath, resolvedRename);
|
|
761
|
+
} else {
|
|
762
|
+
invalidateFsScanAfterWrite(absolutePath);
|
|
763
|
+
}
|
|
537
764
|
const diffResult = generateDiffString(originalNormalized, result.content);
|
|
538
765
|
|
|
539
766
|
const normative = buildNormativeUpdateInput({
|
|
540
767
|
path,
|
|
768
|
+
...(rename ? { rename } : {}),
|
|
541
769
|
oldContent: rawContent,
|
|
542
770
|
newContent: finalContent,
|
|
543
771
|
});
|
|
@@ -546,17 +774,20 @@ export class EditTool implements AgentTool<TInput> {
|
|
|
546
774
|
.diagnostics(diagnostics?.summary ?? "", diagnostics?.messages ?? [])
|
|
547
775
|
.get();
|
|
548
776
|
|
|
777
|
+
const resultText = rename ? `Updated and moved ${path} to ${rename}` : `Updated ${path}`;
|
|
549
778
|
return {
|
|
550
779
|
content: [
|
|
551
780
|
{
|
|
552
781
|
type: "text",
|
|
553
|
-
text:
|
|
782
|
+
text: `${resultText}${result.warnings?.length ? `\n\nWarnings:\n${result.warnings.join("\n")}` : ""}`,
|
|
554
783
|
},
|
|
555
784
|
],
|
|
556
785
|
details: {
|
|
557
786
|
diff: diffResult.diff,
|
|
558
787
|
firstChangedLine: result.firstChangedLine ?? diffResult.firstChangedLine,
|
|
559
788
|
diagnostics,
|
|
789
|
+
op: "update",
|
|
790
|
+
rename,
|
|
560
791
|
meta,
|
|
561
792
|
},
|
|
562
793
|
$normative: normative,
|
|
@@ -602,17 +833,10 @@ export class EditTool implements AgentTool<TInput> {
|
|
|
602
833
|
|
|
603
834
|
// Generate diff for display
|
|
604
835
|
let diffResult = { diff: "", firstChangedLine: undefined as number | undefined };
|
|
605
|
-
let normative: PatchInput | undefined;
|
|
606
836
|
if (result.change.type === "update" && result.change.oldContent && result.change.newContent) {
|
|
607
837
|
const normalizedOld = normalizeToLF(stripBom(result.change.oldContent).text);
|
|
608
838
|
const normalizedNew = normalizeToLF(stripBom(result.change.newContent).text);
|
|
609
839
|
diffResult = generateUnifiedDiffString(normalizedOld, normalizedNew);
|
|
610
|
-
normative = buildNormativeUpdateInput({
|
|
611
|
-
path,
|
|
612
|
-
rename: effRename,
|
|
613
|
-
oldContent: result.change.oldContent,
|
|
614
|
-
newContent: result.change.newContent,
|
|
615
|
-
});
|
|
616
840
|
}
|
|
617
841
|
|
|
618
842
|
let resultText: string;
|
|
@@ -650,7 +874,6 @@ export class EditTool implements AgentTool<TInput> {
|
|
|
650
874
|
rename: effRename,
|
|
651
875
|
meta,
|
|
652
876
|
},
|
|
653
|
-
$normative: normative,
|
|
654
877
|
};
|
|
655
878
|
}
|
|
656
879
|
|
package/src/patch/shared.ts
CHANGED
|
@@ -86,10 +86,10 @@ interface EditRenderArgs {
|
|
|
86
86
|
}
|
|
87
87
|
|
|
88
88
|
type HashlineEditPreview =
|
|
89
|
-
| {
|
|
90
|
-
| {
|
|
91
|
-
| {
|
|
92
|
-
| {
|
|
89
|
+
| { target: string; new_content: string[] }
|
|
90
|
+
| { first: string; last: string; new_content: string[] }
|
|
91
|
+
| { before?: string; after?: string; inserted_lines: string[] }
|
|
92
|
+
| { old_text: string; new_text: string; all?: boolean };
|
|
93
93
|
|
|
94
94
|
/** Extended context for edit tool rendering */
|
|
95
95
|
export interface EditRenderContext {
|
|
@@ -168,52 +168,35 @@ function formatStreamingHashlineEdits(edits: unknown[], uiTheme: Theme, ui: Tool
|
|
|
168
168
|
dst: "",
|
|
169
169
|
};
|
|
170
170
|
}
|
|
171
|
-
if ("
|
|
172
|
-
const
|
|
171
|
+
if ("target" in editRecord) {
|
|
172
|
+
const target = typeof editRecord.target === "string" ? editRecord.target : "…";
|
|
173
|
+
const newContent = editRecord.new_content;
|
|
173
174
|
return {
|
|
174
|
-
srcLabel: `•
|
|
175
|
-
dst: Array.isArray(
|
|
176
|
-
? (setLine.body as string[]).join("\n")
|
|
177
|
-
: typeof setLine?.body === "string"
|
|
178
|
-
? setLine.body
|
|
179
|
-
: "",
|
|
175
|
+
srcLabel: `• line ${target}`,
|
|
176
|
+
dst: Array.isArray(newContent) ? (newContent as string[]).join("\n") : "",
|
|
180
177
|
};
|
|
181
178
|
}
|
|
182
|
-
if ("
|
|
183
|
-
const
|
|
184
|
-
const
|
|
185
|
-
const
|
|
179
|
+
if ("first" in editRecord || "last" in editRecord) {
|
|
180
|
+
const first = typeof editRecord.first === "string" ? editRecord.first : "…";
|
|
181
|
+
const last = typeof editRecord.last === "string" ? editRecord.last : "…";
|
|
182
|
+
const newContent = editRecord.new_content;
|
|
186
183
|
return {
|
|
187
|
-
srcLabel: `•
|
|
188
|
-
dst: Array.isArray(
|
|
189
|
-
? (setRange.body as string[]).join("\n")
|
|
190
|
-
: typeof setRange?.body === "string"
|
|
191
|
-
? setRange.body
|
|
192
|
-
: "",
|
|
184
|
+
srcLabel: `• range ${first}..${last}`,
|
|
185
|
+
dst: Array.isArray(newContent) ? (newContent as string[]).join("\n") : "",
|
|
193
186
|
};
|
|
194
187
|
}
|
|
195
|
-
if ("
|
|
196
|
-
const
|
|
197
|
-
const all = typeof replace?.all === "boolean" ? replace.all : false;
|
|
188
|
+
if ("old_text" in editRecord || "new_text" in editRecord) {
|
|
189
|
+
const all = typeof editRecord.all === "boolean" ? editRecord.all : false;
|
|
198
190
|
return {
|
|
199
191
|
srcLabel: `• replace old_text→new_text${all ? " (all)" : ""}`,
|
|
200
|
-
dst: typeof
|
|
192
|
+
dst: typeof editRecord.new_text === "string" ? editRecord.new_text : "",
|
|
201
193
|
};
|
|
202
194
|
}
|
|
203
|
-
if ("
|
|
204
|
-
const
|
|
205
|
-
const
|
|
206
|
-
const
|
|
207
|
-
const
|
|
208
|
-
const text = Array.isArray(body)
|
|
209
|
-
? (body as string[]).join("\n")
|
|
210
|
-
: typeof body === "string"
|
|
211
|
-
? body
|
|
212
|
-
: typeof insertOp?.text === "string"
|
|
213
|
-
? insertOp.text
|
|
214
|
-
: typeof insertOp?.content === "string"
|
|
215
|
-
? (insertOp.content as string)
|
|
216
|
-
: "";
|
|
195
|
+
if ("inserted_lines" in editRecord || "before" in editRecord || "after" in editRecord) {
|
|
196
|
+
const after = typeof editRecord.after === "string" ? editRecord.after : undefined;
|
|
197
|
+
const before = typeof editRecord.before === "string" ? editRecord.before : undefined;
|
|
198
|
+
const insertedLines = editRecord.inserted_lines;
|
|
199
|
+
const text = Array.isArray(insertedLines) ? (insertedLines as string[]).join("\n") : "";
|
|
217
200
|
const refs = [after, before].filter(Boolean).join("..") || "…";
|
|
218
201
|
return {
|
|
219
202
|
srcLabel: `• insert ${refs}`,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<identity>
|
|
2
|
-
|
|
2
|
+
You are a distinguished staff engineer operating inside Oh My Pi, a Pi-based coding harness.
|
|
3
3
|
|
|
4
4
|
High-agency. Principled. Decisive.
|
|
5
5
|
Expertise: debugging, refactoring, system design.
|
|
@@ -172,6 +172,17 @@ Main branch: {{git.mainBranch}}
|
|
|
172
172
|
{{/if}}
|
|
173
173
|
</project>
|
|
174
174
|
|
|
175
|
+
<harness>
|
|
176
|
+
Oh My Pi ships internal documentation accessible via `docs://` URLs (resolved by tools like read/grep).
|
|
177
|
+
- Read `docs://` to list all available documentation files
|
|
178
|
+
- Read `docs://<file>.md` to read a specific doc
|
|
179
|
+
|
|
180
|
+
<critical>
|
|
181
|
+
- **ONLY** read docs when the user asks about omp/pi itself: its SDK, extensions, themes, skills, TUI, keybindings, or configuration.
|
|
182
|
+
- When working on omp/pi topics, read the relevant docs and follow .md cross-references before implementing.
|
|
183
|
+
</critical>
|
|
184
|
+
</harness>
|
|
185
|
+
|
|
175
186
|
{{#if skills.length}}
|
|
176
187
|
<skills>
|
|
177
188
|
Scan descriptions vs task domain. Skill covers output? Read `skill://<name>` first.
|
|
@@ -221,7 +232,7 @@ Notice the sequential habit:
|
|
|
221
232
|
- Comfort in doing one thing at a time
|
|
222
233
|
- Illusion that order = correctness
|
|
223
234
|
- Assumption that B depends on A
|
|
224
|
-
**Use Task tool when:**
|
|
235
|
+
**Use Task tool when:**
|
|
225
236
|
- Editing 4+ files with no dependencies between edits
|
|
226
237
|
- Investigating 2+ independent subsystems
|
|
227
238
|
- Work decomposes into pieces not needing each other's results
|