@trim21/personal-pi-extensions 0.0.279 → 0.0.281

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.0.279",
3
+ "version": "0.0.281",
4
4
  "type": "module",
5
5
  "description": "Custom pi coding-agent extensions: bwrap sandbox, workspace guard, opencode edit, and more",
6
6
  "keywords": [
@@ -7,6 +7,7 @@ import { fileURLToPath } from "node:url";
7
7
  import type { ImageContent, TextContent } from "@earendil-works/pi-ai";
8
8
  import {
9
9
  type ExtensionAPI,
10
+ type ExtensionContext,
10
11
  generateDiffString,
11
12
  generateUnifiedPatch,
12
13
  withFileMutationQueue,
@@ -16,6 +17,8 @@ import { Type } from "typebox";
16
17
  import { guardWriteAccess } from "../lib/write-guard.js";
17
18
  import {
18
19
  type ClaudeCodeState,
20
+ createClaudeCodeState,
21
+ deserializeReads,
19
22
  didYouMean,
20
23
  type FileSnapshot,
21
24
  requireAbsolutePath,
@@ -528,3 +531,53 @@ export function registerFileTools(pi: ExtensionAPI, state: ClaudeCodeState): voi
528
531
  },
529
532
  });
530
533
  }
534
+
535
+ /** 会更新 reads state 并随 details 持久化快照的工具名。 */
536
+ const FILE_TOOL_NAMES = new Set(["Read", "Edit", "Write"]);
537
+
538
+ /**
539
+ * 从当前分支的历史工具结果重建已读记账。先清空再重放,保证 state 只反映
540
+ * 当前分支:rewind / fork / resume 后,被抛弃分支上的 Read 不再残留。
541
+ */
542
+ function restoreFileReads(
543
+ state: ClaudeCodeState,
544
+ sessionManager: ExtensionContext["sessionManager"],
545
+ ): void {
546
+ state.reads.clear();
547
+ for (const entry of sessionManager.getBranch()) {
548
+ if (entry.type !== "message" || entry.message.role !== "toolResult") continue;
549
+ if (!FILE_TOOL_NAMES.has(entry.message.toolName)) continue;
550
+ const details = entry.message.details as { reads?: unknown } | undefined;
551
+ if (!details?.reads) continue;
552
+ for (const [filePath, snapshot] of deserializeReads(details.reads)) {
553
+ state.reads.set(filePath, snapshot);
554
+ }
555
+ }
556
+ }
557
+
558
+ /**
559
+ * 独立扩展入口:files.ts 可单独经 `-e claude-code/files.ts` 加载(spawn-agent
560
+ * 子代理把 Read/Edit/Write 工具名映射到本文件),无需经 index.ts。reads
561
+ * state 归本文件所有:扩展实例内创建,并随 session 事件从历史分支恢复,
562
+ * 与主进程 index.ts 聚合加载时的行为一致。
563
+ */
564
+ export default function claudeCodeFileTools(pi: ExtensionAPI): void {
565
+ const state = createClaudeCodeState();
566
+
567
+ // 扩展实例在进程启动 / /reload / /new / /resume / /fork 时重建,内存里的
568
+ // 已读记账随之丢失。这里从当前分支的历史工具结果里恢复:digest 是当时的值,
569
+ // 若文件在此期间被外部修改,Edit/Write 时的指纹对比仍会要求重新 Read,
570
+ // 防呆语义不因重建而弱化。
571
+ pi.on("session_start", (_event, ctx) => {
572
+ restoreFileReads(state, ctx.sessionManager);
573
+ });
574
+
575
+ // rewind / 树内跳转走 navigateTree → branch(),只发 session_tree 不发
576
+ // session_start,扩展实例也不重建。这里同样重放当前分支,丢弃被抛弃分支
577
+ // 的记账,避免 state 与当前分支脱节。
578
+ pi.on("session_tree", (_event, ctx) => {
579
+ restoreFileReads(state, ctx.sessionManager);
580
+ });
581
+
582
+ registerFileTools(pi, state);
583
+ }
@@ -154,3 +154,8 @@ export function registerGlobTool(pi: ExtensionAPI): void {
154
154
  },
155
155
  });
156
156
  }
157
+
158
+ // 独立扩展入口:spawn-agent 子代理声明 `Glob` 工具时按本文件 `-e` 加载,
159
+ // 无需经 index.ts / search.ts 聚合。registerGlobTool 本身就是 (pi) => void,
160
+ // 可直接作为扩展 factory。
161
+ export default registerGlobTool;
@@ -342,3 +342,8 @@ export function registerGrepTool(pi: ExtensionAPI): void {
342
342
  },
343
343
  });
344
344
  }
345
+
346
+ // 独立扩展入口:spawn-agent 子代理声明 `Grep` 工具时按本文件 `-e` 加载,
347
+ // 无需经 index.ts / search.ts 聚合。registerGrepTool 本身就是 (pi) => void,
348
+ // 可直接作为扩展 factory。
349
+ export default registerGrepTool;
@@ -1,53 +1,19 @@
1
- import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
1
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
2
 
3
- import { type ClaudeCodeState, createClaudeCodeState, deserializeReads } from "./common.js";
4
- import { registerFileTools } from "./files.js";
3
+ import claudeCodeFileTools from "./files.js";
5
4
  import { registerSearchTools } from "./search.js";
6
5
  import { registerSessionTools } from "./session-tools.js";
7
6
  import { registerShellTools } from "./shell.js";
8
7
 
