@oh-my-pi/pi-coding-agent 14.2.0 → 14.3.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 +59 -0
- package/package.json +19 -19
- package/src/cli/args.ts +10 -1
- package/src/cli/shell-cli.ts +15 -3
- package/src/config/settings-schema.ts +60 -1
- package/src/dap/session.ts +8 -2
- package/src/debug/system-info.ts +6 -2
- package/src/discovery/claude.ts +58 -36
- package/src/discovery/opencode.ts +20 -2
- package/src/edit/index.ts +3 -1
- package/src/edit/modes/chunk.ts +133 -53
- package/src/edit/modes/hashline.ts +36 -11
- package/src/edit/renderer.ts +98 -133
- package/src/edit/streaming.ts +351 -0
- package/src/exec/bash-executor.ts +60 -5
- package/src/internal-urls/docs-index.generated.ts +5 -5
- package/src/internal-urls/pi-protocol.ts +0 -2
- package/src/lsp/client.ts +22 -6
- package/src/lsp/defaults.json +2 -1
- package/src/lsp/index.ts +53 -10
- package/src/lsp/types.ts +2 -0
- package/src/modes/acp/acp-agent.ts +76 -2
- package/src/modes/components/assistant-message.ts +1 -34
- package/src/modes/components/hook-editor.ts +1 -1
- package/src/modes/components/tool-execution.ts +111 -101
- package/src/modes/controllers/input-controller.ts +1 -1
- package/src/modes/interactive-mode.ts +0 -2
- package/src/modes/theme/mermaid-cache.ts +13 -52
- package/src/modes/theme/theme.ts +2 -2
- package/src/prompts/system/system-prompt.md +1 -1
- package/src/prompts/tools/ast-grep.md +1 -0
- package/src/prompts/tools/browser.md +1 -0
- package/src/prompts/tools/chunk-edit.md +25 -22
- package/src/prompts/tools/gh-pr-push.md +2 -1
- package/src/prompts/tools/grep.md +4 -3
- package/src/prompts/tools/lsp.md +6 -0
- package/src/prompts/tools/read-chunk.md +46 -7
- package/src/prompts/tools/read.md +7 -4
- package/src/sdk.ts +8 -5
- package/src/session/agent-session.ts +36 -20
- package/src/session/session-manager.ts +228 -57
- package/src/session/streaming-output.ts +11 -0
- package/src/system-prompt.ts +7 -2
- package/src/task/executor.ts +1 -0
- package/src/tools/ast-edit.ts +37 -2
- package/src/tools/bash.ts +75 -12
- package/src/tools/find.ts +19 -26
- package/src/tools/gh.ts +6 -16
- package/src/tools/grep.ts +94 -37
- package/src/tools/path-utils.ts +31 -3
- package/src/tools/resolve.ts +12 -3
- package/src/tools/sqlite-reader.ts +116 -3
- package/src/tools/vim.ts +1 -1
- package/src/web/search/providers/codex.ts +129 -6
package/src/edit/modes/chunk.ts
CHANGED
|
@@ -32,7 +32,6 @@ export type { ChunkReadTarget };
|
|
|
32
32
|
|
|
33
33
|
export type ChunkEditOperation =
|
|
34
34
|
| { op: "put"; sel?: string; content: string }
|
|
35
|
-
| { op: "replace"; sel?: string; content: string; find: string }
|
|
36
35
|
| { op: "delete"; sel?: string }
|
|
37
36
|
| { op: "before"; sel?: string; content: string }
|
|
38
37
|
| { op: "after"; sel?: string; content: string }
|
|
@@ -120,6 +119,8 @@ type ChunkSourceContext = {
|
|
|
120
119
|
chunkLanguage: string | undefined;
|
|
121
120
|
};
|
|
122
121
|
|
|
122
|
+
type ChunkSourceIntent = "read" | "write";
|
|
123
|
+
|
|
123
124
|
function normalizeLanguage(language: string | undefined): string {
|
|
124
125
|
return language?.trim().toLowerCase() || "";
|
|
125
126
|
}
|
|
@@ -140,11 +141,17 @@ function fileLanguageTag(filePath: string, language?: string): string | undefine
|
|
|
140
141
|
return ext.length > 0 ? ext : undefined;
|
|
141
142
|
}
|
|
142
143
|
|
|
143
|
-
async function resolveChunkSourceContext(
|
|
144
|
+
async function resolveChunkSourceContext(
|
|
145
|
+
session: ToolSession,
|
|
146
|
+
path: string,
|
|
147
|
+
options?: { intent?: ChunkSourceIntent },
|
|
148
|
+
): Promise<ChunkSourceContext> {
|
|
144
149
|
const resolvedPath = resolvePlanPath(session, path);
|
|
145
150
|
const sourceFile = Bun.file(resolvedPath);
|
|
146
151
|
const sourceExists = await sourceFile.exists();
|
|
147
|
-
|
|
152
|
+
if ((options?.intent ?? "write") === "write") {
|
|
153
|
+
enforcePlanModeWrite(session, path, { op: sourceExists ? "update" : "create" });
|
|
154
|
+
}
|
|
148
155
|
|
|
149
156
|
let rawContent = "";
|
|
150
157
|
if (sourceExists) {
|
|
@@ -161,6 +168,57 @@ async function resolveChunkSourceContext(session: ToolSession, path: string): Pr
|
|
|
161
168
|
};
|
|
162
169
|
}
|
|
163
170
|
|
|
171
|
+
/**
|
|
172
|
+
* Preview-safe loader: read raw source without plan-mode enforcement or
|
|
173
|
+
* editable-file guards. Used by streaming diff previews that must not throw
|
|
174
|
+
* side-effecting errors while args are still being streamed.
|
|
175
|
+
*/
|
|
176
|
+
export async function loadChunkSource(params: {
|
|
177
|
+
cwd: string;
|
|
178
|
+
path: string;
|
|
179
|
+
}): Promise<{ resolvedPath: string; rawContent: string; language: string | undefined; exists: boolean }> {
|
|
180
|
+
const resolvedPath = nodePath.isAbsolute(params.path) ? params.path : nodePath.resolve(params.cwd, params.path);
|
|
181
|
+
const sourceFile = Bun.file(resolvedPath);
|
|
182
|
+
const exists = await sourceFile.exists();
|
|
183
|
+
const rawContent = exists ? await sourceFile.text() : "";
|
|
184
|
+
return { resolvedPath, rawContent, language: getLanguageFromPath(resolvedPath), exists };
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Compute a unified diff preview for a chunk edit without applying it.
|
|
189
|
+
* Used for streaming previews while args are still arriving. Returns
|
|
190
|
+
* `{ error }` on any failure so callers can decide whether to surface it.
|
|
191
|
+
*/
|
|
192
|
+
export async function computeChunkDiff(
|
|
193
|
+
input: { path: string; edits: ChunkToolEdit[] },
|
|
194
|
+
cwd: string,
|
|
195
|
+
options?: { anchorStyle?: ChunkAnchorStyle; signal?: AbortSignal },
|
|
196
|
+
): Promise<{ diff: string; firstChangedLine: number | undefined } | { error: string }> {
|
|
197
|
+
try {
|
|
198
|
+
options?.signal?.throwIfAborted?.();
|
|
199
|
+
const { filePath } = parseChunkEditPath(input.path);
|
|
200
|
+
if (!filePath) return { error: "chunk edit path is empty" };
|
|
201
|
+
const { resolvedPath, rawContent, language } = await loadChunkSource({ cwd, path: filePath });
|
|
202
|
+
options?.signal?.throwIfAborted?.();
|
|
203
|
+
const { operations } = normalizeChunkEditOperations(input.edits);
|
|
204
|
+
const result = applyChunkEdits({
|
|
205
|
+
source: rawContent,
|
|
206
|
+
language,
|
|
207
|
+
cwd,
|
|
208
|
+
filePath: resolvedPath,
|
|
209
|
+
operations,
|
|
210
|
+
anchorStyle: options?.anchorStyle,
|
|
211
|
+
});
|
|
212
|
+
options?.signal?.throwIfAborted?.();
|
|
213
|
+
if (!result.changed) {
|
|
214
|
+
return { diff: "", firstChangedLine: undefined };
|
|
215
|
+
}
|
|
216
|
+
return generateUnifiedDiffString(result.diffSourceBefore, result.diffSourceAfter);
|
|
217
|
+
} catch (err) {
|
|
218
|
+
return { error: err instanceof Error ? err.message : String(err) };
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
164
222
|
function normalizeChunkRegionSyntax(text: string): string {
|
|
165
223
|
return text.replaceAll("@body", "~").replaceAll("@head", "^");
|
|
166
224
|
}
|
|
@@ -189,6 +247,20 @@ function chunkReadPathSeparatorIndex(readPath: string): number {
|
|
|
189
247
|
if (/^[a-zA-Z]:[/\\]/.test(readPath)) {
|
|
190
248
|
return readPath.indexOf(":", 2);
|
|
191
249
|
}
|
|
250
|
+
const urlMatch = readPath.match(/^([a-z][a-z0-9+.-]*):\/\//i);
|
|
251
|
+
if (urlMatch) {
|
|
252
|
+
const scheme = urlMatch[1].toLowerCase();
|
|
253
|
+
const urlPrefixEnd = urlMatch[0].length;
|
|
254
|
+
if (scheme === "local") {
|
|
255
|
+
const index = readPath.lastIndexOf(":");
|
|
256
|
+
return index >= urlPrefixEnd ? index : -1;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const pathStart = readPath.indexOf("/", urlPrefixEnd);
|
|
260
|
+
if (pathStart === -1) return -1;
|
|
261
|
+
const index = readPath.lastIndexOf(":");
|
|
262
|
+
return index >= pathStart ? index : -1;
|
|
263
|
+
}
|
|
192
264
|
return readPath.indexOf(":");
|
|
193
265
|
}
|
|
194
266
|
|
|
@@ -376,15 +448,6 @@ function toNativeEditOperation(
|
|
|
376
448
|
region: nativeRegion,
|
|
377
449
|
content: operation.content,
|
|
378
450
|
};
|
|
379
|
-
case "replace":
|
|
380
|
-
return {
|
|
381
|
-
op: ChunkEditOp.Replace,
|
|
382
|
-
sel: selector,
|
|
383
|
-
crc,
|
|
384
|
-
region: nativeRegion,
|
|
385
|
-
find: operation.find,
|
|
386
|
-
content: operation.content,
|
|
387
|
-
};
|
|
388
451
|
case "before":
|
|
389
452
|
return { op: ChunkEditOp.Before, sel: selector, crc, region: nativeRegion, content: operation.content };
|
|
390
453
|
case "after":
|
|
@@ -491,17 +554,14 @@ export const chunkToolEditSchema = Type.Object(
|
|
|
491
554
|
}),
|
|
492
555
|
write: Type.Optional(
|
|
493
556
|
Type.Union([Type.String(), Type.Null()], {
|
|
494
|
-
description:
|
|
557
|
+
description:
|
|
558
|
+
"Write complete new content to the targeted region. Null is rejected; use delete: true for deletion.",
|
|
495
559
|
}),
|
|
496
560
|
),
|
|
497
|
-
|
|
498
|
-
Type.
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
new: Type.String({ description: "Replacement text." }),
|
|
502
|
-
},
|
|
503
|
-
{ description: "Find and replace a substring within the chunk." },
|
|
504
|
-
),
|
|
561
|
+
delete: Type.Optional(
|
|
562
|
+
Type.Boolean({
|
|
563
|
+
description: "Explicitly delete the targeted chunk. Must be true; include the current chunk ID.",
|
|
564
|
+
}),
|
|
505
565
|
),
|
|
506
566
|
insert: Type.Optional(
|
|
507
567
|
Type.Object(
|
|
@@ -549,8 +609,9 @@ export function isChunkParams(params: unknown): params is ChunkParams {
|
|
|
549
609
|
return false;
|
|
550
610
|
}
|
|
551
611
|
const first = params.edits[0];
|
|
552
|
-
|
|
553
|
-
|
|
612
|
+
// Accept a bare `{ path }` entry so the executor can return a targeted
|
|
613
|
+
// "missing operation" error instead of the generic schema failure.
|
|
614
|
+
return typeof first === "object" && first !== null && "path" in first;
|
|
554
615
|
}
|
|
555
616
|
|
|
556
617
|
/** Auto-correct indentation for content targeting a body region (`~`) when autoIndent is on.
|
|
@@ -601,6 +662,29 @@ function autoCorrectBodyIndent(content: string, index: number): { content: strin
|
|
|
601
662
|
return { content, warnings };
|
|
602
663
|
}
|
|
603
664
|
|
|
665
|
+
function chunkEditOperationFields(edit: ChunkToolEdit): string[] {
|
|
666
|
+
const fields: string[] = [];
|
|
667
|
+
if (edit.write !== undefined) fields.push("write");
|
|
668
|
+
if (edit.insert != null) fields.push("insert");
|
|
669
|
+
if (edit.delete === true) fields.push("delete");
|
|
670
|
+
return fields;
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
function assertSingleChunkOperation(edit: ChunkToolEdit, index: number): string {
|
|
674
|
+
const fields = chunkEditOperationFields(edit);
|
|
675
|
+
if (fields.length === 0) {
|
|
676
|
+
throw new Error(
|
|
677
|
+
`Edit ${index + 1}: no operation specified. Use write:"..." to replace, insert:{loc,body} to insert, or delete:true to delete. Use the open tool to inspect chunks.`,
|
|
678
|
+
);
|
|
679
|
+
}
|
|
680
|
+
if (fields.length > 1) {
|
|
681
|
+
throw new Error(
|
|
682
|
+
`Edit ${index + 1}: multiple operation fields set (${fields.join(", ")}). Each chunk edit entry must have exactly one operation.`,
|
|
683
|
+
);
|
|
684
|
+
}
|
|
685
|
+
return fields[0];
|
|
686
|
+
}
|
|
687
|
+
|
|
604
688
|
function normalizeChunkEditOperations(edits: ChunkToolEdit[]): {
|
|
605
689
|
operations: ChunkEditOperation[];
|
|
606
690
|
warnings: string[];
|
|
@@ -608,26 +692,22 @@ function normalizeChunkEditOperations(edits: ChunkToolEdit[]): {
|
|
|
608
692
|
const warnings: string[] = [];
|
|
609
693
|
const operations = edits.map((edit, index): ChunkEditOperation => {
|
|
610
694
|
const { selector } = parseChunkEditPath(edit.path);
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
);
|
|
628
|
-
}
|
|
629
|
-
if (hasWrite) {
|
|
630
|
-
let writeContent = edit.write!;
|
|
695
|
+
const operation = assertSingleChunkOperation(edit, index);
|
|
696
|
+
if (operation === "write") {
|
|
697
|
+
if (edit.write === null) {
|
|
698
|
+
throw new Error(
|
|
699
|
+
`Edit ${index + 1}: write:null no longer deletes chunks. Use delete:true to delete, or open the chunk to inspect its content without modifying the file.`,
|
|
700
|
+
);
|
|
701
|
+
}
|
|
702
|
+
if (typeof edit.write !== "string") {
|
|
703
|
+
throw new Error(`Edit ${index + 1}: write must be a string.`);
|
|
704
|
+
}
|
|
705
|
+
if (edit.write.length === 0) {
|
|
706
|
+
throw new Error(
|
|
707
|
+
`Edit ${index + 1}: write:"" is a destructive empty replacement. Use delete:true to delete the chunk, or open the chunk to inspect its content without modifying the file.`,
|
|
708
|
+
);
|
|
709
|
+
}
|
|
710
|
+
let writeContent = edit.write;
|
|
631
711
|
if (selector?.endsWith("~")) {
|
|
632
712
|
const corrected = autoCorrectBodyIndent(writeContent, index);
|
|
633
713
|
writeContent = corrected.content;
|
|
@@ -635,15 +715,12 @@ function normalizeChunkEditOperations(edits: ChunkToolEdit[]): {
|
|
|
635
715
|
}
|
|
636
716
|
return { op: "put", sel: selector, content: writeContent };
|
|
637
717
|
}
|
|
638
|
-
if (
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
if (hasInsert) {
|
|
645
|
-
const op = edit.insert!.loc === "prepend" ? "before" : "after";
|
|
646
|
-
let insertContent = edit.insert!.body;
|
|
718
|
+
if (operation === "insert") {
|
|
719
|
+
if (edit.insert == null || typeof edit.insert.body !== "string" || edit.insert.body.length === 0) {
|
|
720
|
+
throw new Error(`Edit ${index + 1}: insert.body must be a non-empty string.`);
|
|
721
|
+
}
|
|
722
|
+
const op = edit.insert.loc === "prepend" ? "before" : "after";
|
|
723
|
+
let insertContent = edit.insert.body;
|
|
647
724
|
if (selector?.endsWith("~")) {
|
|
648
725
|
const corrected = autoCorrectBodyIndent(insertContent, index);
|
|
649
726
|
insertContent = corrected.content;
|
|
@@ -651,7 +728,9 @@ function normalizeChunkEditOperations(edits: ChunkToolEdit[]): {
|
|
|
651
728
|
}
|
|
652
729
|
return { op, sel: selector, content: insertContent };
|
|
653
730
|
}
|
|
654
|
-
|
|
731
|
+
if (operation !== "delete") {
|
|
732
|
+
throw new Error(`Edit ${index + 1}: unsupported chunk edit operation "${operation}".`);
|
|
733
|
+
}
|
|
655
734
|
return { op: "delete", sel: selector };
|
|
656
735
|
});
|
|
657
736
|
return { operations, warnings };
|
|
@@ -713,6 +792,7 @@ export async function executeChunkSingle(
|
|
|
713
792
|
const { resolvedPath, sourceFile, sourceExists, rawContent, chunkLanguage } = await resolveChunkSourceContext(
|
|
714
793
|
session,
|
|
715
794
|
path,
|
|
795
|
+
{ intent: "write" },
|
|
716
796
|
);
|
|
717
797
|
const parentDir = nodePath.dirname(resolvedPath);
|
|
718
798
|
if (parentDir && parentDir !== ".") {
|
|
@@ -52,12 +52,14 @@ export type HashlineEdit =
|
|
|
52
52
|
const HASHLINE_PREFIX_RE = /^\s*(?:>>>|>>)?\s*(?:\+?\s*(?:\d+\s*#\s*|#\s*)|\+)\s*[ZPMQVRWSNKTXJBYH]{2}:/;
|
|
53
53
|
const HASHLINE_PREFIX_PLUS_RE = /^\s*(?:>>>|>>)?\s*\+\s*(?:\d+\s*#\s*|#\s*)?[ZPMQVRWSNKTXJBYH]{2}:/;
|
|
54
54
|
const DIFF_PLUS_RE = /^[+](?![+])/;
|
|
55
|
+
const READ_TRUNCATION_NOTICE_RE = /^\[(?:Showing lines \d+-\d+ of \d+|\d+ more lines? in (?:file|\S+))\b.*\bsel=L\d+/;
|
|
55
56
|
|
|
56
57
|
type LinePrefixStats = {
|
|
57
58
|
nonEmpty: number;
|
|
58
59
|
hashPrefixCount: number;
|
|
59
60
|
diffPlusHashPrefixCount: number;
|
|
60
61
|
diffPlusCount: number;
|
|
62
|
+
truncationNoticeCount: number;
|
|
61
63
|
};
|
|
62
64
|
|
|
63
65
|
function collectLinePrefixStats(lines: string[]): LinePrefixStats {
|
|
@@ -66,10 +68,15 @@ function collectLinePrefixStats(lines: string[]): LinePrefixStats {
|
|
|
66
68
|
hashPrefixCount: 0,
|
|
67
69
|
diffPlusHashPrefixCount: 0,
|
|
68
70
|
diffPlusCount: 0,
|
|
71
|
+
truncationNoticeCount: 0,
|
|
69
72
|
};
|
|
70
73
|
|
|
71
74
|
for (const line of lines) {
|
|
72
75
|
if (line.length === 0) continue;
|
|
76
|
+
if (READ_TRUNCATION_NOTICE_RE.test(line)) {
|
|
77
|
+
stats.truncationNoticeCount++;
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
73
80
|
stats.nonEmpty++;
|
|
74
81
|
if (HASHLINE_PREFIX_RE.test(line)) stats.hashPrefixCount++;
|
|
75
82
|
if (HASHLINE_PREFIX_PLUS_RE.test(line)) stats.diffPlusHashPrefixCount++;
|
|
@@ -79,6 +86,20 @@ function collectLinePrefixStats(lines: string[]): LinePrefixStats {
|
|
|
79
86
|
return stats;
|
|
80
87
|
}
|
|
81
88
|
|
|
89
|
+
function stripLeadingHashlinePrefixes(line: string): string {
|
|
90
|
+
let result = line;
|
|
91
|
+
let prev: string;
|
|
92
|
+
do {
|
|
93
|
+
prev = result;
|
|
94
|
+
result = result.replace(HASHLINE_PREFIX_RE, "");
|
|
95
|
+
} while (result !== prev);
|
|
96
|
+
return result;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function _filterTruncationNotices(lines: string[]): string[] {
|
|
100
|
+
return lines.filter(line => !READ_TRUNCATION_NOTICE_RE.test(line));
|
|
101
|
+
}
|
|
102
|
+
|
|
82
103
|
export function stripNewLinePrefixes(lines: string[]): string[] {
|
|
83
104
|
const { nonEmpty, hashPrefixCount, diffPlusHashPrefixCount, diffPlusCount } = collectLinePrefixStats(lines);
|
|
84
105
|
if (nonEmpty === 0) return lines;
|
|
@@ -88,20 +109,24 @@ export function stripNewLinePrefixes(lines: string[]): string[] {
|
|
|
88
109
|
!stripHash && diffPlusHashPrefixCount === 0 && diffPlusCount > 0 && diffPlusCount >= nonEmpty * 0.5;
|
|
89
110
|
if (!stripHash && !stripPlus && diffPlusHashPrefixCount === 0) return lines;
|
|
90
111
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
return line.replace(
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
112
|
+
const mapped = lines
|
|
113
|
+
.filter(line => !READ_TRUNCATION_NOTICE_RE.test(line))
|
|
114
|
+
.map(line => {
|
|
115
|
+
if (stripHash) return stripLeadingHashlinePrefixes(line);
|
|
116
|
+
if (stripPlus) return line.replace(DIFF_PLUS_RE, "");
|
|
117
|
+
if (diffPlusHashPrefixCount > 0 && HASHLINE_PREFIX_PLUS_RE.test(line)) {
|
|
118
|
+
return line.replace(HASHLINE_PREFIX_RE, "");
|
|
119
|
+
}
|
|
120
|
+
return line;
|
|
121
|
+
});
|
|
122
|
+
return mapped;
|
|
99
123
|
}
|
|
100
124
|
|
|
101
125
|
export function stripHashlinePrefixes(lines: string[]): string[] {
|
|
102
126
|
const { nonEmpty, hashPrefixCount } = collectLinePrefixStats(lines);
|
|
103
|
-
if (nonEmpty === 0
|
|
104
|
-
|
|
127
|
+
if (nonEmpty === 0) return lines;
|
|
128
|
+
if (hashPrefixCount !== nonEmpty) return lines;
|
|
129
|
+
return lines.filter(line => !READ_TRUNCATION_NOTICE_RE.test(line)).map(line => stripLeadingHashlinePrefixes(line));
|
|
105
130
|
}
|
|
106
131
|
|
|
107
132
|
const linesSchema = Type.Union([
|
|
@@ -530,7 +555,7 @@ export class HashlineMismatchError extends Error {
|
|
|
530
555
|
const lines: string[] = [];
|
|
531
556
|
|
|
532
557
|
lines.push(
|
|
533
|
-
|
|
558
|
+
`Edit rejected: ${mismatches.length} line${mismatches.length > 1 ? "s have" : " has"} changed since the last read. The edit was NOT applied. Use the updated LINE#ID references shown below (>>> marks changed lines) and retry the edit.`,
|
|
534
559
|
);
|
|
535
560
|
lines.push("");
|
|
536
561
|
|