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 +10 -8
- package/index.ts +55 -51
- package/package.json +1 -1
- package/prompts/read-guidelines.md +1 -1
- package/src/read.ts +4 -7
- package/src/replace-response.ts +1 -1
- package/src/replace-undo.ts +2 -0
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
|
|
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
|
|
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.**
|
|
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
|
|
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
|
-
-
|
|
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
|
|
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
|
-
-
|
|
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 {
|
|
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
|
|
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
|
|
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
|
-
|
|
96
|
+
|
|
70
97
|
if (
|
|
71
|
-
event.toolName !== "write" &&
|
|
72
98
|
event.toolName !== "replace" &&
|
|
73
99
|
event.toolName !== "undo_last_replace"
|
|
74
100
|
) return;
|
|
75
|
-
|
|
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 (
|
|
104
|
+
if (metrics?.classification === "noop") return;
|
|
80
105
|
|
|
81
|
-
|
|
82
|
-
|
|
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
|
-
|
|
98
|
-
|
|
99
|
-
{
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
);
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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,2 +1,2 @@
|
|
|
1
1
|
- `read`: call before `replace` when you need fresh HASH anchors for a file.
|
|
2
|
-
|
|
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(
|
|
25
|
-
|
|
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
|
|
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(
|
|
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)",
|
package/src/replace-response.ts
CHANGED
|
@@ -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,
|
|
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);
|
package/src/replace-undo.ts
CHANGED
|
@@ -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,
|