pi-hashline-edit-pro 4.2.10 → 4.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/README.md +26 -19
- package/index.ts +29 -10
- package/package.json +1 -1
- package/prompts/insert-guidelines.md +2 -1
- package/prompts/replace.md +1 -1
- package/src/anchor-registry.ts +21 -31
- package/src/auto-read-all.ts +305 -0
- package/src/batch.ts +172 -84
- package/src/commit.ts +26 -26
- package/src/config-ui.ts +2 -1
- package/src/config.ts +52 -29
- package/src/constants.ts +6 -0
- package/src/fs-write.ts +1 -1
- package/src/grep.ts +12 -18
- package/src/hash-store/cache.ts +0 -18
- package/src/hash-store/retry.ts +0 -4
- package/src/hash-store/validation.ts +0 -31
- package/src/hash-store.ts +22 -32
- package/src/hashline/apply.ts +2 -2
- package/src/hashline/hash.ts +7 -8
- package/src/hashline/index.ts +1 -0
- package/src/hashline/parse.ts +3 -16
- package/src/hashline/resolve.ts +19 -39
- package/src/insert.ts +2 -4
- package/src/payload-contract.ts +3 -11
- package/src/read.ts +3 -3
- package/src/replace-diff.ts +131 -107
- package/src/replace-render.ts +37 -4
- package/src/replace-response.ts +42 -13
- package/src/replace-undo.ts +10 -10
- package/src/replace.ts +33 -54
- package/src/served.ts +7 -3
- package/src/utils.ts +9 -8
- package/src/write-hook.ts +0 -4
- package/src/boundary-bypass.ts +0 -51
package/src/replace-render.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Markdown, Text } from "@earendil-works/pi-tui";
|
|
2
2
|
import { keyHint, type Theme } from "@earendil-works/pi-coding-agent";
|
|
3
3
|
import type { ReplaceDetails } from "./replace";
|
|
4
|
+
import { abortedBatchMessageFor } from "./batch";
|
|
4
5
|
import { withLineNumbers } from "./utils";
|
|
5
6
|
import { isDedupRow } from "./replace-response";
|
|
6
7
|
import { getPreviewInput } from "./payload-contract";
|
|
@@ -66,6 +67,21 @@ export function fmtResult(diff: string, theme: FgT): string {
|
|
|
66
67
|
return colorLines(diff.split("\n"), theme).join("\n");
|
|
67
68
|
}
|
|
68
69
|
|
|
70
|
+
export function highlightBatchRefs(text: string, theme: FgT): string {
|
|
71
|
+
const pattern = /\b([Bb]atch \d+)\b/g;
|
|
72
|
+
const sections: string[] = [];
|
|
73
|
+
let cursor = 0;
|
|
74
|
+
for (;;) {
|
|
75
|
+
const match = pattern.exec(text);
|
|
76
|
+
if (!match) break;
|
|
77
|
+
if (match.index > cursor) sections.push(theme.fg("error", text.slice(cursor, match.index)));
|
|
78
|
+
sections.push(theme.fg("warning", match[0]));
|
|
79
|
+
cursor = match.index + match[0].length;
|
|
80
|
+
}
|
|
81
|
+
if (cursor < text.length) sections.push(theme.fg("error", text.slice(cursor)));
|
|
82
|
+
return sections.join("");
|
|
83
|
+
}
|
|
84
|
+
|
|
69
85
|
export function fmtCall(
|
|
70
86
|
args: { path?: string; remove_from?: string; remove_to?: string; anchor?: string } | undefined,
|
|
71
87
|
state: RRState,
|
|
@@ -144,6 +160,14 @@ function extractSummary(text: string | undefined): string | undefined {
|
|
|
144
160
|
return summary.length > 0 ? summary : undefined;
|
|
145
161
|
}
|
|
146
162
|
|
|
163
|
+
const BATCH_HEADER_RE = /^batch \d+:\n/;
|
|
164
|
+
|
|
165
|
+
function splitBatchHeader(diff: string, lineNumbers: (number | undefined)[] | undefined): { header: string | undefined; body: string; lineNumbers: (number | undefined)[] | undefined } {
|
|
166
|
+
const match = diff.match(BATCH_HEADER_RE);
|
|
167
|
+
if (!match) return { header: undefined, body: diff, lineNumbers };
|
|
168
|
+
return { header: match[0]!.slice(0, -1), body: diff.slice(match[0].length), lineNumbers: lineNumbers?.slice(1) };
|
|
169
|
+
}
|
|
170
|
+
|
|
147
171
|
export function buildAppliedText(
|
|
148
172
|
text: string | undefined,
|
|
149
173
|
details: ReplaceDetails | undefined,
|
|
@@ -154,8 +178,9 @@ export function buildAppliedText(
|
|
|
154
178
|
const summary = extractSummary(text);
|
|
155
179
|
if (summary) sections.push(summary);
|
|
156
180
|
if (details?.diff) {
|
|
157
|
-
const
|
|
158
|
-
const
|
|
181
|
+
const { header, body, lineNumbers } = splitBatchHeader(details.diff, details.diffLineNumbers);
|
|
182
|
+
const rawDiff = lineNumbers ? toNumberedDiff(body, lineNumbers) : body;
|
|
183
|
+
const diffLines = body.split("\n");
|
|
159
184
|
const diffSection = expanded
|
|
160
185
|
? fmtResult(rawDiff, theme)
|
|
161
186
|
: fmtPreview(rawDiff, false, theme);
|
|
@@ -163,7 +188,7 @@ export function buildAppliedText(
|
|
|
163
188
|
!expanded && diffLines.length > RESULT_PREVIEW_LINES
|
|
164
189
|
? ` (${expandHint()})`
|
|
165
190
|
: "";
|
|
166
|
-
sections.push(`${diffSection}${hint}`);
|
|
191
|
+
sections.push(header !== undefined ? `${theme.fg("warning", header)}\n${diffSection}${hint}` : `${diffSection}${hint}`);
|
|
167
192
|
}
|
|
168
193
|
const warnings = details?.warnings?.length ? `Warnings:\n${details.warnings.join("\n")}` : extractWarnings(text);
|
|
169
194
|
if (warnings) sections.push(warnings);
|
|
@@ -343,9 +368,17 @@ export function renderEditResult(
|
|
|
343
368
|
renderState.preview = undefined;
|
|
344
369
|
renderState.previewGeneration = (renderState.previewGeneration ?? 0) + 1;
|
|
345
370
|
}
|
|
371
|
+
const batch = result.details?.batch;
|
|
372
|
+
if (batch && batch.last === false) {
|
|
373
|
+
const abortedMessage = typeof context?.toolCallId === "string" ? abortedBatchMessageFor(context.toolCallId) : undefined;
|
|
374
|
+
if (abortedMessage !== undefined) {
|
|
375
|
+
return reuseText(context, `\n${highlightBatchRefs(abortedMessage, theme)}`);
|
|
376
|
+
}
|
|
377
|
+
return reuseText(context, theme.fg("warning", `In batch ${batch.id}`));
|
|
378
|
+
}
|
|
346
379
|
if (context.isError) {
|
|
347
380
|
return renderedText
|
|
348
|
-
? reuseText(context, `\n${
|
|
381
|
+
? reuseText(context, `\n${highlightBatchRefs(renderedText, theme)}`)
|
|
349
382
|
: new Text("", 0, 0);
|
|
350
383
|
}
|
|
351
384
|
if (isApplied(result.details)) {
|
package/src/replace-response.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { formatSize, DEFAULT_MAX_BYTES } from "@earendil-works/pi-coding-agent";
|
|
|
2
2
|
import type { NEdit } from "./hashline";
|
|
3
3
|
import { HASH_SEP } from "./hashline";
|
|
4
4
|
import type { ReplaceDetails } from "./replace";
|
|
5
|
-
import { genDiff, genPatch, type DiffSpan } from "./replace-diff";
|
|
5
|
+
import { genDiff, genPatch, fmtDedupRow, type DiffSpan } from "./replace-diff";
|
|
6
6
|
import { visLines, clipLine } from "./utils";
|
|
7
7
|
import { DEDUP_ANCHOR } from "./constants";
|
|
8
8
|
|
|
@@ -106,7 +106,7 @@ export function buildNoop(input: NoopInput, noopNoun = "Replacement"): TResult {
|
|
|
106
106
|
: "The edit produced identical content.";
|
|
107
107
|
const dedupNote =
|
|
108
108
|
boundaryRemovedLines !== undefined && boundaryRemovedLines > 0
|
|
109
|
-
? `\nBoundary dedup removed ${boundaryRemovedLines} line(s)
|
|
109
|
+
? `\nBoundary dedup removed ${boundaryRemovedLines} line(s) that duplicated adjacent lines.`
|
|
110
110
|
: "";
|
|
111
111
|
|
|
112
112
|
const text = `No changes made to ${path}\nClassification: noop\n${noopDetailsText}${dedupNote}${warnBlock(warnings)}`;
|
|
@@ -131,13 +131,7 @@ export function buildNoop(input: NoopInput, noopNoun = "Replacement"): TResult {
|
|
|
131
131
|
},
|
|
132
132
|
};
|
|
133
133
|
}
|
|
134
|
-
export
|
|
135
|
-
const row = `${DEDUP_ANCHOR}${HASH_SEP}${line}`;
|
|
136
|
-
if (Buffer.byteLength(row, "utf-8") <= DEFAULT_MAX_BYTES) return row;
|
|
137
|
-
const size = formatSize(Buffer.byteLength(row, "utf-8"));
|
|
138
|
-
const limit = formatSize(DEFAULT_MAX_BYTES);
|
|
139
|
-
return `${DEDUP_ANCHOR}${HASH_SEP}[Row is ${size}, exceeds ${limit}; content not shown. Use read to see the full line.]`;
|
|
140
|
-
}
|
|
134
|
+
export { fmtDedupRow };
|
|
141
135
|
|
|
142
136
|
export function isDedupRow(line: string): boolean {
|
|
143
137
|
return line.startsWith(`${DEDUP_ANCHOR}${HASH_SEP}`);
|
|
@@ -147,9 +141,42 @@ export function isChangeRow(line: string): boolean {
|
|
|
147
141
|
return line.startsWith("+") || line.startsWith("-");
|
|
148
142
|
}
|
|
149
143
|
|
|
150
|
-
|
|
151
|
-
const
|
|
152
|
-
|
|
144
|
+
function capDedupRows(rows: string[], budget: number): { rows: string[]; bytes: number; omitted: number } {
|
|
145
|
+
const kept: string[] = [];
|
|
146
|
+
let bytes = 0;
|
|
147
|
+
for (const row of rows) {
|
|
148
|
+
const rowBytes = Buffer.byteLength(row, "utf-8") + 1;
|
|
149
|
+
if (bytes + rowBytes > budget) break;
|
|
150
|
+
kept.push(row);
|
|
151
|
+
bytes += rowBytes;
|
|
152
|
+
}
|
|
153
|
+
return { rows: kept, bytes, omitted: rows.length - kept.length };
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function dedupOmissionNote(omitted: number, maxBytes: number): string {
|
|
157
|
+
return fmtDedupRow(`[${omitted} line(s) not shown again; output capped at ${formatSize(maxBytes)}.]`);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export function withDedupRows(diff: string, lineNumbers: (number | undefined)[], above: string[] | undefined, below: string[] | undefined, maxBytes = DEFAULT_MAX_BYTES): { diff: string; lineNumbers: (number | undefined)[] } {
|
|
161
|
+
const topAll = (above ?? []).map(fmtDedupRow);
|
|
162
|
+
const bottomAll = (below ?? []).map(fmtDedupRow);
|
|
163
|
+
if (topAll.length === 0 && bottomAll.length === 0) return { diff, lineNumbers };
|
|
164
|
+
const usedBytes = Buffer.byteLength(diff, "utf-8");
|
|
165
|
+
const fullBudget = Math.max(0, maxBytes - usedBytes);
|
|
166
|
+
const initialTop = capDedupRows(topAll, fullBudget);
|
|
167
|
+
const initialBottom = capDedupRows(bottomAll, Math.max(0, fullBudget - initialTop.bytes));
|
|
168
|
+
const anyOmitted = initialTop.omitted + initialBottom.omitted > 0;
|
|
169
|
+
const reserved = anyOmitted ? Buffer.byteLength(dedupOmissionNote(topAll.length + bottomAll.length, maxBytes), "utf-8") + 1 : 0;
|
|
170
|
+
const budget = Math.max(0, fullBudget - reserved);
|
|
171
|
+
const topCapped = anyOmitted ? capDedupRows(topAll, budget) : initialTop;
|
|
172
|
+
const bottomCapped = anyOmitted ? capDedupRows(bottomAll, Math.max(0, budget - topCapped.bytes)) : initialBottom;
|
|
173
|
+
const top = topCapped.rows;
|
|
174
|
+
const bottom = [...bottomCapped.rows];
|
|
175
|
+
const omitted = topCapped.omitted + bottomCapped.omitted;
|
|
176
|
+
if (omitted > 0) {
|
|
177
|
+
const note = dedupOmissionNote(omitted, maxBytes);
|
|
178
|
+
if (usedBytes + topCapped.bytes + bottomCapped.bytes + Buffer.byteLength(note, "utf-8") + 1 <= maxBytes) bottom.push(note);
|
|
179
|
+
}
|
|
153
180
|
if (top.length === 0 && bottom.length === 0) return { diff, lineNumbers };
|
|
154
181
|
if (diff.length === 0) return { diff: [...top, ...bottom].join("\n"), lineNumbers: [...lineNumbers, ...[...top, ...bottom].map(() => undefined)] };
|
|
155
182
|
const lines = diff.split("\n");
|
|
@@ -175,7 +202,9 @@ export function buildChanged(input: SuccessInput, verb = "replaced", diffContext
|
|
|
175
202
|
const { path, result, warnings, snapshotId, originalNormalized, originalHashes, editMeta, resultHashes, boundaryDedupAbove, boundaryDedupBelow, spans } = input;
|
|
176
203
|
const resultLines = visLines(result);
|
|
177
204
|
const baseDiff = genDiff(originalNormalized, result, diffContextLines, resultHashes, originalHashes, undefined, spans);
|
|
178
|
-
const diffResult =
|
|
205
|
+
const diffResult = baseDiff.spanDedupEmitted
|
|
206
|
+
? baseDiff
|
|
207
|
+
: withDedupRows(baseDiff.diff, baseDiff.lineNumbers, boundaryDedupAbove, boundaryDedupBelow);
|
|
179
208
|
const addedLines = editMeta.addedLines;
|
|
180
209
|
const removedLines = editMeta.removedLines;
|
|
181
210
|
const warningsBlock = warnBlock(warnings);
|
package/src/replace-undo.ts
CHANGED
|
@@ -4,10 +4,9 @@ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
|
4
4
|
import { withFileMutationQueue } from "@earendil-works/pi-coding-agent";
|
|
5
5
|
import { Type } from "typebox";
|
|
6
6
|
import { loadHashStore, persistSnapshot, upsertUndo, getUndoEntry, deleteUndo, type UndoRecord } from "./hash-store";
|
|
7
|
-
import { servedHashesFromDiff,
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import { markServed as markServedScoped, freeAnchors, adoptAnchors, withAnchorSession } from "./anchor-registry";
|
|
7
|
+
import { servedHashesFromDiff, serveRows } from "./served";
|
|
8
|
+
import { lineChecksum } from "./hashline";
|
|
9
|
+
import { freeAnchors, adoptAnchors, withAnchorSession } from "./anchor-registry";
|
|
11
10
|
import { resolveInCwd, writeAtomic, type FileIdentity } from "./fs-write";
|
|
12
11
|
import { toLF, stripBOM, restoreEndings, type LineEnding } from "./normalize";
|
|
13
12
|
import { genDiff, genPatch, spansFromHashes } from "./replace-diff";
|
|
@@ -172,7 +171,7 @@ export function regUndo(pi: ExtensionAPI): void {
|
|
|
172
171
|
content: [
|
|
173
172
|
{
|
|
174
173
|
type: "text",
|
|
175
|
-
text: `[E_UNDO_STALE] Cannot undo last change on ${path}: the file was modified after the edit, so nothing was reverted
|
|
174
|
+
text: `[E_UNDO_STALE] Cannot undo last change on ${path}: the file was modified after the edit, so nothing was reverted and the file was left untouched. The undo record is kept. Do not edit the file to force the undo. Call read() to inspect the current state.`
|
|
176
175
|
},
|
|
177
176
|
],
|
|
178
177
|
isError: true,
|
|
@@ -200,16 +199,17 @@ export function regUndo(pi: ExtensionAPI): void {
|
|
|
200
199
|
try {
|
|
201
200
|
const store = await loadHashStore();
|
|
202
201
|
const undoLines = splitLines(undo.content);
|
|
203
|
-
persistSnapshot(store, mutationTargetPath, undo.content, undo.hashes, undoLines.map(
|
|
202
|
+
persistSnapshot(store, mutationTargetPath, undo.content, undo.hashes, undoLines.map(lineChecksum));
|
|
204
203
|
freeAnchors(mutationTargetPath);
|
|
205
204
|
adoptAnchors(
|
|
206
205
|
mutationTargetPath,
|
|
207
|
-
new Map(undoLines.map((line, i) => [undo.hashes[i]!,
|
|
206
|
+
new Map(undoLines.map((line, i) => [undo.hashes[i]!, lineChecksum(line)])),
|
|
208
207
|
);
|
|
209
|
-
|
|
208
|
+
serveRows(
|
|
210
209
|
mutationTargetPath,
|
|
211
|
-
|
|
212
|
-
|
|
210
|
+
undo.hashes,
|
|
211
|
+
undoLines,
|
|
212
|
+
servedHashesFromDiff(undoDiff),
|
|
213
213
|
);
|
|
214
214
|
} catch (error) {
|
|
215
215
|
console.error("Failed to restore hash store snapshot after undo:", error);
|
package/src/replace.ts
CHANGED
|
@@ -32,10 +32,9 @@ import { loadHashStore, type HashStore } from "./hash-store";
|
|
|
32
32
|
import { adoptAnchors, servedForPath, withAnchorSession } from "./anchor-registry";
|
|
33
33
|
import { resolveTarget } from "./fs-write";
|
|
34
34
|
import { toCwd } from "./paths";
|
|
35
|
-
import { noopPayloadKey, markBoundaryNoop, consumeBoundaryBypass, clearBoundaryBypass } from "./boundary-bypass";
|
|
36
35
|
import { queuedEdit, editToolBase, editRenderCallWrapper, editRenderResultWrapper, resolveEditTargetWithRequirement, throwIfStrictInput, getBoundaryDedupMode, withReplacePrompts, DEFAULT_EDIT_FLAGS, type EditToolFlags } from "./edit-common";
|
|
37
|
-
import { commitEdit } from "./commit";
|
|
38
|
-
import { batchMemberFor, executeBatchMember, noteBatchFailure
|
|
36
|
+
import { commitEdit, dedupRowsFromFixes } from "./commit";
|
|
37
|
+
import { batchMemberFor, executeBatchMember, noteBatchFailure } from "./batch";
|
|
39
38
|
|
|
40
39
|
export { editToolSchema, type ReqParams, assertReq };
|
|
41
40
|
|
|
@@ -67,7 +66,6 @@ export interface PipelineResult {
|
|
|
67
66
|
resultHashes: string[];
|
|
68
67
|
totalAddedLines: number;
|
|
69
68
|
totalRemovedLines: number;
|
|
70
|
-
hadBoundaryDedup: boolean;
|
|
71
69
|
boundaryRemovedLines: number;
|
|
72
70
|
boundaryRemovedLineTexts: string[];
|
|
73
71
|
boundaryDedupAbove: string[];
|
|
@@ -92,7 +90,14 @@ export function hashSpan(hashes: string[], from: string, to: string): [number, n
|
|
|
92
90
|
if (a < 0 || b < 0) return undefined;
|
|
93
91
|
return [Math.min(a, b), Math.max(a, b)];
|
|
94
92
|
}
|
|
95
|
-
|
|
93
|
+
|
|
94
|
+
export function spanForEdit(originalHashes: string[], from: string, to: string, resultContent: string): DiffSpan | undefined {
|
|
95
|
+
const span = hashSpan(originalHashes, from, to);
|
|
96
|
+
if (!span) return undefined;
|
|
97
|
+
const replacementCount = splitLines(resultContent).length - (originalHashes.length - (span[1] - span[0] + 1));
|
|
98
|
+
return { start: span[0], end: span[1], replacementCount };
|
|
99
|
+
}
|
|
100
|
+
async function noteAnchorError(absolutePath: string, error: unknown, noPersist?: boolean): Promise<void> {
|
|
96
101
|
if (noPersist === true) return;
|
|
97
102
|
if (error instanceof RangeStaleError) {
|
|
98
103
|
adoptAnchors(absolutePath, error.rangeServedMap);
|
|
@@ -161,7 +166,7 @@ export async function execPipeline(
|
|
|
161
166
|
strictBoundaryDedup,
|
|
162
167
|
);
|
|
163
168
|
} catch (error) {
|
|
164
|
-
await noteAnchorError(absolutePath, error,
|
|
169
|
+
await noteAnchorError(absolutePath, error, options?.noPersist);
|
|
165
170
|
throw error;
|
|
166
171
|
}
|
|
167
172
|
|
|
@@ -180,12 +185,9 @@ export async function execPipeline(
|
|
|
180
185
|
edit, originalHashes, isNoop, anchorResult.autoFixes?.length ?? 0,
|
|
181
186
|
);
|
|
182
187
|
|
|
183
|
-
const sortedFixes =
|
|
184
|
-
const
|
|
185
|
-
const
|
|
186
|
-
const pipeSpan = isNoop ? undefined : hashSpan(originalHashes, edit.hash_bounds[0].hash, edit.hash_bounds[1].hash);
|
|
187
|
-
const pipeResultCount = splitLines(result).length;
|
|
188
|
-
const pipeSpans = pipeSpan ? [{ start: pipeSpan[0], end: pipeSpan[1], replacementCount: pipeResultCount - (originalHashes.length - (pipeSpan[1] - pipeSpan[0] + 1)) }] : undefined;
|
|
188
|
+
const sortedFixes = dedupRowsFromFixes(anchorResult.autoFixes ?? []);
|
|
189
|
+
const pipeSpan = isNoop ? undefined : spanForEdit(originalHashes, edit.hash_bounds[0].hash, edit.hash_bounds[1].hash, result);
|
|
190
|
+
const pipeSpans = pipeSpan ? [pipeSpan] : undefined;
|
|
189
191
|
return {
|
|
190
192
|
path: displayPath,
|
|
191
193
|
originalNormalized,
|
|
@@ -201,11 +203,10 @@ export async function execPipeline(
|
|
|
201
203
|
originalHashes,
|
|
202
204
|
totalAddedLines,
|
|
203
205
|
totalRemovedLines,
|
|
204
|
-
hadBoundaryDedup: (anchorResult.autoFixes?.length ?? 0) > 0,
|
|
205
206
|
boundaryRemovedLines: anchorResult.autoFixes?.length ?? 0,
|
|
206
|
-
boundaryRemovedLineTexts: sortedFixes.
|
|
207
|
-
boundaryDedupAbove:
|
|
208
|
-
boundaryDedupBelow:
|
|
207
|
+
boundaryRemovedLineTexts: sortedFixes.removedTexts,
|
|
208
|
+
boundaryDedupAbove: sortedFixes.above,
|
|
209
|
+
boundaryDedupBelow: sortedFixes.below,
|
|
209
210
|
identity,
|
|
210
211
|
...(pipeSpans ? { spans: pipeSpans } : {}),
|
|
211
212
|
};
|
|
@@ -287,54 +288,34 @@ export function buildToolDef(flags: EditToolFlags = DEFAULT_EDIT_FLAGS): ToolDef
|
|
|
287
288
|
}).catch((error: unknown) => {
|
|
288
289
|
const member = batchMemberFor(_toolCallId);
|
|
289
290
|
if (member) noteBatchFailure(member, error);
|
|
290
|
-
else suffixPoisonCause(_toolCallId, error);
|
|
291
291
|
throw error;
|
|
292
292
|
});
|
|
293
293
|
return queuedEdit(targetPath, ctx.cwd, signal, async (absolutePath, mutationTargetPath) => {
|
|
294
294
|
const dedupMode = await getBoundaryDedupMode();
|
|
295
|
-
const
|
|
296
|
-
const noopPayload = noopPayloadKey(mutationTargetPath, normalizedParams.remove_from, normalizedParams.remove_to, normalizedParams.replacement_lines);
|
|
297
|
-
const boundaryBypass = dedupOn ? consumeBoundaryBypass(mutationTargetPath, noopPayload) : false;
|
|
298
|
-
const strictBoundaryDedup = dedupMode === "strict" && !boundaryBypass;
|
|
295
|
+
const strictBoundaryDedup = dedupMode === "strict";
|
|
299
296
|
const member = batchMemberFor(_toolCallId);
|
|
300
297
|
if (!member) {
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
mutationTargetPath,
|
|
315
|
-
editAnchors: [normalizedParams.remove_from, normalizedParams.remove_to],
|
|
316
|
-
signal,
|
|
317
|
-
appliedWarnings,
|
|
318
|
-
onApplied: () => { if (dedupOn) clearBoundaryBypass(mutationTargetPath); },
|
|
319
|
-
onNoopDedup: dedupOn ? () => markBoundaryNoop(mutationTargetPath, noopPayload) : undefined,
|
|
320
|
-
});
|
|
321
|
-
} catch (error) {
|
|
322
|
-
const detail = error instanceof Error ? error.message : String(error);
|
|
323
|
-
if (boundaryBypass && !detail.includes("File was written;")) markBoundaryNoop(mutationTargetPath, noopPayload);
|
|
324
|
-
throw error;
|
|
325
|
-
}
|
|
298
|
+
const pipe = await execPipeline(
|
|
299
|
+
targetPath,
|
|
300
|
+
normalizedParams,
|
|
301
|
+
ctx.cwd,
|
|
302
|
+
{ accessMode: constants.R_OK | constants.W_OK, signal },
|
|
303
|
+
);
|
|
304
|
+
return await commitEdit(pipe, {
|
|
305
|
+
path: pipe.path,
|
|
306
|
+
absolutePath,
|
|
307
|
+
mutationTargetPath,
|
|
308
|
+
editAnchors: [normalizedParams.remove_from, normalizedParams.remove_to],
|
|
309
|
+
signal,
|
|
310
|
+
});
|
|
326
311
|
}
|
|
327
312
|
let built: { edit: HEdit; warnings: string[] };
|
|
328
313
|
try {
|
|
329
314
|
built = buildReplaceHEdit(normalizedParams);
|
|
330
315
|
} catch (error) {
|
|
331
316
|
noteBatchFailure(member, error);
|
|
332
|
-
if (boundaryBypass) markBoundaryNoop(mutationTargetPath, noopPayload);
|
|
333
317
|
throw error;
|
|
334
318
|
}
|
|
335
|
-
const appliedWarnings = boundaryBypass
|
|
336
|
-
? ["[W_BOUNDARY_BYPASS] Boundary dedup was off for this call and is back on."]
|
|
337
|
-
: [];
|
|
338
319
|
return executeBatchMember({
|
|
339
320
|
kind: "replace",
|
|
340
321
|
member,
|
|
@@ -343,11 +324,9 @@ export function buildToolDef(flags: EditToolFlags = DEFAULT_EDIT_FLAGS): ToolDef
|
|
|
343
324
|
cwd: ctx.cwd,
|
|
344
325
|
signal,
|
|
345
326
|
hedit: built.edit,
|
|
346
|
-
extraWarnings:
|
|
347
|
-
skipBoundaryDedup:
|
|
327
|
+
extraWarnings: built.warnings,
|
|
328
|
+
skipBoundaryDedup: false,
|
|
348
329
|
strictBoundaryDedup,
|
|
349
|
-
noopPayload,
|
|
350
|
-
bypassConsumed: boundaryBypass,
|
|
351
330
|
});
|
|
352
331
|
});
|
|
353
332
|
});
|
package/src/served.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { HASH_CLASS } from "./hashline/alphabet";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
2
|
+
import { lineChecksum } from "./hashline/hash";
|
|
3
|
+
import { markServed } from "./anchor-registry";
|
|
4
4
|
|
|
5
5
|
const SERVED_DIFF_ROW_RE = new RegExp(`^[+ ](${HASH_CLASS})│`);
|
|
6
6
|
|
|
@@ -26,7 +26,11 @@ export function buildServedMap(
|
|
|
26
26
|
const entries: Array<[string, string]> = [];
|
|
27
27
|
for (const hash of wantedHashes) {
|
|
28
28
|
const idx = index.get(hash);
|
|
29
|
-
if (idx !== undefined) entries.push([hash,
|
|
29
|
+
if (idx !== undefined) entries.push([hash, lineChecksum(fileLines[idx]!)]);
|
|
30
30
|
}
|
|
31
31
|
return entries;
|
|
32
32
|
}
|
|
33
|
+
|
|
34
|
+
export function serveRows(path: string, fileHashes: string[], fileLines: string[], wantedHashes: string[]): void {
|
|
35
|
+
markServed(path, buildServedMap(fileHashes, fileLines, wantedHashes), new Set(fileHashes));
|
|
36
|
+
}
|
package/src/utils.ts
CHANGED
|
@@ -22,14 +22,16 @@ export function normalizeAnchors(record: Record<string, unknown>): void {
|
|
|
22
22
|
}
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
+
export function normalizeRequest(input: unknown): unknown {
|
|
26
|
+
if (!isRec(input)) return input;
|
|
27
|
+
const record: Record<string, unknown> = { ...input };
|
|
28
|
+
normalizeFilePath(record);
|
|
29
|
+
normalizeAnchors(record);
|
|
30
|
+
return record;
|
|
31
|
+
}
|
|
32
|
+
|
|
25
33
|
export function makePrepareArguments(): (args: unknown) => any {
|
|
26
|
-
return
|
|
27
|
-
if (!isRec(args)) return args;
|
|
28
|
-
const record = { ...args };
|
|
29
|
-
normalizeFilePath(record);
|
|
30
|
-
normalizeAnchors(record);
|
|
31
|
-
return record;
|
|
32
|
-
};
|
|
34
|
+
return normalizeRequest;
|
|
33
35
|
}
|
|
34
36
|
|
|
35
37
|
export function splitLines(text: string): string[] {
|
|
@@ -293,7 +295,6 @@ export function decodeStringArray(value: unknown, warnings?: string[], label = "
|
|
|
293
295
|
if (candidate === undefined) return undefined;
|
|
294
296
|
const decoded = decodeArrayText(candidate);
|
|
295
297
|
if (decoded !== undefined) {
|
|
296
|
-
warnings?.push(`[W_BAD_SHAPE] Unwrapped JSON array syntax from a ${label} element.`);
|
|
297
298
|
return decoded;
|
|
298
299
|
}
|
|
299
300
|
if (looksLikeStringArray(candidate)) {
|
package/src/write-hook.ts
CHANGED
|
@@ -19,10 +19,6 @@ export function findServedHashEcho(content: string, served: ReadonlyMap<string,
|
|
|
19
19
|
return searchEcho(splitLines(content), served);
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
-
export function findEditHashEcho(lines: string[], served: ReadonlyMap<string, string> | ReadonlySet<string>): { line: number; hash: string } | undefined {
|
|
23
|
-
return searchEcho(lines, served);
|
|
24
|
-
}
|
|
25
|
-
|
|
26
22
|
export async function servedHashEchoDenial(rawPath: string, content: string, cwd: string, signal?: AbortSignal): Promise<string | undefined> {
|
|
27
23
|
abortIf(signal);
|
|
28
24
|
const { resolved } = await resolveInCwd(rawPath, cwd);
|
package/src/boundary-bypass.ts
DELETED
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
import { parseText } from "./hashline/parse";
|
|
2
|
-
import { ANCHOR_ROW_RE } from "./hashline/resolve";
|
|
3
|
-
import { stripRowPrefix } from "./hashline/hash";
|
|
4
|
-
|
|
5
|
-
function canonRef(ref: string): string {
|
|
6
|
-
const trimmed = ref.trim();
|
|
7
|
-
const match = trimmed.match(ANCHOR_ROW_RE);
|
|
8
|
-
return match ? match[2]! : trimmed;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
function canonLines(lines: string[]): string[] {
|
|
12
|
-
return parseText(lines).map((line) => stripRowPrefix(line).text);
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
const boundaryBypassTracker = new Map<string, string>();
|
|
16
|
-
const BOUNDARY_BYPASS_LIMIT = 256;
|
|
17
|
-
|
|
18
|
-
export function noopPayloadKey(
|
|
19
|
-
absolutePath: string,
|
|
20
|
-
removeFrom: string,
|
|
21
|
-
removeTo: string,
|
|
22
|
-
replacementLines: string[],
|
|
23
|
-
): string {
|
|
24
|
-
return JSON.stringify([
|
|
25
|
-
absolutePath,
|
|
26
|
-
canonRef(removeFrom),
|
|
27
|
-
canonRef(removeTo),
|
|
28
|
-
canonLines(replacementLines),
|
|
29
|
-
]);
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export function markBoundaryNoop(absolutePath: string, payload: string): void {
|
|
33
|
-
boundaryBypassTracker.delete(absolutePath);
|
|
34
|
-
boundaryBypassTracker.set(absolutePath, payload);
|
|
35
|
-
if (boundaryBypassTracker.size > BOUNDARY_BYPASS_LIMIT) {
|
|
36
|
-
const oldest = boundaryBypassTracker.keys().next().value;
|
|
37
|
-
if (oldest !== undefined) boundaryBypassTracker.delete(oldest);
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
export function consumeBoundaryBypass(absolutePath: string, payload: string): boolean {
|
|
42
|
-
if (boundaryBypassTracker.get(absolutePath) === payload) {
|
|
43
|
-
boundaryBypassTracker.delete(absolutePath);
|
|
44
|
-
return true;
|
|
45
|
-
}
|
|
46
|
-
return false;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
export function clearBoundaryBypass(absolutePath: string): void {
|
|
50
|
-
boundaryBypassTracker.delete(absolutePath);
|
|
51
|
-
}
|