pi-hashline-edit-pro 4.2.0 → 4.2.1
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 -3
- package/index.ts +7 -3
- package/package.json +1 -1
- package/src/anchor-registry.ts +5 -1
- package/src/batch.ts +3 -2
- package/src/commit.ts +2 -1
- package/src/config-ui.ts +45 -13
- package/src/config.ts +29 -1
- package/src/insert.ts +2 -2
- package/src/payload-contract.ts +2 -2
- package/src/replace-response.ts +2 -2
- package/src/replace-undo.ts +3 -2
- package/src/write-hook.ts +2 -2
package/README.md
CHANGED
|
@@ -153,7 +153,7 @@ Auto-read is enabled by default. After a successful `write`, the extension reads
|
|
|
153
153
|
|
|
154
154
|
After `replace`, `insert`, and `undo_last_change`, the result shows the post-edit diff. Inside a same-turn batch, only the batch's last call shows the combined diff, headed by a `batch:` line (`batch N:` when several files batch); earlier calls reply `In batch` (`In batch N` when several files batch). The `+anchor│` and ` anchor│` rows carry the current anchors, so follow-up edits can anchor on the diff directly. The `-anchor│` rows show removed lines with their old anchors, which are stale after the edit. When the context line next to a change is blank or whitespace-only, one more context line is shown in that direction, so the change stays anchored to visible content. Call `read` when you want the full file's anchors.
|
|
155
155
|
|
|
156
|
-
Auto-read keeps the same 50KB and 2000-line budget as `read`. Change it in `/hashline-config`; both settings persist across sessions.
|
|
156
|
+
Auto-read keeps the same 50KB and 2000-line budget as `read`. Change it in `/hashline-config`; both settings persist across sessions. The post-edit diff shows 1 surrounding line by default; change Diff context in `/hashline-config` (0-10, needs Auto-read) to show more or fewer.
|
|
157
157
|
|
|
158
158
|
## Tool result details
|
|
159
159
|
|
|
@@ -170,7 +170,7 @@ All five tools return machine-readable metadata in `details` alongside the model
|
|
|
170
170
|
|
|
171
171
|
| Command | Description |
|
|
172
172
|
| --- | --- |
|
|
173
|
-
| `/hashline-config` | Open the settings window: auto-read anchors, `anchor_grep` tool, required `path`, strict input, and boundary dedup. Persists across sessions. |
|
|
173
|
+
| `/hashline-config` | Open the settings window: auto-read anchors, diff context lines, `anchor_grep` tool, required `path`, strict input, and boundary dedup. Persists across sessions. |
|
|
174
174
|
| `/clear-anchors` | Clear the session's anchor claims. Anchors are re-claimed on the next `read`. |
|
|
175
175
|
|
|
176
176
|
Settings live in `~/.config/pi-hashline-edit-pro/config.json`, created when a setting is first changed in `/hashline-config`:
|
|
@@ -181,7 +181,8 @@ Settings live in `~/.config/pi-hashline-edit-pro/config.json`, created when a se
|
|
|
181
181
|
"anchorGrepEnabled": true,
|
|
182
182
|
"requirePath": false,
|
|
183
183
|
"strictInput": false,
|
|
184
|
-
"boundaryDedupMode": "on"
|
|
184
|
+
"boundaryDedupMode": "on",
|
|
185
|
+
"diffContextLines": 1
|
|
185
186
|
}
|
|
186
187
|
```
|
|
187
188
|
|
package/index.ts
CHANGED
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
toggleRequirePath,
|
|
18
18
|
toggleStrictInput,
|
|
19
19
|
cycleBoundaryDedupMode,
|
|
20
|
+
adjustDiffContextLines,
|
|
20
21
|
} from "./src/config";
|
|
21
22
|
import { loadHashStore, persistSnapshot, pruneMissing } from "./src/hash-store";
|
|
22
23
|
import { initRegistry, gcRegistrySidecars, clearRegistry, freeAnchors, markServed as markServedScoped } from "./src/anchor-registry";
|
|
@@ -31,6 +32,8 @@ import { loadFileKindAndText } from "./src/file-kind";
|
|
|
31
32
|
import { resolveInCwd } from "./src/fs-write";
|
|
32
33
|
import { valAccess } from "./src/validation";
|
|
33
34
|
import { splitLines } from "./src/utils";
|
|
35
|
+
import { hashSource } from "./src/hashline";
|
|
36
|
+
import { contentChecksum } from "./src/hashline/hasher";
|
|
34
37
|
|
|
35
38
|
export default function (pi: ExtensionAPI): void {
|
|
36
39
|
regRead(pi);
|
|
@@ -87,7 +90,7 @@ export default function (pi: ExtensionAPI): void {
|
|
|
87
90
|
});
|
|
88
91
|
|
|
89
92
|
pi.registerCommand("hashline-config", {
|
|
90
|
-
description: "Open the hashline settings window (auto-read, grep, path, strict input, dedup)",
|
|
93
|
+
description: "Open the hashline settings window (auto-read, diff context, grep, path, strict input, dedup)",
|
|
91
94
|
handler: async (_args, ctx) => {
|
|
92
95
|
if (!ctx.hasUI) {
|
|
93
96
|
ctx.ui.notify("/hashline-config requires interactive mode", "error");
|
|
@@ -98,8 +101,9 @@ export default function (pi: ExtensionAPI): void {
|
|
|
98
101
|
tui,
|
|
99
102
|
theme,
|
|
100
103
|
done,
|
|
101
|
-
onToggle: async (key) => {
|
|
104
|
+
onToggle: async (key, delta) => {
|
|
102
105
|
if (key === "autoRead") autoRead = await toggleAutoRead();
|
|
106
|
+
else if (key === "diffContextLines") await adjustDiffContextLines(delta ?? 1);
|
|
103
107
|
else if (key === "anchorGrepEnabled") {
|
|
104
108
|
const enabled = await toggleAnchorGrep();
|
|
105
109
|
const active = pi.getActiveTools();
|
|
@@ -177,7 +181,7 @@ export default function (pi: ExtensionAPI): void {
|
|
|
177
181
|
DEFAULT_MAX_LINES,
|
|
178
182
|
);
|
|
179
183
|
const fileLines = splitLines(normalized);
|
|
180
|
-
persistSnapshot(await loadHashStore(), absolutePath, normalized, fileHashes);
|
|
184
|
+
persistSnapshot(await loadHashStore(), absolutePath, normalized, fileHashes, fileLines.map((line) => contentChecksum(hashSource(line))));
|
|
181
185
|
markServedScoped(absolutePath, buildServedMap(fileHashes, fileLines, preview.servedHashes), new Set(fileHashes));
|
|
182
186
|
return {
|
|
183
187
|
content: [
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-hashline-edit-pro",
|
|
3
|
-
"version": "4.2.
|
|
3
|
+
"version": "4.2.1",
|
|
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
|
@@ -65,6 +65,7 @@ export function foldRegistryEvents(events: RegistryEvent[]): SessionState {
|
|
|
65
65
|
} else if (event.kind === "allocate") {
|
|
66
66
|
for (const [anchor, checksum] of event.rows) {
|
|
67
67
|
state.owned.set(anchor, { path: event.path, checksum });
|
|
68
|
+
state.everMinted.add(anchor);
|
|
68
69
|
}
|
|
69
70
|
} else if (event.kind === "free") {
|
|
70
71
|
if (event.anchors) {
|
|
@@ -541,9 +542,12 @@ export async function allocateFileAnchors(
|
|
|
541
542
|
: (() => {
|
|
542
543
|
const state = shadow ? cloneState(current()!) : current()!;
|
|
543
544
|
const reuseIndex = fingerprintIndex(state, path);
|
|
545
|
+
const reuseTaken = new Map<string, number>();
|
|
544
546
|
const anchors: string[] = checksums.map((checksum) => {
|
|
545
547
|
const candidates = reuseIndex.get(checksum) ?? [];
|
|
546
|
-
const
|
|
548
|
+
const taken = reuseTaken.get(checksum) ?? 0;
|
|
549
|
+
reuseTaken.set(checksum, taken + 1);
|
|
550
|
+
const anchor = taken < candidates.length ? candidates[taken]! : mintAnchor(state);
|
|
547
551
|
state.everMinted.add(anchor);
|
|
548
552
|
state.owned.set(anchor, { path, checksum });
|
|
549
553
|
return anchor;
|
package/src/batch.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { readFile } from "fs/promises";
|
|
2
2
|
import { constants } from "fs";
|
|
3
3
|
import { relative } from "path";
|
|
4
|
-
import { readConfig } from "./config";
|
|
4
|
+
import { readConfig, getDiffContextLines } from "./config";
|
|
5
5
|
import { throwIfStrictInput, tryResolveEditTarget } from "./edit-common";
|
|
6
6
|
import { readNormFile, safeSnapId } from "./file-reader";
|
|
7
7
|
import { resolveInCwd, writeAtomic, type FileIdentity } from "./fs-write";
|
|
@@ -441,7 +441,7 @@ function composeBatchLines(baseContent: string, pieces: BatchPiece[]): string {
|
|
|
441
441
|
const descending = [...pieces].sort((a, b) => b.start - a.start);
|
|
442
442
|
for (const piece of descending) lines.splice(piece.start - 1, piece.end - piece.start + 1, ...piece.newLines);
|
|
443
443
|
let composed = lines.join("\n");
|
|
444
|
-
if (lines.length > 0 && baseContent.endsWith("\n")) composed += "\n";
|
|
444
|
+
if (lines.length > 0 && (baseContent.endsWith("\n") || lines[lines.length - 1] === "")) composed += "\n";
|
|
445
445
|
return composed;
|
|
446
446
|
}
|
|
447
447
|
|
|
@@ -561,6 +561,7 @@ async function finishBatch(member: PlannedMember, signal?: AbortSignal): Promise
|
|
|
561
561
|
boundaryDedupBelow: [],
|
|
562
562
|
},
|
|
563
563
|
batchVerb(runtime),
|
|
564
|
+
await getDiffContextLines(),
|
|
564
565
|
);
|
|
565
566
|
changed.details.diff = `${header}\n${changed.details.diff}`;
|
|
566
567
|
changed.details.diffLineNumbers?.unshift(undefined);
|
package/src/commit.ts
CHANGED
|
@@ -4,6 +4,7 @@ import { DEDUP_ANCHOR } from "./constants";
|
|
|
4
4
|
import { HASH_SEP } from "./hashline";
|
|
5
5
|
import { buildChanged, buildNoop, type RMeta, type TResult } from "./replace-response";
|
|
6
6
|
import { saveUndo } from "./replace-undo";
|
|
7
|
+
import { getDiffContextLines } from "./config";
|
|
7
8
|
import { safeSnapId } from "./file-reader";
|
|
8
9
|
import { writeAtomic } from "./fs-write";
|
|
9
10
|
import { servedHashesFromDiff, buildServedMap } from "./served";
|
|
@@ -127,7 +128,7 @@ export async function commitEdit(pipe: PipelineResult, meta: CommitMeta): Promis
|
|
|
127
128
|
boundaryDedupAbove: pipe.boundaryDedupAbove,
|
|
128
129
|
boundaryDedupBelow: pipe.boundaryDedupBelow,
|
|
129
130
|
};
|
|
130
|
-
const changed = buildChanged(successInput, meta.verb);
|
|
131
|
+
const changed = buildChanged(successInput, meta.verb, await getDiffContextLines());
|
|
131
132
|
if (changed.details.diff) {
|
|
132
133
|
markServedScoped(
|
|
133
134
|
mutationTargetPath,
|
package/src/config-ui.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { Key, matchesKey, visibleWidth } from "@earendil-works/pi-tui";
|
|
|
2
2
|
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
3
3
|
import { readConfig, type Config } from "./config";
|
|
4
4
|
|
|
5
|
-
export type ConfigToggleKey = "autoRead" | "anchorGrepEnabled" | "requirePath" | "strictInput" | "boundaryDedupMode";
|
|
5
|
+
export type ConfigToggleKey = "autoRead" | "anchorGrepEnabled" | "requirePath" | "strictInput" | "boundaryDedupMode" | "diffContextLines";
|
|
6
6
|
|
|
7
7
|
export interface ConfigRow {
|
|
8
8
|
key: ConfigToggleKey;
|
|
@@ -11,11 +11,14 @@ export interface ConfigRow {
|
|
|
11
11
|
enabled: boolean;
|
|
12
12
|
mode?: string;
|
|
13
13
|
cycle?: string[];
|
|
14
|
+
value?: number;
|
|
15
|
+
disabled?: boolean;
|
|
14
16
|
}
|
|
15
17
|
|
|
16
18
|
export function configRows(config: Config): ConfigRow[] {
|
|
17
19
|
return [
|
|
18
20
|
{ key: "autoRead", label: "Auto-read", hint: "Anchors after write + post-edit diffs", enabled: config.autoRead !== false },
|
|
21
|
+
{ key: "diffContextLines", label: "Diff context", hint: "Surrounding lines in post-edit diffs (needs Auto-read)", enabled: config.autoRead !== false, value: config.diffContextLines ?? 1, disabled: config.autoRead === false },
|
|
19
22
|
{ key: "anchorGrepEnabled", label: "Anchor grep", hint: "anchor_grep tool (builtin grep off while on)", enabled: config.anchorGrepEnabled === true },
|
|
20
23
|
{ key: "requirePath", label: "Require path", hint: "replace + insert need path (RPC visibility)", enabled: config.requirePath === true },
|
|
21
24
|
{ key: "strictInput", label: "Strict input", hint: "Reject auto-fixable slips instead of warnings", enabled: config.strictInput === true },
|
|
@@ -34,11 +37,16 @@ function modeBox(theme: Theme, mode: string): string {
|
|
|
34
37
|
return theme.fg("success", `[${mode}]`);
|
|
35
38
|
}
|
|
36
39
|
|
|
40
|
+
function numberBox(theme: Theme, value: number, disabled?: boolean): string {
|
|
41
|
+
if (disabled) return theme.fg("dim", `[${value}]`);
|
|
42
|
+
return theme.fg("accent", `[${value}]`);
|
|
43
|
+
}
|
|
44
|
+
|
|
37
45
|
export class HashlineConfigOverlay {
|
|
38
46
|
private rows: ConfigRow[];
|
|
39
47
|
private selected = 0;
|
|
40
48
|
|
|
41
|
-
constructor(private readonly opts: { tui: { requestRender(force?: boolean): void }; theme: Theme; done: () => void; onToggle: (key: ConfigToggleKey) => Promise<void> }) {
|
|
49
|
+
constructor(private readonly opts: { tui: { requestRender(force?: boolean): void }; theme: Theme; done: () => void; onToggle: (key: ConfigToggleKey, delta?: number) => Promise<void> }) {
|
|
42
50
|
this.rows = [];
|
|
43
51
|
}
|
|
44
52
|
|
|
@@ -46,9 +54,24 @@ export class HashlineConfigOverlay {
|
|
|
46
54
|
this.rows = configRows(await readConfig());
|
|
47
55
|
}
|
|
48
56
|
|
|
57
|
+
private runToggle(row: ConfigRow, delta?: number): void {
|
|
58
|
+
this.opts.tui.requestRender(true);
|
|
59
|
+
void this.opts.onToggle(row.key, delta).then(async () => {
|
|
60
|
+
this.rows = configRows(await readConfig());
|
|
61
|
+
this.opts.tui.requestRender(true);
|
|
62
|
+
}).catch((error: unknown) => {
|
|
63
|
+
console.error("Failed to toggle hashline setting:", error);
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
49
67
|
private toggleSelected(): void {
|
|
50
68
|
const row = this.rows[this.selected];
|
|
51
|
-
if (!row) return;
|
|
69
|
+
if (!row || row.disabled) return;
|
|
70
|
+
if (row.value !== undefined) {
|
|
71
|
+
row.value += 1;
|
|
72
|
+
this.runToggle(row, 1);
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
52
75
|
if (row.cycle && row.mode !== undefined) {
|
|
53
76
|
const next = row.cycle[(row.cycle.indexOf(row.mode) + 1) % row.cycle.length] ?? row.cycle[0];
|
|
54
77
|
if (next === undefined) return;
|
|
@@ -57,13 +80,14 @@ export class HashlineConfigOverlay {
|
|
|
57
80
|
} else {
|
|
58
81
|
row.enabled = !row.enabled;
|
|
59
82
|
}
|
|
60
|
-
this.
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
83
|
+
this.runToggle(row);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
private adjustSelected(delta: number): void {
|
|
87
|
+
const row = this.rows[this.selected];
|
|
88
|
+
if (!row || row.disabled || row.value === undefined) return;
|
|
89
|
+
row.value += delta;
|
|
90
|
+
this.runToggle(row, delta);
|
|
67
91
|
}
|
|
68
92
|
|
|
69
93
|
handleInput(data: string): void {
|
|
@@ -75,6 +99,14 @@ export class HashlineConfigOverlay {
|
|
|
75
99
|
this.selected = (this.selected + 1) % this.rows.length;
|
|
76
100
|
return;
|
|
77
101
|
}
|
|
102
|
+
if (matchesKey(data, Key.right) || data === "+" || data === "=") {
|
|
103
|
+
this.adjustSelected(1);
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
if (matchesKey(data, Key.left) || data === "-" || data === "_") {
|
|
107
|
+
this.adjustSelected(-1);
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
78
110
|
if (matchesKey(data, Key.space) || matchesKey(data, Key.enter) || data === " " || data === "\r" || data === "\n") {
|
|
79
111
|
this.toggleSelected();
|
|
80
112
|
return;
|
|
@@ -96,12 +128,12 @@ export class HashlineConfigOverlay {
|
|
|
96
128
|
lines.push(theme.fg("border", `├${"─".repeat(innerWidth)}┤`));
|
|
97
129
|
this.rows.forEach((row, index) => {
|
|
98
130
|
const cursor = index === this.selected ? theme.fg("accent", "> ") : " ";
|
|
99
|
-
const box = row.mode !== undefined ? modeBox(theme, row.mode) : row.enabled ? theme.fg("success", "[x]") : theme.fg("dim", "[ ]");
|
|
100
|
-
const label = index === this.selected ? theme.fg("accent", theme.bold(row.label)) : row.label;
|
|
131
|
+
const box = row.value !== undefined ? numberBox(theme, row.value, row.disabled) : row.mode !== undefined ? modeBox(theme, row.mode) : row.enabled ? theme.fg("success", "[x]") : theme.fg("dim", "[ ]");
|
|
132
|
+
const label = row.disabled ? theme.fg("dim", row.label) : index === this.selected ? theme.fg("accent", theme.bold(row.label)) : row.label;
|
|
101
133
|
lines.push(padRow(theme, innerWidth, `${cursor}${box} ${label} ${theme.fg("dim", `— ${row.hint}`)}`));
|
|
102
134
|
});
|
|
103
135
|
lines.push(theme.fg("border", `├${"─".repeat(innerWidth)}┤`));
|
|
104
|
-
lines.push(padRow(theme, innerWidth, theme.fg("dim", " ↑↓ navigate · space toggle · q close")));
|
|
136
|
+
lines.push(padRow(theme, innerWidth, theme.fg("dim", " ↑↓ navigate · space toggle · ←/→ or -/+ adjust · q close")));
|
|
105
137
|
lines.push(theme.fg("border", `╰${"─".repeat(innerWidth)}╯`));
|
|
106
138
|
return lines;
|
|
107
139
|
}
|
package/src/config.ts
CHANGED
|
@@ -5,12 +5,17 @@ import { writeAtomic } from "./fs-write";
|
|
|
5
5
|
|
|
6
6
|
export type BoundaryDedupMode = "on" | "off" | "strict";
|
|
7
7
|
|
|
8
|
+
export const DEFAULT_DIFF_CONTEXT_LINES = 1;
|
|
9
|
+
export const MIN_DIFF_CONTEXT_LINES = 0;
|
|
10
|
+
export const MAX_DIFF_CONTEXT_LINES = 10;
|
|
11
|
+
|
|
8
12
|
export interface Config {
|
|
9
13
|
autoRead: boolean;
|
|
10
14
|
anchorGrepEnabled: boolean;
|
|
11
15
|
requirePath?: boolean;
|
|
12
16
|
strictInput?: boolean;
|
|
13
17
|
boundaryDedupMode?: BoundaryDedupMode;
|
|
18
|
+
diffContextLines?: number;
|
|
14
19
|
}
|
|
15
20
|
|
|
16
21
|
const DEFAULT_CONFIG: Config = {
|
|
@@ -18,7 +23,8 @@ const DEFAULT_CONFIG: Config = {
|
|
|
18
23
|
anchorGrepEnabled: true,
|
|
19
24
|
requirePath: false,
|
|
20
25
|
strictInput: false,
|
|
21
|
-
boundaryDedupMode: "on"
|
|
26
|
+
boundaryDedupMode: "on",
|
|
27
|
+
diffContextLines: DEFAULT_DIFF_CONTEXT_LINES
|
|
22
28
|
};
|
|
23
29
|
|
|
24
30
|
const BOUNDARY_DEDUP_MODES: BoundaryDedupMode[] = ["on", "strict", "off"];
|
|
@@ -30,6 +36,14 @@ function parseBoundaryDedupMode(mode: unknown, legacy: unknown): BoundaryDedupMo
|
|
|
30
36
|
return DEFAULT_CONFIG.boundaryDedupMode ?? "on";
|
|
31
37
|
}
|
|
32
38
|
|
|
39
|
+
export function normalizeDiffContextLines(value: unknown): number {
|
|
40
|
+
if (typeof value !== "number" || !Number.isFinite(value)) return DEFAULT_DIFF_CONTEXT_LINES;
|
|
41
|
+
const floored = Math.floor(value);
|
|
42
|
+
if (floored < MIN_DIFF_CONTEXT_LINES) return MIN_DIFF_CONTEXT_LINES;
|
|
43
|
+
if (floored > MAX_DIFF_CONTEXT_LINES) return MAX_DIFF_CONTEXT_LINES;
|
|
44
|
+
return floored;
|
|
45
|
+
}
|
|
46
|
+
|
|
33
47
|
function parseConfig(content: string): Config {
|
|
34
48
|
const parsed = JSON.parse(content) as unknown;
|
|
35
49
|
const autoRead = isRec(parsed) ? parsed.autoRead : undefined;
|
|
@@ -41,12 +55,14 @@ function parseConfig(content: string): Config {
|
|
|
41
55
|
const strictInput = isRec(parsed) ? parsed.strictInput : undefined;
|
|
42
56
|
const boundaryDedupMode = isRec(parsed) ? parsed.boundaryDedupMode : undefined;
|
|
43
57
|
const legacyBoundaryDedup = isRec(parsed) ? parsed.boundaryDedupEnabled : undefined;
|
|
58
|
+
const diffContextLines = isRec(parsed) ? parsed.diffContextLines : undefined;
|
|
44
59
|
return {
|
|
45
60
|
autoRead,
|
|
46
61
|
anchorGrepEnabled: typeof anchorGrepEnabled === "boolean" ? anchorGrepEnabled : DEFAULT_CONFIG.anchorGrepEnabled,
|
|
47
62
|
requirePath: typeof requirePath === "boolean" ? requirePath : DEFAULT_CONFIG.requirePath,
|
|
48
63
|
strictInput: typeof strictInput === "boolean" ? strictInput : DEFAULT_CONFIG.strictInput,
|
|
49
64
|
boundaryDedupMode: parseBoundaryDedupMode(boundaryDedupMode, legacyBoundaryDedup),
|
|
65
|
+
diffContextLines: normalizeDiffContextLines(diffContextLines),
|
|
50
66
|
};
|
|
51
67
|
}
|
|
52
68
|
|
|
@@ -103,3 +119,15 @@ export async function cycleBoundaryDedupMode(): Promise<BoundaryDedupMode> {
|
|
|
103
119
|
await writeConfig(config);
|
|
104
120
|
return next;
|
|
105
121
|
}
|
|
122
|
+
|
|
123
|
+
export async function getDiffContextLines(): Promise<number> {
|
|
124
|
+
return normalizeDiffContextLines((await readConfig()).diffContextLines);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export async function adjustDiffContextLines(delta: number): Promise<number> {
|
|
128
|
+
const config = await readConfig();
|
|
129
|
+
const next = normalizeDiffContextLines(normalizeDiffContextLines(config.diffContextLines) + delta);
|
|
130
|
+
config.diffContextLines = next;
|
|
131
|
+
await writeConfig(config);
|
|
132
|
+
return next;
|
|
133
|
+
}
|
package/src/insert.ts
CHANGED
|
@@ -45,7 +45,7 @@ const insertToolSchema = Type.Object(
|
|
|
45
45
|
direction: insertDirectionSchema,
|
|
46
46
|
lines: insertLinesSchema,
|
|
47
47
|
},
|
|
48
|
-
{ additionalProperties:
|
|
48
|
+
{ additionalProperties: true },
|
|
49
49
|
);
|
|
50
50
|
|
|
51
51
|
export function buildInsertToolSchema(requirePath: boolean): typeof insertToolSchema {
|
|
@@ -57,7 +57,7 @@ export function buildInsertToolSchema(requirePath: boolean): typeof insertToolSc
|
|
|
57
57
|
direction: insertDirectionSchema,
|
|
58
58
|
lines: insertLinesSchema,
|
|
59
59
|
},
|
|
60
|
-
{ additionalProperties:
|
|
60
|
+
{ additionalProperties: true },
|
|
61
61
|
) as typeof insertToolSchema;
|
|
62
62
|
}
|
|
63
63
|
|
package/src/payload-contract.ts
CHANGED
|
@@ -32,7 +32,7 @@ export const editToolSchema = Type.Object(
|
|
|
32
32
|
remove_to: removeToSchema,
|
|
33
33
|
replacement_lines: replacementLinesSchema,
|
|
34
34
|
},
|
|
35
|
-
{ additionalProperties:
|
|
35
|
+
{ additionalProperties: true },
|
|
36
36
|
);
|
|
37
37
|
|
|
38
38
|
export function buildEditToolSchema(requirePath: boolean): typeof editToolSchema {
|
|
@@ -44,7 +44,7 @@ export function buildEditToolSchema(requirePath: boolean): typeof editToolSchema
|
|
|
44
44
|
remove_to: removeToSchema,
|
|
45
45
|
replacement_lines: replacementLinesSchema,
|
|
46
46
|
},
|
|
47
|
-
{ additionalProperties:
|
|
47
|
+
{ additionalProperties: true },
|
|
48
48
|
) as typeof editToolSchema;
|
|
49
49
|
}
|
|
50
50
|
|
package/src/replace-response.ts
CHANGED
|
@@ -170,10 +170,10 @@ export function withDedupRows(diff: string, lineNumbers: (number | undefined)[],
|
|
|
170
170
|
return { diff: out.join("\n"), lineNumbers: nums };
|
|
171
171
|
}
|
|
172
172
|
|
|
173
|
-
export function buildChanged(input: SuccessInput, verb = "replaced"): TResult {
|
|
173
|
+
export function buildChanged(input: SuccessInput, verb = "replaced", diffContextLines = 1): TResult {
|
|
174
174
|
const { path, result, warnings, snapshotId, originalNormalized, originalHashes, editMeta, resultHashes, boundaryDedupAbove, boundaryDedupBelow } = input;
|
|
175
175
|
const resultLines = visLines(result);
|
|
176
|
-
const baseDiff = genDiff(originalNormalized, result,
|
|
176
|
+
const baseDiff = genDiff(originalNormalized, result, diffContextLines, resultHashes, originalHashes);
|
|
177
177
|
const diffResult = withDedupRows(baseDiff.diff, baseDiff.lineNumbers, boundaryDedupAbove, boundaryDedupBelow);
|
|
178
178
|
const addedLines = editMeta.addedLines;
|
|
179
179
|
const removedLines = editMeta.removedLines;
|
package/src/replace-undo.ts
CHANGED
|
@@ -11,6 +11,7 @@ import { markServed as markServedScoped, freeAnchors, adoptAnchors } from "./anc
|
|
|
11
11
|
import { resolveInCwd, writeAtomic, type FileIdentity } from "./fs-write";
|
|
12
12
|
import { toLF, stripBOM, restoreEndings, type LineEnding } from "./normalize";
|
|
13
13
|
import { genDiff, genPatch } from "./replace-diff";
|
|
14
|
+
import { getDiffContextLines } from "./config";
|
|
14
15
|
import { cntDiff, errCode, makePrepareArguments, splitLines } from "./utils";
|
|
15
16
|
import { loadP, loadGuide } from "./prompts";
|
|
16
17
|
import { buildMetrics } from "./replace-response";
|
|
@@ -178,13 +179,13 @@ export function regUndo(pi: ExtensionAPI): void {
|
|
|
178
179
|
const linesAddedByReplace = cntDiff(diffResult.diff, "+");
|
|
179
180
|
const linesRemovedByReplace = cntDiff(diffResult.diff, "-");
|
|
180
181
|
const restoredRange = changedRange(currentNormalized, undo.content);
|
|
181
|
-
const undoDiffResult = genDiff(currentNormalized, undo.content,
|
|
182
|
+
const undoDiffResult = genDiff(currentNormalized, undo.content, await getDiffContextLines(), undo.hashes, currentHashes);
|
|
182
183
|
const undoDiff = undoDiffResult.diff;
|
|
183
184
|
|
|
184
185
|
try {
|
|
185
186
|
const store = await loadHashStore();
|
|
186
187
|
const undoLines = splitLines(undo.content);
|
|
187
|
-
persistSnapshot(store, mutationTargetPath, undo.content, undo.hashes);
|
|
188
|
+
persistSnapshot(store, mutationTargetPath, undo.content, undo.hashes, undoLines.map((line) => contentChecksum(hashSource(line))));
|
|
188
189
|
freeAnchors(mutationTargetPath);
|
|
189
190
|
adoptAnchors(
|
|
190
191
|
mutationTargetPath,
|
package/src/write-hook.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { HASH_CLASS } from "./hashline/alphabet";
|
|
3
3
|
import { HASH_SEP } from "./hashline/hash";
|
|
4
|
-
import {
|
|
4
|
+
import { servedForPath } from "./anchor-registry";
|
|
5
5
|
import { resolveInCwd } from "./fs-write";
|
|
6
6
|
import { abortIf, splitLines, isRec, normalizeFilePath } from "./utils";
|
|
7
7
|
|
|
@@ -27,7 +27,7 @@ export async function servedHashEchoDenial(rawPath: string, content: string, cwd
|
|
|
27
27
|
abortIf(signal);
|
|
28
28
|
const { resolved } = await resolveInCwd(rawPath, cwd);
|
|
29
29
|
abortIf(signal);
|
|
30
|
-
const served =
|
|
30
|
+
const served = servedForPath(resolved);
|
|
31
31
|
if (!served || served.size === 0) return undefined;
|
|
32
32
|
const echo = findServedHashEcho(content, served);
|
|
33
33
|
if (!echo) return undefined;
|