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 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.0",
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",
@@ -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 anchor = candidates.length > 0 ? candidates.shift()! : mintAnchor(state);
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.opts.tui.requestRender(true);
61
- void this.opts.onToggle(row.key).then(async () => {
62
- this.rows = configRows(await readConfig());
63
- this.opts.tui.requestRender(true);
64
- }).catch((error: unknown) => {
65
- console.error("Failed to toggle hashline setting:", error);
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: false },
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: false },
60
+ { additionalProperties: true },
61
61
  ) as typeof insertToolSchema;
62
62
  }
63
63
 
@@ -32,7 +32,7 @@ export const editToolSchema = Type.Object(
32
32
  remove_to: removeToSchema,
33
33
  replacement_lines: replacementLinesSchema,
34
34
  },
35
- { additionalProperties: false },
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: false },
47
+ { additionalProperties: true },
48
48
  ) as typeof editToolSchema;
49
49
  }
50
50
 
@@ -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, 1, resultHashes, originalHashes);
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;
@@ -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, 1, undo.hashes, currentHashes);
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 { ownersForPath } from "./anchor-registry";
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 = ownersForPath(resolved);
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;