pi-hashline-edit-pro 6.4.2 → 6.4.3
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 +2 -3
- package/index.ts +2 -3
- package/package.json +1 -1
- package/src/anchor-registry.ts +23 -1
- package/src/edit-common.ts +1 -1
- package/src/hash-store.ts +4 -4
- package/src/payload-contract.ts +1 -1
- package/src/replace-match.ts +0 -5
- package/tool-prompts/replace-match-guidelines.md +3 -3
- package/tool-prompts/replace-match.md +1 -1
package/README.md
CHANGED
|
@@ -219,13 +219,13 @@ After a successful edit, the diff is capped at 50KB. A row over 50KB is shown as
|
|
|
219
219
|
|
|
220
220
|
### replace_match
|
|
221
221
|
|
|
222
|
-
`replace_match` changes part of a line (or a range of lines) without retyping the rest. `replace_from` and `replace_to` are bare anchors marking the first and last line of the range; use the same anchor for a single line. `old_string` is the exact text to find inside that range, and `new_string` replaces every occurrence of it; every other character stays untouched. Send `old_string` as only the fragment that changes — never include text that `new_string` leaves unchanged — and use `replace` with the same anchors when the change covers whole lines or the whole range. That makes it the tool for a change the request quotes as a substring: a whole-line `replace` has to reproduce the rest of the line, so a slipped character becomes a wrong byte, while `replace_match` leaves everything the request did not name untouched. It is enabled by default; turn Replace match off in `/hashline-config` to remove the tool.
|
|
222
|
+
`replace_match` changes part of a line (or a range of lines) without retyping the rest. `replace_from` and `replace_to` are bare anchors marking the first and last line of the range; use the same anchor for a single line. `old_string` is the exact text to find inside that range, and `new_string` replaces every occurrence of it; every other character stays untouched. Send `old_string` as only the fragment that changes — never include text that `new_string` leaves unchanged — and use `replace` with the same anchors when the change covers whole lines or the whole range; if `old_string` is everything on the line except indentation and trailing punctuation, that is a whole-line change. That makes it the tool for a change the request quotes as a substring: a whole-line `replace` has to reproduce the rest of the line, so a slipped character becomes a wrong byte, while `replace_match` leaves everything the request did not name untouched. It is enabled by default; turn Replace match off in `/hashline-config` to remove the tool.
|
|
223
223
|
|
|
224
224
|
`remove_from` and `remove_to` are accepted as aliases for `replace_from` and `replace_to`.
|
|
225
225
|
|
|
226
226
|
`old_string` is matched against the range's text (LF line breaks, no final terminator) and every non-overlapping occurrence is replaced, left to right. A missing match is refused with `[E_SUBSTRING_NOT_FOUND]` and the current `anchor│content` rows, so the retry needs no `read`. The two boundary anchors are verified against what was last shown; lines strictly inside the range are matched against the file as it currently stands on disk.
|
|
227
227
|
|
|
228
|
-
`replace_match` reports `[W_REPLACE_MATCH_WHOLE_RANGE]` when `old_string` is the entire selected range, `[W_REPLACE_MATCH_RETYPE]` when `old_string` retypes a long run of text that `new_string` leaves unchanged
|
|
228
|
+
`replace_match` reports `[W_REPLACE_MATCH_WHOLE_RANGE]` when `old_string` is the entire selected range, and `[W_REPLACE_MATCH_RETYPE]` when `old_string` retypes a long run of text that `new_string` leaves unchanged. Both are refused in strict-input mode with `[E_BAD_SHAPE]`.
|
|
229
229
|
|
|
230
230
|
In a same-message batch it joins the other calls on its file: the batch validates everything against the pre-batch state and commits once, with one undo. A missing `old_string` fails that call on its own and is left out of the batch; the rest of the batch still commits. The post-edit diff carries fresh anchors, and the edit is undoable with `undo_last_change`.
|
|
231
231
|
|
|
@@ -465,7 +465,6 @@ Full reference:
|
|
|
465
465
|
| `[W_BARE_HASH_PREFIX]` | A `text` line starts with an `anchor│` prefix. The prefix is stripped automatically with a warning. |
|
|
466
466
|
| `[W_REPLACE_MATCH_WHOLE_RANGE]` | `replace_match` `old_string` is the entire selected range. The anchors already identify it; use `replace` with the same anchors, or send only the changed fragment. Refused in strict-input mode. |
|
|
467
467
|
| `[W_REPLACE_MATCH_RETYPE]` | `replace_match` `old_string` retypes a long run of text that `new_string` leaves unchanged. Send only the fragment that changes; refused in strict-input mode. |
|
|
468
|
-
| `[W_REPLACE_MATCH_MULTI]` | `replace_match` `old_string` matched more than one occurrence in the range and every one was replaced. Narrow the range or send a longer fragment if you meant only one. |
|
|
469
468
|
| `[W_ANCHOR_RECLAIMED]` | The session's anchor quota was exhausted, so all anchors of the listed files (the least recently read or edited) were freed to make room. Read those files again before editing them. |
|
|
470
469
|
| `[H_LITERAL_ESCAPE]` | A payload field contains literal escaped text such as `\uXXXX`, `\n`, `\t`, `\r`, or `\"` (one hint per distinct escape, up to three). The file receives those backslash characters as written, because escapes in the tool call are decoded once when the call is parsed (`\uXXXX` → the character), so a doubled escape (`\\uXXXX`) lands literally. The hint is one line: `text: "\u200b" written as literal text (Kq3f│ col 31); resend with U+200B if unintended.` names the field, the escape, and up to three affected anchors with their columns, then the fix; when more than three rows carry it, it gives `on 10 rows; undo_last_change + resend with U+200B if unintended.` instead of the anchor list. |
|
|
471
470
|
| `[H_UNICODE_LOST]` | The new text is missing an invisible or look-alike character (for example `U+200B`, `U+2060`, `U+00A0`, or a smart quote) that a replaced row has, or that a context row sharing a long run with an inserted line has. The edit applied as sent; the one-line hint names the character, its column, and the reference anchor, for example `[H_UNICODE_LOST] U+2060 missing at col 31; Kq3f│ has it; resend with U+2060 if unintended.` When another look-alike character (including `U+FFFD`) takes its place, the hint reports the substitute with `[H_UNICODE_SWAPPED]` wording. |
|
package/index.ts
CHANGED
|
@@ -33,7 +33,7 @@ import {
|
|
|
33
33
|
setDisableOnModelsFromText,
|
|
34
34
|
} from "./src/config";
|
|
35
35
|
import { loadHashStore, pruneMissing } from "./src/hash-store";
|
|
36
|
-
import { initRegistry, gcRegistrySidecars, clearRegistry, freeAnchors, sessionKeyFor, withAnchorSession, releaseRegistrySession, formatAnchorReclaimNotice, takeReclaimedPaths } from "./src/anchor-registry";
|
|
36
|
+
import { initRegistry, gcRegistrySidecars, clearRegistry, freeAnchors, freeAnchorsForPaths, sessionKeyFor, withAnchorSession, releaseRegistrySession, formatAnchorReclaimNotice, takeReclaimedPaths } from "./src/anchor-registry";
|
|
37
37
|
import { serveRows } from "./src/served";
|
|
38
38
|
import { finalizeTurn, planAssistantMessage } from "./src/batch";
|
|
39
39
|
import { currentEditFlags } from "./src/edit-common";
|
|
@@ -154,8 +154,7 @@ export default function (pi: ExtensionAPI): void {
|
|
|
154
154
|
await initHasher();
|
|
155
155
|
loadHashStore()
|
|
156
156
|
.then(async store => {
|
|
157
|
-
|
|
158
|
-
for (const path of missing) freeAnchors(path);
|
|
157
|
+
freeAnchorsForPaths(await pruneMissing(store));
|
|
159
158
|
})
|
|
160
159
|
.catch(err => {
|
|
161
160
|
console.error("Failed to load hash store:", err);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-hashline-edit-pro",
|
|
3
|
-
"version": "6.4.
|
|
3
|
+
"version": "6.4.3",
|
|
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",
|
package/src/anchor-registry.ts
CHANGED
|
@@ -512,7 +512,7 @@ function hasOwnedPath(state: SessionState, path: string): boolean {
|
|
|
512
512
|
|
|
513
513
|
function evictPath(state: SessionState, path: string): string[] {
|
|
514
514
|
const anchors: string[] = [];
|
|
515
|
-
for (const [anchor, entry] of
|
|
515
|
+
for (const [anchor, entry] of state.owned) {
|
|
516
516
|
if (entry.path === path) {
|
|
517
517
|
anchors.push(anchor);
|
|
518
518
|
state.owned.delete(anchor);
|
|
@@ -617,6 +617,28 @@ export function freeAnchors(path: string, anchors?: string[]): void {
|
|
|
617
617
|
}
|
|
618
618
|
}
|
|
619
619
|
|
|
620
|
+
export function freeAnchorsForPaths(paths: Iterable<string>): void {
|
|
621
|
+
const state = current();
|
|
622
|
+
if (!state) return;
|
|
623
|
+
const targets = new Set(paths);
|
|
624
|
+
if (targets.size === 0) return;
|
|
625
|
+
const freedByPath = new Map<string, string[]>();
|
|
626
|
+
for (const [anchor, entry] of state.owned) {
|
|
627
|
+
if (!targets.has(entry.path)) continue;
|
|
628
|
+
state.owned.delete(anchor);
|
|
629
|
+
const freed = freedByPath.get(entry.path);
|
|
630
|
+
if (freed === undefined) freedByPath.set(entry.path, [anchor]);
|
|
631
|
+
else freed.push(anchor);
|
|
632
|
+
}
|
|
633
|
+
for (const path of targets) {
|
|
634
|
+
state.served.delete(path);
|
|
635
|
+
state.lastUsedAt.delete(path);
|
|
636
|
+
}
|
|
637
|
+
for (const [path, anchors] of freedByPath) {
|
|
638
|
+
appendEvent({ kind: "free", path, anchors });
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
|
|
620
642
|
export function clearRegistry(): void {
|
|
621
643
|
const state = current();
|
|
622
644
|
if (!state) return;
|
package/src/edit-common.ts
CHANGED
|
@@ -47,7 +47,7 @@ export async function currentEditFlags(codemode = false): Promise<EditToolFlags>
|
|
|
47
47
|
|
|
48
48
|
function preferenceGuideline(flags: EditToolFlags): string {
|
|
49
49
|
const tools = gatedEditOps(["read", "replace", "replace_match", "insert", "copy", "move", "undo_last_change"], flags);
|
|
50
|
-
return `
|
|
50
|
+
return `Remember to use the hashline tools for every file read and edit: ${joinOps(tools, { backtick: true })}.`;
|
|
51
51
|
}
|
|
52
52
|
|
|
53
53
|
const SHARED_EDIT_OPS = ["replace", "replace_match", "insert", "copy", "move"];
|
package/src/hash-store.ts
CHANGED
|
@@ -128,7 +128,7 @@ function requireEngine(): EngineSelection {
|
|
|
128
128
|
interface Prepared {
|
|
129
129
|
get: (...params: SqlParams) => Record<string, unknown> | undefined;
|
|
130
130
|
getState: (...params: SqlParams) => Record<string, unknown> | undefined;
|
|
131
|
-
|
|
131
|
+
snapshotPaths: (...params: SqlParams) => Record<string, unknown>[];
|
|
132
132
|
deleteOne: (...params: SqlParams) => void;
|
|
133
133
|
upsert: (...params: SqlParams) => void;
|
|
134
134
|
undoUpsert: (...params: SqlParams) => void;
|
|
@@ -226,7 +226,7 @@ function buildStore(db: RawDb): { db: RawDb; stmts: Prepared } {
|
|
|
226
226
|
).run(String(HASH_STORE_VERSION));
|
|
227
227
|
const getStmt = db.prepare("SELECT hashes FROM snapshots WHERE path = ? AND checksum = ? AND line_count = ?");
|
|
228
228
|
const getStateStmt = db.prepare("SELECT checksum, hashes, line_checksums FROM snapshots WHERE path = ?");
|
|
229
|
-
const
|
|
229
|
+
const snapshotPathsStmt = db.prepare("SELECT path FROM snapshots");
|
|
230
230
|
const delStmt = db.prepare("DELETE FROM snapshots WHERE path = ?");
|
|
231
231
|
const upsertStmt = db.prepare(
|
|
232
232
|
"INSERT INTO snapshots (path, checksum, line_count, hashes, line_checksums, updated_at) VALUES (?, ?, ?, ?, ?, ?) " +
|
|
@@ -245,7 +245,7 @@ function buildStore(db: RawDb): { db: RawDb; stmts: Prepared } {
|
|
|
245
245
|
const stmts: Prepared = {
|
|
246
246
|
get: (...params) => getStmt.get(...params) as Record<string, unknown> | undefined,
|
|
247
247
|
getState: (...params) => getStateStmt.get(...params) as Record<string, unknown> | undefined,
|
|
248
|
-
|
|
248
|
+
snapshotPaths: (...params) => withBusyRetry(() => snapshotPathsStmt.all(...params) as Record<string, unknown>[]),
|
|
249
249
|
deleteOne: retriedWrite(delStmt),
|
|
250
250
|
upsert: retriedWrite(upsertStmt),
|
|
251
251
|
undoUpsert: retriedWrite(undoUpsertStmt),
|
|
@@ -640,7 +640,7 @@ async function statMissing(rows: { path: string }[]): Promise<string[]> {
|
|
|
640
640
|
}
|
|
641
641
|
|
|
642
642
|
export async function pruneMissing(store: HashStore): Promise<string[]> {
|
|
643
|
-
const rows = store.stmts.
|
|
643
|
+
const rows = store.stmts.snapshotPaths() as { path: string }[];
|
|
644
644
|
const missing = await statMissing(rows);
|
|
645
645
|
if (missing.length === 0) return [];
|
|
646
646
|
withStore(() => {
|
package/src/payload-contract.ts
CHANGED
|
@@ -234,7 +234,7 @@ const replaceMatchToSchema = Type.String({
|
|
|
234
234
|
});
|
|
235
235
|
const replaceMatchOldStringSchema = Type.String({
|
|
236
236
|
description:
|
|
237
|
-
"The exact text to find inside the selected line(s), copied from the served row: only the fragment that changes, never text that `new_string` leaves unchanged. Every occurrence inside the range is replaced; the text around each match is left untouched. Matching uses LF line breaks and excludes the last line's terminator.
|
|
237
|
+
"The exact text to find inside the selected line(s), copied from the served row: only the fragment that changes, never text that `new_string` leaves unchanged. If `old_string` is everything on the line except indentation and trailing punctuation, that is a whole-line match: use `replace` instead. Every occurrence inside the range is replaced; the text around each match is left untouched. Matching uses LF line breaks and excludes the last line's terminator.",
|
|
238
238
|
});
|
|
239
239
|
const replaceMatchNewStringSchema = Type.String({
|
|
240
240
|
description:
|
package/src/replace-match.ts
CHANGED
|
@@ -150,10 +150,6 @@ function wholeRangeWarning(oldText: string, rangeText: string, lineCount: number
|
|
|
150
150
|
return `[W_REPLACE_MATCH_WHOLE_RANGE] "old_string" is the entire selected range (${formatMatchRange(start, end)}); the anchors already identify it. Use replace with remove_from "${startHash}", remove_to "${endHash}" and the new text, or send only the fragment that changes.`;
|
|
151
151
|
}
|
|
152
152
|
|
|
153
|
-
function multiMatchWarning(count: number, start: number, end: number, displayPath: string): string {
|
|
154
|
-
return `[W_REPLACE_MATCH_MULTI] "old_string" matched ${count} occurrences in ${formatMatchRange(start, end)} of ${displayPath}; every one was replaced. Narrow the range or send a longer fragment if you meant only one.`;
|
|
155
|
-
}
|
|
156
|
-
|
|
157
153
|
function describeChar(char: string | undefined): string {
|
|
158
154
|
if (char === undefined) return "the end of the text";
|
|
159
155
|
const codePoint = char.codePointAt(0)!;
|
|
@@ -225,7 +221,6 @@ export function buildReplaceMatchEdit(
|
|
|
225
221
|
const wholeRange = wholeRangeWarning(oldText, rangeText, rangeLines.length, start, end, startRef.hash, endRef.hash);
|
|
226
222
|
if (wholeRange !== undefined) warnings.push(wholeRange);
|
|
227
223
|
}
|
|
228
|
-
if (count > 1) warnings.push(multiMatchWarning(count, start, end, displayPath));
|
|
229
224
|
return {
|
|
230
225
|
editParams: {
|
|
231
226
|
remove_from: startRef.hash,
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
- `replace_match`: `old_string` is only the fragment
|
|
2
|
-
- `replace_match`:
|
|
3
|
-
- `replace_match`: when the change covers whole lines or the whole range, use `replace` with the same anchors and the
|
|
1
|
+
- `replace_match`: `old_string` is only the fragment that changes — never text that `new_string` leaves unchanged; every occurrence inside the range is replaced, so scope the range when only some lines should change.
|
|
2
|
+
- `replace_match`: if `old_string` is everything on the line except indentation and trailing punctuation, that match is the whole line — use `replace`. A shorter fragment repeated on several lines (a value, a name) is the intended multi-line use.
|
|
3
|
+
- `replace_match`: when the change covers whole lines or the whole range, use `replace` with the same anchors and the exact text.
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
Replace part of a line (or a range of lines) without retyping the rest. `replace_from`/`replace_to` are bare anchors from served `anchor│content` rows marking the range's first and last line (the same anchor for one line), and `old_string` is the exact text to find inside that range. Send only the
|
|
1
|
+
Replace part of a line (or a range of lines) without retyping the rest. `replace_from`/`replace_to` are bare anchors from served `anchor│content` rows marking the range's first and last line (the same anchor for one line), and `old_string` is the exact text to find inside that range. Send only the fragment that changes — never text that `new_string` leaves unchanged. If `old_string` is everything on the line except indentation and trailing punctuation, that is a whole-line rewrite: use `replace` with the same anchors. The same short fragment on several lines (a value, a name) may be replaced in one call. `remove_from`/`remove_to` are accepted as aliases.
|
|
2
2
|
|
|
3
3
|
The two boundary anchors must still match what was last shown; lines strictly inside the range are matched against the file as it is on disk.
|