pi-hashline-edit-pro 3.0.1 → 3.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-hashline-edit-pro",
3
- "version": "3.0.1",
3
+ "version": "3.0.2",
4
4
  "type": "module",
5
5
  "description": "Hash-anchored read/replace/insert/grep tools for pi-coding-agent. Every line gets a unique 4-char tokenizer-friendly anchor that stays stable across edits; stale or ambiguous anchors are rejected, never fuzzy-matched. Undo persists across restarts.",
6
6
  "main": "index.ts",
@@ -1,4 +1,5 @@
1
1
  - `replace`: use the same anchor for `remove_from` and `remove_to` to change one line.
2
2
  - `replace`: `replacement_lines` takes bare lines without `│`; `[""]` is one blank line; pasted `anchor│` prefixes are stripped automatically.
3
3
  - `replace`: keep the range tight — only lines that actually change — and copy leading spaces exactly.
4
- - `replace`: post-edit diff `+anchor│`/` anchor│` rows are fresh anchors for the next edit — no new `read` needed. One edit per turn; check the diff before the next edit on that file.
4
+ - `replace`: post-edit diff `+anchor│`/` anchor│` rows are fresh anchors for the next edit — no new `read` needed. One edit per turn; check the diff before the next edit on that file.
5
+ - `replace`: if `replacement_lines` re-include the boundary line adjacent to the range, it is deduplicated automatically, shown as `dedup│content` rows in the diff (not editable, never use `dedup` as an anchor).
package/src/commit.ts CHANGED
@@ -1,5 +1,7 @@
1
1
  import type { PipelineResult } from "./replace";
2
- import { abortIf, clipLine } from "./utils";
2
+ import { abortIf } from "./utils";
3
+ import { DEDUP_ANCHOR } from "./constants";
4
+ import { HASH_SEP } from "./hashline";
3
5
  import { buildChanged, buildNoop, type RMeta, type TResult } from "./replace-response";
4
6
  import { saveUndo } from "./replace-undo";
5
7
  import { safeSnapId } from "./file-reader";
@@ -22,10 +24,10 @@ export interface CommitMeta {
22
24
  onNoopDedup?: () => void;
23
25
  }
24
26
 
25
- function boundaryDedupWarning(lineTexts: string[]): string {
26
- const quoted = lineTexts.map((line) => `"${clipLine(line, 80)}"`).join(", ");
27
- const plural = lineTexts.length > 1;
28
- return `Boundary dedup: ${quoted} already ${plural ? "exist" : "exists"} next to the edited range, so ${plural ? "they were" : "it was"} not added again.`;
27
+ function boundaryDedupWarning(count: number): string {
28
+ const noun = count === 1 ? "1 line" : `${count} lines`;
29
+ const row = count === 1 ? "row" : "rows";
30
+ return `Boundary dedup: ${noun} not added again (see ${DEDUP_ANCHOR}${HASH_SEP} ${row}).`;
29
31
  }
30
32
 
31
33
  export async function commitEdit(pipe: PipelineResult, meta: CommitMeta): Promise<TResult> {
@@ -61,7 +63,7 @@ export async function commitEdit(pipe: PipelineResult, meta: CommitMeta): Promis
61
63
  );
62
64
  }
63
65
  if (pipe.boundaryRemovedLineTexts.length > 0) {
64
- warnings.push(boundaryDedupWarning(pipe.boundaryRemovedLineTexts));
66
+ warnings.push(boundaryDedupWarning(pipe.boundaryRemovedLineTexts.length));
65
67
  }
66
68
 
67
69
  abortIf(signal);
@@ -109,6 +111,8 @@ export async function commitEdit(pipe: PipelineResult, meta: CommitMeta): Promis
109
111
  warnings,
110
112
  snapshotId: updatedSnapshotId,
111
113
  editMeta,
114
+ boundaryDedupAbove: pipe.boundaryDedupAbove,
115
+ boundaryDedupBelow: pipe.boundaryDedupBelow,
112
116
  };
113
117
  const changed = buildChanged(successInput, meta.verb);
