pi-hashline-edit-pro 3.0.1 → 3.0.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 +4 -1
- package/package.json +1 -1
- package/prompts/insert-guidelines.md +2 -1
- package/prompts/replace-guidelines.md +3 -1
- package/src/commit.ts +10 -6
- package/src/constants.ts +2 -0
- package/src/hash-store/cache.ts +22 -0
- package/src/hash-store.ts +56 -5
- package/src/insert.ts +19 -10
- package/src/missing-path.ts +51 -0
- package/src/payload-contract.ts +6 -8
- package/src/replace-render.ts +16 -13
- package/src/replace-response.ts +44 -3
- package/src/replace-undo.ts +7 -1
- package/src/replace.ts +19 -41
- package/src/served.ts +2 -0
package/README.md
CHANGED
|
@@ -91,6 +91,7 @@ One edit per call, with `remove_from`, `remove_to`, and `replacement_lines` at t
|
|
|
91
91
|
|
|
92
92
|
| Field | Description |
|
|
93
93
|
| --- | --- |
|
|
94
|
+
| `path` | Path to edit; always provide it explicitly — it is only auto-resolved from the anchors as a fallback. |
|
|
94
95
|
| `remove_from` | 4-char anchor from `read` output marking the FIRST line to remove (inclusive). |
|
|
95
96
|
| `remove_to` | 4-char anchor from `read` output marking the LAST line to remove (inclusive). |
|
|
96
97
|
| `replacement_lines` | Replacement lines as an array of strings, one element per line. Mirror the removed lines exactly, blank lines included: use `[]` to delete the range, `[""]` for a single blank line, `["a", ""]` for a line followed by a blank line, and `["", ""]` for two blank lines. Do not embed `\n` inside an element: each element is exactly one line. |
|
|
@@ -98,7 +99,7 @@ One edit per call, with `remove_from`, `remove_to`, and `replacement_lines` at t
|
|
|
98
99
|
Notes:
|
|
99
100
|
|
|
100
101
|
- The request is checked before any file I/O, so a bad request never touches the file.
|
|
101
|
-
- Common copy-paste slips are fixed automatically and reported: a leftover `anchor│` prefix (including a truncated or expanded prefix of up to 6 characters, e.g. `L3│` or `ab12│`) in `replacement_lines` or `remove_from`/`remove_to`, diff-preview rows pasted into the replacement, a reversed range, or a boundary line pasted twice. New lines that re-include a block adjacent to the range are stripped automatically when that block is unique in the file. The whole run is stripped as one unit (including repeated structural lines like `}`), so re-including an unchanged block next to the range never duplicates it. A missing `path` is resolved from the anchors when they
|
|
102
|
+
- Common copy-paste slips are fixed automatically and reported: a leftover `anchor│` prefix (including a truncated or expanded prefix of up to 6 characters, e.g. `L3│` or `ab12│`) in `replacement_lines` or `remove_from`/`remove_to`, diff-preview rows pasted into the replacement, a reversed range, or a boundary line pasted twice. New lines that re-include a block adjacent to the range are stripped automatically when that block is unique in the file. The whole run is stripped as one unit (including repeated structural lines like `}`), so re-including an unchanged block next to the range never duplicates it. A missing `path` is resolved from the anchors when they identify known files in the hash store (reported as a warning); when the anchors match multiple known files the most recently touched file is picked, with the candidates named. `file_path` works as an alias for `path` in all five tools.
|
|
102
103
|
- An edit that produces identical content reports `No changes made` and leaves the anchors alone. When such a noop happened because a boundary anti-duplication cut removed lines from the replacement (the cut blocked a line that duplicates the block next to the range from being added), the same replacement sent once more runs with the edge anti-duplication turned off for that single call and is applied literally. The duplicated lines are kept, and the result carries a `[E_BOUNDARY_BYPASS]` notice. The pending bypass is per file and keyed to that payload; copied `anchor│` prefixes, diff markers, and stray whitespace in the resend are normalized before matching, so a copy-paste resend still hits it. Any applied edit clears it, and a successful `write` also clears it.
|
|
103
104
|
- Every line in the removed range must match what was last shown to you. The extension records the `anchor│content` rows it serves (`read` output, the auto-read block after `write`, the `+anchor│`/` anchor│` rows of post-edit diffs (replace, insert, and undo), the current-range rows of `[E_RANGE_STALE]` feedback, and the context rows of stale/ambiguous-anchor feedback) and verifies the whole range against that record before writing. If an interior line changed on disk since it was shown (external editor, formatter-on-save, code generation) or was never shown, the edit is refused with `[E_RANGE_STALE]` and the current range is returned with fresh anchors, so the retry needs no `read`. Edits outside the served record are only possible for files that were never read (for example right after a `write` with auto-read disabled); once the file has been served, every replaced line must have been shown.
|
|
104
105
|
- After a successful edit you get the post-edit diff with fresh anchors, so you can keep editing without re-reading. The diff is capped at 50KB: a row longer than 50KB is shown as a marker that keeps the row's anchor (so the line stays editable via the diff), and when the total cap is hit the diff ends with a truncation note. Only the rows shown in the capped diff are recorded as served. The same caps apply to the `insert` and `undo_last_change` diffs, to the interactive previews, and to `details.patch` (which is flagged with `details.patchTruncated` when it was cut and can no longer be applied as-is).
|
|
@@ -119,12 +120,14 @@ Notes:
|
|
|
119
120
|
|
|
120
121
|
| Field | Description |
|
|
121
122
|
| --- | --- |
|
|
123
|
+
| `path` | Path to edit; always provide it explicitly — it is only auto-resolved from the anchors as a fallback. |
|
|
122
124
|
| `anchor` | 4-char anchor from `read` output marking the line next to which the lines go (inclusive; the line is preserved). A pasted diff row like `+Hasu│x` or an `anchor│` prefix is stripped automatically with a warning. |
|
|
123
125
|
| `direction` | `"after"` to insert below the anchor line, `"before"` to insert above it. |
|
|
124
126
|
| `lines` | Lines to insert as an array of strings, one element per line. Mirror `replacement_lines` semantics: use `[""]` for a blank line and do not embed `\n` inside an element. The anchor line is never part of `lines`. |
|
|
125
127
|
|
|
126
128
|
Notes:
|
|
127
129
|
|
|
130
|
+
- A missing `path` is resolved from the anchor the same way as `replace` (reported as a warning); when the anchor matches multiple known files the most recently touched file is picked, with the candidates named.
|
|
128
131
|
- The anchor line must have been shown to you (read output, a post-edit diff row, anchor_grep output, or stale-range feedback). The same verification as `replace` applies: a stale or unshown anchor is rejected with `[E_STALE_ANCHOR]`, `[E_AMBIGUOUS_ANCHOR]`, or `[E_RANGE_STALE]` and the retry needs no `read`.
|
|
129
132
|
- Lines are applied literally: nothing is removed, and a line that duplicates its neighbor is kept. `replace`'s boundary anti-duplication never runs for `insert`.
|
|
130
133
|
- To seed an empty file, read it and insert after the `anchor│` empty-line row.
|
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.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",
|
|
@@ -1 +1,2 @@
|
|
|
1
|
-
- `insert`: the anchor must have been shown by `read`, a post-edit diff (`+anchor│`/` anchor│`), or any served `anchor│content` row. Empty file: `read` shows one `anchor│` row — insert `after` it.
|
|
1
|
+
- `insert`: the anchor must have been shown by `read`, a post-edit diff (`+anchor│`/` anchor│`), or any served `anchor│content` row. Empty file: `read` shows one `anchor│` row — insert `after` it.
|
|
2
|
+
- `insert`: always include explicit `path`/`file_path`, even when the file can be inferred from anchors. Do not rely on anchor-based path inference.
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
- `replace`: use the same anchor for `remove_from` and `remove_to` to change one line.
|
|
2
|
+
- `replace`: always include explicit `path`/`file_path`, even when the file can be inferred from anchors. Do not rely on anchor-based path inference.
|
|
2
3
|
- `replace`: `replacement_lines` takes bare lines without `│`; `[""]` is one blank line; pasted `anchor│` prefixes are stripped automatically.
|
|
3
4
|
- `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.
|
|
5
|
+
- `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.
|
|
6
|
+
- `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/hash-store/cache.ts
CHANGED
|
@@ -16,3 +16,25 @@ export function cacheSnapshot(path: string, checksum: string, lineCount: number,
|
|
|
16
16
|
if (oldest !== undefined) snapshotCache.delete(oldest);
|
|
17
17
|
}
|
|
18
18
|
}
|
|
19
|
+
|
|
20
|
+
let sessionSeq = 0;
|
|
21
|
+
export const sessionTouched = new Map<string, number>();
|
|
22
|
+
|
|
23
|
+
export function touchSession(path: string): void {
|
|
24
|
+
sessionSeq += 1;
|
|
25
|
+
sessionTouched.delete(path);
|
|
26
|
+
sessionTouched.set(path, sessionSeq);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function sessionRank(path: string): number | undefined {
|
|
30
|
+
return sessionTouched.get(path);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function clearSession(): void {
|
|
34
|
+
sessionTouched.clear();
|
|
35
|
+
sessionSeq = 0;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function forgetSession(path: string): void {
|
|
39
|
+
sessionTouched.delete(path);
|
|
40
|
+
}
|
package/src/hash-store.ts
CHANGED
|
@@ -20,11 +20,7 @@ import {
|
|
|
20
20
|
retriedWrite,
|
|
21
21
|
openDbWithBusyRetryAsync,
|
|
22
22
|
} from "./hash-store/retry";
|
|
23
|
-
import {
|
|
24
|
-
snapshotCache,
|
|
25
|
-
cacheSnapshot,
|
|
26
|
-
SNAPSHOT_CACHE_LIMIT,
|
|
27
|
-
} from "./hash-store/cache";
|
|
23
|
+
import { snapshotCache, cacheSnapshot, SNAPSHOT_CACHE_LIMIT, touchSession, sessionRank, clearSession, forgetSession } from "./hash-store/cache";
|
|
28
24
|
|
|
29
25
|
export { isValidHashList, isValidServedMap, parseHashList, parseServedMap, parseStoredHashes, parseStoredServed, isCorruptionError };
|
|
30
26
|
export { SNAPSHOT_CACHE_LIMIT };
|
|
@@ -99,6 +95,9 @@ interface Prepared {
|
|
|
99
95
|
servedGet: (...params: SqlParams) => Record<string, unknown> | undefined;
|
|
100
96
|
servedUpsert: (...params: SqlParams) => void;
|
|
101
97
|
servedDelete: (...params: SqlParams) => void;
|
|
98
|
+
snapshotTime: (...params: SqlParams) => Record<string, unknown> | undefined;
|
|
99
|
+
undoTime: (...params: SqlParams) => Record<string, unknown> | undefined;
|
|
100
|
+
servedTime: (...params: SqlParams) => Record<string, unknown> | undefined;
|
|
102
101
|
}
|
|
103
102
|
|
|
104
103
|
export interface HashStore {
|
|
@@ -199,6 +198,9 @@ function buildStore(db: RawDb): { db: RawDb; stmts: Prepared } {
|
|
|
199
198
|
"ON CONFLICT(path) DO UPDATE SET hashes = excluded.hashes, updated_at = excluded.updated_at"
|
|
200
199
|
);
|
|
201
200
|
const servedDelStmt = db.prepare("DELETE FROM served WHERE path = ?");
|
|
201
|
+
const snapshotTimeStmt = db.prepare("SELECT updated_at FROM snapshots WHERE path = ?");
|
|
202
|
+
const undoTimeStmt = db.prepare("SELECT updated_at FROM undo WHERE path = ?");
|
|
203
|
+
const servedTimeStmt = db.prepare("SELECT updated_at FROM served WHERE path = ?");
|
|
202
204
|
const stmts: Prepared = {
|
|
203
205
|
get: (...params) => getStmt.get(...params) as Record<string, unknown> | undefined,
|
|
204
206
|
allPaths: (...params) => allStmt.all(...params) as Record<string, unknown>[],
|
|
@@ -212,6 +214,9 @@ function buildStore(db: RawDb): { db: RawDb; stmts: Prepared } {
|
|
|
212
214
|
servedGet: (...params) => servedGetStmt.get(...params) as Record<string, unknown> | undefined,
|
|
213
215
|
servedUpsert: retriedWrite(servedUpsertStmt),
|
|
214
216
|
servedDelete: retriedWrite(servedDelStmt),
|
|
217
|
+
snapshotTime: (...params) => snapshotTimeStmt.get(...params) as Record<string, unknown> | undefined,
|
|
218
|
+
undoTime: (...params) => undoTimeStmt.get(...params) as Record<string, unknown> | undefined,
|
|
219
|
+
servedTime: (...params) => servedTimeStmt.get(...params) as Record<string, unknown> | undefined,
|
|
215
220
|
};
|
|
216
221
|
return { db, stmts };
|
|
217
222
|
}
|
|
@@ -345,6 +350,7 @@ export function shutdownHashStore(): void {
|
|
|
345
350
|
cachedDb = null;
|
|
346
351
|
}
|
|
347
352
|
snapshotCache.clear();
|
|
353
|
+
clearSession();
|
|
348
354
|
}
|
|
349
355
|
|
|
350
356
|
export function withStore(fn: () => void): void {
|
|
@@ -462,6 +468,7 @@ export function upsertSnapshot(
|
|
|
462
468
|
): void {
|
|
463
469
|
store.stmts.upsert(path, checksum, lineCount, JSON.stringify(hashes), Date.now());
|
|
464
470
|
cacheSnapshot(path, checksum, lineCount, hashes);
|
|
471
|
+
touchSession(path);
|
|
465
472
|
}
|
|
466
473
|
export function persistSnapshot(
|
|
467
474
|
store: HashStore,
|
|
@@ -482,6 +489,7 @@ export function upsertUndo(store: HashStore, path: string, entry: UndoRecord): v
|
|
|
482
489
|
entry.resultContent,
|
|
483
490
|
Date.now(),
|
|
484
491
|
);
|
|
492
|
+
touchSession(path);
|
|
485
493
|
}
|
|
486
494
|
|
|
487
495
|
export function getUndoEntry(store: HashStore, path: string): UndoRecord | undefined {
|
|
@@ -540,6 +548,7 @@ export async function pruneMissing(store: HashStore): Promise<void> {
|
|
|
540
548
|
}
|
|
541
549
|
});
|
|
542
550
|
for (const path of missing) snapshotCache.delete(path);
|
|
551
|
+
for (const path of missing) forgetSession(path);
|
|
543
552
|
}
|
|
544
553
|
|
|
545
554
|
function matchPathsByHashes(
|
|
@@ -603,3 +612,45 @@ export function findSnapshotPaths(store: HashStore, hashes: string[]): string[]
|
|
|
603
612
|
export function findServedPaths(store: HashStore, hashes: string[]): string[] {
|
|
604
613
|
return matchPathsByServed(store.stmts.allServed() as { path: string; hashes: string }[], hashes);
|
|
605
614
|
}
|
|
615
|
+
|
|
616
|
+
export function pathActivity(store: HashStore, path: string): number {
|
|
617
|
+
let activity = 0;
|
|
618
|
+
try {
|
|
619
|
+
const snap = withBusyRetry(() => store.stmts.snapshotTime(path)) as { updated_at?: unknown } | undefined;
|
|
620
|
+
const snapTime = typeof snap?.updated_at === "number" ? snap.updated_at : 0;
|
|
621
|
+
if (snapTime > activity) activity = snapTime;
|
|
622
|
+
} catch {
|
|
623
|
+
}
|
|
624
|
+
try {
|
|
625
|
+
const undo = withBusyRetry(() => store.stmts.undoTime(path)) as { updated_at?: unknown } | undefined;
|
|
626
|
+
const undoTime = typeof undo?.updated_at === "number" ? undo.updated_at : 0;
|
|
627
|
+
if (undoTime > activity) activity = undoTime;
|
|
628
|
+
} catch {
|
|
629
|
+
}
|
|
630
|
+
try {
|
|
631
|
+
const served = withBusyRetry(() => store.stmts.servedTime(path)) as { updated_at?: unknown } | undefined;
|
|
632
|
+
const servedTime = typeof served?.updated_at === "number" ? served.updated_at : 0;
|
|
633
|
+
if (servedTime > activity) activity = servedTime;
|
|
634
|
+
} catch {
|
|
635
|
+
}
|
|
636
|
+
return activity;
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
export function pickRecentPath(store: HashStore, candidates: string[]): string {
|
|
640
|
+
let best = candidates[0]!;
|
|
641
|
+
let bestSession = -1;
|
|
642
|
+
let bestActivity = -1;
|
|
643
|
+
for (const candidate of candidates) {
|
|
644
|
+
const rank = sessionRank(candidate) ?? -1;
|
|
645
|
+
let activity = -1;
|
|
646
|
+
if (rank < 0) {
|
|
647
|
+
activity = pathActivity(store, candidate);
|
|
648
|
+
}
|
|
649
|
+
if (rank > bestSession || (rank === bestSession && activity > bestActivity)) {
|
|
650
|
+
best = candidate;
|
|
651
|
+
bestSession = rank;
|
|
652
|
+
bestActivity = activity;
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
return best;
|
|
656
|
+
}
|
package/src/insert.ts
CHANGED
|
@@ -10,6 +10,7 @@ import { loadP, loadGuide } from "./prompts";
|
|
|
10
10
|
import { normReq } from "./payload-contract";
|
|
11
11
|
import { decodeStringArray, isRec, rejectUnknownFields, splitLines } from "./utils";
|
|
12
12
|
import { clearBoundaryBypass } from "./boundary-bypass";
|
|
13
|
+
import { resolveInsertPath } from "./missing-path";
|
|
13
14
|
import type { RPreview, RRState } from "./replace-render";
|
|
14
15
|
import { queuedEdit, editToolBase, editRenderCallWrapper, editRenderResultWrapper } from "./edit-common";
|
|
15
16
|
|
|
@@ -43,10 +44,12 @@ export function assertInsertReq(request: unknown): asserts request is InsertReq
|
|
|
43
44
|
|
|
44
45
|
const insertToolSchema = Type.Object(
|
|
45
46
|
{
|
|
46
|
-
path: Type.
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
47
|
+
path: Type.Optional(
|
|
48
|
+
Type.String({
|
|
49
|
+
description:
|
|
50
|
+
"Path to edit; always provide it explicitly — it is only auto-resolved from the anchors as a fallback.",
|
|
51
|
+
}),
|
|
52
|
+
),
|
|
50
53
|
anchor: Type.String({
|
|
51
54
|
description:
|
|
52
55
|
'Bare 4-char anchor from a read row like `Hasu│content`, never the content. A pasted `+Hasu│x` diff row or `anchor│` prefix is stripped with a warning. The anchor line is preserved; lines go after or before it.',
|
|
@@ -108,6 +111,10 @@ export async function insertPreview(request: unknown, cwd: string, signal?: Abor
|
|
|
108
111
|
const expanded = decodeStringArray(normalized.lines);
|
|
109
112
|
if (expanded) normalized.lines = expanded;
|
|
110
113
|
}
|
|
114
|
+
if (isRec(normalized)) {
|
|
115
|
+
const resolution = await resolveInsertPath(normalized);
|
|
116
|
+
if (resolution) normalized.path = resolution.path;
|
|
117
|
+
}
|
|
111
118
|
assertInsertReq(normalized);
|
|
112
119
|
const { ref } = parseInsertAnchor(normalized.anchor);
|
|
113
120
|
const preload = await readNormFile(normalized.path, cwd, {
|
|
@@ -138,7 +145,7 @@ function getInsertInput(args: unknown): { path?: string; anchor?: string; direct
|
|
|
138
145
|
} catch {
|
|
139
146
|
return null;
|
|
140
147
|
}
|
|
141
|
-
if (!isRec(normalized)
|
|
148
|
+
if (!isRec(normalized)) return null;
|
|
142
149
|
if (
|
|
143
150
|
typeof normalized.anchor !== "string" ||
|
|
144
151
|
(normalized.direction !== "before" && normalized.direction !== "after") ||
|
|
@@ -148,10 +155,10 @@ function getInsertInput(args: unknown): { path?: string; anchor?: string; direct
|
|
|
148
155
|
return null;
|
|
149
156
|
}
|
|
150
157
|
return {
|
|
151
|
-
path: normalized.path,
|
|
152
|
-
anchor: normalized.anchor,
|
|
153
|
-
direction: normalized.direction,
|
|
154
|
-
lines: normalized.lines,
|
|
158
|
+
...(typeof normalized.path === "string" ? { path: normalized.path } : {}),
|
|
159
|
+
anchor: normalized.anchor as string,
|
|
160
|
+
direction: normalized.direction as "before" | "after",
|
|
161
|
+
lines: normalized.lines as string[],
|
|
155
162
|
};
|
|
156
163
|
}
|
|
157
164
|
|
|
@@ -178,6 +185,8 @@ export function buildInsertToolDef(): InsertToolDef {
|
|
|
178
185
|
canonical.lines = expanded;
|
|
179
186
|
}
|
|
180
187
|
}
|
|
188
|
+
const resolution = isRec(canonical) ? await resolveInsertPath(canonical) : undefined;
|
|
189
|
+
if (resolution && isRec(canonical)) canonical.path = resolution.path;
|
|
181
190
|
assertInsertReq(canonical);
|
|
182
191
|
const req = canonical;
|
|
183
192
|
const path = req.path;
|
|
@@ -203,7 +212,7 @@ export function buildInsertToolDef(): InsertToolDef {
|
|
|
203
212
|
verb: "inserted",
|
|
204
213
|
noopNoun: "Insertion",
|
|
205
214
|
foldedAnchorLines: anchorLine === undefined ? 0 : 1,
|
|
206
|
-
prefixWarnings: [...anchorWarnings, ...insertWarnings],
|
|
215
|
+
prefixWarnings: [...(resolution ? [resolution.warning] : []), ...anchorWarnings, ...insertWarnings],
|
|
207
216
|
onApplied: () => clearBoundaryBypass(mutationTargetPath),
|
|
208
217
|
});
|
|
209
218
|
});
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { loadHashStore, findSnapshotPaths, findServedPaths, pickRecentPath } from "./hash-store";
|
|
2
|
+
import { parseHashRef } from "./hashline/parse";
|
|
3
|
+
import { stripAnchorRow } from "./hashline/resolve";
|
|
4
|
+
|
|
5
|
+
export async function resolvePathFromHashes(hashes: string[]): Promise<{ path: string; warning: string } | undefined> {
|
|
6
|
+
let store;
|
|
7
|
+
try {
|
|
8
|
+
store = await loadHashStore();
|
|
9
|
+
} catch {
|
|
10
|
+
return undefined;
|
|
11
|
+
}
|
|
12
|
+
const matches = [...new Set([...findSnapshotPaths(store, hashes), ...findServedPaths(store, hashes)])];
|
|
13
|
+
if (matches.length === 0) return undefined;
|
|
14
|
+
if (matches.length === 1) {
|
|
15
|
+
const single = matches[0]!;
|
|
16
|
+
return { path: single, warning: `[E_BAD_SHAPE] Missing "path" resolved to ${single}.` };
|
|
17
|
+
}
|
|
18
|
+
const sorted = [...matches].sort();
|
|
19
|
+
const picked = pickRecentPath(store, sorted);
|
|
20
|
+
return { path: picked, warning: `[E_BAD_SHAPE] Missing "path" resolved to ${picked} (picked most recent of ${sorted.length}: ${sorted.join(", ")}).` };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export async function resolveReplacePath(request: Record<string, unknown>): Promise<{ path: string; warning: string } | undefined> {
|
|
24
|
+
if (typeof request.path === "string") return undefined;
|
|
25
|
+
const from = request.remove_from;
|
|
26
|
+
const to = request.remove_to;
|
|
27
|
+
if (typeof from !== "string" || typeof to !== "string") return undefined;
|
|
28
|
+
const hashes: string[] = [];
|
|
29
|
+
for (const ref of [from, to]) {
|
|
30
|
+
try {
|
|
31
|
+
hashes.push(parseHashRef(ref).hash);
|
|
32
|
+
} catch {
|
|
33
|
+
return undefined;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return resolvePathFromHashes(hashes);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export async function resolveInsertPath(request: Record<string, unknown>): Promise<{ path: string; warning: string } | undefined> {
|
|
40
|
+
if (typeof request.path === "string") return undefined;
|
|
41
|
+
const anchor = request.anchor;
|
|
42
|
+
if (typeof anchor !== "string") return undefined;
|
|
43
|
+
let hash: string;
|
|
44
|
+
try {
|
|
45
|
+
const stripped = stripAnchorRow(anchor.trim(), "anchor entry");
|
|
46
|
+
hash = parseHashRef(stripped).hash;
|
|
47
|
+
} catch {
|
|
48
|
+
return undefined;
|
|
49
|
+
}
|
|
50
|
+
return resolvePathFromHashes([hash]);
|
|
51
|
+
}
|
package/src/payload-contract.ts
CHANGED
|
@@ -75,16 +75,14 @@ export function normReq(input: unknown): unknown {
|
|
|
75
75
|
return record;
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
-
export function getPreviewInput(args: unknown):
|
|
78
|
+
export function getPreviewInput(args: unknown): { path?: string; remove_from: string; remove_to: string; replacement_lines: string[] } | null {
|
|
79
79
|
let normalized: unknown;
|
|
80
80
|
try {
|
|
81
81
|
normalized = normReq(args);
|
|
82
82
|
} catch {
|
|
83
83
|
return null;
|
|
84
84
|
}
|
|
85
|
-
if (!isRec(normalized)
|
|
86
|
-
return null;
|
|
87
|
-
}
|
|
85
|
+
if (!isRec(normalized)) return null;
|
|
88
86
|
if (
|
|
89
87
|
typeof normalized.remove_from !== "string" ||
|
|
90
88
|
typeof normalized.remove_to !== "string" ||
|
|
@@ -94,9 +92,9 @@ export function getPreviewInput(args: unknown): ReqParams | null {
|
|
|
94
92
|
return null;
|
|
95
93
|
}
|
|
96
94
|
return {
|
|
97
|
-
path: normalized.path,
|
|
98
|
-
remove_from: normalized.remove_from,
|
|
99
|
-
remove_to: normalized.remove_to,
|
|
100
|
-
replacement_lines: normalized.replacement_lines,
|
|
95
|
+
...(typeof normalized.path === "string" ? { path: normalized.path } : {}),
|
|
96
|
+
remove_from: normalized.remove_from as string,
|
|
97
|
+
remove_to: normalized.remove_to as string,
|
|
98
|
+
replacement_lines: normalized.replacement_lines as string[],
|
|
101
99
|
};
|
|
102
100
|
}
|
package/src/replace-render.ts
CHANGED
|
@@ -12,8 +12,7 @@ export type MdTheme = Pick<
|
|
|
12
12
|
"fg" | "bold" | "italic" | "underline" | "strikethrough"
|
|
13
13
|
>;
|
|
14
14
|
|
|
15
|
-
export type RPreview = { diff: string } | { error: string };
|
|
16
|
-
|
|
15
|
+
export type RPreview = { diff: string; path?: string } | { error: string; path?: string };
|
|
17
16
|
export type RRState = {
|
|
18
17
|
argsKey?: string;
|
|
19
18
|
preview?: RPreview;
|
|
@@ -65,17 +64,21 @@ export function fmtResult(diff: string, theme: FgT): string {
|
|
|
65
64
|
}
|
|
66
65
|
|
|
67
66
|
export function fmtCall(
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
67
|
+
args: { path?: string; remove_from?: string; remove_to?: string; anchor?: string } | undefined,
|
|
68
|
+
state: RRState,
|
|
69
|
+
expanded: boolean,
|
|
70
|
+
theme: CallT,
|
|
71
|
+
toolName = "replace",
|
|
73
72
|
): string {
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
73
|
+
const previewPath = state.preview && "path" in state.preview ? state.preview.path : undefined;
|
|
74
|
+
const path = args?.path ?? previewPath;
|
|
75
|
+
const anchorFallback = typeof args?.remove_from === "string" && typeof args?.remove_to === "string" ? `${args.remove_from}→${args.remove_to}` : typeof args?.anchor === "string" ? args.anchor : undefined;
|
|
76
|
+
const pathDisplay =
|
|
77
|
+
typeof path === "string" && path.length > 0
|
|
78
|
+
? theme.fg("accent", path)
|
|
79
|
+
: typeof anchorFallback === "string" && anchorFallback.length > 0
|
|
80
|
+
? theme.fg("accent", anchorFallback)
|
|
81
|
+
: theme.fg("toolOutput", "...");
|
|
79
82
|
let text = `${theme.fg("toolTitle", theme.bold(toolName))} ${pathDisplay}`;
|
|
80
83
|
|
|
81
84
|
if (!state.preview) {
|
|
@@ -234,7 +237,7 @@ export function reuseMarkdown(context: any, content: string, theme: any): Markdo
|
|
|
234
237
|
|
|
235
238
|
export function makeRenderCall(
|
|
236
239
|
preview: (args: unknown, cwd: string, signal?: AbortSignal) => Promise<RPreview>,
|
|
237
|
-
|
|
240
|
+
options: { getInput?: (args: unknown) => { path?: string; remove_from?: string; remove_to?: string; anchor?: string } | null; toolName?: string } = {},
|
|
238
241
|
) {
|
|
239
242
|
const getInput = options.getInput ?? getPreviewInput;
|
|
240
243
|
const toolName = options.toolName ?? "replace";
|
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-undo.ts
CHANGED
|
@@ -11,7 +11,8 @@ import { genDiff, genPatch } from "./replace-diff";
|
|
|
11
11
|
import { cntDiff, errCode, makePrepareArguments, splitLines } from "./utils";
|
|
12
12
|
import { loadP, loadGuide } from "./prompts";
|
|
13
13
|
import { buildMetrics } from "./replace-response";
|
|
14
|
-
import { renderEditResult } from "./replace-render";
|
|
14
|
+
import { renderEditResult, fmtCall } from "./replace-render";
|
|
15
|
+
import { Text } from "@earendil-works/pi-tui";
|
|
15
16
|
import { changedRange, lineHashes } from "./hashline";
|
|
16
17
|
export interface UndoEntry {
|
|
17
18
|
content: string;
|
|
@@ -100,6 +101,11 @@ export function regUndo(pi: ExtensionAPI): void {
|
|
|
100
101
|
}),
|
|
101
102
|
}),
|
|
102
103
|
executionMode: "sequential",
|
|
104
|
+
renderCall(args: any, theme: any, context: any) {
|
|
105
|
+
const text = (context.lastComponent as Text | undefined) ?? new Text("", 0, 0);
|
|
106
|
+
text.setText(fmtCall(args as { path?: string } | undefined, { preview: undefined } as never, context.expanded === true, theme, "undo_last_change"));
|
|
107
|
+
return text;
|
|
108
|
+
},
|
|
103
109
|
renderResult(result, opts, theme, context) {
|
|
104
110
|
return renderEditResult(result as never, opts as { isPartial: boolean; expanded?: boolean }, theme as never, context as never);
|
|
105
111
|
},
|
package/src/replace.ts
CHANGED
|
@@ -15,7 +15,6 @@ import { type FileIdentity } from "./fs-write";
|
|
|
15
15
|
import { applyEdit,
|
|
16
16
|
lineHashes,
|
|
17
17
|
resEdit,
|
|
18
|
-
parseHashRef,
|
|
19
18
|
MAX_HASH_LINES,
|
|
20
19
|
RangeStaleError,
|
|
21
20
|
AnchorMismatchError,
|
|
@@ -23,12 +22,13 @@ import { applyEdit,
|
|
|
23
22
|
type NEdit,
|
|
24
23
|
} from "./hashline";
|
|
25
24
|
import { commitEdit } from "./commit";
|
|
26
|
-
import type
|
|
25
|
+
import { withDedupRows, type RMetrics } from "./replace-response";
|
|
27
26
|
import {
|
|
28
27
|
type RPreview,
|
|
29
28
|
type RRState,
|
|
30
29
|
} from "./replace-render";
|
|
31
|
-
import { loadHashStore,
|
|
30
|
+
import { loadHashStore, type HashStore } from "./hash-store";
|
|
31
|
+
import { resolveReplacePath } from "./missing-path";
|
|
32
32
|
import { getServed, recordServedSafe } from "./served";
|
|
33
33
|
import { noopPayloadKey, markBoundaryNoop, consumeBoundaryBypass, clearBoundaryBypass } from "./boundary-bypass";
|
|
34
34
|
import { queuedEdit, editToolBase, editRenderCallWrapper, editRenderResultWrapper } from "./edit-common";
|
|
@@ -64,44 +64,11 @@ 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
|
|
|
70
|
-
async function resolveMissingPath(
|
|
71
|
-
request: Record<string, unknown>,
|
|
72
|
-
): Promise<{ path: string; warning: string } | undefined> {
|
|
73
|
-
if (typeof request.path === "string") return undefined;
|
|
74
|
-
const from = request.remove_from;
|
|
75
|
-
const to = request.remove_to;
|
|
76
|
-
if (typeof from !== "string" || typeof to !== "string") return undefined;
|
|
77
|
-
const hashes: string[] = [];
|
|
78
|
-
for (const ref of [from, to]) {
|
|
79
|
-
try {
|
|
80
|
-
hashes.push(parseHashRef(ref).hash);
|
|
81
|
-
} catch {
|
|
82
|
-
return undefined;
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
let store: HashStore;
|
|
86
|
-
try {
|
|
87
|
-
store = await loadHashStore();
|
|
88
|
-
} catch {
|
|
89
|
-
return undefined;
|
|
90
|
-
}
|
|
91
|
-
const matches = [...new Set([...findSnapshotPaths(store, hashes), ...findServedPaths(store, hashes)])];
|
|
92
|
-
if (matches.length === 1) {
|
|
93
|
-
return {
|
|
94
|
-
path: matches[0]!,
|
|
95
|
-
warning: `[E_BAD_SHAPE] Missing "path" resolved to ${matches[0]}.`,
|
|
96
|
-
};
|
|
97
|
-
}
|
|
98
|
-
if (matches.length > 1) {
|
|
99
|
-
throw new Error(
|
|
100
|
-
`[E_BAD_SHAPE] Edit request requires a non-empty "path" string; the anchors match multiple known files: ${matches.join(", ")}.`,
|
|
101
|
-
);
|
|
102
|
-
}
|
|
103
|
-
return undefined;
|
|
104
|
-
}
|
|
105
72
|
|
|
106
73
|
export interface ExecPipelineOptions {
|
|
107
74
|
accessMode?: number;
|
|
@@ -216,6 +183,9 @@ export async function execPipeline(
|
|
|
216
183
|
edit, originalHashes, isNoop, anchorResult.autoFixes?.length ?? 0,
|
|
217
184
|
);
|
|
218
185
|
|
|
186
|
+
const sortedFixes = [...(anchorResult.autoFixes ?? [])].sort((a, b) => a.removedLineIndex - b.removedLineIndex);
|
|
187
|
+
const aboveFixes = sortedFixes.filter((fix) => fix.kind === "leading" || fix.kind === "last-new-before");
|
|
188
|
+
const belowFixes = sortedFixes.filter((fix) => fix.kind === "trailing" || fix.kind === "first-new-after");
|
|
219
189
|
return {
|
|
220
190
|
path,
|
|
221
191
|
originalNormalized,
|
|
@@ -233,7 +203,9 @@ export async function execPipeline(
|
|
|
233
203
|
totalRemovedLines,
|
|
234
204
|
hadBoundaryDedup: (anchorResult.autoFixes?.length ?? 0) > 0,
|
|
235
205
|
boundaryRemovedLines: anchorResult.autoFixes?.length ?? 0,
|
|
236
|
-
boundaryRemovedLineTexts:
|
|
206
|
+
boundaryRemovedLineTexts: sortedFixes.map((fix) => fix.removedLine),
|
|
207
|
+
boundaryDedupAbove: aboveFixes.map((fix) => fix.removedLine),
|
|
208
|
+
boundaryDedupBelow: belowFixes.map((fix) => fix.removedLine),
|
|
237
209
|
identity,
|
|
238
210
|
};
|
|
239
211
|
}
|
|
@@ -242,9 +214,11 @@ export function previewFromPipe(pipe: PipelineResult): RPreview {
|
|
|
242
214
|
if (pipe.originalNormalized === pipe.result) {
|
|
243
215
|
return {
|
|
244
216
|
error: `No changes made to ${pipe.path}. The edit produced identical content.`,
|
|
217
|
+
path: pipe.path,
|
|
245
218
|
};
|
|
246
219
|
}
|
|
247
|
-
|
|
220
|
+
const base = genDiff(pipe.originalNormalized, pipe.result, 4, pipe.resultHashes, pipe.originalHashes);
|
|
221
|
+
return { diff: withDedupRows(base.diff, base.lineNumbers, pipe.boundaryDedupAbove, pipe.boundaryDedupBelow).diff, path: pipe.path };
|
|
248
222
|
}
|
|
249
223
|
export function previewError(error: unknown): RPreview {
|
|
250
224
|
return { error: error instanceof Error ? error.message : String(error) };
|
|
@@ -256,6 +230,10 @@ export async function compPreview(
|
|
|
256
230
|
): Promise<RPreview> {
|
|
257
231
|
try {
|
|
258
232
|
const normalized = normReq(request);
|
|
233
|
+
if (isRec(normalized)) {
|
|
234
|
+
const resolution = await resolveReplacePath(normalized);
|
|
235
|
+
if (resolution) normalized.path = resolution.path;
|
|
236
|
+
}
|
|
259
237
|
assertReq(normalized);
|
|
260
238
|
const pipe = await execPipeline(
|
|
261
239
|
normalized,
|
|
@@ -292,7 +270,7 @@ export function buildToolDef(): ToolDef {
|
|
|
292
270
|
renderResult: editRenderResultWrapper,
|
|
293
271
|
async execute(_toolCallId, params, signal, _onUpdate, ctx) {
|
|
294
272
|
const canonical = normReq(params);
|
|
295
|
-
const resolution = isRec(canonical) ? await
|
|
273
|
+
const resolution = isRec(canonical) ? await resolveReplacePath(canonical) : undefined;
|
|
296
274
|
if (resolution && isRec(canonical)) {
|
|
297
275
|
canonical.path = resolution.path;
|
|
298
276
|
}
|
package/src/served.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { loadHashStore, parseStoredServed, STORE_NOT_OPEN_MESSAGE, withStore, type HashStore } from "./hash-store";
|
|
2
|
+
import { touchSession } from "./hash-store/cache";
|
|
2
3
|
import { withBusyRetry } from "./hash-store/retry";
|
|
3
4
|
import { HASH_CLASS } from "./hashline/alphabet";
|
|
4
5
|
import { contentChecksum } from "./hashline/hasher";
|
|
@@ -81,6 +82,7 @@ export function recordServed(
|
|
|
81
82
|
entries: Map<string, string>,
|
|
82
83
|
scope?: ReadonlySet<string>,
|
|
83
84
|
): void {
|
|
85
|
+
touchSession(path);
|
|
84
86
|
try {
|
|
85
87
|
withStore(() => {
|
|
86
88
|
const map = computeUpdate(store, path, entries, scope);
|