pi-hashline-edit-pro 1.0.8 → 1.1.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 CHANGED
@@ -11,7 +11,7 @@ Fork of [pi-hashline-edit](https://github.com/RimuruW/pi-hashline-edit) by Rimur
11
11
  - **Stable anchors.** Editing one part of a file leaves the hashes of untouched lines unchanged, so anchors from earlier reads stay valid.
12
12
  - **Autocorrection with warnings.** Unambiguous copy-paste mistakes — hash prefixes, diff-preview rows, reversed ranges — are fixed automatically and reported.
13
13
  - **Safe writes.** Atomic temp-file-then-rename writes preserve permissions, BOMs, line endings, symlinks, and hard links.
14
- - **Auto-read.** Fresh anchors are appended to the result of every `write`, `replace`, and `undo_last_replace`.
14
+ - **Auto-read.** Fresh anchors are appended to the result of every `write` that changes the file; after `replace` and `undo_last_replace`, the post-edit diff is shown instead.
15
15
 
16
16
  ## Installation
17
17
 
@@ -47,7 +47,7 @@ kQm│}
47
47
  }
48
48
  ```
49
49
 
50
- 3. Keep editing. Anchors for untouched lines remain valid across edits, so hashes from earlier reads keep working; changed lines get fresh anchors, which auto-read appends to each result.
50
+ 3. Keep editing. Anchors for untouched lines remain valid across edits, so hashes from earlier reads keep working; changed lines get fresh anchors, which auto-read appends after each `write`.
51
51
 
52
52
  ## The `read` tool
53
53
 
@@ -102,7 +102,7 @@ Behavior:
102
102
  - A reversed range (start hash after end hash) is swapped and applied.
103
103
  - A duplicated boundary line — the classic `}`, `});`, or `} else {` pasted twice — is silently removed; the duplicate never reaches the file.
104
104
  - `file_path` is accepted as an alias for `path`; a JSON-string `content_lines` is parsed into an array.
105
- - **Response.** A successful edit reports `Successfully replaced in {path}. Added X line(s), removed Y line(s).` plus any warnings. An edit that produces identical content reports `No changes made` and never rotates anchors. The post-edit diff is exposed to the host UI via `details.diff` only it is intentionally not part of the model-visible text.
105
+ - **Response.** With auto-read enabled (the default), a successful edit returns the post-edit diff — the same `+HASH│` / `- │` / ` HASH│` rows the user sees — instead of the summary. With auto-read disabled, the edit reports `Successfully replaced in {path}. Added X line(s), removed Y line(s).` plus any warnings, and no diff is shown to the model. Warnings are appended in both modes. An edit that produces identical content reports `No changes made` and never rotates anchors. The post-edit diff is exposed to the host UI via `details.diff` — the TUI always shows it and reaches the model-visible text only while auto-read is on.
106
106
  - **Undo.** Every successful replace is undoable once via `undo_last_replace` — see [Undo](#undo).
107
107
 
108
108
  ## Anchor stability
@@ -118,13 +118,15 @@ A no-op replace never changes the file, so anchors remain valid. On first run af
118
118
 
119
119
  ## Auto-read
120
120
 
121
- Enabled by default. After a successful `write`, `replace`, or `undo_last_replace`, the extension reads the file and appends an `--- Auto-read (hashline anchors) ---` block to the result, so the model gets immediate `HASH│content` anchors without a separate `read` call.
121
+ Enabled by default. After a successful `write` that changes the file, the extension reads the file and appends an `--- Auto-read (hashline anchors) ---` block to the result, so the model gets immediate `HASH│content` anchors without a separate `read` call.
122
122
 
123
- - After `replace` / `undo_last_replace`, the block covers the changed span plus 2 lines of context above and below — the rest of the file keeps its anchors from the persistent store.
123
+ - A no-op `replace` produces no diff — the file is unchanged, so existing anchors remain valid.
124
+ - After `replace` / `undo_last_replace`, the success summary is replaced by the post-edit diff (the same `+HASH│` / `- │` / ` HASH│` rows used for replace) plus any warnings, so the model sees the change like a git diff instead of line counts; no anchor block is appended — call `read` for fresh anchors.
125
+ - With auto-read disabled, `replace` / `undo_last_replace` results keep the plain summary in the model-visible text — no diff and no anchor block reach the model (the post-edit diff is still shown to the user).
124
126
  - After `write`, the block dumps from the top of the file. For files over 2000 lines, the dump is truncated with a pagination hint — use `read` with `offset` to continue.
125
127
  - Auto-read keeps a 50KB display budget: lines over 50KB are skipped with a marker instead of their content (use `read` for lines up to 200KB).
126
128
  - Toggle at runtime with `/toggle-auto-read`; the setting persists across sessions.
127
- - If the auto-read itself fails (e.g. the file was deleted between the operation and the read), a short `--- Auto-read failed: ... ---` notice is appended instead of the anchor block, so the model knows the anchors are missing.
129
+ - If the auto-read itself fails (e.g. the file was deleted between the write and the read), a short `--- Auto-read failed: ... ---` notice is appended instead of the anchor block, so the model knows the anchors are missing.
128
130
 
129
131
  ## Undo
130
132
 
@@ -134,14 +136,14 @@ Enabled by default. After a successful `write`, `replace`, or `undo_last_replace
134
136
  - History is persisted in the hash store (`~/.config/pi-hashline-edit-pro/hash-store.sqlite`) and survives session restarts; a failed `write` does not clear it.
135
137
  - **Undo is a precondition, not a convenience.** The undo record is persisted *before* the edit is written; if it cannot be persisted, the `replace` is refused with `[E_UNDO_UNAVAILABLE]` and the file is not touched, so every applied edit is undoable. If the file write itself then fails, the previous undo record is restored, so a refused edit never destroys earlier undo history.
136
138
  - A successful `write` clears the history for that file.
137
- - Call `read` after an undo to get fresh anchors for follow-up edits.
139
+ - With auto-read enabled, the model sees the post-edit diff after an undo, just like a replace; with auto-read disabled it sees the plain summary. No anchors are appended after an undo — call `read` to get fresh anchors for follow-up edits.
138
140
  - **Safety guard.** If the file was modified or deleted since the last replace, `undo_last_replace` refuses with `[E_UNDO_STALE]` rather than overwriting those changes.
139
141
 
140
142
  ## Commands and configuration
141
143
 
142
144
  | Command | Description |
143
145
  | --- | --- |
144
- | `/toggle-auto-read` | Toggle automatic hashline anchors after write and replace operations. Persists across sessions. |
146
+ | `/toggle-auto-read` | Toggle automatic hashline anchors after write and post-edit diffs after replace and undo_last_replace operations. Persists across sessions. |
145
147
 
146
148
  Settings live in `~/.config/pi-hashline-edit-pro/config.json`, created automatically when a setting is toggled:
147
149
 
package/index.ts CHANGED
@@ -5,7 +5,7 @@ import { regReplace } from "./src/replace";
5
5
  import { regReplaceUndo, clearUndo } from "./src/replace-undo";
6
6
  import { regRead, fmtReadPreview } from "./src/read";
7
7
  import type { RMetrics } from "./src/replace-response";
8
- import { AUTO_READ_MAX } from "./src/constants";
8
+ import { extractWarnings } from "./src/replace-render";
9
9
  import { MAX_HASH_LINES } from "./src/hashline";
10
10
  import {
11
11
  readConfig,
@@ -17,7 +17,7 @@ import { toCwd } from "./src/paths";
17
17
  import { resolveTarget } from "./src/fs-write";
18
18
 
19
19
  export default function (pi: ExtensionAPI): void {
20
- regRead(pi, { autoRead: true });
20
+ regRead(pi);
21
21
 
22
22
  regReplace(pi);
23
23
  regReplaceUndo(pi);
@@ -37,25 +37,23 @@ export default function (pi: ExtensionAPI): void {
37
37
  }
38
38
  const config = await readConfig();
39
39
  autoRead = config.autoRead;
40
- regRead(pi, { autoRead });
41
-
42
40
  if (debugValue === "1" || debugValue === "true") {
43
41
  ctx.ui.notify(`Hashline Edit mode active`, "info");
44
42
  }
45
43
  });
46
44
 
47
45
  pi.registerCommand("toggle-auto-read", {
48
- description: "Toggle automatic hashline anchors after write and replace operations",
46
+ description: "Toggle automatic hashline anchors after write and post-edit diffs after replace and undo_last_replace operations",
49
47
  handler: async (_args, ctx) => {
50
48
  autoRead = await toggleAutoRead();
51
- regRead(pi, { autoRead });
52
49
  const state = autoRead ? "enabled" : "disabled";
53
- ctx.ui.notify(`Auto-read after write/replace: ${state}`, "info");
50
+ ctx.ui.notify(`Auto-read anchors (write) and post-edit diffs (replace/undo): ${state}`, "info");
54
51
  },
55
52
  });
56
53
 
57
54
  pi.on("tool_result", async (event, ctx) => {
58
55
  if (event.isError) return;
56
+
59
57
  if (event.toolName === "write") {
60
58
  const writtenPath = (event.input as Record<string, unknown>)?.path;
61
59
  if (typeof writtenPath === "string") {
@@ -65,58 +63,64 @@ export default function (pi: ExtensionAPI): void {
65
63
  console.error("Failed to clear undo after write:", error);
66
64
  }
67
65
  }
66
+ if (!autoRead) return;
67
+ if (typeof writtenPath !== "string") return;
68
+ try {
69
+ const { normalized, fileHashes, absolutePath } = await readNormFile(
70
+ writtenPath, ctx.cwd, { maxLines: MAX_HASH_LINES },
71
+ );
72
+ const preview = await fmtReadPreview(
73
+ normalized,
74
+ {},
75
+ fileHashes,
76
+ absolutePath,
77
+ DEFAULT_MAX_BYTES,
78
+ );
79
+ return {
80
+ content: [
81
+ ...(event.content ?? []),
82
+ { type: "text", text: `\n\n--- Auto-read (hashline anchors) ---\n${preview.text}` },
83
+ ],
84
+ };
85
+ } catch (error) {
86
+ console.error("Auto-read after write failed:", error);
87
+ const message = error instanceof Error ? error.message : String(error);
88
+ return {
89
+ content: [
90
+ ...(event.content ?? []),
91
+ { type: "text", text: `\n\n--- Auto-read failed: ${message} ---` },
92
+ ],
93
+ };
94
+ }
68
95
  }
69
- if (!autoRead) return;
96
+
70
97
  if (
71
- event.toolName !== "write" &&
72
98
  event.toolName !== "replace" &&
73
99
  event.toolName !== "undo_last_replace"
74
100
  ) return;
75
- const filePath = (event.input as Record<string, unknown>)?.path;
76
- if (typeof filePath !== "string") return;
101
+ if (!autoRead) return;
77
102
 
78
103
  const metrics = (event.details as { metrics?: RMetrics } | undefined)?.metrics;
79
- if (event.toolName !== "write" && metrics?.classification === "noop") return;
104
+ if (metrics?.classification === "noop") return;
80
105
 
81
- try {
82
- const { normalized, fileHashes, absolutePath } = await readNormFile(
83
- filePath, ctx.cwd, { maxLines: MAX_HASH_LINES },
84
- );
85
-
86
- const changedLines =
87
- event.toolName === "replace" || event.toolName === "undo_last_replace"
88
- ? metrics?.changed_lines
89
- : undefined;
90
- let offset: number | undefined;
91
- let limit = AUTO_READ_MAX;
92
- if (changedLines) {
93
- offset = Math.max(1, changedLines.first - 2);
94
- limit = Math.min(changedLines.last + 2 - offset + 1, AUTO_READ_MAX);
95
- }
106
+ const diff = (event.details as { diff?: string } | undefined)?.diff;
107
+ if (!diff) return;
96
108
 
97
- const preview = await fmtReadPreview(
98
- normalized,
99
- { offset, limit },
100
- fileHashes,
101
- absolutePath,
102
- DEFAULT_MAX_BYTES,
103
- );
104
-
105
- return {
106
- content: [
107
- ...(event.content ?? []),
108
- { type: "text", text: `\n\n--- Auto-read (hashline anchors) ---\n${preview.text}` },
109
- ],
110
- };
111
- } catch (error) {
112
- console.error("Auto-read after write/replace failed:", error);
113
- const message = error instanceof Error ? error.message : String(error);
114
- return {
115
- content: [
116
- ...(event.content ?? []),
117
- { type: "text", text: `\n\n--- Auto-read failed: ${message} ---` },
118
- ],
119
- };
120
- }
109
+ const rendered = (event.content ?? [])
110
+ .filter(
111
+ (entry): entry is { type: "text"; text: string } =>
112
+ entry.type === "text" && typeof entry.text === "string",
113
+ )
114
+ .map((entry) => entry.text)
115
+ .join("\n");
116
+ const warnings = extractWarnings(rendered);
117
+ return {
118
+ content: [
119
+ {
120
+ type: "text",
121
+ text: warnings ? `${diff}\n\n${warnings}` : diff,
122
+ },
123
+ ],
124
+ };
121
125
  });
122
126
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-hashline-edit-pro",
3
- "version": "1.0.8",
3
+ "version": "1.1.0",
4
4
  "type": "module",
5
5
  "description": "Strict hashline read/replace tool for pi-coding-agent with hash-anchored edits (3-char, 62-symbol, perfect hashing)",
6
6
  "main": "index.ts",
@@ -1,2 +1,2 @@
1
1
  - `read`: call before `replace` when you need fresh HASH anchors for a file.
2
- {{AUTO_READ_NOTE}}
2
+ - `read`: call again after any edit to that file — changed lines get new anchors.
package/src/read.ts CHANGED
@@ -21,11 +21,8 @@ const R_DESC = loadP("../prompts/read.md");
21
21
 
22
22
  const R_SNIPPET = loadP("../prompts/read-snippet.md");
23
23
 
24
- function readGuide(autoRead: boolean): string[] {
25
- const note = "- `read`: call again after any edit to that file — changed lines get new anchors.";
26
- return loadGuide("../prompts/read-guidelines.md", {
27
- AUTO_READ_NOTE: autoRead ? "" : note,
28
- });
24
+ function readGuide(): string[] {
25
+ return loadGuide("../prompts/read-guidelines.md");
29
26
  }
30
27
 
31
28
  function normPosInt(
@@ -148,13 +145,13 @@ export async function fmtReadPreview(
148
145
  };
149
146
  }
150
147
 
151
- export function regRead(pi: ExtensionAPI, opts?: { autoRead?: boolean }): void {
148
+ export function regRead(pi: ExtensionAPI): void {
152
149
  pi.registerTool({
153
150
  name: "read",
154
151
  label: "Read",
155
152
  description: R_DESC,
156
153
  promptSnippet: R_SNIPPET,
157
- promptGuidelines: readGuide(opts?.autoRead ?? true),
154
+ promptGuidelines: readGuide(),
158
155
  parameters: Type.Object({
159
156
  path: Type.String({
160
157
  description: "Path to the file to read (relative or absolute)",
@@ -126,7 +126,7 @@ export function buildChanged(input: SuccessInput): TResult {
126
126
  const { path, result, warnings, snapshotId, originalNormalized, editMeta, resultHashes } = input;
127
127
 
128
128
  const resultLines = visLines(result);
129
- const diffResult = genDiff(originalNormalized, result, 2, resultHashes);
129
+ const diffResult = genDiff(originalNormalized, result, 1, resultHashes);
130
130
  const addedLines = editMeta.addedLines;
131
131
  const removedLines = editMeta.removedLines;
132
132
  const warningsBlock = warnBlock(warnings);
@@ -157,6 +157,7 @@ export function regReplaceUndo(pi: ExtensionAPI): void {
157
157
  const linesAddedByReplace = cntDiff(diffResult.diff, "+");
158
158
  const linesRemovedByReplace = cntDiff(diffResult.diff, "-");
159
159
  const restoredRange = changedRange(currentNormalized, undo.content);
160
+ const undoDiff = genDiff(currentNormalized, undo.content, 1, undo.hashes).diff;
160
161
 
161
162
  await writeAtomic(
162
163
  mutationTargetPath,
@@ -192,6 +193,7 @@ export function regReplaceUndo(pi: ExtensionAPI): void {
192
193
  },
193
194
  ],
194
195
  details: {
196
+ diff: undoDiff,
195
197
  metrics: buildMetrics({
196
198
  classification: "applied",
197
199
  editsAttempted: 1,