114
118
  if (changed.details.diff) {
package/src/constants.ts CHANGED
@@ -12,3 +12,5 @@ export const HASH_STORE_BUSY_TIMEOUT = 1000;
12
12
  export const HASH_STORE_VERSION = 7;
13
13
  export const NEW_CONTENT_NOT_ARRAY_MSG =
14
14
  `[E_BAD_SHAPE] "replacement_lines" must be an array of strings, one per line (use [] to delete).`;
15
+
16
+ export const DEDUP_ANCHOR = "dedup";
@@ -1,7 +1,10 @@
1
+ import { formatSize, DEFAULT_MAX_BYTES } from "@earendil-works/pi-coding-agent";
1
2
  import type { NEdit } from "./hashline";
3
+ import { HASH_SEP } from "./hashline";
2
4
  import type { ReplaceDetails } from "./replace";
3
5
  import { genDiff, genPatch } from "./replace-diff";
4
6
  import { visLines, clipLine } from "./utils";
7
+ import { DEDUP_ANCHOR } from "./constants";
5
8
 
6
9
  export type TResult = {
7
10
  content: Array<{ type: "text"; text: string }>;
@@ -46,6 +49,8 @@ export interface SuccessInput {
46
49
  warnings: string[] | undefined;
47
50
  snapshotId?: string;
48
51
  editMeta: RMeta;
52
+ boundaryDedupAbove?: string[];
53
+ boundaryDedupBelow?: string[];
49
54
  }
50
55
 
51
56
 
@@ -124,11 +129,47 @@ export function buildNoop(input: NoopInput, noopNoun = "Replacement"): TResult {
124
129
  },
125
130
  };
126
131
  }
132
+ export function fmtDedupRow(line: string): string {
133
+ const row = `${DEDUP_ANCHOR}${HASH_SEP}${line}`;
134
+ if (Buffer.byteLength(row, "utf-8") <= DEFAULT_MAX_BYTES) return row;
135
+ const size = formatSize(Buffer.byteLength(row, "utf-8"));
136
+ const limit = formatSize(DEFAULT_MAX_BYTES);
137
+ return `${DEDUP_ANCHOR}${HASH_SEP}[Row is ${size}, exceeds ${limit}; content not shown. Use read to see the full line.]`;
138
+ }
139
+
140
+ export function isChangeRow(line: string): boolean {
141
+ return line.startsWith("+") || line.startsWith("-");
142
+ }
143
+
144
+ export function withDedupRows(diff: string, lineNumbers: (number | undefined)[], above: string[] | undefined, below: string[] | undefined): { diff: string; lineNumbers: (number | undefined)[] } {
145
+ const top = (above ?? []).map(fmtDedupRow);
146
+ const bottom = (below ?? []).map(fmtDedupRow);
147
+ if (top.length === 0 && bottom.length === 0) return { diff, lineNumbers };
148
+ if (diff.length === 0) return { diff: [...top, ...bottom].join("\n"), lineNumbers: [...lineNumbers, ...[...top, ...bottom].map(() => undefined)] };
149
+ const lines = diff.split("\n");
150
+ let first = -1;
151
+ let last = -1;
152
+ for (let i = 0; i < lines.length; i++) {
153
+ if (isChangeRow(lines[i]!)) {
154
+ if (first < 0) first = i;
155
+ last = i;
156
+ }
157
+ }
158
+ if (first < 0) return { diff: `${diff}\n${[...top, ...bottom].join("\n")}`, lineNumbers: [...lineNumbers, ...[...top, ...bottom].map(() => undefined)] };
159
+ const out = [...lines];
160
+ const nums = [...lineNumbers];
161
+ out.splice(last + 1, 0, ...bottom);
162
+ nums.splice(last + 1, 0, ...bottom.map(() => undefined));
163
+ out.splice(first, 0, ...top);
164
+ nums.splice(first, 0, ...top.map(() => undefined));
165
+ return { diff: out.join("\n"), lineNumbers: nums };
166
+ }
127
167
 
