pi-hashline-edit-pro 2.7.0 → 2.7.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/README.md +37 -35
- package/index.ts +8 -8
- package/package.json +1 -1
- package/prompts/grep-guidelines.md +3 -2
- package/prompts/grep-snippet.md +1 -1
- package/prompts/grep.md +1 -1
- package/prompts/insert-guidelines.md +3 -2
- package/prompts/insert-snippet.md +1 -1
- package/prompts/insert.md +1 -1
- package/prompts/read-guidelines.md +1 -1
- package/prompts/read-snippet.md +1 -1
- package/prompts/read.md +1 -1
- package/prompts/replace-guidelines.md +5 -4
- package/prompts/replace-snippet.md +1 -1
- package/prompts/replace.md +1 -1
- package/prompts/undo-last-change-guidelines.md +4 -2
- package/prompts/undo-last-change.md +1 -1
- package/src/boundary-bypass.ts +2 -10
- package/src/constants.ts +4 -2
- package/src/grep.ts +120 -16
- package/src/hash-store.ts +45 -13
- package/src/hashline/apply.ts +2 -15
- package/src/hashline/hash.ts +34 -5
- package/src/hashline/index.ts +2 -0
- package/src/hashline/parse.ts +2 -2
- package/src/hashline/resolve.ts +30 -28
- package/src/insert.ts +2 -2
- package/src/read.ts +17 -10
- package/src/replace-diff.ts +92 -8
- package/src/replace-render.ts +32 -6
- package/src/replace-response.ts +5 -7
- package/src/replace-undo.ts +18 -25
- package/src/replace.ts +5 -5
- package/src/served.ts +2 -3
- package/src/utils.ts +13 -0
package/src/read.ts
CHANGED
|
@@ -3,14 +3,15 @@ import {
|
|
|
3
3
|
createReadTool,
|
|
4
4
|
formatSize,
|
|
5
5
|
truncateHead,
|
|
6
|
+
DEFAULT_MAX_BYTES,
|
|
6
7
|
DEFAULT_MAX_LINES,
|
|
7
8
|
type TruncationResult,
|
|
8
9
|
} from "@earendil-works/pi-coding-agent";
|
|
9
10
|
import { Type } from "typebox";
|
|
10
|
-
import { MAX_READ_LINE_BYTES } from "./constants";
|
|
11
11
|
import { loadFileKindAndText } from "./file-kind";
|
|
12
|
+
import { MAX_OVERSIZED_WARNING_LINES } from "./constants";
|
|
12
13
|
import { readNormFile, safeSnapId } from "./file-reader";
|
|
13
|
-
import { lineHashes, fmtRegion, HASH_SEP, MAX_HASH_LINES } from "./hashline";
|
|
14
|
+
import { lineHashes, fmtRegion, fmtRow, HASH_SEP, MAX_HASH_LINES } from "./hashline";
|
|
14
15
|
import { toCwd } from "./paths";
|
|
15
16
|
import { abortIf, makePrepareArguments, visLines } from "./utils";
|
|
16
17
|
import { recordServedSafe } from "./served";
|
|
@@ -56,7 +57,7 @@ export async function fmtReadPreview(
|
|
|
56
57
|
options: { offset?: number; limit?: number },
|
|
57
58
|
precomputedHashes?: string[],
|
|
58
59
|
path?: string,
|
|
59
|
-
maxLineBytes =
|
|
60
|
+
maxLineBytes = DEFAULT_MAX_BYTES,
|
|
60
61
|
maxTruncLines = DEFAULT_MAX_LINES,
|
|
61
62
|
): Promise<{ text: string; truncation?: TruncationResult; nextOffset?: number; servedHashes: string[] }> {
|
|
62
63
|
const allLines = visLines(text);
|
|
@@ -100,21 +101,27 @@ export async function fmtReadPreview(
|
|
|
100
101
|
const oversized = rowSizes.filter((row) => row.bytes > maxBytes);
|
|
101
102
|
const rows = rowSizes.map((row, index) =>
|
|
102
103
|
row.bytes > maxBytes
|
|
103
|
-
? `[Line ${row.lineNumber} is ${formatSize(row.bytes)}, exceeds ${formatSize(maxBytes)}; content not shown. Use bash: sed -n '${row.lineNumber}p' <path> | head -c ${maxBytes}]`
|
|
104
|
+
? fmtRow(selectedHashes[index]!, `[Line ${row.lineNumber} is ${formatSize(row.bytes)}, exceeds ${formatSize(maxBytes)}; content not shown. Use bash: sed -n '${row.lineNumber}p' <path> | head -c ${maxBytes}]`)
|
|
104
105
|
: fmtRegion([selectedHashes[index]!], [selected[index]!]),
|
|
105
106
|
);
|
|
106
107
|
const skippedTruncation = truncateHead(rows.join("\n"), { maxBytes, maxLines: maxTruncLines });
|
|
107
108
|
const shownRowCount = skippedTruncation.content === "" ? 0 : skippedTruncation.content.split("\n").length;
|
|
108
109
|
const lastShownLine = shownRowCount > 0 ? startLine + shownRowCount - 1 : startLine - 1;
|
|
109
|
-
const oversizedIndexes = new Set(rowSizes.map((row, index) => row.bytes > maxBytes ? index : -1).filter((index) => index >= 0));
|
|
110
110
|
const servedHashes: string[] = [];
|
|
111
111
|
for (let index = 0; index < Math.min(shownRowCount, rows.length); index++) {
|
|
112
|
-
|
|
112
|
+
servedHashes.push(selectedHashes[index]!);
|
|
113
113
|
}
|
|
114
|
-
const
|
|
115
|
-
const
|
|
116
|
-
const
|
|
117
|
-
|
|
114
|
+
const listed = oversized.slice(0, MAX_OVERSIZED_WARNING_LINES);
|
|
115
|
+
const hiddenCount = oversized.length - listed.length;
|
|
116
|
+
const lineLabel = oversized.length === 1
|
|
117
|
+
? `Line ${oversized[0]!.lineNumber}`
|
|
118
|
+
: `Lines ${listed.map((row) => row.lineNumber).join(', ')}${hiddenCount > 0 ? `, ... (+${hiddenCount} more)` : ''}`;
|
|
119
|
+
const verb = oversized.length === 1 ? 'exceeds' : 'exceed';
|
|
120
|
+
const addresses = listed.map((row) => `${row.lineNumber}p`).join(';');
|
|
121
|
+
const moreHint = hiddenCount > 0
|
|
122
|
+
? ` ${hiddenCount} more oversized line(s). Use read with offset to inspect them.`
|
|
123
|
+
: '';
|
|
124
|
+
const warning = `[${lineLabel} ${verb} ${formatSize(maxBytes)}; content not shown. Inspect with bash: sed -n '${addresses}' <path> | head -c ${maxBytes}${moreHint}]`;
|
|
118
125
|
let preview = skippedTruncation.content;
|
|
119
126
|
let nextOffset: number | undefined;
|
|
120
127
|
if (shownRowCount > 0 && (skippedTruncation.truncated || lastShownLine < totalLines)) {
|
package/src/replace-diff.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import * as Diff from "diff";
|
|
2
|
+
import { formatSize, DEFAULT_MAX_BYTES } from "@earendil-works/pi-coding-agent";
|
|
2
3
|
import {
|
|
3
4
|
_lineHashesPure,
|
|
4
5
|
ANCHOR_LEN,
|
|
@@ -55,14 +56,23 @@ const ELLIPSIS_MARKER: unique symbol = Symbol("ellipsis");
|
|
|
55
56
|
const isEllipsisMarker = (line: string | symbol): line is symbol =>
|
|
56
57
|
line === ELLIPSIS_MARKER;
|
|
57
58
|
|
|
59
|
+
export interface DiffLimits {
|
|
60
|
+
maxLineBytes?: number;
|
|
61
|
+
maxBytes?: number;
|
|
62
|
+
unlimited?: boolean;
|
|
63
|
+
}
|
|
64
|
+
|
|
58
65
|
export function genDiff(
|
|
59
66
|
oldContent: string,
|
|
60
67
|
newContent: string,
|
|
61
68
|
contextLines = 2,
|
|
62
69
|
newContentHashes?: string[],
|
|
63
70
|
oldContentHashes?: string[],
|
|
71
|
+
limits?: DiffLimits,
|
|
64
72
|
): { diff: string; firstChangedLine: number | undefined } {
|
|
65
73
|
const effectiveNewHashes = newContentHashes ?? _lineHashesPure(newContent);
|
|
74
|
+
const maxLineBytes = limits?.unlimited ? Number.POSITIVE_INFINITY : (limits?.maxLineBytes ?? DEFAULT_MAX_BYTES);
|
|
75
|
+
const maxBytes = limits?.unlimited ? Number.POSITIVE_INFINITY : (limits?.maxBytes ?? DEFAULT_MAX_BYTES);
|
|
66
76
|
|
|
67
77
|
const parts = Diff.diffLines(oldContent, newContent);
|
|
68
78
|
const output: string[] = [];
|
|
@@ -70,8 +80,42 @@ export function genDiff(
|
|
|
70
80
|
let oldLineNum = 1;
|
|
71
81
|
let lastWasChange = false;
|
|
72
82
|
let firstChangedLine: number | undefined;
|
|
83
|
+
let outBytes = 0;
|
|
84
|
+
let stopped = false;
|
|
85
|
+
let diffTruncated = false;
|
|
86
|
+
|
|
87
|
+
const emitPlain = (line: string): void => {
|
|
88
|
+
if (stopped) return;
|
|
89
|
+
const lineBytes = Buffer.byteLength(line, "utf-8") + 1;
|
|
90
|
+
if (outBytes + lineBytes > maxBytes) {
|
|
91
|
+
stopped = true;
|
|
92
|
+
diffTruncated = true;
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
outBytes += lineBytes;
|
|
96
|
+
output.push(line);
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
const emitRow = (prefix: " " | "+" | "-", line: string, hash: string | undefined): void => {
|
|
100
|
+
if (stopped) return;
|
|
101
|
+
const full = fmtDiffLine(prefix, line, hash);
|
|
102
|
+
const rowBytes = Buffer.byteLength(full, "utf-8");
|
|
103
|
+
if (rowBytes > maxLineBytes) {
|
|
104
|
+
const marker = `[Row is ${formatSize(rowBytes)}, exceeds ${formatSize(maxLineBytes)}; content not shown. Use read to see the full line.]`;
|
|
105
|
+
emitPlain(fmtDiffLine(prefix, marker, hash));
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
if (outBytes + rowBytes + 1 > maxBytes) {
|
|
109
|
+
stopped = true;
|
|
110
|
+
diffTruncated = true;
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
outBytes += rowBytes + 1;
|
|
114
|
+
output.push(full);
|
|
115
|
+
};
|
|
73
116
|
|
|
74
117
|
for (let i = 0; i < parts.length; i++) {
|
|
118
|
+
if (stopped) break;
|
|
75
119
|
const part = parts[i]!;
|
|
76
120
|
const raw = part.value.split("\n");
|
|
77
121
|
if (raw[raw.length - 1] === "") raw.pop();
|
|
@@ -80,16 +124,18 @@ export function genDiff(
|
|
|
80
124
|
if (part.added || part.removed) {
|
|
81
125
|
if (firstChangedLine === undefined) firstChangedLine = newLineNum;
|
|
82
126
|
for (let k = 0; k < displayLines.length; k++) {
|
|
127
|
+
if (stopped) break;
|
|
83
128
|
if (part.added) {
|
|
84
129
|
const hash = effectiveNewHashes[newLineNum - 1];
|
|
85
|
-
|
|
130
|
+
emitRow("+", displayLines[k]!, hash);
|
|
86
131
|
newLineNum++;
|
|
87
132
|
} else {
|
|
88
133
|
const hash = oldContentHashes?.[oldLineNum - 1];
|
|
89
|
-
|
|
134
|
+
emitRow("-", displayLines[k]!, hash);
|
|
90
135
|
oldLineNum++;
|
|
91
136
|
}
|
|
92
137
|
}
|
|
138
|
+
if (stopped) break;
|
|
93
139
|
lastWasChange = true;
|
|
94
140
|
continue;
|
|
95
141
|
}
|
|
@@ -152,24 +198,25 @@ export function genDiff(
|
|
|
152
198
|
}
|
|
153
199
|
|
|
154
200
|
if (skipStart > 0) {
|
|
155
|
-
|
|
201
|
+
emitPlain(" ...");
|
|
156
202
|
newLineNum += skipStart;
|
|
157
203
|
oldLineNum += skipStart;
|
|
158
204
|
}
|
|
159
205
|
for (const line of linesToShow) {
|
|
206
|
+
if (stopped) break;
|
|
160
207
|
if (isEllipsisMarker(line)) {
|
|
161
|
-
|
|
208
|
+
emitPlain(" ...");
|
|
162
209
|
newLineNum += skipMiddle;
|
|
163
210
|
oldLineNum += skipMiddle;
|
|
164
211
|
continue;
|
|
165
212
|
}
|
|
166
213
|
const hash = effectiveNewHashes[newLineNum - 1];
|
|
167
|
-
|
|
214
|
+
emitRow(" ", line, hash);
|
|
168
215
|
newLineNum++;
|
|
169
216
|
oldLineNum++;
|
|
170
217
|
}
|
|
171
218
|
if (skipTail > 0) {
|
|
172
|
-
|
|
219
|
+
emitPlain(" ...");
|
|
173
220
|
}
|
|
174
221
|
} else {
|
|
175
222
|
newLineNum += displayLines.length;
|
|
@@ -178,6 +225,11 @@ export function genDiff(
|
|
|
178
225
|
lastWasChange = false;
|
|
179
226
|
}
|
|
180
227
|
|
|
228
|
+
if (diffTruncated) {
|
|
229
|
+
output.push(" ...");
|
|
230
|
+
output.push(`[diff truncated at ${formatSize(maxBytes)}; use read to see the rest.]`);
|
|
231
|
+
}
|
|
232
|
+
|
|
181
233
|
return { diff: output.join("\n"), firstChangedLine };
|
|
182
234
|
}
|
|
183
235
|
|
|
@@ -185,9 +237,41 @@ export function genPatch(
|
|
|
185
237
|
path: string,
|
|
186
238
|
oldContent: string,
|
|
187
239
|
newContent: string,
|
|
188
|
-
|
|
189
|
-
|
|
240
|
+
limits?: DiffLimits,
|
|
241
|
+
): { patch: string; truncated: boolean } {
|
|
242
|
+
const full = Diff.createTwoFilesPatch(path, path, oldContent, newContent, undefined, undefined, {
|
|
190
243
|
context: 4,
|
|
191
244
|
headerOptions: Diff.FILE_HEADERS_ONLY,
|
|
192
245
|
});
|
|
246
|
+
const maxLineBytes = limits?.unlimited ? Number.POSITIVE_INFINITY : (limits?.maxLineBytes ?? DEFAULT_MAX_BYTES);
|
|
247
|
+
const maxBytes = limits?.unlimited ? Number.POSITIVE_INFINITY : (limits?.maxBytes ?? DEFAULT_MAX_BYTES);
|
|
248
|
+
const out: string[] = [];
|
|
249
|
+
let outBytes = 0;
|
|
250
|
+
let truncated = false;
|
|
251
|
+
for (const line of full.split("\n")) {
|
|
252
|
+
const lineBytes = Buffer.byteLength(line, "utf-8");
|
|
253
|
+
if (lineBytes > maxLineBytes) {
|
|
254
|
+
truncated = true;
|
|
255
|
+
const prefix = /^[ +-]/.test(line) ? line[0]! : "";
|
|
256
|
+
const marker = `${prefix}[Patch line is ${formatSize(lineBytes)}, exceeds ${formatSize(maxLineBytes)}; content not shown. Use read to see the full line.]`;
|
|
257
|
+
const markerBytes = Buffer.byteLength(marker, "utf-8") + 1;
|
|
258
|
+
if (outBytes + markerBytes > maxBytes) {
|
|
259
|
+
break;
|
|
260
|
+
}
|
|
261
|
+
outBytes += markerBytes;
|
|
262
|
+
out.push(marker);
|
|
263
|
+
continue;
|
|
264
|
+
}
|
|
265
|
+
if (outBytes + lineBytes + 1 > maxBytes) {
|
|
266
|
+
truncated = true;
|
|
267
|
+
break;
|
|
268
|
+
}
|
|
269
|
+
outBytes += lineBytes + 1;
|
|
270
|
+
out.push(line);
|
|
271
|
+
}
|
|
272
|
+
if (truncated) {
|
|
273
|
+
out.push("...");
|
|
274
|
+
out.push(`[patch truncated at ${formatSize(maxBytes)}; the patch cannot be applied as-is. Use read to see the full file.]`);
|
|
275
|
+
}
|
|
276
|
+
return { patch: out.join("\n"), truncated };
|
|
193
277
|
}
|
package/src/replace-render.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Markdown, Text } from "@earendil-works/pi-tui";
|
|
2
|
-
import type
|
|
2
|
+
import { keyHint, type Theme } from "@earendil-works/pi-coding-agent";
|
|
3
3
|
import { normReq } from "./replace-normalize";
|
|
4
4
|
import type { ReqParams, ReplaceDetails } from "./replace";
|
|
5
5
|
import { isRec } from "./utils";
|
|
@@ -145,20 +145,46 @@ export function isApplied(
|
|
|
145
145
|
);
|
|
146
146
|
}
|
|
147
147
|
|
|
148
|
+
const RESULT_PREVIEW_LINES = 16;
|
|
149
|
+
|
|
150
|
+
function expandHint(): string {
|
|
151
|
+
try {
|
|
152
|
+
return keyHint("app.tools.expand", "to expand");
|
|
153
|
+
} catch {
|
|
154
|
+
return "ctrl+o to expand";
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function extractSummary(text: string | undefined): string | undefined {
|
|
159
|
+
if (!text) return undefined;
|
|
160
|
+
if (text.includes("│")) return undefined;
|
|
161
|
+
const warningsIdx = text.indexOf("\n\nWarnings:");
|
|
162
|
+
const summary = warningsIdx >= 0 ? text.slice(0, warningsIdx) : text;
|
|
163
|
+
return summary.length > 0 ? summary : undefined;
|
|
164
|
+
}
|
|
165
|
+
|
|
148
166
|
export function buildAppliedText(
|
|
149
167
|
text: string | undefined,
|
|
150
168
|
details: ReplaceDetails | undefined,
|
|
151
169
|
theme: FgT,
|
|
170
|
+
expanded: boolean,
|
|
152
171
|
): string | undefined {
|
|
153
172
|
const sections: string[] = [];
|
|
154
|
-
|
|
173
|
+
const summary = extractSummary(text);
|
|
174
|
+
if (summary) sections.push(summary);
|
|
155
175
|
if (details?.diff) {
|
|
156
|
-
|
|
176
|
+
const diffLines = details.diff.split("\n");
|
|
177
|
+
const diffSection = expanded
|
|
178
|
+
? fmtResult(details.diff, theme)
|
|
179
|
+
: fmtPreview(details.diff, false, theme);
|
|
180
|
+
const hint =
|
|
181
|
+
!expanded && diffLines.length > RESULT_PREVIEW_LINES
|
|
182
|
+
? ` (${expandHint()})`
|
|
183
|
+
: "";
|
|
184
|
+
sections.push(`${diffSection}${hint}`);
|
|
157
185
|
}
|
|
158
|
-
|
|
159
186
|
const warnings = extractWarnings(text);
|
|
160
187
|
if (warnings) sections.push(warnings);
|
|
161
|
-
|
|
162
188
|
return sections.length > 0 ? sections.join("\n\n") : undefined;
|
|
163
189
|
}
|
|
164
190
|
|
|
@@ -317,7 +343,7 @@ export function renderEditResult(
|
|
|
317
343
|
: new Text("", 0, 0);
|
|
318
344
|
}
|
|
319
345
|
if (isApplied(result.details)) {
|
|
320
|
-
const appliedText = buildAppliedText(renderedText, result.details, theme);
|
|
346
|
+
const appliedText = buildAppliedText(renderedText, result.details, theme, context.expanded === true);
|
|
321
347
|
return appliedText ? reuseText(context, appliedText) : new Text("", 0, 0);
|
|
322
348
|
}
|
|
323
349
|
if (!renderedText) return new Text("", 0, 0);
|
package/src/replace-response.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { NEdit } from "./hashline";
|
|
1
2
|
import type { ReplaceDetails } from "./replace";
|
|
2
3
|
import { genDiff, genPatch } from "./replace-diff";
|
|
3
4
|
import { visLines, clipLine } from "./utils";
|
|
@@ -27,14 +28,9 @@ export type RMeta = {
|
|
|
27
28
|
removedLines: number;
|
|
28
29
|
};
|
|
29
30
|
|
|
30
|
-
type NEditEntry = {
|
|
31
|
-
loc: string;
|
|
32
|
-
currentContent: string;
|
|
33
|
-
};
|
|
34
|
-
|
|
35
31
|
export interface NoopInput {
|
|
36
32
|
path: string;
|
|
37
|
-
noopEdit:
|
|
33
|
+
noopEdit: NEdit | undefined;
|
|
38
34
|
snapshotId?: string;
|
|
39
35
|
editMeta: RMeta;
|
|
40
36
|
warnings: string[] | undefined;
|
|
@@ -157,11 +153,13 @@ export function buildChanged(input: SuccessInput, verb = "replaced"): TResult {
|
|
|
157
153
|
removedLines,
|
|
158
154
|
});
|
|
159
155
|
|
|
156
|
+
const patchResult = genPatch(path, originalNormalized, result);
|
|
160
157
|
return {
|
|
161
158
|
content: [{ type: "text", text }],
|
|
162
159
|
details: {
|
|
163
160
|
diff: diffResult.diff,
|
|
164
|
-
patch:
|
|
161
|
+
patch: patchResult.patch,
|
|
162
|
+
...(patchResult.truncated ? { patchTruncated: true as const } : {}),
|
|
165
163
|
firstChangedLine:
|
|
166
164
|
editMeta.firstChangedLine ?? diffResult.firstChangedLine,
|
|
167
165
|
snapshotId,
|
package/src/replace-undo.ts
CHANGED
|
@@ -126,26 +126,15 @@ export function regUndo(pi: ExtensionAPI): void {
|
|
|
126
126
|
if (errCode(error) !== "ENOENT") throw error;
|
|
127
127
|
}
|
|
128
128
|
|
|
129
|
-
if (
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
{
|
|
134
|
-
type: "text",
|
|
135
|
-
text: `[E_UNDO_STALE] Cannot undo last change on ${path}: the file no longer exists.`
|
|
136
|
-
},
|
|
137
|
-
],
|
|
138
|
-
isError: true,
|
|
139
|
-
details: {},
|
|
140
|
-
};
|
|
141
|
-
}
|
|
142
|
-
if (currentRaw !== undo.bom + restoreEndings(undo.resultContent, undo.originalEnding)) {
|
|
143
|
-
await clearUndo(mutationTargetPath);
|
|
129
|
+
if (
|
|
130
|
+
currentRaw !== undefined &&
|
|
131
|
+
currentRaw !== undo.bom + restoreEndings(undo.resultContent, undo.originalEnding)
|
|
132
|
+
) {
|
|
144
133
|
return {
|
|
145
134
|
content: [
|
|
146
135
|
{
|
|
147
136
|
type: "text",
|
|
148
|
-
text: `[E_UNDO_STALE] Cannot undo last change on ${path}: the file changed after the edit. Call read() to inspect the current state.`
|
|
137
|
+
text: `[E_UNDO_STALE] Cannot undo last change on ${path}: the file changed after the edit. The undo record is kept; once the file matches the edited state again, undo_last_change will succeed. Call read() to inspect the current state.`
|
|
149
138
|
},
|
|
150
139
|
],
|
|
151
140
|
isError: true,
|
|
@@ -153,20 +142,19 @@ export function regUndo(pi: ExtensionAPI): void {
|
|
|
153
142
|
};
|
|
154
143
|
}
|
|
155
144
|
|
|
156
|
-
|
|
157
|
-
|
|
145
|
+
await writeAtomic(
|
|
146
|
+
mutationTargetPath,
|
|
147
|
+
undo.bom + restoreEndings(undo.content, undo.originalEnding),
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
const currentNormalized = currentRaw === undefined ? "" : toLF(stripBOM(currentRaw).text);
|
|
158
151
|
const currentHashes = await lineHashes(currentNormalized, mutationTargetPath);
|
|
159
|
-
const diffResult = genDiff(undo.content,
|
|
152
|
+
const diffResult = genDiff(undo.content, undo.resultContent, 0, undefined, undo.hashes, { unlimited: true });
|
|
160
153
|
const linesAddedByReplace = cntDiff(diffResult.diff, "+");
|
|
161
154
|
const linesRemovedByReplace = cntDiff(diffResult.diff, "-");
|
|
162
155
|
const restoredRange = changedRange(currentNormalized, undo.content);
|
|
163
156
|
const undoDiff = genDiff(currentNormalized, undo.content, 1, undo.hashes, currentHashes).diff;
|
|
164
157
|
|
|
165
|
-
await writeAtomic(
|
|
166
|
-
mutationTargetPath,
|
|
167
|
-
undo.bom + restoreEndings(undo.content, undo.originalEnding),
|
|
168
|
-
);
|
|
169
|
-
|
|
170
158
|
try {
|
|
171
159
|
const store = await loadHashStore();
|
|
172
160
|
upsertSnapshot(store, mutationTargetPath, contentChecksum(undo.content), splitLines(undo.content).length, undo.hashes);
|
|
@@ -180,6 +168,9 @@ export function regUndo(pi: ExtensionAPI): void {
|
|
|
180
168
|
const parts: string[] = [
|
|
181
169
|
`Undone last change on ${path}.`,
|
|
182
170
|
];
|
|
171
|
+
if (currentRaw === undefined) {
|
|
172
|
+
parts.push("The file was deleted; restored it from undo history.");
|
|
173
|
+
}
|
|
183
174
|
if (linesAddedByReplace > 0 || linesRemovedByReplace > 0) {
|
|
184
175
|
parts.push(
|
|
185
176
|
`Removed ${linesAddedByReplace} line(s), restored ${linesRemovedByReplace} line(s).`,
|
|
@@ -189,6 +180,7 @@ export function regUndo(pi: ExtensionAPI): void {
|
|
|
189
180
|
"Call read for fresh anchors.",
|
|
190
181
|
);
|
|
191
182
|
|
|
183
|
+
const patchResult = genPatch(path, currentNormalized, undo.content);
|
|
192
184
|
return {
|
|
193
185
|
content: [
|
|
194
186
|
{
|
|
@@ -198,7 +190,8 @@ export function regUndo(pi: ExtensionAPI): void {
|
|
|
198
190
|
],
|
|
199
191
|
details: {
|
|
200
192
|
diff: undoDiff,
|
|
201
|
-
patch:
|
|
193
|
+
patch: patchResult.patch,
|
|
194
|
+
...(patchResult.truncated ? { patchTruncated: true as const } : {}),
|
|
202
195
|
metrics: buildMetrics({
|
|
203
196
|
classification: "applied",
|
|
204
197
|
editsAttempted: 1,
|
package/src/replace.ts
CHANGED
|
@@ -31,9 +31,8 @@ import {
|
|
|
31
31
|
type RPreview,
|
|
32
32
|
type RRState,
|
|
33
33
|
} from "./replace-render";
|
|
34
|
-
export { reuseText, reuseMarkdown } from "./replace-render";
|
|
35
34
|
import { loadP, loadGuide } from "./prompts";
|
|
36
|
-
import { loadHashStore, findSnapshotPaths, type HashStore } from "./hash-store";
|
|
35
|
+
import { loadHashStore, findSnapshotPaths, findServedPaths, type HashStore } from "./hash-store";
|
|
37
36
|
import { getServed, recordServedSafe } from "./served";
|
|
38
37
|
import { noopPayloadKey, markBoundaryNoop, consumeBoundaryBypass, clearBoundaryBypass } from "./boundary-bypass";
|
|
39
38
|
import { commitEdit } from "./commit";
|
|
@@ -50,11 +49,11 @@ const replacementLinesSchema = Type.Array(
|
|
|
50
49
|
);
|
|
51
50
|
|
|
52
51
|
const removeFromSchema = Type.String({
|
|
53
|
-
description: "Bare 3-char
|
|
52
|
+
description: "Bare 3-char anchor only (e.g. \"aB3\"): copy just the anchor from the leftmost column of a read row like `aB3│content`; never the line content. Marks the FIRST line to remove (inclusive)",
|
|
54
53
|
});
|
|
55
54
|
|
|
56
55
|
const removeToSchema = Type.String({
|
|
57
|
-
description: "Bare 3-char
|
|
56
|
+
description: "Bare 3-char anchor only (e.g. \"aB3\"): copy just the anchor from the leftmost column of a read row like `aB3│content`; never the line content. Marks the LAST line to remove (inclusive)",
|
|
58
57
|
});
|
|
59
58
|
|
|
60
59
|
export const editToolSchema = Type.Object(
|
|
@@ -76,6 +75,7 @@ export type ReqParams = {
|
|
|
76
75
|
export type ReplaceDetails = {
|
|
77
76
|
diff: string;
|
|
78
77
|
patch?: string;
|
|
78
|
+
patchTruncated?: boolean;
|
|
79
79
|
firstChangedLine?: number;
|
|
80
80
|
snapshotId?: string;
|
|
81
81
|
classification?: "noop";
|
|
@@ -149,7 +149,7 @@ async function resolveMissingPath(
|
|
|
149
149
|
} catch {
|
|
150
150
|
return undefined;
|
|
151
151
|
}
|
|
152
|
-
const matches = findSnapshotPaths(store, hashes);
|
|
152
|
+
const matches = [...new Set([...findSnapshotPaths(store, hashes), ...findServedPaths(store, hashes)])];
|
|
153
153
|
if (matches.length === 1) {
|
|
154
154
|
return {
|
|
155
155
|
path: matches[0]!,
|
package/src/served.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { loadHashStore,
|
|
1
|
+
import { loadHashStore, parseStoredHashes, type HashStore } from "./hash-store";
|
|
2
2
|
import { HASH_CLASS } from "./hashline/alphabet";
|
|
3
3
|
|
|
4
4
|
const SERVED_DIFF_ROW_RE = new RegExp(`^[+ ](${HASH_CLASS})│`);
|
|
@@ -14,8 +14,7 @@ export function servedHashesFromDiff(diff: string): string[] {
|
|
|
14
14
|
|
|
15
15
|
export function getServed(store: HashStore, path: string): Set<string> | undefined {
|
|
16
16
|
const row = store.stmts.servedGet(path);
|
|
17
|
-
|
|
18
|
-
const parsed = parseHashList(row.hashes as string, () => store.stmts.servedDelete(path));
|
|
17
|
+
const parsed = parseStoredHashes(row, () => store.stmts.servedDelete(path));
|
|
19
18
|
if (!parsed) return undefined;
|
|
20
19
|
return new Set(parsed);
|
|
21
20
|
}
|
package/src/utils.ts
CHANGED
|
@@ -93,6 +93,19 @@ export function firstNonEmpty(lines: string[]): string | undefined {
|
|
|
93
93
|
return idx >= 0 ? lines[idx] : undefined;
|
|
94
94
|
}
|
|
95
95
|
|
|
96
|
+
export function truncateToBytes(s: string, maxBytes: number): string {
|
|
97
|
+
if (Buffer.byteLength(s, "utf-8") <= maxBytes) return s;
|
|
98
|
+
let out = "";
|
|
99
|
+
let bytes = 0;
|
|
100
|
+
for (const ch of s) {
|
|
101
|
+
const chBytes = Buffer.byteLength(ch, "utf-8");
|
|
102
|
+
if (bytes + chBytes > maxBytes) break;
|
|
103
|
+
out += ch;
|
|
104
|
+
bytes += chBytes;
|
|
105
|
+
}
|
|
106
|
+
return out;
|
|
107
|
+
}
|
|
108
|
+
|
|
96
109
|
export function clipLine(line: string, maxLen = 200): string {
|
|
97
110
|
const flat = line.replace(/\n/g, "\\n");
|
|
98
111
|
return flat.length > maxLen ? `${flat.slice(0, maxLen)}...` : flat;
|