@trim21/personal-pi-extensions 0.1.643 → 0.1.646

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@trim21/personal-pi-extensions",
3
- "version": "0.1.643",
3
+ "version": "0.1.646",
4
4
  "type": "module",
5
5
  "description": "Custom pi coding-agent extensions: bwrap sandbox, workspace guard, opencode edit, and more",
6
6
  "keywords": [
@@ -17,7 +17,8 @@ import {
17
17
  createReadsState,
18
18
  type FileSnapshot,
19
19
  type ReadsState,
20
- readStateKey,
20
+ recordRead,
21
+ recordReads,
21
22
  requireCurrentRead,
22
23
  restoreReads,
23
24
  snapshotOf,
@@ -247,13 +248,11 @@ export function registerFileTools(
247
248
  { type: "text", text: `Read image file [${imageMime}]` },
248
249
  { type: "image", data, mimeType: imageMime },
249
250
  ];
250
- const snapshot = snapshotOf(image, false);
251
- const key = await readStateKey(filePath);
252
- state.reads.set(key, snapshot);
251
+ const reads = await recordRead(state, filePath, snapshotOf(image, false));
253
252
  return {
254
253
  content,
255
254
  details: {
256
- reads: { [key]: snapshot },
255
+ reads,
257
256
  pendant: {
258
257
  subtitle: formatSubtitlePath(ctx.cwd, filePath),
259
258
  title: "Read",
@@ -264,7 +263,6 @@ export function registerFileTools(
264
263
 
265
264
  const offset = params.offset ?? 1;
266
265
  const limit = params.limit;
267
- const key = await readStateKey(filePath);
268
266
  const buffer = await readFile(filePath);
269
267
 
270
268
  if (isBinary(buffer.subarray(0, SAMPLE_BYTES))) {
@@ -285,8 +283,7 @@ export function registerFileTools(
285
283
  `File content (${estimatedTokens} tokens) exceeds maximum allowed tokens (${MAX_READ_TOKENS}). Use offset and limit parameters to read specific portions of the file, or search for specific content instead of reading the whole file.`,
286
284
  );
287
285
  }
288
- const snapshot = snapshotOf(buffer);
289
- state.reads.set(key, snapshot);
286
+ const reads = await recordRead(state, filePath, snapshotOf(buffer));
290
287
  // 与 Edit / Write 同一条驻留路径:didOpen 后等待该文件的诊断并报告
291
288
  const diagnostics = await getService().lspDiagnosticsForFile(filePath, ctx.cwd, {
292
289
  notify: (message, level) => ctx.ui.notify(message, level),
@@ -300,7 +297,7 @@ export function registerFileTools(
300
297
  },
301
298
  ],
302
299
  details: {
303
- reads: { [key]: snapshot },
300
+ reads,
304
301
  pendant: {
305
302
  subtitle: formatSubtitlePath(
306
303
  ctx.cwd,
@@ -384,9 +381,7 @@ export function registerFileTools(
384
381
  await mkdir(dirname(filePath), { recursive: true });
385
382
  }
386
383
  await writeFile(filePath, newString, "utf8");
387
- const snapshot = snapshotOf(newString);
388
- const key = await readStateKey(filePath);
389
- state.reads.set(key, snapshot);
384
+ const reads = await recordRead(state, filePath, snapshotOf(newString));
390
385
  const diff = generateDiffString("", convertLeadingTabsToSpaces(newString));
391
386
  signal?.throwIfAborted();
392
387
  const diagnostics = await getService().lspDiagnosticsForFile(filePath, ctx.cwd, {
@@ -399,7 +394,7 @@ export function registerFileTools(
399
394
  diff: diff.diff,
400
395
  patch: generateUnifiedPatch(filePath, "", convertLeadingTabsToSpaces(newString)),
401
396
  firstChangedLine: diff.firstChangedLine,
402
- reads: { [key]: snapshot },
397
+ reads,
403
398
  },
404
399
  diagnostics,
405
400
  ];
@@ -436,8 +431,7 @@ export function registerFileTools(
436
431
  "File is a Jupyter Notebook. Use the NotebookEditTool to edit this file.",
437
432
  );
438
433
  }
439
- const key = await readStateKey(filePath);
440
- requireCurrentRead(state, key, filePath, content);
434
+ await requireCurrentRead(state, filePath, content);
441
435
  await access(filePath, constants.R_OK | constants.W_OK);
442
436
  signal?.throwIfAborted();
443
437
  const original = content.toString("utf8");
@@ -450,8 +444,7 @@ export function registerFileTools(
450
444
  signal,
451
445
  });
452
446
  await writeFile(filePath, restored, "utf8");
453
- const snapshot = snapshotOf(restored);
454
- state.reads.set(key, snapshot);
447
+ const reads = await recordRead(state, filePath, snapshotOf(restored));
455
448
  // patch/diff 仅供显示:前导 tab 转空格,避免 UI 渲染错位(对齐 Claude Code)
456
449
  const diff = generateDiffString(
457
450
  convertLeadingTabsToSpaces(original),
@@ -475,7 +468,7 @@ export function registerFileTools(
475
468
  convertLeadingTabsToSpaces(restored),
476
469
  ),
477
470
  firstChangedLine: diff.firstChangedLine,
478
- reads: { [key]: snapshot },
471
+ reads,
479
472
  },
480
473
  diagnostics,
481
474
  ];
@@ -527,13 +520,11 @@ export function registerFileTools(
527
520
  [string, FileToolDetails, DiagnosticReport]
528
521
  >(filePath, async () => {
529
522
  let original: string | undefined;
530
- let key: string | undefined;
531
523
  try {
532
524
  const value = await stat(filePath);
533
525
  if (value.isFile()) {
534
526
  const content = await readFile(filePath);
535
- key = await readStateKey(filePath);
536
- requireCurrentRead(state, key, filePath, content);
527
+ await requireCurrentRead(state, filePath, content);
537
528
  original = content.toString("utf8");
538
529
  }
539
530
  } catch (error) {
@@ -552,10 +543,7 @@ export function registerFileTools(
552
543
  });
553
544
  await mkdir(dirname(filePath), { recursive: true });
554
545
  await writeFile(filePath, params.content, "utf8");
555
- const snapshot = snapshotOf(params.content);
556
- // 新建文件:writeFile 之后 realpath 才能解析;覆盖写则复用上面的 key
557
- const resolvedKey = key ?? (await readStateKey(filePath));
558
- state.reads.set(resolvedKey, snapshot);
546
+ const reads = await recordRead(state, filePath, snapshotOf(params.content));
559
547
  const diff = generateDiffString(original ?? "", params.content);
560
548
  const text =
561
549
  original === undefined
@@ -572,7 +560,7 @@ export function registerFileTools(
572
560
  diff: diff.diff,
573
561
  patch: generateUnifiedPatch(filePath, original ?? "", params.content),
574
562
  firstChangedLine: diff.firstChangedLine,
575
- reads: { [resolvedKey]: snapshot },
563
+ reads,
576
564
  },
577
565
  diagnostics,
578
566
  ];
@@ -639,16 +627,14 @@ export default function claudeCodeFileTools(
639
627
  onEnabled: (pi, service) => {
640
628
  registerLspRenameTool(pi, service, {
641
629
  policy,
642
- recordReads: async (applied) => {
643
- const reads: Record<string, FileSnapshot> = {};
644
- for (const fileEdit of applied) {
645
- const key = await readStateKey(fileEdit.path);
646
- const snapshot = snapshotOf(fileEdit.newText);
647
- state.reads.set(key, snapshot);
648
- reads[key] = snapshot;
649
- }
650
- return reads;
651
- },
630
+ recordReads: (applied) =>
631
+ recordReads(
632
+ state,
633
+ applied.map((fileEdit) => ({
634
+ path: fileEdit.path,
635
+ snapshot: snapshotOf(fileEdit.newText),
636
+ })),
637
+ ),
652
638
  });
653
639
  registerLspInspectTools(pi, service);
654
640
  },
@@ -41,9 +41,12 @@ export function addListIssuesTool(gh: GhClient, pi: ExtensionAPI) {
41
41
  }),
42
42
  ),
43
43
  label: Type.Optional(Type.String({ description: "Filter by label" })),
44
- author: Type.Optional(Type.String({ description: "Filter by author" })),
44
+ // `@me` 由 GitHub 的搜索 / 过滤 API 解析:不带 keywords 时 gh 自行展开(GraphQL 过滤分支)
45
+ // 或按字面量转发(搜索分支),带 keywords 时我们直接把 `assignee:@me` 交给搜索 API。
46
+ // 两条传输都支持,因此本仓库不要再实现一次展开。
47
+ author: Type.Optional(Type.String({ description: "Filter by author ('@me' for yourself)" })),
45
48
  assignee: Type.Optional(
46
- Type.String({ description: "Filter by assignee (@me for yourself)" }),
49
+ Type.String({ description: "Filter by assignee ('@me' for yourself)" }),
47
50
  ),
48
51
  milestone: Type.Optional(Type.String({ description: "Filter by milestone" })),
49
52
  limit: Type.Optional(Type.Number({ description: "Max results (default 30, max 100)" })),
@@ -41,9 +41,12 @@ export function addListPrsTool(gh: GhClient, pi: ExtensionAPI) {
41
41
  }),
42
42
  ),
43
43
  label: Type.Optional(Type.String({ description: "Filter by label" })),
44
- author: Type.Optional(Type.String({ description: "Filter by author" })),
44
+ // `@me` 由 GitHub 的搜索 / 过滤 API 解析:不带 keywords 时 gh 自行展开(GraphQL 过滤分支)
45
+ // 或按字面量转发(搜索分支),带 keywords 时我们直接把 `assignee:@me` 交给搜索 API。
46
+ // 两条传输都支持,因此本仓库不要再实现一次展开。
47
+ author: Type.Optional(Type.String({ description: "Filter by author ('@me' for yourself)" })),
45
48
  assignee: Type.Optional(
46
- Type.String({ description: "Filter by assignee (@me for yourself)" }),
49
+ Type.String({ description: "Filter by assignee ('@me' for yourself)" }),
47
50
  ),
48
51
  milestone: Type.Optional(Type.String({ description: "Filter by milestone" })),
49
52
  limit: Type.Optional(Type.Number({ description: "Max results (default 30, max 100)" })),
@@ -6,11 +6,15 @@
6
6
  * - 工具名集合:claude-code 是大写 Read/Edit/Write,opencode 是小写;
7
7
  * - 指纹来源:claude-code 手里已有内容,直接 snapshotOf(content);opencode 的
8
8
  * read 是流式分页读取,整文件指纹用 fileDigest(path)。
9
+ *
10
+ * 记账 key 的解析与读写由本模块独占(readStateKey / recordRead / require*),
11
+ * 调用点只给路径与快照,不自己拼 details 里的 map。
9
12
  */
10
13
 
11
14
  import { createHash } from "node:crypto";
12
15
  import { createReadStream } from "node:fs";
13
16
  import { realpath } from "node:fs/promises";
17
+ import { basename, dirname, join } from "node:path";
14
18
 
15
19
  import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
16
20
  import { type Static, Type } from "typebox";
@@ -72,32 +76,73 @@ export async function digestIfExists(filePath: string): Promise<string | undefin
72
76
  }
73
77
 
74
78
  /**
75
- * 记账 key:解析 symlink 后的真实路径。文件尚不存在(Write 新建 / Edit 空
76
- * old_string 创建)时 realpath 抛 ENOENT,回退到调用方给出的路径。
79
+ * 记账 key:解析 symlink 后的真实路径。文件尚未落盘(Write 新建 / Edit 空
80
+ * old_string 创建)时向上找到最深的已存在祖先、解析它再把剩余路径段拼回去——
81
+ * 结果与文件落盘后 realpath(filePath) 相同,因此调用点不必关心算 key 的时机
82
+ * (写入路径尤其容易在创建分支上算早)。
77
83
  */
78
84
  export async function readStateKey(filePath: string): Promise<string> {
79
- try {
80
- return await realpath(filePath);
81
- } catch (error) {
82
- if (isMissingPath(error)) {
85
+ const pending: string[] = [];
86
+ let current = filePath;
87
+ for (;;) {
88
+ try {
89
+ return join(await realpath(current), ...pending.toReversed());
90
+ } catch (error) {
91
+ if (!isMissingPath(error)) {
92
+ throw error;
93
+ }
94
+ }
95
+ const parent = dirname(current);
96
+ if (parent === current) {
97
+ // 一路到文件系统根仍不存在:无法解析,回退调用方给出的路径
83
98
  return filePath;
84
99
  }
85
- throw error;
100
+ pending.push(basename(current));
101
+ current = parent;
86
102
  }
87
103
  }
88
104
 
105
+ /**
106
+ * 记账「这些文件的内容即各自的 snapshot」:解析 key 写入 state,返回可直接放进
107
+ * 工具 details 的 reads 片段。
108
+ *
109
+ * 这一侧是对象而不是 Map:details 是 JSON(宿主按 `JsonValue` 写进 session
110
+ * JSONL),Map 序列化过去会变成 `{}`,记账就丢了;读取方向与之相对(
111
+ * `deserializeReads` 从同一个对象形状还原成 Map)。
112
+ */
113
+ export async function recordReads(
114
+ state: ReadsState,
115
+ entries: readonly { path: string; snapshot: FileSnapshot }[],
116
+ ): Promise<Record<string, FileSnapshot>> {
117
+ const reads: Record<string, FileSnapshot> = {};
118
+ for (const entry of entries) {
119
+ const key = await readStateKey(entry.path);
120
+ state.reads.set(key, entry.snapshot);
121
+ reads[key] = entry.snapshot;
122
+ }
123
+ return reads;
124
+ }
125
+
126
+ /** 单文件记账:`recordReads` 的单项形式。 */
127
+ export async function recordRead(
128
+ state: ReadsState,
129
+ filePath: string,
130
+ snapshot: FileSnapshot,
131
+ ): Promise<Record<string, FileSnapshot>> {
132
+ return await recordReads(state, [{ path: filePath, snapshot }]);
133
+ }
134
+
89
135
  function snapshotsEqual(left: FileSnapshot, right: FileSnapshot): boolean {
90
136
  return left.digest === right.digest;
91
137
  }
92
138
 
93
139
  /** 校验「已读且未变」:未读过、非文本、指纹不符都拒绝写入。 */
94
- export function requireCurrentRead(
140
+ export async function requireCurrentRead(
95
141
  state: ReadsState,
96
- key: string,
97
142
  filePath: string,
98
143
  currentContent: Uint8Array | string,
99
- ): void {
100
- const readSnapshot = state.reads.get(key);
144
+ ): Promise<void> {
145
+ const readSnapshot = state.reads.get(await readStateKey(filePath));
101
146
  if (!readSnapshot) {
102
147
  throw new Error("File has not been read yet. Read it first before writing to it.");
103
148
  }
@@ -113,21 +158,17 @@ export function requireCurrentRead(
113
158
 
114
159
  /**
115
160
  * 过期校验:该文件已有读取记录时,要求磁盘上的当前指纹与记录一致,否则拒绝
116
- * 写入;从未读过(没有记录)时直接放行。
161
+ * 写入;从未读过(没有记录)时直接放行,且不做整文件指纹计算。
117
162
  *
118
163
  * opencode 的 write 用这个:没读过的文件允许直接写,读过之后再被外部改动就必须
119
- * 重新 read。`currentDigest` 为 undefined(文件已不存在)同样算过期。
164
+ * 重新 read。文件已不存在(指纹 undefined)同样算过期。
120
165
  */
121
- export function requireUnchangedRead(
122
- state: ReadsState,
123
- key: string,
124
- currentDigest: string | undefined,
125
- ): void {
126
- const readSnapshot = state.reads.get(key);
166
+ export async function requireUnchangedRead(state: ReadsState, filePath: string): Promise<void> {
167
+ const readSnapshot = state.reads.get(await readStateKey(filePath));
127
168
  if (!readSnapshot) {
128
169
  return;
129
170
  }
130
- if (currentDigest === undefined || readSnapshot.digest !== currentDigest) {
171
+ if (readSnapshot.digest !== (await digestIfExists(filePath))) {
131
172
  throw new Error(
132
173
  "File has been modified since read, either by the user or by a linter. Read it again before attempting to write it.",
133
174
  );
package/src/lib/github.ts CHANGED
@@ -310,22 +310,18 @@ export function createGithubSearch(options: GithubClientOptions = {}): GithubSea
310
310
  return {
311
311
  async search(kind, params) {
312
312
  const limit = Math.min(Math.max(params.limit ?? 30, 1), 100);
313
- const effective = { ...params, limit };
314
313
 
315
314
  try {
316
- const octokit = await api.call(async (client) => {
317
- if (effective.assignee === "@me") {
318
- const { data } = await client.rest.users.getAuthenticated();
319
- effective.assignee = data.login;
320
- }
321
- return client;
322
- });
323
- const q = buildSearchQuery(kind, effective);
324
- const { data } = await octokit.rest.search.issuesAndPullRequests({
325
- q,
326
- per_page: limit,
315
+ // `@me` 是 gh CLI 的简写,REST 搜索不认识:这里不展开,带关键词时它按字面量
316
+ // 进入查询串(无关键词的列表走 gh CLI,由 gh 自己展开)。
317
+ const q = buildSearchQuery(kind, { ...params, limit });
318
+ return await api.call(async (client) => {
319
+ const { data } = await client.rest.search.issuesAndPullRequests({
320
+ q,
321
+ per_page: limit,
322
+ });
323
+ return data.items.map((item) => normalize(parseWithSchema(searchItemSchema, item)));
327
324
  });
328
- return data.items.map((item) => normalize(parseWithSchema(searchItemSchema, item)));
329
325
  } catch (error) {
330
326
  const status = (error as { status?: number }).status;
331
327
  const message = (error as { message?: string }).message ?? String(error);
@@ -38,11 +38,11 @@ import { Type } from "typebox";
38
38
 
39
39
  import {
40
40
  createReadsState,
41
- digestIfExists,
42
41
  fileDigest,
43
42
  type FileSnapshot,
44
43
  type ReadsState,
45
- readStateKey,
44
+ recordRead,
45
+ recordReads,
46
46
  requireCurrentRead,
47
47
  requireUnchangedRead,
48
48
  restoreReads,
@@ -500,13 +500,11 @@ function registerReadTool(pi: ExtensionAPI, getService: () => LspService, state:
500
500
  { type: "text", text: "Image read successfully" },
501
501
  { type: "image", data: base64, mimeType },
502
502
  ];
503
- const key = await readStateKey(absolutePath);
504
- const snapshot = snapshotOf(buffer, false);
505
- state.reads.set(key, snapshot);
503
+ const reads = await recordRead(state, absolutePath, snapshotOf(buffer, false));
506
504
  return {
507
505
  content,
508
506
  details: {
509
- reads: { [key]: snapshot },
507
+ reads,
510
508
  pendant: { subtitle: formatSubtitlePath(ctx.cwd, absolutePath) },
511
509
  },
512
510
  };
@@ -521,7 +519,6 @@ function registerReadTool(pi: ExtensionAPI, getService: () => LspService, state:
521
519
 
522
520
  // 记账指纹先于分页读取:文件若在读取期间被改动,指纹对应的是改动前的
523
521
  // 内容,后续 edit 会要求重新 read(失败方向安全)。
524
- const key = await readStateKey(absolutePath);
525
522
  const snapshot: FileSnapshot = { digest: await fileDigest(absolutePath), textEditable: true };
526
523
 
527
524
  const effectiveOffset = offset || 1;
@@ -555,7 +552,7 @@ function registerReadTool(pi: ExtensionAPI, getService: () => LspService, state:
555
552
  outputText = `${header}${numbered}\n\n(End of file - total ${page.count} lines)${footer}`;
556
553
  }
557
554
 
558
- state.reads.set(key, snapshot);
555
+ const reads = await recordRead(state, absolutePath, snapshot);
559
556
 
560
557
  // opencode: 与 edit / write 同一条驻留路径:didOpen 后等待该文件的诊断并报告
561
558
  const diagnostics = await getService().lspDiagnosticsForFile(absolutePath, ctx.cwd, {
@@ -572,7 +569,7 @@ function registerReadTool(pi: ExtensionAPI, getService: () => LspService, state:
572
569
  content,
573
570
  details: {
574
571
  ...details,
575
- reads: { [key]: snapshot },
572
+ reads,
576
573
  pendant: {
577
574
  subtitle: formatSubtitlePath(
578
575
  ctx.cwd,
@@ -667,16 +664,14 @@ function registerEditTool(
667
664
  await mkdir(dirname(absolutePath), { recursive: true });
668
665
  signal?.throwIfAborted();
669
666
  await writeFile(absolutePath, newString, "utf8");
670
- // 新建文件无需先 read(无处可读),写后记账;realpath 要等文件落盘
671
- const key = await readStateKey(absolutePath);
672
- const snapshot = snapshotOf(newString);
673
- state.reads.set(key, snapshot);
667
+ // 新建文件无需先 read(无处可读),写后记账
668
+ const reads = await recordRead(state, absolutePath, snapshotOf(newString));
674
669
  const diagnostics = await getService().lspDiagnosticsForFile(absolutePath, ctx.cwd, {
675
670
  signal,
676
671
  });
677
672
  return [
678
673
  "Edit applied successfully.",
679
- { diff: "", patch: "", firstChangedLine: 0, reads: { [key]: snapshot } },
674
+ { diff: "", patch: "", firstChangedLine: 0, reads },
680
675
  diagnostics,
681
676
  ] as const;
682
677
  }
@@ -702,8 +697,7 @@ function registerEditTool(
702
697
 
703
698
  // 指纹校验必须发生在写入之前:未 read 或外部已改动都拒绝编辑
704
699
  const rawContent = await readFile(absolutePath, "utf8");
705
- const key = await readStateKey(absolutePath);
706
- requireCurrentRead(state, key, absolutePath, rawContent);
700
+ await requireCurrentRead(state, absolutePath, rawContent);
707
701
 
708
702
  const applied = applyEdit(rawContent, oldString, newString, replaceAll);
709
703
  signal?.throwIfAborted();
@@ -715,8 +709,7 @@ function registerEditTool(
715
709
  signal,
716
710
  });
717
711
  await writeFile(absolutePath, applied.finalContent, "utf8");
718
- const snapshot = snapshotOf(applied.finalContent);
719
- state.reads.set(key, snapshot);
712
+ const reads = await recordRead(state, absolutePath, snapshotOf(applied.finalContent));
720
713
 
721
714
  const diffOld = normalizeToLF(applied.contentOld);
722
715
  const diffNew = normalizeToLF(applied.contentNew);
@@ -731,7 +724,7 @@ function registerEditTool(
731
724
  diff: diffResult.diff,
732
725
  patch,
733
726
  firstChangedLine: diffResult.firstChangedLine,
734
- reads: { [key]: snapshot },
727
+ reads,
735
728
  },
736
729
  diagnostics,
737
730
  ] as const;
@@ -811,12 +804,9 @@ function registerWriteTool(
811
804
  async () => {
812
805
  signal?.throwIfAborted();
813
806
 
814
- const key = await readStateKey(absolutePath);
815
807
  // write 不要求先读过;但若已有读取记录,磁盘内容必须仍是读取时的样子,
816
808
  // 否则先重新 read(外部改动过的文件不让盲写覆盖)
817
- if (state.reads.has(key)) {
818
- requireUnchangedRead(state, key, await digestIfExists(absolutePath));
819
- }
809
+ await requireUnchangedRead(state, absolutePath);
820
810
 
821
811
  // opencode: desiredBom = source.bom || next.bom —— 保留原文件 BOM,
822
812
  // 否则用新内容自带的 BOM。旧内容整读一次,既是 BOM 的来源,也是审批预览
@@ -842,14 +832,13 @@ function registerWriteTool(
842
832
  await mkdir(dir, { recursive: true });
843
833
  signal?.throwIfAborted();
844
834
  await writeFile(absolutePath, finalContent, "utf8");
845
- // 写后记账(key 在写入前已算过):紧接着的 edit 不该再要求重新 read
846
- const snapshot = snapshotOf(finalContent);
847
- state.reads.set(key, snapshot);
835
+ // 写后记账:紧接着的 edit 不该再要求重新 read
836
+ const reads = await recordRead(state, absolutePath, snapshotOf(finalContent));
848
837
  const diagnostics = await getService().lspDiagnosticsForFile(absolutePath, ctx.cwd, {
849
838
  signal,
850
839
  });
851
840
 
852
- return ["Wrote file successfully.", { reads: { [key]: snapshot } }, diagnostics] as const;
841
+ return ["Wrote file successfully.", { reads }, diagnostics] as const;
853
842
  },
854
843
  );
855
844
 
@@ -906,16 +895,14 @@ export default function opencodeFileTools(
906
895
  onEnabled: (pi, service) => {
907
896
  registerLspRenameTool(pi, service, {
908
897
  policy,
909
- recordReads: async (applied) => {
910
- const reads: Record<string, FileSnapshot> = {};
911
- for (const fileEdit of applied) {
912
- const key = await readStateKey(fileEdit.path);
913
- const snapshot = snapshotOf(fileEdit.newText);
914
- state.reads.set(key, snapshot);
915
- reads[key] = snapshot;
916
- }
917
- return reads;
918
- },
898
+ recordReads: (applied) =>
899
+ recordReads(
900
+ state,
901
+ applied.map((fileEdit) => ({
902
+ path: fileEdit.path,
903
+ snapshot: snapshotOf(fileEdit.newText),
904
+ })),
905
+ ),
919
906
  });
920
907
  registerLspInspectTools(pi, service);
921
908
  },