@jerryan/pi-hashline-edit 0.7.1 → 0.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 CHANGED
@@ -1,114 +1,127 @@
1
- ![pi-hashline-edit](assets/banner.jpeg)
2
-
3
- # pi-hashline-edit
4
-
5
- A [pi-coding-agent](https://github.com/badlogic/pi-mono/tree/main/packages/coding-agent) extension that replaces the built-in `read` and `edit` tools with a hash-anchored line-editing workflow.
6
-
7
- Every line returned by `read` carries a short content hash. Edits reference these hashes instead of raw text, so the tool can detect stale context and reject outdated changes before they reach the file.
8
-
9
- Inspired by [oh-my-pi](https://github.com/can1357/oh-my-pi).
10
-
11
- ## Differences from upstream
12
-
13
- This is a fork of the original [pi-hashline-edit](https://github.com/earendil-works/pi-hashline-edit). The core protocol (hash-anchored reads, stale-anchor rejection, atomic writes) is unchanged from upstream. Key differences:
14
-
15
- - **Single edit shape.** One entry type: `{ range: [start, end], lines: [...] }`. No `op` field, no `append`/`prepend`/`replace_text` ops, no `after`/`before`. The tuple enforces explicit endpoint anchors, eliminating the common "forgot `end`" failure mode.
16
- - **Standard hex hash alphabet.** `0-9 A-F` instead of `ZPMQVRWSNKTXJBYH`. Hex pairs are more likely to be single tokens.
17
- - **Symmetric boundary-duplication detection.** Runtime warnings catch duplicated boundary lines on both sides of a replacement, not just trailing.
18
- - **`read` raw mode.** `raw: true` returns plain text without `LINE#HASH:` anchors, for reads that don't plan to edit.
19
- - **Inline FNV-1a hashing.** Replaces `xxhashjs` dependency. Always incorporates line index.
20
- - **Minimal prompt surface.** Prompt text describes what the model needs to use the tool; return-format documentation and error catalogues are omitted.
21
- - **No legacy compatibility.** The `{ oldText, newText }` substring-replace format is not accepted. The schema is hashline-only.
22
-
23
- ## Installation
24
-
25
- ```bash
26
- # From npm
27
- pi install npm:@jerryan/pi-hashline-edit
28
-
29
- # From a local checkout
30
- pi install /path/to/pi-hashline-edit
31
- ```
32
-
33
- ## How It Works
34
-
35
- ### `read` — tagged line output
36
-
37
- Text files are returned with a `LINE#HASH:` prefix on every line. Line numbers may be left-padded within each returned block so the `#HASH:` columns align:
38
-
39
- ```text
40
- 8#A4:function hello() {
41
- 9#3F: console.log("world");
42
- 10#B2:}
43
- ```
44
-
45
- - `LINE` — 1-indexed line number.
46
- - `HASH` — 2-character content hash (hex digits `0-9 A-F`).
47
-
48
- Optional parameters:
49
- - `offset` — start reading from this line number (1-indexed).
50
- - `limit` — maximum number of lines to return.
51
- - `raw` — when `true`, returns plain text without LINE#HASH anchors. Saves tokens when you don't plan to edit this file.
52
-
53
- Images (JPEG, PNG, GIF, WebP) are passed through as attachments and do not participate in the hashline protocol. Binary and directory paths are rejected with a descriptive error.
54
-
55
- ### `edit` — hash-anchored modifications
56
-
57
- Each edit entry replaces an inclusive anchor range:
58
-
59
- ```json
60
- {
61
- "path": "src/main.ts",
62
- "edits": [
63
- { "range": ["11#3F", "11#3F"], "lines": [" console.log('hashline');"] },
64
- { "range": ["42#B2", "45#C7"], "lines": ["function foo() {", " return 42;", "}"] }
65
- ]
66
- }
67
- ```
68
-
69
- - `range` — `[start, end]` pair of LINE#HASH anchors. Use the same anchor twice for single-line.
70
- - `lines` — new content replacing the range (string array). Use `[]` to delete.
71
-
72
- All edits in a single call validate against the same pre-edit snapshot and apply bottom-up, so line numbers stay consistent across operations.
73
-
74
- ### Chained edits
75
-
76
- After a successful edit, the result includes an `--- Updated anchors ---` block with fresh `LINE#HASH` references for the changed region. These can be used directly in the next `edit` call on the same file without a full re-read, provided the next edit targets the same or nearby lines. For distant changes, use `read` first.
77
-
78
- ### Diff preview
79
-
80
- Each edit result includes a compact `Diff preview:` block showing the changed lines with `+`/`-` markers and their new `LINE#HASH` anchors, making quick follow-up edits possible without a full re-read.
81
-
82
- ## Design Decisions
83
-
84
- - **Stale anchors fail.** A hash mismatch means the file has changed since the last `read`. The error includes a snippet with fresh `LINE#HASH` references for the affected lines for immediate retry.
85
- - **No fallback relocation.** Mismatched anchors are never silently relocated to a "close enough" line. This trades convenience for correctness.
86
- - **Strict patch content.** If `lines` contains `LINE#HASH:` display prefixes or diff `+`/`-` markers, the edit is rejected with `[E_INVALID_PATCH]`. The model must send literal file content; the runtime does not silently strip accidental prefixes.
87
- - **Atomic writes.** Files are written via temp-file-then-rename to avoid corruption from interrupted writes. Symlink chains are resolved so the target file is updated without replacing the symlink. Hard-linked files are updated in place to preserve the shared inode. File permissions are preserved across atomic renames.
88
- - **Per-file mutation queue.** Edits queue by the canonical write target, so concurrent edits through different symlink paths still serialize onto the same underlying file.
89
- - **Schema-delegated validation.** Field-type and schema validation are the responsibility of pi's AJV layer. The extension's runtime guard only prevents crashes from missing required top-level fields.
90
-
91
- ## Hashing
92
-
93
- Hashes are computed with inline FNV-1a (32-bit, mask-reduced to 8 bits), then mapped to a 2-character hex string from `0-9 A-F`.
94
-
95
- The line index is always incorporated into the hash, so identical content on different lines produces different hashes.
96
-
97
- ## Development
98
-
99
- Requires [Node.js](https://nodejs.org) and npm.
100
-
101
- ```bash
102
- npm install
103
- npm test
104
- ```
105
-
106
- Set `PI_HASHLINE_DEBUG=1` to show an "active" notification at session start.
107
-
108
- ## Credits
109
-
110
- Thanks to [can1357](https://github.com/can1357) for the original [oh-my-pi](https://github.com/can1357/oh-my-pi) implementation and the hashline concept.
111
-
112
- ## License
113
-
114
- [MIT](LICENSE)
1
+ ![pi-hashline-edit](assets/banner.jpeg)
2
+
3
+ # pi-hashline-edit
4
+
5
+ A [pi-coding-agent](https://github.com/badlogic/pi-mono/tree/main/packages/coding-agent) extension that replaces the built-in `read` and `edit` tools with a hash-anchored line-editing workflow.
6
+
7
+ Every line returned by `read` carries a short content hash. Edits reference these hashes instead of raw text, so the tool can detect stale context and reject outdated changes before they reach the file.
8
+
9
+ Inspired by [oh-my-pi](https://github.com/can1357/oh-my-pi).
10
+
11
+ ## Differences from upstream
12
+
13
+ This is a fork of the original [pi-hashline-edit](https://github.com/earendil-works/pi-hashline-edit). The core protocol (hash-anchored reads, stale-anchor rejection, atomic writes) is unchanged from upstream. Key differences:
14
+
15
+ - **Single edit shape.** One entry type: `{ range: [start, end], lines: [...] }`. No `op` field, no `append`/`prepend`/`replace_text` ops, no `after`/`before`. The tuple enforces explicit endpoint anchors, eliminating the common "forgot `end`" failure mode.
16
+ - **Standard hex hash alphabet.** `0-9 A-F` instead of `ZPMQVRWSNKTXJBYH`. Hex pairs are more likely to be single tokens.
17
+ - **Symmetric boundary-duplication detection.** Runtime warnings catch duplicated boundary lines on both sides of a replacement, not just trailing.
18
+ - **`read` raw mode.** `raw: true` returns plain text without `LINE#HASH:` anchors, for reads that don't plan to edit.
19
+ - **Inline FNV-1a hashing.** Replaces `xxhashjs` dependency. Always incorporates line index.
20
+ - **Minimal prompt surface.** Prompt text describes what the model needs to use the tool; return-format documentation and error catalogues are omitted.
21
+ - **No legacy compatibility.** The `{ oldText, newText }` substring-replace format is not accepted. The schema is hashline-only.
22
+
23
+ ## Installation
24
+
25
+ ```bash
26
+ # From npm
27
+ pi install npm:@jerryan/pi-hashline-edit
28
+
29
+ # From a local checkout
30
+ pi install /path/to/pi-hashline-edit
31
+ ```
32
+
33
+ ## How It Works
34
+
35
+ ### `read` — tagged line output
36
+
37
+ Text files are returned with a `LINE#HASH:` prefix on every line. Line numbers may be left-padded within each returned block so the `#HASH:` columns align:
38
+
39
+ ```text
40
+ 8#A4:function hello() {
41
+ 9#3F: console.log("world");
42
+ 10#B2:}
43
+ ```
44
+
45
+ - `LINE` — 1-indexed line number.
46
+ - `HASH` — 2-character content hash (hex digits `0-9 A-F`).
47
+
48
+ Optional parameters:
49
+ - `offset` — start reading from this line number (1-indexed).
50
+ - `limit` — maximum number of lines to return.
51
+ - `raw` — when `true`, returns plain text without LINE#HASH anchors. Saves tokens when you don't plan to edit this file.
52
+
53
+ Images (JPEG, PNG, GIF, WebP) are passed through as attachments and do not participate in the hashline protocol. Binary and directory paths are rejected with a descriptive error.
54
+
55
+ ### `edit` — hash-anchored modifications
56
+
57
+ Each edit entry replaces an inclusive anchor range:
58
+
59
+ ```json
60
+ {
61
+ "path": "src/main.ts",
62
+ "edits": [
63
+ { "range": ["11#3F", "11#3F"], "lines": [" console.log('hashline');"] },
64
+ { "range": ["42#B2", "45#C7"], "lines": ["function foo() {", " return 42;", "}"] }
65
+ ]
66
+ }
67
+ ```
68
+
69
+ - `range` — `[start, end]` pair of LINE#HASH anchors. Use the same anchor twice for single-line.
70
+ - `lines` — new content replacing the range (string array). Use `[]` to delete.
71
+
72
+ All edits in a single call validate against the same pre-edit snapshot and apply bottom-up, so line numbers stay consistent across operations.
73
+
74
+ ### Chained edits
75
+
76
+ After a successful edit, the response contains a unified diff where context and added lines carry fresh `LINE#HASH` anchors. These can be used directly in the next `edit` call on the same file without a full re-read, provided the next edit targets the same or nearby lines. For distant changes, use `read` first.
77
+
78
+ ### Diff output
79
+
80
+ Each edit result shows a unified diff with hashline-formatted lines:
81
+
82
+ ```text
83
+ 8#A4:function hello() {
84
+ -9 : console.log("world");
85
+ +9#B1: console.log("hashline");
86
+ 10#B2:}
87
+ ```
88
+
89
+ - Context lines: ` NN#HH:content` (space prefix)
90
+ - Removed lines: `-NN :content` (no hash, aligned colon)
91
+ - Added lines: `+NN#HH:content` (hash for new anchors)
92
+ - Multiple hunks are shown when edits are far apart.
93
+
94
+ ## Design Decisions
95
+
96
+ - **Stale anchors fail.** A hash mismatch means the file has changed since the last `read`. The error includes a snippet with fresh `LINE#HASH` references for the affected lines for immediate retry.
97
+ - **No fallback relocation.** Mismatched anchors are never silently relocated to a "close enough" line. This trades convenience for correctness.
98
+ - **Strict patch content.** If `lines` contains `LINE#HASH:` display prefixes or diff `+`/`-` markers, the edit is rejected with `[E_INVALID_PATCH]`. The model must send literal file content; the runtime does not silently strip accidental prefixes.
99
+ - **Full-file deletion guardrail.** Edits that would empty a file with more than 50 lines are rejected with `[E_WOULD_EMPTY]`. Small files show the full diff normally; large deletions are almost always mistakes.
100
+ - **Atomic writes.** Files are written via temp-file-then-rename to avoid corruption from interrupted writes. Symlink chains are resolved so the target file is updated without replacing the symlink. Hard-linked files are updated in place to preserve the shared inode. File permissions are preserved across atomic renames.
101
+ - **Per-file mutation queue.** Edits queue by the canonical write target, so concurrent edits through different symlink paths still serialize onto the same underlying file.
102
+ - **Schema-delegated validation.** Field-type and schema validation are the responsibility of pi's AJV layer. The extension's runtime guard only prevents crashes from missing required top-level fields.
103
+
104
+ ## Hashing
105
+
106
+ Hashes are computed with inline FNV-1a (32-bit, mask-reduced to 8 bits), then mapped to a 2-character hex string from `0-9 A-F`.
107
+
108
+ The line index is always incorporated into the hash, so identical content on different lines produces different hashes.
109
+
110
+ ## Development
111
+
112
+ Requires [Node.js](https://nodejs.org) and npm.
113
+
114
+ ```bash
115
+ npm install
116
+ npm test
117
+ ```
118
+
119
+ Set `PI_HASHLINE_DEBUG=1` to show an "active" notification at session start.
120
+
121
+ ## Credits
122
+
123
+ Thanks to [can1357](https://github.com/can1357) for the original [oh-my-pi](https://github.com/can1357/oh-my-pi) implementation and the hashline concept.
124
+
125
+ ## License
126
+
127
+ [MIT](LICENSE)
package/package.json CHANGED
@@ -1,53 +1,53 @@
1
- {
2
- "name": "@jerryan/pi-hashline-edit",
3
- "version": "0.7.1",
4
- "description": "Hashline read/edit tool override for pi-coding-agent",
5
- "repository": {
6
- "type": "git",
7
- "url": "git+https://github.com/JerryAZR/pi-hashline-edit.git"
8
- },
9
- "author": "JerryAZR",
10
- "publishConfig": {
11
- "registry": "https://registry.npmjs.org/"
12
- },
13
- "keywords": [
14
- "pi-package",
15
- "pi",
16
- "coding-agent",
17
- "extension",
18
- "hashline"
19
- ],
20
- "license": "MIT",
21
- "files": [
22
- "index.ts",
23
- "src",
24
- "prompts",
25
- "README.md",
26
- "LICENSE"
27
- ],
28
- "pi": {
29
- "extensions": [
30
- "./index.ts"
31
- ]
32
- },
33
- "dependencies": {
34
- "diff": "^8.0.2",
35
- "file-type": "^21.3.0"
36
- },
37
- "peerDependencies": {
38
- "@earendil-works/pi-ai": ">=0.74.0",
39
- "@earendil-works/pi-coding-agent": ">=0.74.0",
40
- "@earendil-works/pi-tui": "*",
41
- "@sinclair/typebox": "*"
42
- },
43
- "scripts": {
44
- "test": "vitest run",
45
- "test:watch": "vitest"
46
- },
47
- "devDependencies": {
48
- "@earendil-works/pi-coding-agent": "^0.74.0",
49
- "@types/node": "^22.0.0",
50
- "ajv": "^8.20.0",
51
- "vitest": "^3.0.0"
52
- }
53
- }
1
+ {
2
+ "name": "@jerryan/pi-hashline-edit",
3
+ "version": "0.7.2",
4
+ "description": "Hashline read/edit tool override for pi-coding-agent",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "git+https://github.com/JerryAZR/pi-hashline-edit.git"
8
+ },
9
+ "author": "JerryAZR",
10
+ "publishConfig": {
11
+ "registry": "https://registry.npmjs.org/"
12
+ },
13
+ "keywords": [
14
+ "pi-package",
15
+ "pi",
16
+ "coding-agent",
17
+ "extension",
18
+ "hashline"
19
+ ],
20
+ "license": "MIT",
21
+ "files": [
22
+ "index.ts",
23
+ "src",
24
+ "prompts",
25
+ "README.md",
26
+ "LICENSE"
27
+ ],
28
+ "pi": {
29
+ "extensions": [
30
+ "./index.ts"
31
+ ]
32
+ },
33
+ "dependencies": {
34
+ "diff": "^8.0.2",
35
+ "file-type": "^21.3.0"
36
+ },
37
+ "peerDependencies": {
38
+ "@earendil-works/pi-ai": ">=0.74.0",
39
+ "@earendil-works/pi-coding-agent": ">=0.74.0",
40
+ "@earendil-works/pi-tui": "*",
41
+ "@sinclair/typebox": "*"
42
+ },
43
+ "scripts": {
44
+ "test": "vitest run",
45
+ "test:watch": "vitest"
46
+ },
47
+ "devDependencies": {
48
+ "@earendil-works/pi-coding-agent": "^0.74.0",
49
+ "@types/node": "^22.0.0",
50
+ "ajv": "^8.20.0",
51
+ "vitest": "^3.0.0"
52
+ }
53
+ }
package/prompts/edit.md CHANGED
@@ -1,12 +1,12 @@
1
1
  Patch a UTF-8 text file using `LINE#HASH` anchors copied verbatim from `read`.
2
2
 
3
- Submit one `edit` call per file. All operations go in a single `edits` array; anchors must come from the same fresh source — the most recent `read` or `--- Anchors ---` block of a successful `edit` on this file.
3
+ Submit one `edit` call per file. All operations go in a single `edits` array; anchors must come from the same fresh source — the most recent `read` or diff output of a successful `edit` on this file.
4
4
 
5
5
  Each edit entry replaces an inclusive anchor range:
6
6
  ```json
7
7
  { "range": [startAnchor, endAnchor], "lines": [...] }
8
8
  ```
9
- - `range` — `[start, end]` pair of LINE#HASH anchors from the most recent `read`.
9
+ - `range` — `[start, end]` pair of LINE#HASH anchors from the most recent `read` or diff output.
10
10
  Use the same anchor twice for single-line: `["42#A4", "42#A4"]`.
11
11
  - `lines` — new content replacing the range (string array). Use `[]` to delete.
12
12
  Must be literal file content, not LINE#HASH-prefixed output. Match indentation exactly.
@@ -19,5 +19,5 @@ Example:
19
19
  ] }
20
20
 
21
21
  Rules:
22
- - Do not guess, shift, or construct anchors. Copy them from the most recent `read` of this file.
22
+ - Do not guess, shift, or construct anchors. Copy them from the most recent `read` or diff output of this file.
23
23
  - Do not emit overlapping or adjacent edits — merge them into one.
package/src/edit-diff.ts CHANGED
@@ -223,102 +223,63 @@ export function replaceText(
223
223
 
224
224
  // ─── Diff generation ────────────────────────────────────────────────────
225
225
 
226
- function formatDiffPreviewLine(
227
- prefix: " " | "+" | "-",
228
- lineNum: number,
229
- lineNumWidth: number,
230
- line: string,
231
- includeHash: boolean,
232
- ): string {
233
- const paddedLineNum = String(lineNum).padStart(lineNumWidth, " ");
234
- if (!includeHash) {
235
- return `${prefix}${paddedLineNum} ${line}`;
236
- }
237
- return `${prefix}${paddedLineNum}#${computeLineHash(lineNum, line)}:${line}`;
238
- }
239
-
240
226
  export function generateDiffString(
241
227
  oldContent: string,
242
228
  newContent: string,
243
229
  contextLines = 4,
244
- ): { diff: string; firstChangedLine: number | undefined } {
245
- const parts = Diff.diffLines(oldContent, newContent);
246
- const output: string[] = [];
230
+ ): { diff: string } {
231
+ const patch = Diff.structuredPatch("a", "b", oldContent, newContent, undefined, undefined, {
232
+ context: contextLines,
233
+ });
234
+
235
+ if (!patch.hunks.length) {
236
+ return { diff: "" };
237
+ }
238
+
247
239
  const maxLineNum = Math.max(
248
240
  oldContent.split("\n").length,
249
241
  newContent.split("\n").length,
250
242
  );
251
243
  const lineNumWidth = String(maxLineNum).length;
252
- let oldLineNum = 1;
253
- let newLineNum = 1;
254
- let lastWasChange = false;
255
- let firstChangedLine: number | undefined;
256
-
257
- for (let i = 0; i < parts.length; i++) {
258
- const part = parts[i]!;
259
- const raw = part.value.split("\n");
260
- if (raw[raw.length - 1] === "") raw.pop();
261
-
262
- if (part.added || part.removed) {
263
- if (firstChangedLine === undefined) firstChangedLine = newLineNum;
264
- for (const line of raw) {
265
- if (part.added) {
266
- output.push(
267
- formatDiffPreviewLine("+", newLineNum, lineNumWidth, line, true),
268
- );
269
- newLineNum++;
270
- } else {
271
- output.push(
272
- formatDiffPreviewLine("-", oldLineNum, lineNumWidth, line, false),
273
- );
274
- oldLineNum++;
275
- }
276
- }
277
- lastWasChange = true;
278
- continue;
244
+ const hashPad = " ".repeat(3); // align with `#HH:`
245
+
246
+ const output: string[] = [];
247
+
248
+ for (let h = 0; h < patch.hunks.length; h++) {
249
+ const hunk = patch.hunks[h]!;
250
+ if (h > 0) {
251
+ output.push(" ...");
279
252
  }
280
253
 
281
- const nextPartIsChange =
282
- i < parts.length - 1 && (parts[i + 1]!.added || parts[i + 1]!.removed);
283
- if (lastWasChange || nextPartIsChange) {
284
- let linesToShow = raw;
285
- let skipStart = 0;
286
- let skipEnd = 0;
254
+ let oldLineNum = hunk.oldStart;
255
+ let newLineNum = hunk.newStart;
287
256
 
288
- if (!lastWasChange) {
289
- skipStart = Math.max(0, raw.length - contextLines);
290
- linesToShow = raw.slice(skipStart);
291
- }
292
- if (!nextPartIsChange && linesToShow.length > contextLines) {
293
- skipEnd = linesToShow.length - contextLines;
294
- linesToShow = linesToShow.slice(0, contextLines);
295
- }
257
+ for (const line of hunk.lines) {
258
+ if (line === "\") continue;
296
259
 
297
- if (skipStart > 0) {
298
- output.push(` ${"".padStart(lineNumWidth, " ")} ...`);
299
- oldLineNum += skipStart;
300
- newLineNum += skipStart;
301
- }
302
- for (const line of linesToShow) {
303
- output.push(
304
- formatDiffPreviewLine(" ", newLineNum, lineNumWidth, line, true),
305
- );
260
+ const prefix = line[0] as " " | "+" | "-";
261
+ const text = line.slice(1);
262
+
263
+ if (prefix === "-") {
264
+ const padded = String(oldLineNum).padStart(lineNumWidth, " ");
265
+ output.push(`-${padded}${hashPad}:${text}`);
266
+ oldLineNum++;
267
+ } else if (prefix === "+") {
268
+ const padded = String(newLineNum).padStart(lineNumWidth, " ");
269
+ const hash = computeLineHash(newLineNum, text);
270
+ output.push(`+${padded}#${hash}:${text}`);
271
+ newLineNum++;
272
+ } else {
273
+ const padded = String(newLineNum).padStart(lineNumWidth, " ");
274
+ const hash = computeLineHash(newLineNum, text);
275
+ output.push(` ${padded}#${hash}:${text}`);
306
276
  oldLineNum++;
307
277
  newLineNum++;
308
278
  }
309
- if (skipEnd > 0) {
310
- output.push(` ${"".padStart(lineNumWidth, " ")} ...`);
311
- oldLineNum += skipEnd;
312
- newLineNum += skipEnd;
313
- }
314
- } else {
315
- oldLineNum += raw.length;
316
- newLineNum += raw.length;
317
279
  }
318
- lastWasChange = false;
319
280
  }
320
281
 
321
- return { diff: output.join("\n"), firstChangedLine };
282
+ return { diff: output.join("\n") };
322
283
  }
323
284
 
324
285
  export interface CompactHashlineDiffPreview {
@@ -1,21 +1,11 @@
1
1
  /**
2
2
  * Edit response builders.
3
3
  *
4
- * Pulled out of `src/edit.ts` execute() so the noop and changed branches
5
- * are independently testable and the top-level execute path stays narrative.
6
- *
7
- * No behaviour change: outputs are byte-identical to the previous inline
8
- * implementation. The only additive surface is `details.metrics` (Phase 2 C
9
- * — observability for hosts; the LLM-visible text is unchanged).
4
+ * Unified diff output: agent and user see the same content. The diff is
5
+ * generated from structuredPatch hunks with hashline-formatted lines.
10
6
  */
11
7
 
12
8
  import { generateDiffString } from "./edit-diff";
13
- import {
14
- computeAffectedLineRange,
15
- formatHashlineRegion,
16
- } from "./hashline";
17
-
18
- const CHANGED_ANCHOR_TEXT_BUDGET_BYTES = 50 * 1024;
19
9
 
20
10
  // ─── Public types ───────────────────────────────────────────────────────
21
11
 
@@ -24,7 +14,6 @@ export type EditMetrics = {
24
14
  edits_noop: number;
25
15
  warnings: number;
26
16
  classification: "applied" | "noop";
27
- changed_lines?: { first: number; last: number };
28
17
  added_lines?: number;
29
18
  removed_lines?: number;
30
19
  };
@@ -51,8 +40,6 @@ export interface SuccessResponseInput {
51
40
  originalNormalized: string;
52
41
  result: string;
53
42
  warnings: string[] | undefined;
54
- firstChangedLine: number | undefined;
55
- lastChangedLine: number | undefined;
56
43
  snapshotId: string;
57
44
  editsAttempted: number;
58
45
  noopEditsCount: number;
@@ -60,12 +47,6 @@ export interface SuccessResponseInput {
60
47
 
61
48
  // ─── Helpers ────────────────────────────────────────────────────────────
62
49
 
63
- function getVisibleLines(text: string): string[] {
64
- if (text.length === 0) return [];
65
- const lines = text.split("\n");
66
- return text.endsWith("\n") ? lines.slice(0, -1) : lines;
67
- }
68
-
69
50
  function countDiffLines(diff: string, marker: "+" | "-"): number {
70
51
  if (!diff) return 0;
71
52
  let count = 0;
@@ -82,8 +63,6 @@ function buildMetrics(args: {
82
63
  editsAttempted: number;
83
64
  noopEditsCount: number;
84
65
  warningsCount: number;
85
- firstChangedLine?: number;
86
- lastChangedLine?: number;
87
66
  addedLines?: number;
88
67
  removedLines?: number;
89
68
  }): EditMetrics {
@@ -93,16 +72,6 @@ function buildMetrics(args: {
93
72
  warnings: args.warningsCount,
94
73
  classification: args.classification,
95
74
  };
96
- if (
97
- args.classification === "applied" &&
98
- args.firstChangedLine !== undefined &&
99
- args.lastChangedLine !== undefined
100
- ) {
101
- metrics.changed_lines = {
102
- first: args.firstChangedLine,
103
- last: args.lastChangedLine,
104
- };
105
- }
106
75
  if (args.addedLines !== undefined) metrics.added_lines = args.addedLines;
107
76
  if (args.removedLines !== undefined) metrics.removed_lines = args.removedLines;
108
77
  return metrics;
@@ -145,7 +114,6 @@ export function buildNoopResponse(input: NoopResponseInput): ToolResult {
145
114
  content: [{ type: "text", text }],
146
115
  details: {
147
116
  diff: "",
148
- firstChangedLine: undefined,
149
117
  snapshotId,
150
118
  classification: "noop" as const,
151
119
  metrics,
@@ -154,42 +122,15 @@ export function buildNoopResponse(input: NoopResponseInput): ToolResult {
154
122
  }
155
123
 
156
124
  export function buildChangedResponse(input: SuccessResponseInput): ToolResult {
157
- const {
158
- result,
159
- warnings,
160
- firstChangedLine,
161
- lastChangedLine,
162
- snapshotId,
163
- originalNormalized,
164
- editsAttempted,
165
- noopEditsCount,
166
- } = input;
125
+ const { result, warnings, snapshotId, originalNormalized, editsAttempted, noopEditsCount } =
126
+ input;
167
127
 
168
128
  const diffResult = generateDiffString(originalNormalized, result);
169
129
  const addedLines = countDiffLines(diffResult.diff, "+");
170
130
  const removedLines = countDiffLines(diffResult.diff, "-");
171
131
  const warningsBlock = warningsBlockOf(warnings);
172
132
 
173
- const resultLines = getVisibleLines(result);
174
- const anchorRange = computeAffectedLineRange({
175
- firstChangedLine,
176
- lastChangedLine,
177
- resultLineCount: resultLines.length,
178
- });
179
- const anchorsBlock = anchorRange
180
- ? (() => {
181
- const region = resultLines.slice(anchorRange.start - 1, anchorRange.end);
182
- const formatted = formatHashlineRegion(region, anchorRange.start);
183
- const block = `--- Anchors ${anchorRange.start}-${anchorRange.end} ---\n${formatted}`;
184
- return Buffer.byteLength(block, "utf8") <= CHANGED_ANCHOR_TEXT_BUDGET_BYTES
185
- ? block
186
- : "Anchors omitted; use read for subsequent edits.";
187
- })()
188
- : resultLines.length === 0
189
- ? "File is empty. Use edit with prepend or append and omit pos to insert content."
190
- : "Anchors omitted; use read for subsequent edits.";
191
-
192
- const text = [anchorsBlock, warningsBlock.trimStart()]
133
+ const text = [diffResult.diff, warningsBlock.trimStart()]
193
134
  .filter((section) => section.length > 0)
194
135
  .join("\n\n");
195
136
 
@@ -198,8 +139,6 @@ export function buildChangedResponse(input: SuccessResponseInput): ToolResult {
198
139
  editsAttempted,
199
140
  noopEditsCount,
200
141
  warningsCount: warnings?.length ?? 0,
201
- firstChangedLine,
202
- lastChangedLine,
203
142
  addedLines,
204
143
  removedLines,
205
144
  });
@@ -208,7 +147,6 @@ export function buildChangedResponse(input: SuccessResponseInput): ToolResult {
208
147
  content: [{ type: "text", text }],
209
148
  details: {
210
149
  diff: diffResult.diff,
211
- firstChangedLine: firstChangedLine ?? diffResult.firstChangedLine,
212
150
  snapshotId,
213
151
  metrics,
214
152
  },