128
168
  export function buildChanged(input: SuccessInput, verb = "replaced"): TResult {
129
- const { path, result, warnings, snapshotId, originalNormalized, originalHashes, editMeta, resultHashes } = input;
169
+ const { path, result, warnings, snapshotId, originalNormalized, originalHashes, editMeta, resultHashes, boundaryDedupAbove, boundaryDedupBelow } = input;
130
170
  const resultLines = visLines(result);
131
- const diffResult = genDiff(originalNormalized, result, 1, resultHashes, originalHashes);
171
+ const baseDiff = genDiff(originalNormalized, result, 1, resultHashes, originalHashes);
172
+ const diffResult = withDedupRows(baseDiff.diff, baseDiff.lineNumbers, boundaryDedupAbove, boundaryDedupBelow);
132
173
  const addedLines = editMeta.addedLines;
133
174
  const removedLines = editMeta.removedLines;
134
175
  const warningsBlock = warnBlock(warnings);
@@ -161,7 +202,7 @@ export function buildChanged(input: SuccessInput, verb = "replaced"): TResult {
161
202
  patch: patchResult.patch,
162
203
  ...(patchResult.truncated ? { patchTruncated: true as const } : {}),
163
204
  firstChangedLine:
164
- editMeta.firstChangedLine ?? diffResult.firstChangedLine,
205
+ editMeta.firstChangedLine ?? baseDiff.firstChangedLine,
165
206
  snapshotId,
166
207
  metrics,
167
208
  diffLineNumbers: diffResult.lineNumbers,
package/src/replace.ts CHANGED
@@ -23,7 +23,7 @@ import { applyEdit,
23
23
  type NEdit,
24
24
  } from "./hashline";
25
25
  import { commitEdit } from "./commit";
26
- import type { RMetrics } from "./replace-response";
26
+ import { withDedupRows, type RMetrics } from "./replace-response";
27
27
  import {
28
28
  type RPreview,
29
29
  type RRState,
@@ -64,6 +64,8 @@ export interface PipelineResult {
64
64
  hadBoundaryDedup: boolean;
65
65
  boundaryRemovedLines: number;
66
66
  boundaryRemovedLineTexts: string[];
67
+ boundaryDedupAbove: string[];
68
+ boundaryDedupBelow: string[];
67
69
  identity: FileIdentity;
68
70
  }
69
71
 
@@ -216,6 +218,9 @@ export async function execPipeline(
216
218
  edit, originalHashes, isNoop, anchorResult.autoFixes?.length ?? 0,
217
219
  );
218
220
 
221
+ const sortedFixes = [...(anchorResult.autoFixes ?? [])].sort((a, b) => a.removedLineIndex - b.removedLineIndex);
222
+ const aboveFixes = sortedFixes.filter((fix) => fix.kind === "leading" || fix.kind === "last-new-before");
223
+ const belowFixes = sortedFixes.filter((fix) => fix.kind === "trailing" || fix.kind === "first-new-after");
219
224
  return {
220
225
  path,
221
226
  originalNormalized,
@@ -233,7 +238,9 @@ export async function execPipeline(
233
238
  totalRemovedLines,
234
239
  hadBoundaryDedup: (anchorResult.autoFixes?.length ?? 0) > 0,
235
240
  boundaryRemovedLines: anchorResult.autoFixes?.length ?? 0,
236
- boundaryRemovedLineTexts: anchorResult.autoFixes?.map((fix) => fix.removedLine) ?? [],
241
+ boundaryRemovedLineTexts: sortedFixes.map((fix) => fix.removedLine),
242
+ boundaryDedupAbove: aboveFixes.map((fix) => fix.removedLine),
243
+ boundaryDedupBelow: belowFixes.map((fix) => fix.removedLine),
237
244
  identity,
238
245
  };
239
246
  }
@@ -244,7 +251,8 @@ export function previewFromPipe(pipe: PipelineResult): RPreview {
244
251
  error: `No changes made to ${pipe.path}. The edit produced identical content.`,
245
252
  };
246
253
  }
247
- return { diff: genDiff(pipe.originalNormalized, pipe.result, 4, pipe.resultHashes, pipe.originalHashes).diff };
254
+ const base = genDiff(pipe.originalNormalized, pipe.result, 4, pipe.resultHashes, pipe.originalHashes);
255
+ return { diff: withDedupRows(base.diff, base.lineNumbers, pipe.boundaryDedupAbove, pipe.boundaryDedupBelow).diff };
248
256
  }
249
257
  export function previewError(error: unknown): RPreview {
250
258
  return { error: error instanceof Error ? error.message : String(error) };