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 +1 -1
- package/prompts/replace-guidelines.md +2 -1
- package/src/commit.ts +10 -6
- package/src/constants.ts +2 -0
- package/src/replace-response.ts +44 -3
- package/src/replace.ts +11 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-hashline-edit-pro",
|
|
3
|
-
"version": "3.0.
|
|
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
|
|
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(
|
|
26
|
-
const
|
|
27
|
-
const
|
|
28
|
-
return `Boundary dedup: ${
|
|
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";
|
package/src/replace-response.ts
CHANGED
|
@@ -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
|
|
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 ??
|
|
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
|
|
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:
|
|
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
|
-
|
|
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) };
|