@trim21/personal-pi-extensions 0.0.241 → 0.0.242
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,10 +1,18 @@
|
|
|
1
1
|
import { isAbsolute, normalize, resolve } from "node:path";
|
|
2
2
|
|
|
3
|
+
import { Type } from "typebox";
|
|
4
|
+
import { Value } from "typebox/value";
|
|
5
|
+
|
|
3
6
|
export interface FileSnapshot {
|
|
4
7
|
digest: string;
|
|
5
8
|
textEditable: boolean;
|
|
6
9
|
}
|
|
7
10
|
|
|
11
|
+
const fileSnapshotSchema = Type.Object({
|
|
12
|
+
digest: Type.String(),
|
|
13
|
+
textEditable: Type.Boolean(),
|
|
14
|
+
});
|
|
15
|
+
|
|
8
16
|
export interface ClaudeCodeState {
|
|
9
17
|
readonly reads: Map<string, FileSnapshot>;
|
|
10
18
|
}
|
|
@@ -33,3 +41,17 @@ export function throwIfAborted(signal: AbortSignal | undefined): void {
|
|
|
33
41
|
export function snapshotsEqual(left: FileSnapshot, right: FileSnapshot): boolean {
|
|
34
42
|
return left.digest === right.digest;
|
|
35
43
|
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* 从工具结果 details 里恢复文件已读记账(跨进程 resume / reload / fork)。
|
|
47
|
+
* 数据来自 session 文件,可能缺失或损坏:逐条 TypeBox 校验,非法条目丢弃。
|
|
48
|
+
* 只接受 plain object,数组、null 等异常形态直接返回空 map。
|
|
49
|
+
*/
|
|
50
|
+
export function deserializeReads(data: unknown): Map<string, FileSnapshot> {
|
|
51
|
+
const reads = new Map<string, FileSnapshot>();
|
|
52
|
+
if (typeof data !== "object" || data === null || Array.isArray(data)) return reads;
|
|
53
|
+
for (const [filePath, snapshot] of Object.entries(data)) {
|
|
54
|
+
if (Value.Check(fileSnapshotSchema, snapshot)) reads.set(filePath, snapshot);
|
|
55
|
+
}
|
|
56
|
+
return reads;
|
|
57
|
+
}
|
package/src/claude-code/files.ts
CHANGED
|
@@ -44,6 +44,11 @@ export interface FileToolDetails {
|
|
|
44
44
|
diff?: string;
|
|
45
45
|
patch?: string;
|
|
46
46
|
firstChangedLine?: number;
|
|
47
|
+
/**
|
|
48
|
+
* 本轮受影响文件的已读快照(path → snapshot)。随工具结果持久化到
|
|
49
|
+
* session 文件,resume 后由 session_start 重建 reads state。
|
|
50
|
+
*/
|
|
51
|
+
reads?: Record<string, FileSnapshot>;
|
|
47
52
|
}
|
|
48
53
|
|
|
49
54
|
function snapshotOf(content: Uint8Array | string, textEditable = true): FileSnapshot {
|
|
@@ -216,8 +221,9 @@ export function registerFileTools(pi: ExtensionAPI, state: ClaudeCodeState): voi
|
|
|
216
221
|
{ type: "text", text: `Read image file [${imageMime}]` },
|
|
217
222
|
{ type: "image", data, mimeType: imageMime },
|
|
218
223
|
];
|
|
219
|
-
|
|
220
|
-
|
|
224
|
+
const snapshot = snapshotOf(image, false);
|
|
225
|
+
state.reads.set(filePath, snapshot);
|
|
226
|
+
return { content, details: { reads: { [filePath]: snapshot } } };
|
|
221
227
|
}
|
|
222
228
|
|
|
223
229
|
const buffer = await readFile(filePath);
|
|
@@ -228,8 +234,12 @@ export function registerFileTools(pi: ExtensionAPI, state: ClaudeCodeState): voi
|
|
|
228
234
|
params.offset ?? 1,
|
|
229
235
|
params.limit ?? DEFAULT_READ_LINES,
|
|
230
236
|
);
|
|
231
|
-
|
|
232
|
-
|
|
237
|
+
const snapshot = snapshotOf(buffer);
|
|
238
|
+
state.reads.set(filePath, snapshot);
|
|
239
|
+
return {
|
|
240
|
+
content: [{ type: "text", text: formatted.text }],
|
|
241
|
+
details: { reads: { [filePath]: snapshot } },
|
|
242
|
+
};
|
|
233
243
|
},
|
|
234
244
|
});
|
|
235
245
|
|
|
@@ -279,7 +289,8 @@ export function registerFileTools(pi: ExtensionAPI, state: ClaudeCodeState): voi
|
|
|
279
289
|
params.replace_all ?? false,
|
|
280
290
|
);
|
|
281
291
|
await writeFile(filePath, updated, "utf8");
|
|
282
|
-
|
|
292
|
+
const snapshot = snapshotOf(updated);
|
|
293
|
+
state.reads.set(filePath, snapshot);
|
|
283
294
|
const diff = generateDiffString(original, updated);
|
|
284
295
|
return {
|
|
285
296
|
content: [{ type: "text", text: `The file ${filePath} has been updated successfully.` }],
|
|
@@ -287,6 +298,7 @@ export function registerFileTools(pi: ExtensionAPI, state: ClaudeCodeState): voi
|
|
|
287
298
|
diff: diff.diff,
|
|
288
299
|
patch: generateUnifiedPatch(filePath, original, updated),
|
|
289
300
|
firstChangedLine: diff.firstChangedLine,
|
|
301
|
+
reads: { [filePath]: snapshot },
|
|
290
302
|
} satisfies FileToolDetails,
|
|
291
303
|
};
|
|
292
304
|
});
|
|
@@ -334,7 +346,8 @@ export function registerFileTools(pi: ExtensionAPI, state: ClaudeCodeState): voi
|
|
|
334
346
|
throwIfAborted(signal);
|
|
335
347
|
await mkdir(dirname(filePath), { recursive: true });
|
|
336
348
|
await writeFile(filePath, params.content, "utf8");
|
|
337
|
-
|
|
349
|
+
const snapshot = snapshotOf(params.content);
|
|
350
|
+
state.reads.set(filePath, snapshot);
|
|
338
351
|
const diff = generateDiffString(original ?? "", params.content);
|
|
339
352
|
return {
|
|
340
353
|
content: [{ type: "text", text: `File created successfully at: ${filePath}` }],
|
|
@@ -342,6 +355,7 @@ export function registerFileTools(pi: ExtensionAPI, state: ClaudeCodeState): voi
|
|
|
342
355
|
diff: diff.diff,
|
|
343
356
|
patch: generateUnifiedPatch(filePath, original ?? "", params.content),
|
|
344
357
|
firstChangedLine: diff.firstChangedLine,
|
|
358
|
+
reads: { [filePath]: snapshot },
|
|
345
359
|
} satisfies FileToolDetails,
|
|
346
360
|
};
|
|
347
361
|
});
|
package/src/claude-code/index.ts
CHANGED
|
@@ -1,13 +1,33 @@
|
|
|
1
1
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
|
|
3
|
-
import { createClaudeCodeState } from "./common.js";
|
|
3
|
+
import { createClaudeCodeState, deserializeReads } from "./common.js";
|
|
4
4
|
import { registerFileTools } from "./files.js";
|
|
5
5
|
import { registerSearchTools } from "./search.js";
|
|
6
6
|
import { registerSessionTools } from "./session-tools.js";
|
|
7
7
|
import { registerShellTools } from "./shell.js";
|
|
8
8
|
|
|
9
|
+
/** 会更新 reads state 并随 details 持久化快照的工具名。 */
|
|
10
|
+
const FILE_TOOL_NAMES = new Set(["Read", "Edit", "Write"]);
|
|
11
|
+
|
|
9
12
|
export default function claudeCodeTools(pi: ExtensionAPI): void {
|
|
10
13
|
const state = createClaudeCodeState();
|
|
14
|
+
|
|
15
|
+
// 扩展实例在进程启动 / /reload / /new / /resume / /fork 时重建,内存里的
|
|
16
|
+
// 已读记账随之丢失。这里从当前分支的历史工具结果里恢复:digest 是当时的值,
|
|
17
|
+
// 若文件在此期间被外部修改,Edit/Write 时的指纹对比仍会要求重新 Read,
|
|
18
|
+
// 防呆语义不因重建而弱化。
|
|
19
|
+
pi.on("session_start", (_event, ctx) => {
|
|
20
|
+
for (const entry of ctx.sessionManager.getBranch()) {
|
|
21
|
+
if (entry.type !== "message" || entry.message.role !== "toolResult") continue;
|
|
22
|
+
if (!FILE_TOOL_NAMES.has(entry.message.toolName)) continue;
|
|
23
|
+
const details = entry.message.details as { reads?: unknown } | undefined;
|
|
24
|
+
if (!details?.reads) continue;
|
|
25
|
+
for (const [filePath, snapshot] of deserializeReads(details.reads)) {
|
|
26
|
+
state.reads.set(filePath, snapshot);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
|
|
11
31
|
registerFileTools(pi, state);
|
|
12
32
|
registerSearchTools(pi);
|
|
13
33
|
registerShellTools(pi);
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { StringEnum } from "@earendil-works/pi-ai";
|
|
2
2
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
3
3
|
import { Type } from "typebox";
|
|
4
|
+
import { Value } from "typebox/value";
|
|
4
5
|
|
|
5
6
|
import { selectWithOptionalInput } from "../lib/ui.js";
|
|
6
7
|
|
|
@@ -102,23 +103,34 @@ async function askMultiple(
|
|
|
102
103
|
}
|
|
103
104
|
|
|
104
105
|
export function registerSessionTools(pi: ExtensionAPI): void {
|
|
106
|
+
const todoItemSchema = Type.Object(
|
|
107
|
+
{
|
|
108
|
+
content: Type.String({ minLength: 1 }),
|
|
109
|
+
status: StringEnum(TODO_STATUSES),
|
|
110
|
+
activeForm: Type.String({ minLength: 1 }),
|
|
111
|
+
},
|
|
112
|
+
{ additionalProperties: false },
|
|
113
|
+
);
|
|
105
114
|
const todoSchema = Type.Object(
|
|
106
115
|
{
|
|
107
|
-
todos: Type.Array(
|
|
108
|
-
Type.Object(
|
|
109
|
-
{
|
|
110
|
-
content: Type.String({ minLength: 1 }),
|
|
111
|
-
status: StringEnum(TODO_STATUSES),
|
|
112
|
-
activeForm: Type.String({ minLength: 1 }),
|
|
113
|
-
},
|
|
114
|
-
{ additionalProperties: false },
|
|
115
|
-
),
|
|
116
|
-
{ description: "The updated todo list" },
|
|
117
|
-
),
|
|
116
|
+
todos: Type.Array(todoItemSchema, { description: "The updated todo list" }),
|
|
118
117
|
},
|
|
119
118
|
{ additionalProperties: false },
|
|
120
119
|
);
|
|
121
120
|
|
|
121
|
+
// TodoWrite 的列表随工具结果 details 持久化(跟随会话分支),但 widget 是
|
|
122
|
+
// 纯 TUI 状态,进程重启后丢失。session 恢复时从当前分支取最后一个 TodoWrite
|
|
123
|
+
// 的列表重新渲染(完整列表替换语义,后出现的覆盖前面的)。
|
|
124
|
+
pi.on("session_start", (_event, ctx) => {
|
|
125
|
+
for (const entry of ctx.sessionManager.getBranch()) {
|
|
126
|
+
if (entry.type !== "message" || entry.message.role !== "toolResult") continue;
|
|
127
|
+
if (entry.message.toolName !== "TodoWrite") continue;
|
|
128
|
+
if (Value.Check(todoSchema, entry.message.details)) {
|
|
129
|
+
ctx.ui.setWidget("claude-code-todos", formatTodos(entry.message.details.todos));
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
|
|
122
134
|
pi.registerTool({
|
|
123
135
|
name: "TodoWrite",
|
|
124
136
|
label: "Todo Write",
|