pi-hashline-edit-pro 4.3.9 → 4.4.1

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.
@@ -1,4 +1,4 @@
1
- import { abortIf, clipLine, splitLines } from "../utils";
1
+ import { abortIf, splitLines } from "../utils";
2
2
  import {
3
3
  valEdit,
4
4
  stripBarePrefixes,
@@ -11,8 +11,6 @@ import {
11
11
  type RHEdit,
12
12
  type NEdit,
13
13
  type HEdit,
14
- type AutoFix,
15
- type BDup,
16
14
  } from "./resolve";
17
15
 
18
16
  type LIdx = {
@@ -150,19 +148,9 @@ function assemble(
150
148
  export interface PlannedEdit {
151
149
  resolved: RHEdit;
152
150
  warnings: string[];
153
- autoFixes?: AutoFix[];
154
151
  }
155
152
 
156
153
 
157
- function throwStrictBoundaryDedup(edit: HEdit, boundaryDups: BDup[], resolved: RHEdit): never {
158
- const startLine = resolved.hash_bounds[0].line;
159
- const endLine = resolved.hash_bounds[1].line;
160
- const range = startLine === endLine ? `line ${startLine}` : `lines ${startLine}-${endLine}`;
161
- const seen = new Set<number>();
162
- for (const dup of boundaryDups) seen.add(dup.replacementLineIndex);
163
- const removed = [...seen].sort((a, b) => a - b).map((index) => `replacement_lines line ${index + 1}: ${clipLine(edit.content_lines[index] ?? "")}`);
164
- throw new Error(`[E_BOUNDARY_STRICT] Strict boundary dedup rejects this edit: ${seen.size} replacement line(s) re-include edge lines (${range}). ${removed.join("; ")}. Resend without those lines.`);
165
- }
166
154
  export function planEdit(
167
155
  content: string,
168
156
  edit: HEdit,
@@ -170,8 +158,6 @@ export function planEdit(
170
158
  options?: {
171
159
  filePath?: string;
172
160
  servedHashes?: ReadonlyMap<string, string>;
173
- skipBoundaryDedup?: boolean;
174
- strictBoundaryDedup?: boolean;
175
161
  signal?: AbortSignal;
176
162
  baseFileLines?: string[];
177
163
  },
@@ -189,7 +175,7 @@ export function planEdit(
189
175
  warnings,
190
176
  );
191
177
 
192
- const { resolved: initialResolved, mismatches, boundaryDups } = valEdit(
178
+ const { resolved: initialResolved, mismatches } = valEdit(
193
179
  prefixFixed,
194
180
  lineIndex.fileLines,
195
181
  fileHashes,
@@ -208,51 +194,7 @@ export function planEdit(
208
194
 
209
195
  warnUnicodeEsc(prefixFixed, warnings);
210
196
 
211
- if (options?.strictBoundaryDedup === true && boundaryDups.length > 0) {
212
- throwStrictBoundaryDedup(prefixFixed, boundaryDups, initialResolved);
213
- }
214
- let resolved = initialResolved;
215
- let autoFixes: AutoFix[] | undefined;
216
- if (boundaryDups.length > 0 && !(options?.skipBoundaryDedup === true)) {
217
- autoFixes = [];
218
- const correctedEdit: HEdit = {
219
- ...prefixFixed,
220
- content_lines: [...prefixFixed.content_lines],
221
- };
222
- const seen = new Set<number>();
223
- const uniqueDups: BDup[] = [];
224
- for (const dup of boundaryDups) {
225
- if (seen.has(dup.replacementLineIndex)) continue;
226
- seen.add(dup.replacementLineIndex);
227
- uniqueDups.push(dup);
228
- }
229
- const dupsByIndex = uniqueDups.sort(
230
- (a, b) => b.replacementLineIndex - a.replacementLineIndex,
231
- );
232
- for (const dup of dupsByIndex) {
233
- const idx = dup.replacementLineIndex;
234
- if (idx < 0 || idx >= correctedEdit.content_lines.length) continue;
235
- const removed = correctedEdit.content_lines.splice(idx, 1)[0];
236
- autoFixes.push({ kind: dup.kind, removedLine: removed, removedLineIndex: idx });
237
- }
238
- const correctedResult = valEdit(
239
- correctedEdit,
240
- lineIndex.fileLines,
241
- fileHashes,
242
- warnings,
243
- signal,
244
- );
245
- if (correctedResult.mismatches.length || !correctedResult.resolved) {
246
- const feedback = fmtMismatchWithHashes(
247
- correctedResult.mismatches,
248
- lineIndex.fileLines,
249
- fileHashes,
250
- options?.filePath,
251
- );
252
- throw new AnchorMismatchError(feedback.text, feedback.hashes, feedback.servedMap);
253
- }
254
- resolved = correctedResult.resolved;
255
- }
197
+ const resolved = initialResolved;
256
198
 
257
199
  if (options?.servedHashes) {
258
200
  abortIf(signal);
@@ -262,7 +204,6 @@ export function planEdit(
262
204
  return {
263
205
  resolved,
264
206
  warnings,
265
- ...(autoFixes ? { autoFixes } : {}),
266
207
  };
267
208
  }
268
209
  export function applyEdit(
@@ -272,25 +213,21 @@ export function applyEdit(
272
213
  precomputedHashes?: string[],
273
214
  filePath?: string,
274
215
  servedHashes?: ReadonlyMap<string, string>,
275
- skipBoundaryDedup?: boolean,
276
- strictBoundaryDedup?: boolean,
277
216
  ): {
278
217
  content: string;
279
218
  firstChangedLine: number | undefined;
280
219
  lastChangedLine: number | undefined;
281
220
  warnings?: string[];
282
221
  noopEdit?: NEdit;
283
- autoFixes?: AutoFix[];
284
222
  } {
285
223
  abortIf(signal);
286
224
  if (precomputedHashes === undefined) {
287
225
  throw new Error("[E_BAD_SHAPE] applyEdit requires the file's allocated anchors; derive them via lineHashes(content, path) first.");
288
226
  }
289
- const planned = planEdit(content, edit, precomputedHashes, { filePath, servedHashes, skipBoundaryDedup, strictBoundaryDedup, signal });
227
+ const planned = planEdit(content, edit, precomputedHashes, { filePath, servedHashes, signal });
290
228
  const lineIndex = buildIdx(content);
291
229
  const warnings = planned.warnings;
292
230
  const resolved = planned.resolved;
293
- const autoFixes = planned.autoFixes;
294
231
  const spanResult = resToSpan(resolved, content, lineIndex);
295
232
  if (spanResult.kind === "noop") {
296
233
  return {
@@ -298,7 +235,6 @@ export function applyEdit(
298
235
  firstChangedLine: undefined,
299
236
  lastChangedLine: undefined,
300
237
  ...(warnings.length ? { warnings } : {}),
301
- ...(autoFixes ? { autoFixes } : {}),
302
238
  noopEdit: { loc: spanResult.loc, currentContent: spanResult.currentContent },
303
239
  };
304
240
  }
@@ -312,7 +248,6 @@ export function applyEdit(
312
248
  firstChangedLine: range?.firstChangedLine,
313
249
  lastChangedLine: range?.lastChangedLine,
314
250
  ...(warnings.length ? { warnings } : {}),
315
- ...(autoFixes ? { autoFixes } : {}),
316
251
  };
317
252
  }
318
253
 
@@ -26,8 +26,6 @@ export {
26
26
  type RHEdit,
27
27
  type HTEdit,
28
28
  type NEdit,
29
- type BDup,
30
- type AutoFix,
31
29
  resEdit,
32
30
  stripAnchorRow,
33
31
  resolveAnchorLine,
@@ -35,7 +33,6 @@ export {
35
33
  stripBarePrefixes,
36
34
  stripDiffPrefixes,
37
35
  swapReversedRanges,
38
- findNewEdge,
39
36
  assertRangeServed,
40
37
  RangeStaleError,
41
38
  AnchorMismatchError,
@@ -1,6 +1,6 @@
1
- import { abortIf, rejectUnknownFields, firstNonEmptyIndex, lastNonEmptyIndex, clipLine, getCached, decodeStringArray, assertNoNul } from "../utils";
1
+ import { abortIf, rejectUnknownFields, clipLine, decodeStringArray, assertNoNul } from "../utils";
2
2
  import { parseHashRef, parseText, type Anchor } from "./parse";
3
- import { HASH_SEP, stripRowPrefix, canon, lineChecksum, type RowPrefixKind } from "./hash";
3
+ import { HASH_SEP, stripRowPrefix, lineChecksum, type RowPrefixKind } from "./hash";
4
4
  import { HASH_RUN } from "./alphabet";
5
5
  import { NEW_CONTENT_NOT_ARRAY_MSG, MAX_RANGE_STALE_LINES } from "../constants";
6
6
 
@@ -21,17 +21,6 @@ interface HMismatch {
21
21
  context?: RAnchor;
22
22
  }
23
23
 
24
- export interface BDup {
25
- kind: "trailing" | "leading" | "first-new-after" | "last-new-before";
26
- replacementLineIndex: number;
27
- }
28
-
29
- export interface AutoFix {
30
- kind: "trailing" | "leading" | "first-new-after" | "last-new-before";
31
- removedLine: string;
32
- removedLineIndex: number;
33
- }
34
-
35
24
  export interface NEdit {
36
25
  loc: string;
37
26
  currentContent: string;
@@ -240,147 +229,15 @@ export function swapReversedRanges(
240
229
  return { ...edit, hash_bounds: [endRef, startRef] as [Anchor, Anchor] };
241
230
  }
242
231
 
243
- function trailingDups(
244
- contentLines: string[],
245
- fileLines: string[],
246
- endLine: number,
247
- ): BDup[] {
248
- const start = lastNonEmptyIndex(contentLines);
249
- if (start < 0) return [];
250
- const dups: BDup[] = [];
251
- const maxK = Math.min(start + 1, fileLines.length - endLine);
252
- for (let k = 0; k < maxK; k++) {
253
- if (contentLines[start - k] !== fileLines[endLine + k]) break;
254
- dups.push({ kind: "trailing", replacementLineIndex: start - k });
255
- }
256
- return dups;
257
- }
258
-
259
- function leadingDups(
260
- contentLines: string[],
261
- fileLines: string[],
262
- startLine: number,
263
- ): BDup[] {
264
- const start = firstNonEmptyIndex(contentLines);
265
- if (start < 0) return [];
266
- const dups: BDup[] = [];
267
- const maxK = Math.min(contentLines.length - start, startLine - 1);
268
- for (let k = 0; k < maxK; k++) {
269
- if (contentLines[start + k] !== fileLines[startLine - 2 - k]) break;
270
- dups.push({ kind: "leading", replacementLineIndex: start + k });
271
- }
272
- return dups;
273
- }
274
-
275
- function sectionIsUnique(
276
- canonLines: string[],
277
- start: number,
278
- length: number,
279
- ): boolean {
280
- let count = 0;
281
- for (let i = 0; i + length <= canonLines.length; i++) {
282
- let k = 0;
283
- while (k < length && canonLines[i + k] === canonLines[start + k]) k++;
284
- if (k < length) continue;
285
- count++;
286
- if (count > 1) return false;
287
- }
288
- return true;
289
- }
290
-
291
- function firstNewAfterDups(
292
- contentLines: string[],
293
- rangeLines: string[],
294
- canonLines: string[],
295
- endLine: number,
296
- ): BDup[] {
297
- const firstNew = findNewEdge(contentLines, rangeLines, false);
298
- if (!firstNew) return [];
299
- const maxK = Math.min(contentLines.length - firstNew.index, canonLines.length - endLine);
300
- let runLen = 0;
301
- while (
302
- runLen < maxK &&
303
- canonLines[endLine + runLen]!.length > 0 &&
304
- canon(contentLines[firstNew.index + runLen]!) === canonLines[endLine + runLen]!
305
- ) {
306
- runLen++;
307
- }
308
- if (runLen === 0 || !sectionIsUnique(canonLines, endLine, runLen)) return [];
309
- const dups: BDup[] = [];
310
- for (let k = 0; k < runLen; k++) {
311
- dups.push({ kind: "first-new-after", replacementLineIndex: firstNew.index + k });
312
- }
313
- return dups;
314
- }
315
-
316
- function lastNewBeforeDups(
317
- contentLines: string[],
318
- rangeLines: string[],
319
- canonLines: string[],
320
- startLine: number,
321
- ): BDup[] {
322
- const lastNew = findNewEdge(contentLines, rangeLines, true);
323
- if (!lastNew) return [];
324
- const maxK = Math.min(lastNew.index + 1, startLine - 1);
325
- let runLen = 0;
326
- while (
327
- runLen < maxK &&
328
- canonLines[startLine - 2 - runLen]!.length > 0 &&
329
- canon(contentLines[lastNew.index - runLen]!) === canonLines[startLine - 2 - runLen]!
330
- ) {
331
- runLen++;
332
- }
333
- if (runLen === 0) return [];
334
- const sectionStart = startLine - 1 - runLen;
335
- if (!sectionIsUnique(canonLines, sectionStart, runLen)) return [];
336
- const dups: BDup[] = [];
337
- for (let k = 0; k < runLen; k++) {
338
- dups.push({ kind: "last-new-before", replacementLineIndex: lastNew.index - k });
339
- }
340
- return dups;
341
- }
342
-
343
- function canonCounts(lines: string[]): Map<string, number> {
344
- const counts = new Map<string, number>();
345
- for (const line of lines) {
346
- const key = canon(line);
347
- counts.set(key, (counts.get(key) ?? 0) + 1);
348
- }
349
- return counts;
350
- }
351
-
352
- export function findNewEdge(
353
- contentLines: string[],
354
- rangeLines: string[],
355
- fromEnd: boolean,
356
- ): { index: number; line: string } | undefined {
357
- const multiset = canonCounts(rangeLines);
358
- const step = fromEnd ? -1 : 1;
359
- const start = fromEnd ? contentLines.length - 1 : 0;
360
- for (let i = start; i >= 0 && i < contentLines.length; i += step) {
361
- const line = contentLines[i]!;
362
- const key = canon(line);
363
- if (key.length === 0) continue;
364
- const count = multiset.get(key) ?? 0;
365
- if (count > 0) {
366
- multiset.set(key, count - 1);
367
- } else {
368
- return { index: i, line };
369
- }
370
- }
371
- return undefined;
372
- }
373
-
374
232
  export function valEdit(
375
233
  edit: HEdit,
376
234
  fileLines: string[],
377
235
  fileHashes: string[],
378
236
  warnings: string[],
379
237
  signal: AbortSignal | undefined,
380
- ): { resolved: RHEdit | undefined; mismatches: HMismatch[]; boundaryDups: BDup[] } {
238
+ ): { resolved: RHEdit | undefined; mismatches: HMismatch[] } {
381
239
  assertAligned(fileLines, fileHashes, "valEdit");
382
240
  const mismatches: HMismatch[] = [];
383
- const boundaryDups: BDup[] = [];
384
241
 
385
242
  const hashIndex = new Map<string, number[]>();
386
243
  for (let i = 0; i < fileHashes.length; i++) {
@@ -410,26 +267,14 @@ export function valEdit(
410
267
  const endMismatch = mismatches.findLast((m) => m.ref === edit.hash_bounds[1]);
411
268
  if (endMismatch && endMismatch.kind === "not_found") endMismatch.context = startResolved;
412
269
  }
413
- return { resolved: undefined, mismatches, boundaryDups };
270
+ return { resolved: undefined, mismatches };
414
271
  }
415
- const endLine = endResolved.line;
416
- const rangeLines = fileLines.slice(startResolved.line - 1, endLine);
417
- const canonCache = new Map<string, string>();
418
- const canonLines = fileLines.map((line) => getCached(canonCache, line, canon));
419
- boundaryDups.push(
420
- ...trailingDups(edit.content_lines, fileLines, endLine),
421
- ...leadingDups(edit.content_lines, fileLines, startResolved.line),
422
- ...firstNewAfterDups(edit.content_lines, rangeLines, canonLines, endLine),
423
- ...lastNewBeforeDups(edit.content_lines, rangeLines, canonLines, startResolved.line),
424
- );
425
-
426
272
  return {
427
273
  resolved: {
428
274
  content_lines: edit.content_lines,
429
275
  hash_bounds: [startResolved, endResolved],
430
276
  },
431
277
  mismatches,
432
- boundaryDups,
433
278
  };
434
279
  }
435
280
 
package/src/insert.ts CHANGED
@@ -119,7 +119,6 @@ export async function insertPreview(request: unknown, cwd: string, signal?: Abor
119
119
  accessMode: constants.R_OK,
120
120
  noPersist: true,
121
121
  preloadedNorm: preload,
122
- skipBoundaryDedup: true,
123
122
  signal,
124
123
  });
125
124
  return previewFromPipe(pipe);
@@ -224,8 +223,6 @@ export function buildInsertToolDef(flags: EditToolFlags = DEFAULT_EDIT_FLAGS): I
224
223
  signal,
225
224
  hedit,
226
225
  extraWarnings: [...anchorWarnings, ...insertWarnings, ...resWarnings],
227
- skipBoundaryDedup: true,
228
- strictBoundaryDedup: false,
229
226
  foldedLines: built.anchorLine === undefined ? 0 : 1,
230
227
  });
231
228
  }
@@ -239,7 +236,6 @@ export function buildInsertToolDef(flags: EditToolFlags = DEFAULT_EDIT_FLAGS): I
239
236
  accessMode: constants.R_OK | constants.W_OK,
240
237
  signal,
241
238
  preloadedNorm: preload,
242
- skipBoundaryDedup: true,
243
239
  });
244
240
  return commitEdit(pipe, {
245
241
  path: pipe.path,
@@ -7,7 +7,7 @@ import {
7
7
  HASH_CLASS,
8
8
  changedRange,
9
9
  } from "./hashline";
10
- import { MAX_DIFF_INPUT_BYTES, DEDUP_ANCHOR } from "./constants";
10
+ import { MAX_DIFF_INPUT_BYTES } from "./constants";
11
11
  import { splitLines } from "./utils";
12
12
  import {
13
13
  detectEnding,
@@ -23,8 +23,6 @@ export interface DiffSpan {
23
23
  start: number;
24
24
  end: number;
25
25
  replacementCount: number;
26
- dedupAbove?: string[];
27
- dedupBelow?: string[];
28
26
  }
29
27
 
30
28
  interface PlacedSpan {
@@ -32,16 +30,6 @@ interface PlacedSpan {
32
30
  oldEnd: number;
33
31
  newStart: number;
34
32
  newEnd: number;
35
- dedupAbove?: string[];
36
- dedupBelow?: string[];
37
- }
38
-
39
- export function fmtDedupRow(line: string): string {
40
- const row = `${DEDUP_ANCHOR}${HASH_SEP}${line}`;
41
- if (Buffer.byteLength(row, "utf-8") <= DEFAULT_MAX_BYTES) return row;
42
- const size = formatSize(Buffer.byteLength(row, "utf-8"));
43
- const limit = formatSize(DEFAULT_MAX_BYTES);
44
- return `${DEDUP_ANCHOR}${HASH_SEP}[Row is ${size}, exceeds ${limit}; content not shown. Use read to see the full line.]`;
45
33
  }
46
34
 
47
35
  function fmtDiffLine(
@@ -168,7 +156,7 @@ function placeSpans(
168
156
  if (span.replacementCount === 0) {
169
157
  if (newEnd !== newStart - 1) return undefined;
170
158
  } else if (newEnd >= newLen) return undefined;
171
- placed.push({ oldStart: span.start, oldEnd: span.end, newStart, newEnd, dedupAbove: span.dedupAbove, dedupBelow: span.dedupBelow });
159
+ placed.push({ oldStart: span.start, oldEnd: span.end, newStart, newEnd });
172
160
  offset += span.replacementCount - spanLen;
173
161
  removedSum += spanLen;
174
162
  addedSum += span.replacementCount;
@@ -197,7 +185,7 @@ function trimPlacedSpans(
197
185
  newEnd -= 1;
198
186
  }
199
187
  if (oldStart > oldEnd && newStart > newEnd) continue;
200
- out.push({ oldStart, oldEnd, newStart, newEnd, dedupAbove: span.dedupAbove, dedupBelow: span.dedupBelow });
188
+ out.push({ oldStart, oldEnd, newStart, newEnd });
201
189
  }
202
190
  return out;
203
191
  }
@@ -212,7 +200,6 @@ function createRowEmitter(
212
200
  readonly truncated: boolean;
213
201
  emitPlain(line: string, num?: number): void;
214
202
  emitRow(prefix: " " | "+" | "-", line: string, hash: string | undefined, num?: number): void;
215
- emitDedup(line: string): boolean;
216
203
  } {
217
204
  let outBytes = 0;
218
205
  let stopped = false;
@@ -247,26 +234,11 @@ function createRowEmitter(
247
234
  output.push(full);
248
235
  lineNumbers.push(num);
249
236
  };
250
- const emitDedup = (line: string): boolean => {
251
- if (stopped) return false;
252
- const row = fmtDedupRow(line);
253
- const rowBytes = Buffer.byteLength(row, "utf-8") + 1;
254
- if (outBytes + rowBytes > maxBytes) {
255
- stopped = true;
256
- truncated = true;
257
- return false;
258
- }
259
- outBytes += rowBytes;
260
- output.push(row);
261
- lineNumbers.push(undefined);
262
- return true;
263
- };
264
237
  return {
265
238
  get stopped() { return stopped; },
266
239
  get truncated() { return truncated; },
267
240
  emitPlain,
268
241
  emitRow,
269
- emitDedup,
270
242
  };
271
243
  }
272
244
 
@@ -279,7 +251,7 @@ function genSpanDiff(
279
251
  maxLineBytes: number,
280
252
  maxBytes: number,
281
253
  spans: DiffSpan[],
282
- ): { diff: string; firstChangedLine: number | undefined; lineNumbers: (number | undefined)[]; dedupEmitted: boolean } | undefined {
254
+ ): { diff: string; firstChangedLine: number | undefined; lineNumbers: (number | undefined)[] } | undefined {
283
255
  const oldLines = splitLines(oldContent);
284
256
  const newLines = splitLines(newContent);
285
257
  if (oldHashes.length !== oldLines.length || newHashes.length !== newLines.length) return undefined;
@@ -301,7 +273,6 @@ function genSpanDiff(
301
273
  return undefined;
302
274
  }
303
275
  const output: string[] = [];
304
- let dedupEmitted = false;
305
276
  const lineNumbers: (number | undefined)[] = [];
306
277
  let firstChangedLine: number | undefined;
307
278
  const em = createRowEmitter(output, lineNumbers, maxLineBytes, maxBytes);
@@ -360,20 +331,12 @@ function genSpanDiff(
360
331
  emitGap(newPos, gapLen, spanIdx === 0 ? "leading" : "middle");
361
332
  if (em.stopped) break;
362
333
  if (firstChangedLine === undefined) firstChangedLine = span.newStart + 1;
363
- for (const dedup of span.dedupAbove ?? []) {
364
- if (em.stopped) break;
365
- if (em.emitDedup(dedup)) dedupEmitted = true;
366
- }
367
334
  for (let oldIdx = span.oldStart; oldIdx <= span.oldEnd && !em.stopped; oldIdx++) {
368
335
  em.emitRow("-", oldLines[oldIdx]!, oldHashes[oldIdx], oldIdx + 1);
369
336
  }
370
337
  for (let newIdx = span.newStart; newIdx <= span.newEnd && !em.stopped; newIdx++) {
371
338
  em.emitRow("+", newLines[newIdx]!, newHashes[newIdx], newIdx + 1);
372
339
  }
373
- for (const dedup of span.dedupBelow ?? []) {
374
- if (em.stopped) break;
375
- if (em.emitDedup(dedup)) dedupEmitted = true;
376
- }
377
340
  oldPos = span.oldEnd + 1;
378
341
  newPos = span.newEnd + 1;
379
342
  }
@@ -387,7 +350,7 @@ function genSpanDiff(
387
350
  output.push(`[diff truncated at ${formatSize(maxBytes)}; use read to see the rest.]`);
388
351
  lineNumbers.push(undefined);
389
352
  }
390
- return { diff: output.join("\n"), firstChangedLine, lineNumbers, dedupEmitted };
353
+ return { diff: output.join("\n"), firstChangedLine, lineNumbers };
391
354
  }
392
355
 
393
356
  function overDiffInputLimit(oldContent: string, newContent: string): boolean {
@@ -402,14 +365,14 @@ export function genDiff(
402
365
  oldContentHashes?: string[],
403
366
  limits?: DiffLimits,
404
367
  spans?: DiffSpan[],
405
- ): { diff: string; firstChangedLine: number | undefined; lineNumbers: (number|undefined)[]; spanDedupEmitted?: boolean } {
368
+ ): { diff: string; firstChangedLine: number | undefined; lineNumbers: (number|undefined)[] } {
406
369
  const maxLineBytes = limits?.unlimited ? Number.POSITIVE_INFINITY : (limits?.maxLineBytes ?? DEFAULT_MAX_BYTES);
407
370
  const maxBytes = limits?.unlimited ? Number.POSITIVE_INFINITY : (limits?.maxBytes ?? DEFAULT_MAX_BYTES);
408
371
  if (spans && newContentHashes && oldContentHashes) {
409
372
  const anchored = genSpanDiff(oldContent, newContent, contextLines, newContentHashes, oldContentHashes, maxLineBytes, maxBytes, spans);
410
373
  if (anchored) {
411
374
  const diff = disambiguateDuplicateAnchors(anchored.diff);
412
- return { diff, firstChangedLine: anchored.firstChangedLine, lineNumbers: anchored.lineNumbers, ...(anchored.dedupEmitted ? { spanDedupEmitted: true as const } : {}) };
375
+ return { diff, firstChangedLine: anchored.firstChangedLine, lineNumbers: anchored.lineNumbers };
413
376
  }
414
377
  }
415
378
  if (!limits?.unlimited && overDiffInputLimit(oldContent, newContent)) {
@@ -3,7 +3,6 @@ import { keyHint, type Theme } from "@earendil-works/pi-coding-agent";
3
3
  import type { ReplaceDetails } from "./replace";
4
4
  import { abortedBatchMessageFor } from "./batch";
5
5
  import { withLineNumbers } from "./utils";
6
- import { isDedupRow } from "./replace-response";
7
6
  import { getPreviewInput } from "./payload-contract";
8
7
  export { getPreviewInput };
9
8
 
@@ -28,7 +27,6 @@ type DiffRowKind = "added" | "removed" | "context";
28
27
 
29
28
  function diffRowKind(line: string): DiffRowKind {
30
29
  const withoutGutter = line.replace(/^\s*\d+\s+│\s*/, "");
31
- if (isDedupRow(withoutGutter) || isDedupRow(withoutGutter.replace(/^\s*│\s*/, ""))) return "removed";
32
30
  if (withoutGutter.startsWith("+") && !withoutGutter.startsWith("+++")) return "added";
33
31
  if (withoutGutter.startsWith("-") && !withoutGutter.startsWith("---")) return "removed";
34
32
  return "context";