9
- /** 会更新 reads state 并随 details 持久化快照的工具名。 */
10
- const FILE_TOOL_NAMES = new Set(["Read", "Edit", "Write"]);
11
-
12
8
  /**
13
- * 从当前分支的历史工具结果重建已读记账。先清空再重放,保证 state 只反映
14
- * 当前分支:rewind / fork / resume 后,被抛弃分支上的 Read 不再残留。
9
+ * Claude Code 风格工具集入口:聚合 files / search / shell / session 四组工具。
10
+ *
11
+ * reads state 的创建与 session 恢复归 files.ts 所有(见其 default export),
12
+ * 本文件不再持有 state;spawn-agent 子代理按工具名直接 `-e` 加载各模块文件
13
+ * (files.ts / grep.ts / glob.ts 均为独立扩展入口)。
15
14
  */
16
- function restoreReads(
17
- state: ClaudeCodeState,
18
- sessionManager: ExtensionContext["sessionManager"],
19
- ): void {
20
- state.reads.clear();
21
- for (const entry of sessionManager.getBranch()) {
22
- if (entry.type !== "message" || entry.message.role !== "toolResult") continue;
23
- if (!FILE_TOOL_NAMES.has(entry.message.toolName)) continue;
24
- const details = entry.message.details as { reads?: unknown } | undefined;
25
- if (!details?.reads) continue;
26
- for (const [filePath, snapshot] of deserializeReads(details.reads)) {
27
- state.reads.set(filePath, snapshot);
28
- }
29
- }
30
- }
31
-
32
15
  export default function claudeCodeTools(pi: ExtensionAPI): void {
33
- const state = createClaudeCodeState();
34
-
35
- // 扩展实例在进程启动 / /reload / /new / /resume / /fork 时重建,内存里的
36
- // 已读记账随之丢失。这里从当前分支的历史工具结果里恢复:digest 是当时的值,
37
- // 若文件在此期间被外部修改,Edit/Write 时的指纹对比仍会要求重新 Read,
38
- // 防呆语义不因重建而弱化。
39
- pi.on("session_start", (_event, ctx) => {
40
- restoreReads(state, ctx.sessionManager);
41
- });
42
-
43
- // rewind / 树内跳转走 navigateTree → branch(),只发 session_tree 不发
44
- // session_start,扩展实例也不重建。这里同样重放当前分支,丢弃被抛弃分支
45
- // 的记账,避免 state 与当前分支脱节。
46
- pi.on("session_tree", (_event, ctx) => {
47
- restoreReads(state, ctx.sessionManager);
48
- });
49
-
50
- registerFileTools(pi, state);
16
+ claudeCodeFileTools(pi);
51
17
  registerSearchTools(pi);
52
18
  registerShellTools(pi);
53
19
  registerSessionTools(pi);
@@ -12,7 +12,9 @@
12
12
  * `onUpdate`, the same channel the built-in bash tool uses for live output.
13
13
  * Progress is a rolling log: `tool: <name>` lines for tool calls and
14
14
  * `text: <content>` lines for completed text blocks, keeping the last
15
- * `MAX_PROGRESS_LINES` lines.
15
+ * `MAX_PROGRESS_LINES` lines. Consecutive tool calls are merged into a
16
+ * single `tool:` line (`read x 2, glob`), so a burst of tool calls does
17
+ * not flood the window; any text block starts a new line.
16
18
  *
17
19
  * Security default: without an explicit `tools:` in the frontmatter, the
18
20
  * subagent only gets read-only tools (read/grep/find/ls) — no bash/write/edit.
@@ -345,12 +347,44 @@ export async function runAgent(
345
347
  });
346
348
 
347
349
  let logLines: string[] = [];
350
+ // 工具调用行合并:连续的 tool_execution_start 事件合并在同一 `tool:` 行
351
+ // (如 `tool: read x 2, glob`),相同工具名连续出现时计为 `name x N`,
352
+ // 不同名按调用顺序罗列;任何非工具行都会打断合并。
353
+ let toolLineSegments: string[] = [];
354
+ let toolLine: { name: string; count: number } | undefined;
355
+
356
+ const toolSegment = (name: string, count: number) => (count > 1 ? `${name} x ${count}` : name);
348
357
 
349
358
  const pushLogLine = (line: string) => {
350
359
  logLines.push(line);
351
360
  if (logLines.length > MAX_PROGRESS_LINES) {
352
361
  logLines = logLines.slice(-MAX_PROGRESS_LINES);
353
362
  }
363
+ // 任何非工具行都会打断工具调用合并,下一批调用另起一行。
364
+ toolLine = undefined;
365
+ };
366
+
367
+ const appendToolLine = (name: string) => {
368
+ const firstInBatch = toolLine === undefined;
369
+ if (toolLine === undefined) {
370
+ toolLineSegments = [];
371
+ toolLine = { name, count: 1 };
372
+ } else if (toolLine.name === name) {
373
+ toolLine.count++;
374
+ } else {
375
+ toolLineSegments.push(toolSegment(toolLine.name, toolLine.count));
376
+ toolLine = { name, count: 1 };
377
+ }
378
+ const line = `tool: ${[...toolLineSegments, toolSegment(toolLine.name, toolLine.count)].join(", ")}`;
379
+ if (firstInBatch) {
380
+ logLines.push(line);
381
+ if (logLines.length > MAX_PROGRESS_LINES) {
382
+ logLines = logLines.slice(-MAX_PROGRESS_LINES);
383
+ }
384
+ } else {
385
+ logLines[logLines.length - 1] = line;
386
+ }
387
+ emitUpdate();
354
388
  };
355
389
 
356
390
  const emitUpdate = () => {
@@ -431,9 +465,7 @@ export async function runAgent(
431
465
  break;
432
466
  }
433
467
  case "tool_execution_start": {
434
- pushLogLine(`tool: ${event.toolName}`);
435
- emitUpdate();
436
-
468
+ appendToolLine(event.toolName);
437
469
  break;
438
470
  }
439
471
  case "message_end": {