@oh-my-pi/pi-coding-agent 13.0.1 → 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/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 +92 -128
- package/src/patch/shared.ts +17 -23
- package/src/prompts/system/agent-creation-architect.md +2 -2
- 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 -155
- package/src/prompts/tools/task.md +3 -3
- 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,7 +167,7 @@ export function stripNewLinePrefixes(lines: string[]): string[] {
|
|
|
168
167
|
});
|
|
169
168
|
}
|
|
170
169
|
|
|
171
|
-
export function
|
|
170
|
+
export function hashlineParseText(edit: string[] | string | null): string[] {
|
|
172
171
|
if (edit === null) return [];
|
|
173
172
|
if (Array.isArray(edit)) return edit;
|
|
174
173
|
const lines = stripNewLinePrefixes(edit.split("\n"));
|
|
@@ -177,52 +176,45 @@ export function hashlineParseContent(edit: string | string[] | null): string[] {
|
|
|
177
176
|
return lines;
|
|
178
177
|
}
|
|
179
178
|
|
|
180
|
-
const
|
|
179
|
+
const hashlineEditSchema = Type.Object(
|
|
181
180
|
{
|
|
182
|
-
op: StringEnum(["replace", "append", "prepend"
|
|
183
|
-
|
|
184
|
-
}),
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
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(),
|
|
188
187
|
Type.Null(),
|
|
189
|
-
Type.Array(Type.String(), { description: "Content lines" }),
|
|
190
|
-
Type.String({ description: "Content line" }),
|
|
191
188
|
]),
|
|
192
189
|
},
|
|
193
190
|
{ additionalProperties: false },
|
|
194
191
|
);
|
|
195
192
|
|
|
196
|
-
const
|
|
193
|
+
const hashlineEditParamsSchema = Type.Object(
|
|
197
194
|
{
|
|
198
|
-
|
|
199
|
-
edits: Type.Array(
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
}),
|
|
203
|
-
delete: Type.Optional(Type.Boolean({ description: "Delete the file when true" })),
|
|
204
|
-
rename: Type.Optional(Type.String({ description: "New path if moving" })),
|
|
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" })),
|
|
205
199
|
},
|
|
206
200
|
{ additionalProperties: false },
|
|
207
201
|
);
|
|
208
202
|
|
|
209
|
-
export type HashlineToolEdit = Static<typeof
|
|
210
|
-
export type HashlineParams = Static<typeof
|
|
203
|
+
export type HashlineToolEdit = Static<typeof hashlineEditSchema>;
|
|
204
|
+
export type HashlineParams = Static<typeof hashlineEditParamsSchema>;
|
|
211
205
|
|
|
212
206
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
213
207
|
// Resilient anchor resolution
|
|
214
208
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
215
209
|
|
|
216
210
|
/**
|
|
217
|
-
* Map flat tool-schema edits (
|
|
211
|
+
* Map flat tool-schema edits (tag/end) into typed HashlineEdit objects.
|
|
218
212
|
*
|
|
219
213
|
* Resilient: as long as at least one anchor exists, we execute.
|
|
220
|
-
* - replace +
|
|
221
|
-
* - replace +
|
|
222
|
-
* - append +
|
|
223
|
-
* - prepend +
|
|
224
|
-
* - insert + first + last → insert between them
|
|
225
|
-
* - insert + one anchor → degrade to append/prepend
|
|
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
|
|
226
218
|
* - no anchors → file-level append/prepend (only for those ops)
|
|
227
219
|
*
|
|
228
220
|
* Unknown ops default to "replace".
|
|
@@ -230,51 +222,29 @@ export type HashlineParams = Static<typeof hashlineEditSchema>;
|
|
|
230
222
|
function resolveEditAnchors(edits: HashlineToolEdit[]): HashlineEdit[] {
|
|
231
223
|
const result: HashlineEdit[] = [];
|
|
232
224
|
for (const edit of edits) {
|
|
233
|
-
const
|
|
234
|
-
const
|
|
235
|
-
const
|
|
225
|
+
const lines = hashlineParseText(edit.lines);
|
|
226
|
+
const tag = edit.pos ? tryParseTag(edit.pos) : undefined;
|
|
227
|
+
const end = edit.end ? tryParseTag(edit.end) : undefined;
|
|
236
228
|
|
|
237
229
|
// Normalize op — default unknown values to "replace"
|
|
238
|
-
const op = edit.op === "append" || edit.op === "prepend"
|
|
239
|
-
|
|
230
|
+
const op = edit.op === "append" || edit.op === "prepend" ? edit.op : "replace";
|
|
240
231
|
switch (op) {
|
|
241
232
|
case "replace": {
|
|
242
|
-
if (
|
|
243
|
-
result.push({ op: "replace",
|
|
244
|
-
} else if (
|
|
245
|
-
result.push({ op: "replace",
|
|
246
|
-
} else if (last) {
|
|
247
|
-
result.push({ op: "replace", tag: last, content });
|
|
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 });
|
|
248
237
|
} else {
|
|
249
|
-
throw new Error("Replace requires at least one anchor (
|
|
238
|
+
throw new Error("Replace requires at least one anchor (tag or end).");
|
|
250
239
|
}
|
|
251
240
|
break;
|
|
252
241
|
}
|
|
253
242
|
case "append": {
|
|
254
|
-
|
|
255
|
-
const anchor = first ?? last;
|
|
256
|
-
result.push({ op: "append", ...(anchor ? { after: anchor } : {}), content });
|
|
243
|
+
result.push({ op: "append", pos: tag ?? end, lines });
|
|
257
244
|
break;
|
|
258
245
|
}
|
|
259
246
|
case "prepend": {
|
|
260
|
-
|
|
261
|
-
const anchor = last ?? first;
|
|
262
|
-
result.push({ op: "prepend", ...(anchor ? { before: anchor } : {}), content });
|
|
263
|
-
break;
|
|
264
|
-
}
|
|
265
|
-
case "insert": {
|
|
266
|
-
if (first && last) {
|
|
267
|
-
result.push({ op: "insert", after: first, before: last, content });
|
|
268
|
-
} else if (first) {
|
|
269
|
-
// Degrade: insert after first
|
|
270
|
-
result.push({ op: "append", after: first, content });
|
|
271
|
-
} else if (last) {
|
|
272
|
-
// Degrade: insert before last
|
|
273
|
-
result.push({ op: "prepend", before: last, content });
|
|
274
|
-
} else {
|
|
275
|
-
// No anchors — append to end
|
|
276
|
-
result.push({ op: "append", content });
|
|
277
|
-
}
|
|
247
|
+
result.push({ op: "prepend", pos: end ?? tag, lines });
|
|
278
248
|
break;
|
|
279
249
|
}
|
|
280
250
|
}
|
|
@@ -283,7 +253,7 @@ function resolveEditAnchors(edits: HashlineToolEdit[]): HashlineEdit[] {
|
|
|
283
253
|
}
|
|
284
254
|
|
|
285
255
|
/** Parse a tag, returning undefined instead of throwing on garbage. */
|
|
286
|
-
function tryParseTag(raw: string):
|
|
256
|
+
function tryParseTag(raw: string): Anchor | undefined {
|
|
287
257
|
try {
|
|
288
258
|
return parseTag(raw);
|
|
289
259
|
} catch {
|
|
@@ -379,11 +349,11 @@ function mergeDiagnosticsWithWarnings(
|
|
|
379
349
|
// Tool Class
|
|
380
350
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
381
351
|
|
|
382
|
-
type TInput = typeof replaceEditSchema | typeof patchEditSchema | typeof
|
|
352
|
+
type TInput = typeof replaceEditSchema | typeof patchEditSchema | typeof hashlineEditParamsSchema;
|
|
383
353
|
|
|
384
354
|
export type EditMode = "replace" | "patch" | "hashline";
|
|
385
355
|
|
|
386
|
-
export const DEFAULT_EDIT_MODE: EditMode = "
|
|
356
|
+
export const DEFAULT_EDIT_MODE: EditMode = "hashline";
|
|
387
357
|
|
|
388
358
|
export function normalizeEditMode(mode?: string | null): EditMode | null {
|
|
389
359
|
switch (mode) {
|
|
@@ -408,6 +378,7 @@ export class EditTool implements AgentTool<TInput> {
|
|
|
408
378
|
readonly label = "Edit";
|
|
409
379
|
readonly nonAbortable = true;
|
|
410
380
|
readonly concurrency = "exclusive";
|
|
381
|
+
readonly strict = true;
|
|
411
382
|
|
|
412
383
|
readonly #allowFuzzy: boolean;
|
|
413
384
|
readonly #fuzzyThreshold: number;
|
|
@@ -499,7 +470,7 @@ export class EditTool implements AgentTool<TInput> {
|
|
|
499
470
|
case "patch":
|
|
500
471
|
return patchEditSchema;
|
|
501
472
|
case "hashline":
|
|
502
|
-
return
|
|
473
|
+
return hashlineEditParamsSchema;
|
|
503
474
|
default:
|
|
504
475
|
return replaceEditSchema;
|
|
505
476
|
}
|
|
@@ -518,21 +489,20 @@ export class EditTool implements AgentTool<TInput> {
|
|
|
518
489
|
// Hashline mode execution
|
|
519
490
|
// ─────────────────────────────────────────────────────────────────
|
|
520
491
|
if (this.mode === "hashline") {
|
|
521
|
-
const { path, edits, delete: deleteFile,
|
|
492
|
+
const { file: path, edits, delete: deleteFile, move } = params as HashlineParams;
|
|
522
493
|
|
|
523
|
-
enforcePlanModeWrite(this.session, path, { op: deleteFile ? "delete" : "update",
|
|
494
|
+
enforcePlanModeWrite(this.session, path, { op: deleteFile ? "delete" : "update", move });
|
|
524
495
|
|
|
525
496
|
if (path.endsWith(".ipynb") && edits?.length > 0) {
|
|
526
497
|
throw new Error("Cannot edit Jupyter notebooks with the Edit tool. Use the NotebookEdit tool instead.");
|
|
527
498
|
}
|
|
528
499
|
|
|
529
500
|
const absolutePath = resolvePlanPath(this.session, path);
|
|
530
|
-
const
|
|
531
|
-
const file = Bun.file(absolutePath);
|
|
501
|
+
const resolvedMove = move ? resolvePlanPath(this.session, move) : undefined;
|
|
532
502
|
|
|
533
503
|
if (deleteFile) {
|
|
534
|
-
if (await
|
|
535
|
-
await
|
|
504
|
+
if (await fs.exists(absolutePath)) {
|
|
505
|
+
await fs.unlink(absolutePath);
|
|
536
506
|
}
|
|
537
507
|
invalidateFsScanAfterDelete(absolutePath);
|
|
538
508
|
return {
|
|
@@ -545,21 +515,21 @@ export class EditTool implements AgentTool<TInput> {
|
|
|
545
515
|
};
|
|
546
516
|
}
|
|
547
517
|
|
|
548
|
-
if (!(await
|
|
549
|
-
const
|
|
518
|
+
if (!(await fs.exists(absolutePath))) {
|
|
519
|
+
const lines: string[] = [];
|
|
550
520
|
for (const edit of edits) {
|
|
551
521
|
// For file creation, only anchorless appends/prepends are valid
|
|
552
|
-
if ((edit.op === "append" || edit.op === "prepend") && !edit.
|
|
522
|
+
if ((edit.op === "append" || edit.op === "prepend") && !edit.pos && !edit.end) {
|
|
553
523
|
if (edit.op === "prepend") {
|
|
554
|
-
|
|
524
|
+
lines.unshift(...hashlineParseText(edit.lines));
|
|
555
525
|
} else {
|
|
556
|
-
|
|
526
|
+
lines.push(...hashlineParseText(edit.lines));
|
|
557
527
|
}
|
|
558
528
|
} else {
|
|
559
529
|
throw new Error(`File not found: ${path}`);
|
|
560
530
|
}
|
|
561
531
|
}
|
|
562
|
-
await
|
|
532
|
+
await fs.writeFile(absolutePath, lines.join("\n"));
|
|
563
533
|
return {
|
|
564
534
|
content: [{ type: "text", text: `Created ${path}` }],
|
|
565
535
|
details: {
|
|
@@ -572,29 +542,29 @@ export class EditTool implements AgentTool<TInput> {
|
|
|
572
542
|
|
|
573
543
|
const anchorEdits = resolveEditAnchors(edits);
|
|
574
544
|
|
|
575
|
-
const rawContent = await
|
|
576
|
-
const { bom, text
|
|
577
|
-
const originalEnding = detectLineEnding(
|
|
578
|
-
const originalNormalized = normalizeToLF(
|
|
579
|
-
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;
|
|
580
550
|
|
|
581
|
-
// Apply anchor-based edits first (
|
|
582
|
-
const anchorResult = applyHashlineEdits(
|
|
583
|
-
|
|
551
|
+
// Apply anchor-based edits first (replace, append, prepend)
|
|
552
|
+
const anchorResult = applyHashlineEdits(normalizedText, anchorEdits);
|
|
553
|
+
normalizedText = anchorResult.lines;
|
|
584
554
|
|
|
585
555
|
const result = {
|
|
586
|
-
|
|
556
|
+
text: normalizedText,
|
|
587
557
|
firstChangedLine: anchorResult.firstChangedLine,
|
|
588
558
|
warnings: anchorResult.warnings,
|
|
589
559
|
noopEdits: anchorResult.noopEdits,
|
|
590
560
|
};
|
|
591
|
-
if (originalNormalized === result.
|
|
561
|
+
if (originalNormalized === result.text && !move) {
|
|
592
562
|
let diagnostic = `No changes made to ${path}. The edits produced identical content.`;
|
|
593
563
|
if (result.noopEdits && result.noopEdits.length > 0) {
|
|
594
564
|
const details = result.noopEdits
|
|
595
565
|
.map(
|
|
596
566
|
e =>
|
|
597
|
-
`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}`,
|
|
598
568
|
)
|
|
599
569
|
.join("\n");
|
|
600
570
|
diagnostic += `\n${details}`;
|
|
@@ -602,27 +572,24 @@ export class EditTool implements AgentTool<TInput> {
|
|
|
602
572
|
"\nYour content must differ from what the file already contains. Re-read the file to see the current state.";
|
|
603
573
|
} else {
|
|
604
574
|
// Edits were not literally identical but heuristics normalized them back
|
|
605
|
-
const lines = result.
|
|
575
|
+
const lines = result.text.split("\n");
|
|
606
576
|
const targetLines: string[] = [];
|
|
607
|
-
const refs:
|
|
577
|
+
const refs: Anchor[] = [];
|
|
608
578
|
for (const edit of anchorEdits) {
|
|
609
579
|
refs.length = 0;
|
|
610
580
|
switch (edit.op) {
|
|
611
581
|
case "replace":
|
|
612
|
-
if (
|
|
613
|
-
refs.push(edit.
|
|
582
|
+
if (edit.end) {
|
|
583
|
+
refs.push(edit.end, edit.pos);
|
|
614
584
|
} else {
|
|
615
|
-
refs.push(edit.
|
|
585
|
+
refs.push(edit.pos);
|
|
616
586
|
}
|
|
617
587
|
break;
|
|
618
588
|
case "append":
|
|
619
|
-
if (edit.
|
|
589
|
+
if (edit.pos) refs.push(edit.pos);
|
|
620
590
|
break;
|
|
621
591
|
case "prepend":
|
|
622
|
-
if (edit.
|
|
623
|
-
break;
|
|
624
|
-
case "insert":
|
|
625
|
-
refs.push(edit.after, edit.before);
|
|
592
|
+
if (edit.pos) refs.push(edit.pos);
|
|
626
593
|
break;
|
|
627
594
|
default:
|
|
628
595
|
break;
|
|
@@ -631,9 +598,9 @@ export class EditTool implements AgentTool<TInput> {
|
|
|
631
598
|
for (const ref of refs) {
|
|
632
599
|
try {
|
|
633
600
|
if (ref.line >= 1 && ref.line <= lines.length) {
|
|
634
|
-
const
|
|
635
|
-
const hash = computeLineHash(ref.line,
|
|
636
|
-
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}`);
|
|
637
604
|
}
|
|
638
605
|
} catch {
|
|
639
606
|
/* skip malformed refs */
|
|
@@ -648,8 +615,8 @@ export class EditTool implements AgentTool<TInput> {
|
|
|
648
615
|
throw new Error(diagnostic);
|
|
649
616
|
}
|
|
650
617
|
|
|
651
|
-
const finalContent = bom + restoreLineEndings(result.
|
|
652
|
-
const writePath =
|
|
618
|
+
const finalContent = bom + restoreLineEndings(result.text, originalEnding);
|
|
619
|
+
const writePath = resolvedMove ?? absolutePath;
|
|
653
620
|
const diagnostics = await this.#writethrough(
|
|
654
621
|
writePath,
|
|
655
622
|
finalContent,
|
|
@@ -657,26 +624,19 @@ export class EditTool implements AgentTool<TInput> {
|
|
|
657
624
|
Bun.file(writePath),
|
|
658
625
|
batchRequest,
|
|
659
626
|
);
|
|
660
|
-
if (
|
|
661
|
-
await
|
|
662
|
-
invalidateFsScanAfterRename(absolutePath,
|
|
627
|
+
if (resolvedMove && resolvedMove !== absolutePath) {
|
|
628
|
+
await fs.unlink(absolutePath);
|
|
629
|
+
invalidateFsScanAfterRename(absolutePath, resolvedMove);
|
|
663
630
|
} else {
|
|
664
631
|
invalidateFsScanAfterWrite(absolutePath);
|
|
665
632
|
}
|
|
666
|
-
const diffResult = generateDiffString(originalNormalized, result.
|
|
667
|
-
|
|
668
|
-
const normative = buildNormativeUpdateInput({
|
|
669
|
-
path,
|
|
670
|
-
...(rename ? { rename } : {}),
|
|
671
|
-
oldContent: rawContent,
|
|
672
|
-
newContent: finalContent,
|
|
673
|
-
});
|
|
633
|
+
const diffResult = generateDiffString(originalNormalized, result.text);
|
|
674
634
|
|
|
675
635
|
const meta = outputMeta()
|
|
676
636
|
.diagnostics(diagnostics?.summary ?? "", diagnostics?.messages ?? [])
|
|
677
637
|
.get();
|
|
678
638
|
|
|
679
|
-
const resultText =
|
|
639
|
+
const resultText = move ? `Moved ${path} to ${move}` : `Updated ${path}`;
|
|
680
640
|
return {
|
|
681
641
|
content: [
|
|
682
642
|
{
|
|
@@ -689,10 +649,9 @@ export class EditTool implements AgentTool<TInput> {
|
|
|
689
649
|
firstChangedLine: result.firstChangedLine ?? diffResult.firstChangedLine,
|
|
690
650
|
diagnostics,
|
|
691
651
|
op: "update",
|
|
692
|
-
|
|
652
|
+
move,
|
|
693
653
|
meta,
|
|
694
654
|
},
|
|
695
|
-
$normative: normative,
|
|
696
655
|
};
|
|
697
656
|
}
|
|
698
657
|
|
|
@@ -700,12 +659,12 @@ export class EditTool implements AgentTool<TInput> {
|
|
|
700
659
|
// Patch mode execution
|
|
701
660
|
// ─────────────────────────────────────────────────────────────────
|
|
702
661
|
if (this.mode === "patch") {
|
|
703
|
-
const { path, op: rawOp, rename, diff } = params as PatchParams;
|
|
662
|
+
const { file: path, op: rawOp, rename, diff } = params as PatchParams;
|
|
704
663
|
|
|
705
664
|
// Normalize unrecognized operations to "update"
|
|
706
665
|
const op: Operation = rawOp === "create" || rawOp === "delete" ? rawOp : "update";
|
|
707
666
|
|
|
708
|
-
enforcePlanModeWrite(this.session, path, { op, rename });
|
|
667
|
+
enforcePlanModeWrite(this.session, path, { op, move: rename });
|
|
709
668
|
const resolvedPath = resolvePlanPath(this.session, path);
|
|
710
669
|
const resolvedRename = rename ? resolvePlanPath(this.session, rename) : undefined;
|
|
711
670
|
|
|
@@ -773,7 +732,7 @@ export class EditTool implements AgentTool<TInput> {
|
|
|
773
732
|
firstChangedLine: diffResult.firstChangedLine,
|
|
774
733
|
diagnostics: mergedDiagnostics,
|
|
775
734
|
op,
|
|
776
|
-
|
|
735
|
+
move: effRename,
|
|
777
736
|
meta,
|
|
778
737
|
},
|
|
779
738
|
};
|
|
@@ -782,7 +741,7 @@ export class EditTool implements AgentTool<TInput> {
|
|
|
782
741
|
// ─────────────────────────────────────────────────────────────────
|
|
783
742
|
// Replace mode execution
|
|
784
743
|
// ─────────────────────────────────────────────────────────────────
|
|
785
|
-
const { path, old_text, new_text, all } = params as ReplaceParams;
|
|
744
|
+
const { file: path, old_text, new_text, all } = params as ReplaceParams;
|
|
786
745
|
|
|
787
746
|
enforcePlanModeWrite(this.session, path);
|
|
788
747
|
|
|
@@ -795,13 +754,12 @@ export class EditTool implements AgentTool<TInput> {
|
|
|
795
754
|
}
|
|
796
755
|
|
|
797
756
|
const absolutePath = resolvePlanPath(this.session, path);
|
|
798
|
-
const file = Bun.file(absolutePath);
|
|
799
757
|
|
|
800
|
-
if (!(await
|
|
758
|
+
if (!(await fs.exists(absolutePath))) {
|
|
801
759
|
throw new Error(`File not found: ${path}`);
|
|
802
760
|
}
|
|
803
761
|
|
|
804
|
-
const rawContent = await
|
|
762
|
+
const rawContent = await fs.readFile(absolutePath, "utf-8");
|
|
805
763
|
const { bom, text: content } = stripBom(rawContent);
|
|
806
764
|
const originalEnding = detectLineEnding(content);
|
|
807
765
|
const normalizedContent = normalizeToLF(content);
|
|
@@ -844,7 +802,13 @@ export class EditTool implements AgentTool<TInput> {
|
|
|
844
802
|
}
|
|
845
803
|
|
|
846
804
|
const finalContent = bom + restoreLineEndings(result.content, originalEnding);
|
|
847
|
-
const diagnostics = await this.#writethrough(
|
|
805
|
+
const diagnostics = await this.#writethrough(
|
|
806
|
+
absolutePath,
|
|
807
|
+
finalContent,
|
|
808
|
+
signal,
|
|
809
|
+
Bun.file(absolutePath),
|
|
810
|
+
batchRequest,
|
|
811
|
+
);
|
|
848
812
|
invalidateFsScanAfterWrite(absolutePath);
|
|
849
813
|
const diffResult = generateDiffString(normalizedContent, result.content);
|
|
850
814
|
|
package/src/patch/shared.ts
CHANGED
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
truncateDiffByHunk,
|
|
23
23
|
} from "../tools/render-utils";
|
|
24
24
|
import { Ellipsis, Hasher, type RenderCache, renderStatusLine, truncateToWidth } from "../tui";
|
|
25
|
+
import type { HashlineToolEdit } from "./index";
|
|
25
26
|
import type { DiffError, DiffResult, Operation } from "./types";
|
|
26
27
|
|
|
27
28
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
@@ -58,7 +59,7 @@ export interface EditToolDetails {
|
|
|
58
59
|
/** Operation type (patch mode only) */
|
|
59
60
|
op?: Operation;
|
|
60
61
|
/** New path after move/rename (patch mode only) */
|
|
61
|
-
|
|
62
|
+
move?: string;
|
|
62
63
|
/** Structured output metadata */
|
|
63
64
|
meta?: OutputMeta;
|
|
64
65
|
}
|
|
@@ -70,6 +71,7 @@ export interface EditToolDetails {
|
|
|
70
71
|
interface EditRenderArgs {
|
|
71
72
|
path?: string;
|
|
72
73
|
file_path?: string;
|
|
74
|
+
file?: string;
|
|
73
75
|
oldText?: string;
|
|
74
76
|
newText?: string;
|
|
75
77
|
patch?: string;
|
|
@@ -83,16 +85,9 @@ interface EditRenderArgs {
|
|
|
83
85
|
*/
|
|
84
86
|
previewDiff?: string;
|
|
85
87
|
// Hashline mode fields
|
|
86
|
-
edits?:
|
|
88
|
+
edits?: Partial<HashlineToolEdit>[];
|
|
87
89
|
}
|
|
88
90
|
|
|
89
|
-
type HashlineEditPreview = {
|
|
90
|
-
op: string;
|
|
91
|
-
first?: string;
|
|
92
|
-
last?: string;
|
|
93
|
-
content: string | string[] | null;
|
|
94
|
-
};
|
|
95
|
-
|
|
96
91
|
/** Extended context for edit tool rendering */
|
|
97
92
|
export interface EditRenderContext {
|
|
98
93
|
/** Pre-computed diff preview (computed before tool executes) */
|
|
@@ -123,7 +118,7 @@ function formatStreamingDiff(diff: string, rawPath: string, uiTheme: Theme, labe
|
|
|
123
118
|
return text;
|
|
124
119
|
}
|
|
125
120
|
|
|
126
|
-
function formatStreamingHashlineEdits(edits:
|
|
121
|
+
function formatStreamingHashlineEdits(edits: Partial<HashlineToolEdit>[], uiTheme: Theme): string {
|
|
127
122
|
const MAX_EDITS = 4;
|
|
128
123
|
const MAX_DST_LINES = 8;
|
|
129
124
|
let text = "\n\n";
|
|
@@ -158,22 +153,21 @@ function formatStreamingHashlineEdits(edits: unknown[], uiTheme: Theme): string
|
|
|
158
153
|
}
|
|
159
154
|
|
|
160
155
|
return text.trimEnd();
|
|
161
|
-
function formatHashlineEdit(edit:
|
|
162
|
-
|
|
163
|
-
if (!editRecord) {
|
|
156
|
+
function formatHashlineEdit(edit: Partial<HashlineToolEdit>): { srcLabel: string; dst: string } {
|
|
157
|
+
if (typeof edit !== "object" || !edit) {
|
|
164
158
|
return { srcLabel: "• (incomplete edit)", dst: "" };
|
|
165
159
|
}
|
|
166
160
|
|
|
167
|
-
const contentLines = Array.isArray(
|
|
161
|
+
const contentLines = Array.isArray(edit.lines) ? (edit.lines as string[]).join("\n") : "";
|
|
168
162
|
|
|
169
|
-
const op = typeof
|
|
170
|
-
const
|
|
171
|
-
const
|
|
163
|
+
const op = typeof edit.op === "string" ? edit.op : "?";
|
|
164
|
+
const pos = typeof edit.pos === "string" ? edit.pos : undefined;
|
|
165
|
+
const end = typeof edit.end === "string" ? edit.end : undefined;
|
|
172
166
|
|
|
173
|
-
if (
|
|
174
|
-
return { srcLabel:
|
|
167
|
+
if (pos && end && pos !== end) {
|
|
168
|
+
return { srcLabel: `• ${op} ${pos}…${end}`, dst: contentLines };
|
|
175
169
|
}
|
|
176
|
-
const anchor =
|
|
170
|
+
const anchor = pos ?? end;
|
|
177
171
|
if (anchor) {
|
|
178
172
|
return { srcLabel: `\u2022 ${op} ${anchor}`, dst: contentLines };
|
|
179
173
|
}
|
|
@@ -226,7 +220,7 @@ export const editToolRenderer = {
|
|
|
226
220
|
mergeCallAndResult: true,
|
|
227
221
|
|
|
228
222
|
renderCall(args: EditRenderArgs, options: RenderResultOptions, uiTheme: Theme): Component {
|
|
229
|
-
const rawPath = args.file_path || args.path || "";
|
|
223
|
+
const rawPath = args.file_path || args.path || args.file || "";
|
|
230
224
|
const filePath = shortenPath(rawPath);
|
|
231
225
|
const editLanguage = getLanguageFromPath(rawPath) ?? "text";
|
|
232
226
|
const editIcon = uiTheme.fg("muted", uiTheme.getLangIcon(editLanguage));
|
|
@@ -281,13 +275,13 @@ export const editToolRenderer = {
|
|
|
281
275
|
uiTheme: Theme,
|
|
282
276
|
args?: EditRenderArgs,
|
|
283
277
|
): Component {
|
|
284
|
-
const rawPath = args?.file_path || args?.path || "";
|
|
278
|
+
const rawPath = args?.file_path || args?.path || args?.file || "";
|
|
285
279
|
const filePath = shortenPath(rawPath);
|
|
286
280
|
const editLanguage = getLanguageFromPath(rawPath) ?? "text";
|
|
287
281
|
const editIcon = uiTheme.fg("muted", uiTheme.getLangIcon(editLanguage));
|
|
288
282
|
|
|
289
283
|
const op = args?.op || result.details?.op;
|
|
290
|
-
const rename = args?.rename || result.details?.
|
|
284
|
+
const rename = args?.rename || result.details?.move;
|
|
291
285
|
const opTitle = op === "create" ? "Create" : op === "delete" ? "Delete" : "Edit";
|
|
292
286
|
|
|
293
287
|
// Pre-compute metadata line (static across renders)
|
|
@@ -50,8 +50,8 @@ When a user describes what they want an agent to do, you will:
|
|
|
50
50
|
Your output MUST be a valid JSON object with exactly these fields:
|
|
51
51
|
{
|
|
52
52
|
"identifier": "A unique, descriptive identifier using lowercase letters, numbers, and hyphens (e.g., 'test-runner', 'api-docs-writer', 'code-formatter')",
|
|
53
|
-
"whenToUse": "A precise, actionable description starting with 'Use this agent when
|
|
54
|
-
"systemPrompt": "The complete system prompt that will govern the agent's behavior, written in second person ('You are
|
|
53
|
+
"whenToUse": "A precise, actionable description starting with 'Use this agent when…' that clearly defines the triggering conditions and use cases. Ensure you include examples as described above.",
|
|
54
|
+
"systemPrompt": "The complete system prompt that will govern the agent's behavior, written in second person ('You are…', 'You will…') and structured for maximum clarity and effectiveness"
|
|
55
55
|
}
|
|
56
56
|
|
|
57
57
|
Key principles for your system prompts:
|
|
@@ -90,7 +90,7 @@ Semantic questions MUST be answered with semantic tools.
|
|
|
90
90
|
{{#has tools "ssh"}}
|
|
91
91
|
### SSH: match commands to host shell
|
|
92
92
|
Commands MUST match the host shell. linux/bash, macos/zsh: Unix. windows/cmd: dir, type, findstr. windows/powershell: Get-ChildItem, Get-Content.
|
|
93
|
-
Remote filesystems: `~/.omp/remote/<hostname>/`. Windows paths need colons: `C:/Users
|
|
93
|
+
Remote filesystems: `~/.omp/remote/<hostname>/`. Windows paths need colons: `C:/Users/…`
|
|
94
94
|
{{/has}}
|
|
95
95
|
|
|
96
96
|
{{#ifAny (includes tools "grep") (includes tools "find")}}
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
Executes bash command in shell session for terminal operations like git, bun, cargo, python.
|
|
4
4
|
|
|
5
5
|
<instruction>
|
|
6
|
-
- You MUST use `cwd` parameter to set working directory instead of `cd dir &&
|
|
6
|
+
- You MUST use `cwd` parameter to set working directory instead of `cd dir && …`
|
|
7
7
|
- PTY mode is opt-in: set `pty: true` only when command expects a real terminal (for example `sudo`, `ssh` where you need input from the user); default is `false`
|
|
8
8
|
- You MUST use `;` only when later commands should run regardless of earlier failures
|
|
9
9
|
- `skill://` URIs are auto-resolved to filesystem paths before execution
|
|
@@ -13,6 +13,15 @@ Fast file pattern matching that works with any codebase size.
|
|
|
13
13
|
Matching file paths sorted by modification time (most recent first). Results truncated at 1000 entries or 50KB (configurable via `limit`).
|
|
14
14
|
</output>
|
|
15
15
|
|
|
16
|
+
<example name="find files">
|
|
17
|
+
```
|
|
18
|
+
{
|
|
19
|
+
"pattern": "src/**/*.ts",
|
|
20
|
+
"limit": 1000
|
|
21
|
+
}
|
|
22
|
+
```
|
|
23
|
+
</example>
|
|
24
|
+
|
|
16
25
|
<avoid>
|
|
17
26
|
For open-ended searches requiring multiple rounds of globbing and grepping, you MUST use Task tool instead.
|
|
18
27
|
</avoid>
|