{"locale":"zh-CN","source":{"rawBase":"https://raw.githubusercontent.com/earendil-works/pi/main/packages/coding-agent/docs","githubBase":"https://github.com/earendil-works/pi/blob/main/packages/coding-agent/docs","editBase":"https://github.com/earendil-works/pi/edit/main/packages/coding-agent/docs"},"redirects":[{"from":"/docs/latest/session","to":"/docs/latest/session-format"},{"from":"/docs/latest/tree","to":"/docs/latest/sessions"}],"fileToSlug":{"compaction.md":"compaction","containerization.md":"containerization","custom-provider.md":"custom-provider","development.md":"development","environment-variables.md":"environment-variables","extensions.md":"extensions","index.md":"index","json.md":"json","keybindings.md":"keybindings","llama-cpp.md":"llama-cpp","models.md":"models","packages.md":"packages","prompt-templates.md":"prompt-templates","providers.md":"providers","quickstart.md":"quickstart","rpc.md":"rpc","sdk.md":"sdk","security.md":"security","session-format.md":"session-format","sessions.md":"sessions","settings.md":"settings","shell-aliases.md":"shell-aliases","skills.md":"skills","terminal-setup.md":"terminal-setup","termux.md":"termux","themes.md":"themes","tmux.md":"tmux","tui.md":"tui","usage.md":"usage","windows.md":"windows"},"pages":{"zh-CN":{"compaction":{"title":"压缩和分支汇总","markdown":"法学硕士的背景窗口有限。当对话变得太长时，Pi 使用压缩来总结旧内容，同时保留最近的工作。本页涵盖了自动压缩和branch summarization。\n\n**源文件** ([pi-mono](https://github.com/earendil-works/pi-mono)):\n- [`packages/coding-agent/src/core/compaction/compaction.ts`](https://github.com/earendil-works/pi-mono/blob/main/packages/coding-agent/src/core/compaction/compaction.ts) - 自动压缩逻辑\n- [`packages/coding-agent/src/core/compaction/branch-summarization.ts`](https://github.com/earendil-works/pi-mono/blob/main/packages/coding-agent/src/core/compaction/branch-summarization.ts) - 分支总结\n- [`packages/coding-agent/src/core/compaction/utils.ts`](https://github.com/earendil-works/pi-mono/blob/main/packages/coding-agent/src/core/compaction/utils.ts) - 共享实用程序（文件跟踪、序列化）\n- [`packages/coding-agent/src/core/session-manager.ts`](https://github.com/earendil-works/pi-mono/blob/main/packages/coding-agent/src/core/session-manager.ts) - 条目类型（`CompactionEntry`、`BranchSummaryEntry`）\n- [`packages/coding-agent/src/core/extensions/types.ts`](https://github.com/earendil-works/pi-mono/blob/main/packages/coding-agent/src/core/extensions/types.ts) - 扩展事件类型\n\n对于项目中的 TypeScript 定义，请检查 `node_modules/@earendil-works/pi-coding-agent/dist/`。\n\n## 概览\n\nPi有两种总结机制：\n\n| 机制 | 扳机 | 目的 |\n|-----------|---------|---------|\n| 压缩 | 上下文超过阈值，或`/compact` | 总结旧消息以释放上下文 |\n| 分支总结 | `/tree`导航 | 切换分支时保留上下文 |\n\n两者都使用相同的结构化摘要格式并累积跟踪文件操作。压缩和分支摘要请求使用新的路由会话 ID，并且在提供程序支持的情况下禁用提示缓存写入，因为这些一次性提示不太可能被重用。\n\n## 压缩\n\n### 当它触发时\n\n自动压缩在以下情况下触发：\n\n```\ncontextTokens > contextWindow - reserveTokens\n```\n\n默认情况下，`reserveTokens`为16384个令牌（可在`~/.pi/agent/settings.json`或`<project-dir>/.pi/settings.json`中配置）。这为LLM的回应留下了空间。\n\n您还可以使用 `/compact [instructions]` 手动触发，其中可选指令重点关注摘要。\n\n### 它是如何运作的\n\n1. **查找切点**：从最新消息向后走，累积令牌估计，直到达到`keepRecentTokens`（默认20k，可在`~/.pi/agent/settings.json`或`<project-dir>/.pi/settings.json`中配置）\n2. **提取消息**：收集从先前保留的边界（或会话开始）到切入点的消息\n3. **生成摘要**：调用LLM以结构化格式进行摘要，将先前的摘要作为迭代上下文（如果存在）传递\n4. **追加条目**：保存 `CompactionEntry` 和摘要以及 `firstKeptEntryId`\n5. **重新加载**：会话重新加载，使用从`firstKeptEntryId`开始的摘要+消息\n\n```\nBefore compaction:\n\n  entry:  0     1     2     3      4     5     6      7      8     9\n        ┌─────┬─────┬─────┬─────┬──────┬─────┬──── ─┬──────┬─────┬─────┐\n        │ hdr │ usr │ ass │ tool │ usr │ ass │ tool │ tool │ ass │ tool│\n        └─────┴─────┴─────┴──────┴─────┴─────┴──────┴──────┴─────┴─────┘\n                └────────┬───────┘ └──────────────┬──────────────┘\n               messagesToSummarize            kept messages\n                                   ↑\n                          firstKeptEntryId (entry 4)\n\nAfter compaction (new entry appended):\n\n  entry:  0     1     2     3      4     5     6      7      8     9     10\n        ┌─────┬─────┬─────┬─────┬──────┬─────┬──── ─┬──────┬─────┬─────┬─────┐\n        │ hdr │ usr │ ass │ tool │ usr │ ass │ tool │ tool │ ass │ tool│ cmp │\n        └─────┴─────┴─────┴──────┴─────┴─────┴──────┴──────┴─────┴─────┴─────┘\n               └──────────┬──────┘ └──────────────────────┬───────────────────┘\n                 not sent to LLM                    sent to LLM\n                                                         ↑\n                                              starts from firstKeptEntryId\n\nWhat the LLM sees:\n\n  ┌────────┬─────────┬─────┬─────┬──────┬──────┬─────┬──────┐\n  │ system │ summary │ usr │ ass │ tool │ tool │ ass │ tool │\n  └────────┴─────────┴─────┴─────┴──────┴──────┴─────┴──────┘\n       ↑         ↑      └─────────────────┬────────────────┘\n    prompt   from cmp          messages from firstKeptEntryId\n```\n\n在重复压缩时，汇总的跨度从先前压缩的保留边界（`firstKeptEntryId`）开始，而不是从压缩条目本身开始，如果在路径中找不到该保留条目，则返回到先前压缩后的条目。这通过将早期压缩中幸存下来的消息也包含在下一个摘要过程中来保留它们。在写入新的 `CompactionEntry` 之前，Pi 还会根据重建的会话上下文重新计算 `tokensBefore`，因此令牌计数反映了被替换的实际预压缩上下文。\n\n### 分叉转弯\n\n“轮次”以用户消息开始，包括所有助手响应和工具调用，直到下一条用户消息。通常，压实会在转弯边界处进行切割。\n\n当单圈超过 `keepRecentTokens` 时，切入点会在转弯中途出现一条辅助消息。这是一个“分裂回合”：\n\n```\nSplit turn (one huge turn exceeds budget):\n\n  entry:  0     1     2      3     4      5      6     7      8\n        ┌─────┬─────┬─────┬──────┬─────┬──────┬──────┬─────┬──────┐\n        │ hdr │ usr │ ass │ tool │ ass │ tool │ tool │ ass │ tool │\n        └─────┴─────┴─────┴──────┴─────┴──────┴──────┴─────┴──────┘\n                ↑                                     ↑\n         turnStartIndex = 1                  firstKeptEntryId = 7\n                │                                     │\n                └──── turnPrefixMessages (1-6) ───────┘\n                                                      └── kept (7-8)\n\n  isSplitTurn = true\n  messagesToSummarize = []  (no complete turns before)\n  turnPrefixMessages = [usr, ass, tool, ass, tool, tool]\n```\n\n对于分割回合，Pi 生成两个摘要并将它们合并：\n1. **历史摘要**：以前的背景（如果有）\n2. **回合前缀总结**：分割回合的早期部分\n\n### 切点规则\n\n有效的切点是：\n- 用户留言\n- 助理消息\n- Bash执行消息\n- 自定义消息（custom_message、branch_summary）\n\n切勿削减工具结果（它们必须保留工具调用）。\n\n### 压实入口结构\n\n定义于[`session-manager.ts`](https://github.com/earendil-works/pi-mono/blob/main/packages/coding-agent/src/core/session-manager.ts)：\n\n```typescript\ninterface CompactionEntry<T = unknown> {\n  type: \"compaction\";\n  id: string;\n  parentId: string;\n  timestamp: number;\n  summary: string;\n  firstKeptEntryId: string;\n  tokensBefore: number;\n  usage?: Usage;       // LLM usage that generated the summary\n  fromHook?: boolean;  // true if provided by extension (legacy field name)\n  details?: T;         // implementation-specific data\n}\n\n// Default compaction uses this for details (from compaction.ts):\ninterface CompactionDetails {\n  readFiles: string[];\n  modifiedFiles: string[];\n}\n```\n\nExtensions可以在`details`中存储任何JSON可序列化的数据。默认压缩跟踪文件操作，但自定义扩展实现可以使用自己的结构。生成的和扩展提供的摘要会存储其 LLM `usage`（如果可用），因此会话总数包括摘要工作。\n\n具体实现参见[`prepareCompaction()`](https://github.com/earendil-works/pi-mono/blob/main/packages/coding-agent/src/core/compaction/compaction.ts)和[`compact()`](https://github.com/earendil-works/pi-mono/blob/main/packages/coding-agent/src/core/compaction/compaction.ts)。对于直接编程摘要，`generateSummary()` 返回摘要文本，`generateSummaryWithUsage()` 返回`{ text, usage }`。\n\n## 分支总结\n\n### 当它触发时\n\n当您使用 `/tree` 导航到不同的分支时，Pi 会总结您要离开的工作。这会将左分支的上下文注入到新分支中。\n\n### 它是如何运作的\n\n1. **找到共同祖先**：新旧位置共享的最深节点\n2. **收集条目**：从老叶子回到共同的祖先\n3. **准备预算**：包括不超过代币预算的消息（最新的优先）\n4. **生成摘要**：以结构化格式调用LLM\n5. **追加条目**：在导航点保存`BranchSummaryEntry`\n\n```\nTree before navigation:\n\n         ┌─ B ─ C ─ D (old leaf, being abandoned)\n    A ───┤\n         └─ E ─ F (target)\n\nCommon ancestor: A\nEntries to summarize: B, C, D\n\nAfter navigation with summary:\n\n         ┌─ B ─ C ─ D\n    A ───┤\n         └─ E ─ F ─ [summary of B,C,D] (new leaf)\n```\n\n### 累积文件追踪\n\n压缩和branch summarization都累积跟踪文件。生成摘要时，pi 从以下位置提取文件操作：\n- 正在汇总的消息中的工具调用\n- 先前的压缩或分支摘要`details`（如果有）\n\n这意味着文件跟踪会在多个压缩或嵌套分支摘要中累积，从而保留读取和修改文件的完整历史记录。\n\n### BranchSummaryEntry 结构\n\n定义于[`session-manager.ts`](https://github.com/earendil-works/pi-mono/blob/main/packages/coding-agent/src/core/session-manager.ts)：\n\n```typescript\ninterface BranchSummaryEntry<T = unknown> {\n  type: \"branch_summary\";\n  id: string;\n  parentId: string;\n  timestamp: number;\n  summary: string;\n  fromId: string;      // Entry we navigated from\n  usage?: Usage;       // LLM usage that generated the summary\n  fromHook?: boolean;  // true if provided by extension (legacy field name)\n  details?: T;         // implementation-specific data\n}\n\n// Default branch summarization uses this for details (from branch-summarization.ts):\ninterface BranchSummaryDetails {\n  readFiles: string[];\n  modifiedFiles: string[];\n}\n```\n\n与压缩相同，扩展可以将自定义数据存储在`details`中。\n\n具体实现请参见[`collectEntriesForBranchSummary()`](https://github.com/earendil-works/pi-mono/blob/main/packages/coding-agent/src/core/compaction/branch-summarization.ts)、[`prepareBranchEntries()`](https://github.com/earendil-works/pi-mono/blob/main/packages/coding-agent/src/core/compaction/branch-summarization.ts)和[`generateBranchSummary()`](https://github.com/earendil-works/pi-mono/blob/main/packages/coding-agent/src/core/compaction/branch-summarization.ts)。\n\n## 摘要格式\n\n压缩和branch summarization都使用相同的结构化格式：\n\n```markdown\n## Goal\n[What the user is trying to accomplish]\n\n## Constraints & Preferences\n- [Requirements mentioned by user]\n\n## Progress\n### Done\n- [x] [Completed tasks]\n\n### In Progress\n- [ ] [Current work]\n\n### Blocked\n- [Issues, if any]\n\n## Key Decisions\n- **[Decision]**: [Rationale]\n\n## Next Steps\n1. [What should happen next]\n\n## Critical Context\n- [Data needed to continue]\n\n<read-files>\npath/to/file1.ts\npath/to/file2.ts\n</read-files>\n\n<modified-files>\npath/to/changed.ts\n</modified-files>\n```\n\n### 消息序列化\n\n在汇总之前，消息通过[`serializeConversation()`](https://github.com/earendil-works/pi-mono/blob/main/packages/coding-agent/src/core/compaction/utils.ts)序列化为文本：\n\n```\n[User]: What they said\n[Assistant thinking]: Internal reasoning\n[Assistant]: Response text\n[Assistant tool calls]: read(path=\"foo.ts\"); edit(path=\"bar.ts\", ...)\n[Tool result]: Output from tool\n```\n\n这会阻止模型将其视为继续对话。\n\n工具结果在序列化期间被截断为 2000 个字符。超出该限制的内容将替换为指示被截断字符数的标记。这使汇总请求保持在合理的令牌预算内，因为工具结果（尤其是来自`read`和`bash`）通常是上下文大小的最大贡献者。\n\n## 通过 Extensions 自定义摘要\n\nExtensions可以拦截并自定义compaction和branch summarization。有关事件类型定义，请参阅[`extensions/types.ts`](https://github.com/earendil-works/pi-mono/blob/main/packages/coding-agent/src/core/extensions/types.ts)。\n\n### 压缩前的会话\n\n在自动压缩或 `/compact` 之前触发。可以取消或提供自定义摘要。请参阅类型文件中的 `SessionBeforeCompactEvent` 和 `CompactionPreparation`。\n\n```typescript\npi.on(\"session_before_compact\", async (event, ctx) => {\n  const { preparation, branchEntries, customInstructions, reason, willRetry, signal } = event;\n\n  // preparation.messagesToSummarize - messages to summarize\n  // preparation.turnPrefixMessages - split turn prefix (if isSplitTurn)\n  // preparation.previousSummary - previous compaction summary\n  // preparation.fileOps - extracted file operations\n  // preparation.tokensBefore - context tokens before compaction\n  // preparation.firstKeptEntryId - where kept messages start\n  // preparation.settings - compaction settings\n\n  // branchEntries - all entries on current branch (for custom state)\n  // reason - \"manual\" (/compact), \"threshold\", or \"overflow\"\n  // willRetry - whether the aborted turn is retried after compaction (overflow recovery)\n  // signal - AbortSignal (pass to LLM calls)\n\n  // Cancel:\n  return { cancel: true };\n\n  // Custom summary:\n  return {\n    compaction: {\n      summary: \"Your summary...\",\n      firstKeptEntryId: preparation.firstKeptEntryId,\n      tokensBefore: preparation.tokensBefore,\n      // usage: summaryResponse.usage, // Optional; included in session totals\n      details: { /* custom data */ },\n    }\n  };\n});\n```\n\n#### 将消息转换为文本\n\n要使用您自己的模型生成摘要，请使用 `serializeConversation` 将消息转换为文本：\n\n```typescript\nimport { convertToLlm, serializeConversation } from \"@earendil-works/pi-coding-agent\";\n\npi.on(\"session_before_compact\", async (event, ctx) => {\n  const { preparation } = event;\n  \n  // Convert AgentMessage[] to Message[], then serialize to text\n  const conversationText = serializeConversation(\n    convertToLlm(preparation.messagesToSummarize)\n  );\n  // Returns:\n  // [User]: message text\n  // [Assistant thinking]: thinking content\n  // [Assistant]: response text\n  // [Assistant tool calls]: read(path=\"...\"); bash(command=\"...\")\n  // [Tool result]: output text\n\n  // Now send to your model for summarization\n  const { summary, usage } = await myModel.summarize(conversationText);\n  \n  return {\n    compaction: {\n      summary,\n      firstKeptEntryId: preparation.firstKeptEntryId,\n      tokensBefore: preparation.tokensBefore,\n      usage,\n    }\n  };\n});\n```\n\n有关使用不同模型的完整示例，请参阅[custom-compaction.ts](../examples/extensions/custom-compaction.ts)。\n\n### 树之前的会话\n\n在 `/tree` 导航之前触发。无论用户是否选择总结，总是触发。可以取消导航或提供自定义摘要。\n\n```typescript\npi.on(\"session_before_tree\", async (event, ctx) => {\n  const { preparation, signal } = event;\n\n  // preparation.targetId - where we're navigating to\n  // preparation.oldLeafId - current position (being abandoned)\n  // preparation.commonAncestorId - shared ancestor\n  // preparation.entriesToSummarize - entries that would be summarized\n  // preparation.userWantsSummary - whether user chose to summarize\n\n  // Cancel navigation entirely:\n  return { cancel: true };\n\n  // Provide custom summary (only used if userWantsSummary is true):\n  if (preparation.userWantsSummary) {\n    return {\n      summary: {\n        summary: \"Your summary...\",\n        // usage: summaryResponse.usage, // Optional; included in session totals\n        details: { /* custom data */ },\n      }\n    };\n  }\n});\n```\n\n请参阅类型文件中的 `SessionBeforeTreeEvent` 和 `TreePreparation`。\n\n## 设置\n\n在`~/.pi/agent/settings.json`或`<project-dir>/.pi/settings.json`中配置压缩：\n\n```json\n{\n  \"compaction\": {\n    \"enabled\": true,\n    \"reserveTokens\": 16384,\n    \"keepRecentTokens\": 20000\n  }\n}\n```\n\n| 环境 | 默认 | 描述 |\n|---------|---------|-------------|\n| `enabled` | `true` | 启用自动压缩 |\n| `reserveTokens` | `16384` | 为 LLM 响应保留的代币 |\n| `keepRecentTokens` | `20000` | 最近要保留的令牌（未汇总） |\n\n使用 `\"enabled\": false` 禁用自动压缩。您仍然可以使用 `/compact` 手动压缩。","sourceFile":"compaction.md"},"containerization":{"title":"容器化","markdown":"默认情况下，Pi以所有权限运行，但在某些情况下，您需要更多地控制Pi可以写入的目录以及它具有哪些访问权限。\n\n有两个一般选项。你可以\n1. 在隔离环境中运行整个 `pi` 进程，或者\n2. 在主机上运行 `pi` 并将工具执行路由到隔离环境中。\n\n## 选择图案\n\n| 图案 | 什么是孤立的 | 最适合 | 笔记 |\n| --- | --- | --- | --- |\n| Gondolin 扩展 | 内置工具和`!`命令 | 本地微虚拟机隔离，同时在主机上保持身份验证 | 参见[`examples/extensions/gondolin/`](../examples/extensions/gondolin/)。 |\n| 普通Docker | 本地容器中的整个`pi`过程 | 简单的本地隔离 | 提供者API key进入容器。 |\n| OpenShell | 整个`pi`过程在政策控制sandbox中 | 本地或远程管理 sandbox | 需要OpenShell网关 |\n\nExtensions 在 `pi` 进程运行的地方运行。如果您使用工具路由扩展运行主机 `pi`，其他自定义扩展工具仍会在主机上运行，​​除非它们也委托其操作。\n\n## Gondolin\n\n[Gondolin](https://github.com/earendil-works/gondolin) 是本地 Linux 微型虚拟机。\n当您希望在主机上使用 `pi` 但将所有内置工具路由到 VM 中时，请使用 [example extension](../examples/extensions/gondolin)。\n\n设置：\n\n```bash\ncp -R packages/coding-agent/examples/extensions/gondolin ~/.pi/agent/extensions/gondolin\ncd ~/.pi/agent/extensions/gondolin\nnpm install --ignore-scripts\n```\n\n从要安装的项目运行：\n\n```bash\ncd /path/to/project\npi -e ~/.pi/agent/extensions/gondolin\n```\n\n该扩展将主机 cwd 安装在虚拟机中的 `/workspace` 处，并覆盖 `read`、`write`、`edit`、`bash`、`grep`、`find` 和 `ls`。\n用户 `!` 命令也被路由到虚拟机中。\n`/workspace`下的文件更改写入主机。\n\n要求：Node.js >= 23.6.0（对于 `@earendil-works/gondolin`），加上 QEMU（需要通过包管理器安装）。\n\n## 普通Docker\n\n当您想要最简单的本地容器边界时，请在Docker中运行整个`pi`流程。\n\n`Dockerfile.pi`:\n\n```dockerfile\nFROM node:24-bookworm-slim\n\nRUN apt-get update \\\n  && apt-get install -y --no-install-recommends bash ca-certificates git ripgrep \\\n  && rm -rf /var/lib/apt/lists/*\nRUN npm install -g --ignore-scripts @earendil-works/pi-coding-agent\n\nWORKDIR /workspace\nENTRYPOINT [\"pi\"]\n```\n\n构建并运行：\n\n```bash\ndocker build -t pi-sandbox -f Dockerfile.pi .\n\ndocker run --rm -it \\\n  -e ANTHROPIC_API_KEY \\\n  -v \"$PWD:/workspace\" \\\n  -v pi-agent-home:/root/.pi/agent \\\n  pi-sandbox\n```\n\n`-v \"$PWD:/workspace\"` 将当前目录挂载到位于 /workspace 的容器中，这样在 Docker 内的 `/workspace` 中的读写操作将直接影响您的主机文件，如 Gondolin 示例中所示。\n\n如果您需要容器本地设置和会话，请使用 `/root/.pi/agent` 的命名卷。挂载主机 `~/.pi/agent` 会将主机身份验证和会话文件公开给容器。\n\n## OpenShell\n\n当您需要具有文件系统、进程、网络、凭据和推理控制的策略控制 sandbox 时，请使用[NVIDIA OpenShell](https://docs.nvidia.com/openshell/about/overview)。\nOpenShell 可以通过Docker、Podman 或 VM 运行时支持的本地网关，或通过远程 Kubernetes 网关运行 sandboxes。\n\n每个sandbox都需要一个活动网关。\n在创建 sandbox 之前注册并选择一个：\n\n```bash\nopenshell gateway add <gateway-url> --name <name>\nopenshell gateway select <name>\n```\n\n在 OpenShell sandbox 内启动 `pi`：\n\n```bash\nopenshell sandbox create --name pi-sandbox --from pi -- pi\n```\n\n在这种模式中，整个`pi`进程在sandbox内部运行。\n内置工具、`!` 命令和扩展工具在OpenShell 边界内执行。\n\n如果网关是远程的，则项目文件不会从主机绑定安装，这意味着 sandbox 中的写入不会反映在您的计算机上。\n在sandbox内克隆存储库或使用OpenShell文件传输命令：\n\n```bash\nopenshell sandbox upload pi-sandbox ./repo /workspace\nopenshell sandbox download pi-sandbox /workspace/repo ./repo-out\n```\n\nOpenShell 提供商可以将原始模型 API key 保留在 sandbox 之外。\n配置推理路由后，sandbox内的代码可以调用`https://inference.local`，并且网关将配置的提供者凭据注入上游。\n如果您希望模型流量使用此路由，请配置 Pi 使用相应的 OpenAI 兼容或 Anthropic 兼容端点。","sourceFile":"containerization.md"},"custom-provider":{"title":"定制Providers","markdown":"Extensions可以通过`pi.registerProvider()`注册自定义模型提供者。这使得：\n\n- **代理** - 通过公司代理或 API 网关路由请求\n- **自定义端点** - 使用自托管或私有模型部署\n- **OAuth/SSO** - 为企业提供商添加身份验证流程\n- **自定义 APIs** - 为非标准 LLM APIs 实现流式传输\n\n## 示例Extensions\n\n请参阅这些完整的提供商示例：\n\n- [`examples/extensions/custom-provider-anthropic/`](../examples/extensions/custom-provider-anthropic/)\n- [`examples/extensions/custom-provider-gitlab-duo/`](../examples/extensions/custom-provider-gitlab-duo/)\n\n## 目录\n\n- [Example Extensions](#example-extensions)\n- [Quick Reference](#quick-reference)\n- [Override Existing Provider](#override-existing-provider)\n- [Register New Provider](#register-new-provider)\n- [Unregister Provider](#unregister-provider)\n- [OAuth Support](#oauth-support)\n- [Custom Streaming API](#custom-streaming-api)\n- [Context Overflow Errors](#context-overflow-errors)\n- [Testing Your Implementation](#testing-your-implementation)\n- [Config Reference](#config-reference)\n- [Model Definition Reference](#model-definition-reference)\n\n## 快速参考\n\nExtensions 可以注册完整的 pi-ai `Provider` 或使用旧的提供程序配置表单。当需要自定义身份验证、过滤、刷新或流行为时，首选完整的提供程序。 Pi 组成 `models.json` 覆盖上面注册的本地提供者。\n\n```typescript\nimport { createProvider, openAICompletionsApi } from \"@earendil-works/pi-ai\";\nimport type { ExtensionAPI } from \"@earendil-works/pi-coding-agent\";\n\nexport default function (pi: ExtensionAPI) {\n  pi.registerProvider(createProvider({\n    id: \"native-local\",\n    name: \"Native Local\",\n    baseUrl: \"http://localhost:8080/v1\",\n    auth: {\n      apiKey: {\n        name: \"Local server API key\",\n        async login(interaction) {\n          return {\n            type: \"api_key\",\n            key: await interaction.prompt({ type: \"secret\", message: \"API key\" })\n          };\n        },\n        async resolve({ credential }) {\n          return credential?.key\n            ? { auth: { apiKey: credential.key }, source: \"stored API key\" }\n            : undefined;\n        }\n      }\n    },\n    models: [],\n    api: openAICompletionsApi()\n  }));\n\n  // Legacy provider-config form:\n  // Override baseUrl for existing provider\n  pi.registerProvider(\"anthropic\", {\n    baseUrl: \"https://proxy.example.com\"\n  });\n\n  // Register new provider with models\n  pi.registerProvider(\"my-provider\", {\n    name: \"My Provider\",\n    baseUrl: \"https://api.example.com\",\n    apiKey: \"$MY_API_KEY\",\n    api: \"openai-completions\",\n    models: [\n      {\n        id: \"my-model\",\n        name: \"My Model\",\n        reasoning: false,\n        input: [\"text\", \"image\"],\n        cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },\n        contextWindow: 128000,\n        maxTokens: 4096\n      }\n    ]\n  });\n}\n```\n\n扩展工厂也可以是`async`。对于动态模型发现，在工厂中获取并注册模型而不是`session_start`。 pi 在启动继续之前等待工厂，因此提供程序在交互式启动期间和 `pi --list-models` 期间可用。\n\n## 覆盖现有提供者\n\n最简单的用例：通过代理重定向现有提供者。\n\n```typescript\n// All Anthropic requests now go through your proxy\npi.registerProvider(\"anthropic\", {\n  baseUrl: \"https://proxy.example.com\"\n});\n\n// Add custom headers to OpenAI requests\npi.registerProvider(\"openai\", {\n  headers: {\n    \"X-Custom-Header\": \"value\"\n  }\n});\n\n// Both baseUrl and headers\npi.registerProvider(\"google\", {\n  baseUrl: \"https://ai-gateway.corp.com/google\",\n  headers: {\n    \"X-Corp-Auth\": \"$CORP_AUTH_TOKEN\"  // env var or literal\n  }\n});\n```\n\n当仅提供 `baseUrl` 和/或 `headers`（无 `models`）时，该提供者的所有现有模型都将与新端点一起保留。\n\n## 注册新提供商\n\n要添加全新的提供程序，请指定 `models` 以及所需的配置。\n\n如果模型列表来自远程端点，请使用异步扩展工厂：\n\n```typescript\nimport type { ExtensionAPI } from \"@earendil-works/pi-coding-agent\";\n\nexport default async function (pi: ExtensionAPI) {\n  const response = await fetch(\"http://localhost:1234/v1/models\");\n  const payload = (await response.json()) as {\n    data: Array<{\n      id: string;\n      name?: string;\n      context_window?: number;\n      max_tokens?: number;\n    }>;\n  };\n\n  pi.registerProvider(\"local-openai\", {\n    baseUrl: \"http://localhost:1234/v1\",\n    apiKey: \"$LOCAL_OPENAI_API_KEY\",\n    api: \"openai-completions\",\n    models: payload.data.map((model) => ({\n      id: model.id,\n      name: model.name ?? model.id,\n      reasoning: false,\n      input: [\"text\"],\n      cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },\n      contextWindow: model.context_window ?? 128000,\n      maxTokens: model.max_tokens ?? 4096,\n    })),\n  });\n}\n```\n\n这会在启动完成之前注册获取的模型。\n\n```typescript\npi.registerProvider(\"my-llm\", {\n  baseUrl: \"https://api.my-llm.com/v1\",\n  apiKey: \"$MY_LLM_API_KEY\",  // env var reference\n  api: \"openai-completions\",  // which streaming API to use\n  models: [\n    {\n      id: \"my-llm-large\",\n      name: \"My LLM Large\",\n      reasoning: true,        // supports extended thinking\n      input: [\"text\", \"image\"],\n      cost: {\n        input: 3.0,           // $/million tokens\n        output: 15.0,\n        cacheRead: 0.3,\n        cacheWrite: 3.75\n      },\n      contextWindow: 200000,\n      maxTokens: 16384\n    }\n  ]\n});\n```\n\n当提供 `models` 时，它会**替换**该提供者的所有现有模型。\n\n`apiKey`和自定义标头值使用与`models.json`相同的配置值语法：`!command`在开始时对整个值执行命令，`$ENV_VAR`和`${ENV_VAR}`插入环境变量，`$`发出文字``apiKey`和自定义标头值使用与`models.json`相同的配置值语法：`!command`在开始时对整个值执行命令，`$ENV_VAR`和`${ENV_VAR}`插入环境变量，`$`发出文字，`$!`发出文字`!`。\n\n## 取消注册提供商\n\n使用 `pi.unregisterProvider(name)` 删除之前通过 `pi.registerProvider(name,...)` 注册的提供者：\n\n```typescript\n// Register\npi.registerProvider(\"my-llm\", {\n  baseUrl: \"https://api.my-llm.com/v1\",\n  apiKey: \"$MY_LLM_API_KEY\",\n  api: \"openai-completions\",\n  models: [\n    {\n      id: \"my-llm-large\",\n      name: \"My LLM Large\",\n      reasoning: true,\n      input: [\"text\", \"image\"],\n      cost: { input: 3.0, output: 15.0, cacheRead: 0.3, cacheWrite: 3.75 },\n      contextWindow: 200000,\n      maxTokens: 16384\n    }\n  ]\n});\n\n// Later, remove it\npi.unregisterProvider(\"my-llm\");\n```\n\n取消注册会删除该提供程序的动态模型、API key 后备、OAuth 提供程序注册和自定义流处理程序注册。任何被覆盖的内置模型或提供者行为都会被恢复。\n\n初始扩展加载阶段之后进行的调用会立即应用，因此不需要 `/reload`。\n\n### API 类型\n\n`api`字段决定使用哪种流实现：\n\n| API | 用于 |\n|-----|---------|\n| `anthropic-messages` | 人择克劳德 API 及其兼容者 |\n| `openai-completions` | OpenAI 聊天完成 API 和兼容版本 |\n| `openai-responses` | OpenAI 回应 API |\n| `azure-openai-responses` | Azure OpenAI 响应 API |\n| `openai-codex-responses` | OpenAI Codex 回复 API |\n| `mistral-conversations` | 本地米斯特拉尔聊天完成流 |\n| `google-generative-ai` | 谷歌生成人工智能API |\n| `google-vertex` | 谷歌 Vertex AI API |\n| `bedrock-converse-stream` | 亚马逊 Bedrock 匡威 API |\n\n大多数与 OpenAI 兼容的提供商都使用 `openai-completions`。使用模型级别 `thinkingLevelMap` 来实现特定于模型的思维级别，使用 `compat` 来实现提供商的怪癖。 `xhigh` 和 `max` 级别是可选的，需要非空映射条目，并且可能被不支持的孔分隔：\n\n```typescript\nmodels: [{\n  id: \"custom-model\",\n  // ...\n  reasoning: true,\n  thinkingLevelMap: {              // map pi levels to provider values; null hides unsupported levels\n    minimal: null,\n    low: null,\n    medium: null,\n    high: \"default\",\n    xhigh: null,\n    max: \"max\"\n  },\n  compat: {\n    supportsDeveloperRole: false,   // use \"system\" instead of \"developer\"\n    supportsReasoningEffort: true,\n    maxTokensField: \"max_tokens\",   // instead of \"max_completion_tokens\"\n    requiresToolResultName: true,   // tool results need name field\n    thinkingFormat: \"qwen\",        // top-level enable_thinking: true\n    cacheControlFormat: \"anthropic\" // Anthropic-style cache_control markers\n  }\n}]\n```\n\n将 `openrouter` 用于 OpenRouter 样式 `reasoning: { effort }` 控件。将 `together` 用于 Together 样式 `reasoning: { enabled }` 控件；对于`supportsReasoningEffort`，它还发送`reasoning_effort`。对于读取 `chat_template_kwargs.enable_thinking` 并需要 `preserve_thinking` 的本地 Qwen 兼容服务器，请使用 `qwen-chat-template`。\n将 `cacheControlFormat: \"anthropic\"` 用于与 OpenAI 兼容的提供程序，通过 `cache_control` 在系统提示、最后一个工具定义以及最后一个用户、助手或工具结果文本内容上公开人类风格的提示缓存。\n\n对于使用`api: \"anthropic-messages\"`的人类兼容提供者，在其上游模型需要自适应思维的模型或提供者上设置`compat.forceAdaptiveThinking: true`（`thinking.type: \"adaptive\"`加`output_config.effort`）。内置自适应克劳德模型会自动设置此功能。仅针对发出空思维签名并期望重播时 `signature: \"\"` 的提供者设置 `compat.allowEmptySignature: true`。\n\n> 迁移注意：米斯特拉尔从`openai-completions`移至`mistral-conversations`。\n> 对原生 Mistral 模型使用 `mistral-conversations`。\n> 如果您有意通过 `openai-completions` 路由 Mistral 兼容/自定义端点，请根据需要显式设置 `compat` 标志。\n\n### 验证头\n\n如果您的提供商期望 `Authorization: Bearer <key>` 但不使用标准 API，请设置 `authHeader: true`：\n\n```typescript\npi.registerProvider(\"custom-api\", {\n  baseUrl: \"https://api.example.com\",\n  apiKey: \"$MY_API_KEY\",\n  authHeader: true,  // adds Authorization: Bearer header\n  api: \"openai-completions\",\n  models: [...]\n});\n```\n\n每个请求都会解析密钥。显式请求 `Authorization` 标头优先于生成的值。\n\n## OAuth 支持\n\n添加与`/login`集成的OAuth/SSO身份验证：\n\n```typescript\nimport type { OAuthCredentials, OAuthLoginCallbacks } from \"@earendil-works/pi-ai\";\n\npi.registerProvider(\"corporate-ai\", {\n  baseUrl: \"https://ai.corp.com/v1\",\n  api: \"openai-responses\",\n  models: [...],\n  oauth: {\n    name: \"Corporate AI (SSO)\",\n\n    async login(callbacks: OAuthLoginCallbacks): Promise<OAuthCredentials> {\n      const method = await callbacks.onSelect({\n        message: \"Select login method:\",\n        options: [\n          { id: \"browser\", label: \"Browser OAuth\" },\n          { id: \"device\", label: \"Device code\" }\n        ]\n      });\n      if (!method) throw new Error(\"Login cancelled\");\n\n      let code: string;\n      if (method === \"device\") {\n        callbacks.onDeviceCode({\n          userCode: \"ABCD-1234\",\n          verificationUri: \"https://sso.corp.com/device\",\n          intervalSeconds: 5,\n          expiresInSeconds: 900\n        });\n        code = await pollDeviceCodeUntilComplete();\n      } else {\n        callbacks.onAuth({ url: \"https://sso.corp.com/authorize?...\" });\n        code = await callbacks.onPrompt({ message: \"Enter SSO code:\" });\n      }\n\n      // Exchange for tokens (your implementation)\n      const tokens = await exchangeCodeForTokens(code);\n\n      return {\n        refresh: tokens.refreshToken,\n        access: tokens.accessToken,\n        expires: Date.now() + tokens.expiresIn * 1000\n      };\n    },\n\n    async refreshToken(credentials: OAuthCredentials, signal: AbortSignal): Promise<OAuthCredentials> {\n      const tokens = await refreshAccessToken(credentials.refresh, signal);\n      return {\n        refresh: tokens.refreshToken ?? credentials.refresh,\n        access: tokens.accessToken,\n        expires: Date.now() + tokens.expiresIn * 1000\n      };\n    },\n\n    getApiKey(credentials: OAuthCredentials): string {\n      return credentials.access;\n    }\n  }\n});\n```\n\n注册后，用户可以通过`/login corporate-ai`进行身份验证。\n\n### OAuth登录回调\n\n`callbacks` 对象为提供商拥有的流程提供 UI 中立的交互：\n\n```typescript\ninterface OAuthLoginCallbacks {\n  // Open URL in browser (for OAuth redirects)\n  onAuth(params: { url: string }): void;\n\n  // Show device code (for device authorization flow)\n  onDeviceCode(params: {\n    userCode: string;\n    verificationUri: string;\n    intervalSeconds?: number;\n    expiresInSeconds?: number;\n  }): void;\n\n  // Show transient progress\n  onProgress?(message: string): void;\n\n  // Prompt user for input (for manual token entry)\n  onPrompt(params: { message: string }): Promise<string>;\n\n  // Show an interactive selector, e.g. to choose browser OAuth vs device code\n  onSelect(params: {\n    message: string;\n    options: { id: string; label: string }[];\n  }): Promise<string | undefined>;\n}\n```\n\n### OAuth凭证\n\n凭证保存在 `~/.pi/agent/auth.json` 中：\n\n```typescript\ninterface OAuthCredentials {\n  refresh: string;   // Refresh token (for refreshToken())\n  access: string;    // Access token (returned by getApiKey())\n  expires: number;   // Expiration timestamp in milliseconds\n}\n```\n\n## 自定义流媒体API\n\n对于具有非标准API的提供商，实施`streamSimple`。在编写自己的提供程序之前，请先研究现有的提供程序实现：\n\n**参考实现：**\n- [anthropic.ts](https://github.com/earendil-works/pi-mono/blob/main/packages/ai/src/providers/anthropic.ts) - 人为消息 API\n- [mistral.ts](https://github.com/earendil-works/pi-mono/blob/main/packages/ai/src/providers/mistral.ts) - 米斯特拉尔对话 API\n- [openai-completions.ts](https://github.com/earendil-works/pi-mono/blob/main/packages/ai/src/providers/openai-completions.ts) - OpenAI 聊天完成\n- [openai-responses.ts](https://github.com/earendil-works/pi-mono/blob/main/packages/ai/src/providers/openai-responses.ts) - OpenAI 回应 API\n- [google.ts](https://github.com/earendil-works/pi-mono/blob/main/packages/ai/src/providers/google.ts) - 谷歌生成式人工智能\n- [amazon-bedrock.ts](https://github.com/earendil-works/pi-mono/blob/main/packages/ai/src/providers/amazon-bedrock.ts) - AWS 基岩\n\n### 流模式\n\n所有提供商都遵循相同的模式：\n\n```typescript\nimport {\n  type AssistantMessage,\n  type AssistantMessageEventStream,\n  type Context,\n  type Model,\n  type SimpleStreamOptions,\n  calculateCost,\n  createAssistantMessageEventStream,\n} from \"@earendil-works/pi-ai\";\n\nfunction streamMyProvider(\n  model: Model<any>,\n  context: Context,\n  options?: SimpleStreamOptions\n): AssistantMessageEventStream {\n  const stream = createAssistantMessageEventStream();\n\n  (async () => {\n    // Initialize output message\n    const output: AssistantMessage = {\n      role: \"assistant\",\n      content: [],\n      api: model.api,\n      provider: model.provider,\n      model: model.id,\n      usage: {\n        input: 0,\n        output: 0,\n        cacheRead: 0,\n        cacheWrite: 0,\n        totalTokens: 0,\n        cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },\n      },\n      stopReason: \"pending\",\n      timestamp: Date.now(),\n    };\n\n    try {\n      // Push start event\n      stream.push({ type: \"start\", partial: output });\n\n      // Make API request and process response...\n      // Push content events as they arrive and set stopReason from the terminal event.\n      if (output.stopReason === \"pending\") {\n        throw new Error(\"Provider stream ended without a stop reason\");\n      }\n      if (output.stopReason === \"error\" || output.stopReason === \"aborted\") {\n        throw new Error(output.errorMessage || \"An unknown error occurred\");\n      }\n\n      // Push done event\n      stream.push({\n        type: \"done\",\n        reason: output.stopReason,\n        message: output\n      });\n      stream.end();\n    } catch (error) {\n      output.stopReason = options?.signal?.aborted ? \"aborted\" : \"error\";\n      output.errorMessage = error instanceof Error ? error.message : String(error);\n      stream.push({ type: \"error\", reason: output.stopReason, error: output });\n      stream.end();\n    }\n  })();\n\n  return stream;\n}\n```\n\n### 事件类型\n\n按以下顺序通过 `stream.push()` 推送事件：\n\n1. `{ type: \"start\", partial: output }` - 直播开始\n\n2. 内容事件（可重复，跟踪每个块的`contentIndex`）：\n   - `{ type: \"text_start\", contentIndex, partial }` - 文本块开始\n   - `{ type: \"text_delta\", contentIndex, delta, partial }` - 文本块\n   - `{ type: \"text_end\", contentIndex, content, partial }` - 文本块结束\n   - `{ type: \"thinking_start\", contentIndex, partial }` - 思考开始\n   - `{ type: \"thinking_delta\", contentIndex, delta, partial }` - 思考块\n   - `{ type: \"thinking_end\", contentIndex, content, partial }` - 思考结束\n   - `{ type: \"toolcall_start\", contentIndex, partial }` - 工具调用开始\n   - `{ type: \"toolcall_delta\", contentIndex, delta, partial }` - 工具调用JSON块\n   - `{ type: \"toolcall_end\", contentIndex, toolCall, partial }` - 工具调用结束\n\n3. `{ type: \"done\", reason, message }` 或 `{ type: \"error\", reason, error }` - 直播结束\n\n每个事件中的 `partial` 字段包含当前 `AssistantMessage` 状态。收到数据时更新 `output.content`，然后将 `output` 包含为 `partial`。\n\n### 内容块\n\n当内容块到达时将其添加到 `output.content`：\n\n```typescript\n// Text block\noutput.content.push({ type: \"text\", text: \"\" });\nstream.push({ type: \"text_start\", contentIndex: output.content.length - 1, partial: output });\n\n// As text arrives\nconst block = output.content[contentIndex];\nif (block.type === \"text\") {\n  block.text += delta;\n  stream.push({ type: \"text_delta\", contentIndex, delta, partial: output });\n}\n\n// When block completes\nstream.push({ type: \"text_end\", contentIndex, content: block.text, partial: output });\n```\n\n### 工具调用\n\n工具调用需要累加JSON并解析：\n\n```typescript\n// Start tool call\noutput.content.push({\n  type: \"toolCall\",\n  id: toolCallId,\n  name: toolName,\n  arguments: {}\n});\nstream.push({ type: \"toolcall_start\", contentIndex: output.content.length - 1, partial: output });\n\n// Accumulate JSON\nlet partialJson = \"\";\npartialJson += jsonDelta;\ntry {\n  block.arguments = JSON.parse(partialJson);\n} catch {}\nstream.push({ type: \"toolcall_delta\", contentIndex, delta: jsonDelta, partial: output });\n\n// Complete\nstream.push({\n  type: \"toolcall_end\",\n  contentIndex,\n  toolCall: { type: \"toolCall\", id, name, arguments: block.arguments },\n  partial: output\n});\n```\n\n### 使用和成本\n\n从 API 响应更新使用情况并计算成本：\n\n```typescript\noutput.usage.input = response.usage.input_tokens;\noutput.usage.output = response.usage.output_tokens;\noutput.usage.cacheRead = response.usage.cache_read_tokens ?? 0;\noutput.usage.cacheWrite = response.usage.cache_write_tokens ?? 0;\noutput.usage.totalTokens = output.usage.input + output.usage.output +\n                           output.usage.cacheRead + output.usage.cacheWrite;\ncalculateCost(model, output.usage);\n```\n\n### 上下文溢出错误\n\n当请求超出模型的上下文窗口时，pi 可以通过压缩对话并重试来自动恢复。仅当 pi 将故障识别为溢出时，此恢复才会启动。\n\n检测在最终确定的助理消息上运行：\n\n- `stopReason === \"error\"`\n- `errorMessage` 匹配 pi 的已知溢出模式之一（参见 [`packages/ai/src/utils/overflow.ts`](https://github.com/earendil-works/pi-mono/blob/main/packages/ai/src/utils/overflow.ts)）\n\n如果您的提供程序返回溢出错误并显示 pi 无法识别的消息，请规范化来自注册提供程序的同一扩展的错误。使用 `message_end` 处理程序重写助手消息，使其 `errorMessage` 以 pi 识别的短语开头。通用后备`context_length_exceeded`是最安全的选择。\n\n```typescript\nconst MY_PROVIDER_OVERFLOW_PATTERN = /your provider's overflow phrase/i;\n\nexport default function (pi: ExtensionAPI) {\n  pi.registerProvider(\"my-provider\", { /* ... */ });\n\n  pi.on(\"message_end\", (event, ctx) => {\n    const message = event.message;\n    if (message.role !== \"assistant\") return;\n    if (message.stopReason !== \"error\") return;\n    if (\n      message.provider !== \"my-provider\" &&\n      ctx.model?.provider !== \"my-provider\"\n    )\n      return;\n\n    const errorMessage = message.errorMessage ?? \"\";\n    if (errorMessage.includes(\"context_length_exceeded\")) return;\n    if (!MY_PROVIDER_OVERFLOW_PATTERN.test(errorMessage)) return;\n\n    return {\n      message: {\n        ...message,\n        errorMessage: `context_length_exceeded: ${errorMessage}`,\n      },\n    };\n  });\n}\n```\n\n`message_end`在pi跟踪自动压缩的辅助消息之前运行，因此重写的`errorMessage`是pi检查的内容。完成此操作后，pi 将：\n\n1. 检测从`errorMessage`开始的溢出。\n2. 从实时上下文中删除失败的助手消息。\n3. 运行压实。\n4. 重试该请求一次。\n\n仔细保护重写：\n\n- 将其范围限定为您的提供商（`message.provider` 和 `ctx.model?.provider`），因此来自其他提供商的不相关错误不会受到影响。\n- 匹配特定于提供者的模式，而不是 pi 的通用溢出模式。重写速率限制或限制错误（`rate limit`、`too many requests`）会错误地触发压缩，而不是 pi 的正常重试与回退路径。\n- 当 `errorMessage` 已包含 `context_length_exceeded` 时跳过，因此处理程序是幂等的。\n\n### 登记\n\n注册您的流函数：\n\n```typescript\npi.registerProvider(\"my-provider\", {\n  baseUrl: \"https://api.example.com\",\n  apiKey: \"$MY_API_KEY\",\n  api: \"my-custom-api\",\n  models: [...],\n  streamSimple: streamMyProvider\n});\n```\n\n## 测试您的实施\n\n根据内置提供程序使用的相同测试套件来测试您的提供程序。从 [packages/ai/test/](https://github.com/earendil-works/pi-mono/tree/main/packages/ai/test) 复制并调整这些测试文件：\n\n| 测试 | 目的 |\n|------|---------|\n| `stream.test.ts` | 基本流式传输、文本输出 |\n| `tokens.test.ts` | 令牌计数和使用 |\n| `abort.test.ts` | Abort信号处理 |\n| `empty.test.ts` | 空/最少回复 |\n| `context-overflow.test.ts` | 上下文窗口限制 |\n| `image-limits.test.ts` | 图像输入处理 |\n| `unicode-surrogate.test.ts` | Unicode 边缘情况 |\n| `tool-call-without-result.test.ts` | 工具调用边缘情况 |\n| `image-tool-result.test.ts` | 工具结果中的图像 |\n| `total-tokens.test.ts` | 总代币计算 |\n| `cross-provider-handoff.test.ts` | 提供者之间的上下文切换 |\n\n使用您的提供商/模型对运行测试以验证兼容性。\n\n## 配置参考\n\n```typescript\ninterface ProviderConfig {\n  /** Display name for the provider in UI such as /login. */\n  name?: string;\n\n  /** API endpoint URL. Required when defining models. */\n  baseUrl?: string;\n\n  /** API key literal, env interpolation ($ENV_VAR or ${ENV_VAR}), or !command. Required when defining models (unless oauth). */\n  apiKey?: string;\n\n  /** API type for streaming. Required at provider or model level when defining models. */\n  api?: Api;\n\n  /** Custom streaming implementation for non-standard APIs. */\n  streamSimple?: (\n    model: Model<Api>,\n    context: Context,\n    options?: SimpleStreamOptions\n  ) => AssistantMessageEventStream;\n\n  /** Custom headers to include in requests. Values use the same resolution syntax as apiKey. */\n  headers?: Record<string, string>;\n\n  /** If true, adds Authorization: Bearer header with the resolved API key. */\n  authHeader?: boolean;\n\n  /** Models to register. If provided, replaces all existing models for this provider. */\n  models?: ProviderModelConfig[];\n\n  /** OAuth provider for /login support. */\n  oauth?: {\n    name: string;\n    login(callbacks: OAuthLoginCallbacks): Promise<OAuthCredentials>;\n    refreshToken(credentials: OAuthCredentials, signal: AbortSignal): Promise<OAuthCredentials>;\n    getApiKey(credentials: OAuthCredentials): string;\n  };\n}\n```\n\n## 模型定义参考\n\n```typescript\ninterface ProviderModelConfig {\n  /** Model ID (e.g., \"claude-sonnet-4-20250514\"). */\n  id: string;\n\n  /** Display name (e.g., \"Claude 4 Sonnet\"). */\n  name: string;\n\n  /** API type override for this specific model. */\n  api?: Api;\n\n  /** API endpoint URL override for this specific model. */\n  baseUrl?: string;\n\n  /** Whether the model supports extended thinking. */\n  reasoning: boolean;\n\n  /** Maps pi thinking levels to provider/model-specific values; null marks a level unsupported. */\n  thinkingLevelMap?: Partial<Record<\"off\" | \"minimal\" | \"low\" | \"medium\" | \"high\" | \"xhigh\" | \"max\", string | null>>;\n\n  /** Supported input types. */\n  input: (\"text\" | \"image\")[];\n\n  /** Cost per million tokens (for usage tracking). */\n  cost: {\n    input: number;\n    output: number;\n    cacheRead: number;\n    cacheWrite: number;\n  };\n\n  /** Maximum context window size in tokens. */\n  contextWindow: number;\n\n  /** Maximum output tokens. */\n  maxTokens: number;\n\n  /** Custom headers for this specific model. */\n  headers?: Record<string, string>;\n\n  /** Compatibility settings for the selected API. */\n  compat?: {\n    // openai-completions\n    supportsStore?: boolean;\n    supportsDeveloperRole?: boolean;\n    supportsReasoningEffort?: boolean;\n    supportsUsageInStreaming?: boolean;\n    supportsFinishReason?: boolean;\n    supportsStrictMode?: boolean;\n    supportsOpenAIGrammarTools?: boolean; // openai-completions/openai-responses; false falls back to normal function tools\n    maxTokensField?: \"max_completion_tokens\" | \"max_tokens\";\n    requiresToolResultName?: boolean;\n    requiresAssistantAfterToolResult?: boolean;\n    requiresThinkingAsText?: boolean;\n    requiresReasoningContentOnAssistantMessages?: boolean;\n    thinkingFormat?: \"openai\" | \"openrouter\" | \"deepseek\" | \"together\" | \"baseten\" | \"zai\" | \"qwen\" | \"chat-template\" | \"qwen-chat-template\" | \"string-thinking\" | \"ant-ling\";\n    chatTemplateKwargs?: Record<string, string | number | boolean | null | { \"$var\": \"thinking.enabled\" | \"thinking.effort\"; omitWhenOff?: boolean }>;\n    chatTemplateArgs?: Record<string, string | number | boolean | null | { \"$var\": \"thinking.enabled\" | \"thinking.effort\"; omitWhenOff?: boolean }>;\n    cacheControlFormat?: \"anthropic\";\n    sessionAffinityFormat?: \"openai\" | \"openai-nosession\" | \"openrouter\";\n    sendSessionAffinityHeaders?: boolean;\n\n    // anthropic-messages\n    supportsEagerToolInputStreaming?: boolean;\n    supportsLongCacheRetention?: boolean;\n    sendSessionAffinityHeaders?: boolean;\n    supportsCacheControlOnTools?: boolean;\n    forceAdaptiveThinking?: boolean;\n    allowEmptySignature?: boolean;\n    supportsStrictTools?: boolean;\n  };\n}\n```\n\n`openrouter` 发送`reasoning: { effort }`。启用后，`deepseek` 会发送 `thinking: { type: \"enabled\" | \"disabled\" }` 和 `reasoning_effort`。当`supportsReasoningEffort`启用时，`together`会发送`reasoning: { enabled }`，还会发送`reasoning_effort`。 `qwen` 适用于 DashScope 样式的顶级 `enable_thinking`。对于读取 `chat_template_kwargs.enable_thinking` 且需要 `preserve_thinking` 的本地 Qwen 兼容服务器，请使用 `qwen-chat-template`。使用 `chat-template` 来配置 `chat_template_kwargs`，例如 vLLM 后面的 DeepSeek V3.x 带有 `chatTemplateKwargs: { \"thinking\": { \"$var\": \"thinking.enabled\" } }`。当提供程序期望切换值低于 `chat_template_args` 并可选择支持顶级 `reasoning_effort` 时，请使用 `thinkingFormat: \"baseten\"` 和 `chatTemplateArgs`。\n`cacheControlFormat: \"anthropic\"` 将人类风格的 `cache_control` 标记应用于系统提示、最后一个工具定义以及最后一个用户、助手或工具结果文本内容。","sourceFile":"custom-provider.md"},"development":{"title":"开发","markdown":"请参阅 [AGENTS.md](https://github.com/earendil-works/pi-mono/blob/main/AGENTS.md) 了解更多指南。\n\n## 设置\n\n```bash\ngit clone https://github.com/earendil-works/pi-mono\ncd pi-mono\nnpm install\nnpm run build\n```\n\n从源运行：\n\n```bash\n/path/to/pi-mono/pi-test.sh\n```\n\n该脚本可以从任何目录运行。 Pi 保留调用者当前的工作目录。\n\n## 分叉/品牌重塑\n\n通过`package.json`配置：\n\n```json\n{\n  \"piConfig\": {\n    \"name\": \"pi\",\n    \"configDir\": \".pi\"\n  }\n}\n```\n\n更改您的 fork 的 `name`、`configDir` 和 `bin` 字段。影响 CLI 横幅、配置路径和环境变量名称。\n\n## 路径分辨率\n\n三种执行模式：npm安装、独立二进制、来自源代码的tsx。\n\n**始终对包资源使用 `src/config.ts`**：\n\n```typescript\nimport { getPackageDir, getThemeDir } from \"./config.js\";\n```\n\n切勿直接将 `__dirname` 用于包资源。\n\n## 调试命令\n\n`/debug`（隐藏）写入`~/.pi/agent/pi-debug.log`：\n- 使用 ANSI 代码渲染 TUI 行\n- 发送给 LLM 的最新消息\n\n## 测试\n\n```bash\n./test.sh                         # Run non-LLM tests (no API keys needed)\nnpm test                          # Run all tests\nnpm test -- test/specific.test.ts # Run specific test\n```\n\n## 项目结构\n\n```\npackages/\n  ai/           # LLM provider abstraction\n  agent/        # Agent loop and message types  \n  tui/          # Terminal UI components\n  coding-agent/ # CLI and interactive mode\n```","sourceFile":"development.md"},"environment-variables":{"title":"环境变量","markdown":"Pi以三种方式使用环境变量：\n\n- `PI_OFFLINE` 等变量配置Pi 进程。\n- Pi 设置`PI_CODING_AGENT`，以便子进程可以检测到它们在Pi 内运行。\n- 由 LLM 可调用 bash 工具运行的命令接收描述当前会话的 `PI_*` 变量。\n\n提供者API-关键变量单独记录在[Providers](providers.md#environment-variables-or-auth-file)中。\n\n## 过程标记\n\nCLI和RPC入口点设置为`PI_CODING_AGENT=true`。子进程继承它并可以使用它来检测它们是否在Pi内运行。它不是特定于会话的，并且当通过 SDK 嵌入 Pi 时不会自动设置。\n\n## Bash 工具会话环境\n\n由 bash 工具运行的命令接收当前 Pi 会话状态：\n\n| 多变的 | 描述 |\n|----------|-------------|\n| `PI_SESSION_ID` | 当前会话ID |\n| `PI_SESSION_FILE` | 当前会话JSONL文件的绝对路径；未设置临时会话 |\n| `PI_PROVIDER` | 当前选择的模型提供商 |\n| `PI_MODEL` | 当前选择的型号 ID |\n| `PI_REASONING_LEVEL` | 当前有效推理级别：`off`、`minimal`、`low`、`medium`、`high`、`xhigh`、`max` |\n\n每个命令启动时都会解析这些值。因此，切换模型或更改推理级别会影响下一个bash命令，而无需重新启动Pi。 `PI_PROVIDER`和`PI_MODEL`标识所选的Pi模型，而不是路由器可能在内部选择的不同上游模型。\n\n当询问哪个模型或提供程序正在运行时，请检查这些变量，而不是从系统提示中推断答案：\n\n```bash\nprintf '%s/%s\\n' \"$PI_PROVIDER\" \"$PI_MODEL\"\nprintf 'reasoning=%s session=%s\\n' \"$PI_REASONING_LEVEL\" \"$PI_SESSION_ID\"\n```\n\n当会话持久化时，可以直接检查会话文件：\n\n```bash\nif [ -n \"$PI_SESSION_FILE\" ]; then\n  tail -n 1 \"$PI_SESSION_FILE\"\nfi\n```\n\n这些变量被注入到 LLM 可调用的 bash 工具中。它们不会注入到用户输入的 `!` 或 `!!` 命令中。\n\n### 自定义 Bash 工具\n\n使用 `createBashTool()` 创建的 Bash 工具在使用 Pi 注册时默认公开会话环境。注入发生在`spawnHook`之前，因此钩子接收`ctx.env`中的变量：\n\n```typescript\nconst bashTool = createBashTool(cwd, {\n  spawnHook: (ctx) => ({\n    ...ctx,\n    env: { ...ctx.env, CI: \"1\" },\n  }),\n});\n```\n\n独立于生成钩子禁用会话元数据：\n\n```typescript\nconst bashTool = createBashTool(cwd, {\n  exposeSessionEnvironment: false,\n  spawnHook: (ctx) => ctx,\n});\n```\n\n禁用后，Pi 会删除这些变量的继承值，因此嵌套的 Pi 进程不会公开过时的父会话元数据。\n\n## Pi 流程配置\n\n这些变量由 Pi 本身读取：\n\n| 多变的 | 描述 |\n|----------|-------------|\n| `PI_CODING_AGENT_DIR` | 覆盖config目录；默认为 `~/.pi/agent` |\n| `PI_CODING_AGENT_SESSION_DIR` | 覆盖会话存储；被 `--session-dir` 覆盖 |\n| `PI_PACKAGE_DIR` | 覆盖包目录，对于 Nix/Guix 存储路径很有用 |\n| `PI_OFFLINE` | 禁用启动网络操作，包括更新检查、包更新和安装/更新遥测 |\n| `PI_SKIP_VERSION_CHECK` | 禁用 `pi.dev` 最新版本请求 |\n| `PI_TELEMETRY` | 覆盖安装/更新遥测和提供商归因标头：`1`/`true`/`yes`或`0`/`false`/`no` |\n| `PI_CACHE_RETENTION` | 设置为 `long` 以获得支持的扩展提供者提示缓存 |\n| `PI_SHARE_VIEWER_URL` | 覆盖 `/share` 使用的基本 URL |\n| `PI_HARDWARE_CURSOR` | 设置为`1`显示硬件光标；见[Terminal setup](terminal-setup.md) |\n| `VISUAL`, `EDITOR` | 未设置 `externalEditor` 时外部编辑器回退 |\n| `HTTP_PROXY`, `HTTPS_PROXY` | 代理出站 HTTP 请求 |\n\n[Providers](providers.md#environment-variables-or-auth-file) 中列出了`ANTHROPIC_API_KEY`、`OPENAI_API_KEY` 等提供商凭据和云提供商配置。","sourceFile":"environment-variables.md"},"extensions":{"title":"Extensions","markdown":"> pi 可以创建扩展。要求它为您的用例构建一个。\n\n\nExtensions 是 TypeScript 扩展 pi 行为的模块。他们可以订阅生命周期事件、注册可由 LLM 调用的自定义工具、添加命令等。\n\n> **/reload 的放置：** 将扩展放入 `~/.pi/agent/extensions/`（全局）或 `.pi/extensions/`（项目本地）以进行自动发现。仅使用 `pi -e./path.ts` 进行快速测试。自动发现位置中的Extensions可以使用`/reload`进行热重载。\n\n**关键能力：**\n- **自定义工具** - 注册LLM可以通过`pi.registerTool()`调用的工具\n- **事件拦截** - 阻止或修改工具调用、注入上下文、自定义压缩\n- **用户交互** - 通过`ctx.ui`提示用户（选择、确认、输入、通知）\n- **自定义 UI 组件** - 完整的 TUI 组件，通过 `ctx.ui.custom()` 进行键盘输入以实现复杂的交互\n- **自定义命令** - 通过 `pi.registerCommand()` 注册诸如 `/mycommand` 之类的命令\n- **会话持久性** - 存储通过 `pi.appendEntry()` 重新启动后仍然存在的状态\n- **自定义渲染** - 控制工具调用/结果和消息在 TUI 中的显示方式\n\n**用例示例：**\n- 权限门（`rm -rf`、`sudo`等之前确认）\n- Git 检查点（每回合隐藏，在分支上恢复）\n- 路径保护（阻止写入`.env`、`node_modules/`）\n- 自定义压缩（以您的方式总结对话）\n- 对话摘要（参见`summarize.ts`示例）\n- 交互式工具（问题、向导、自定义对话框）\n- 有状态工具（待办事项列表、连接池）\n- 外部集成（文件观察器、webhooks、CI 触发器）\n- 等待时玩游戏（参见`snake.ts`示例）\n\n请参阅[examples/extensions/](../examples/extensions/)了解有效的实现。\n\n## 目录\n\n- [Quick Start](#quick-start)\n- [Extension Locations](#extension-locations)\n- [Available Imports](#available-imports)\n- [Writing an Extension](#writing-an-extension)\n  - [Extension Styles](#extension-styles)\n- [Events](#events)\n  - [Lifecycle Overview](#lifecycle-overview)\n  - [Resource Events](#resource-events)\n  - [Session Events](#session-events)\n  - [Agent Events](#agent-events)\n  - [Model Events](#model-events)\n  - [Tool Events](#tool-events)\n- [ExtensionContext](#extensioncontext)\n- [ExtensionCommandContext](#extensioncommandcontext)\n- [ExtensionAPI Methods](#extensionapi-methods)\n- [State Management](#state-management)\n- [Custom Tools](#custom-tools)\n  - [Dynamic Tool Loading](#dynamic-tool-loading)\n- [Custom UI](#custom-ui)\n- [Error Handling](#error-handling)\n- [Mode Behavior](#mode-behavior)\n- [Examples Reference](#examples-reference)\n\n## 快速入门\n\n创建`~/.pi/agent/extensions/my-extension.ts`：\n\n```typescript\nimport type { ExtensionAPI } from \"@earendil-works/pi-coding-agent\";\nimport { Type } from \"typebox\";\n\nexport default function (pi: ExtensionAPI) {\n  // React to events\n  pi.on(\"session_start\", async (_event, ctx) => {\n    ctx.ui.notify(\"Extension loaded!\", \"info\");\n  });\n\n  pi.on(\"tool_call\", async (event, ctx) => {\n    if (event.toolName === \"bash\" && event.input.command?.includes(\"rm -rf\")) {\n      const ok = await ctx.ui.confirm(\"Dangerous!\", \"Allow rm -rf?\");\n      if (!ok) return { block: true, reason: \"Blocked by user\" };\n    }\n  });\n\n  // Register a custom tool\n  pi.registerTool({\n    name: \"greet\",\n    label: \"Greet\",\n    description: \"Greet someone by name\",\n    parameters: Type.Object({\n      name: Type.String({ description: \"Name to greet\" }),\n    }),\n    async execute(toolCallId, params, signal, onUpdate, ctx) {\n      return {\n        content: [{ type: \"text\", text: `Hello, ${params.name}!` }],\n        details: {},\n      };\n    },\n  });\n\n  // Register a command\n  pi.registerCommand(\"hello\", {\n    description: \"Say hello\",\n    handler: async (args, ctx) => {\n      ctx.ui.notify(`Hello ${args || \"world\"}!`, \"info\");\n    },\n  });\n}\n```\n\n使用 `--extension`（或 `-e`）标志进行测试：\n\n```bash\npi -e ./my-extension.ts\n```\n\n## 扩展地点\n\n> **安全性：** Extensions 以完整的系统权限运行，可以执行任意代码。仅从您信任的来源安装。\n\nExtensions 是从受信任的位置自动发现的。项目本地`.pi/extensions`条目仅在项目受信任后加载。\n\n| 地点 | 范围 |\n|----------|-------|\n| `~/.pi/agent/extensions/*.ts` | 全球（所有项目） |\n| `~/.pi/agent/extensions/*/index.ts` | 全局（子目录） |\n| `.pi/extensions/*.ts` | 项目本地化 |\n| `.pi/extensions/*/index.ts` | 项目本地（子目录） |\n\n通过 `settings.json` 的其他路径：\n\n```json\n{\n  \"packages\": [\n    \"npm:@foo/bar@1.0.0\",\n    \"git:github.com/user/repo@v1\"\n  ],\n  \"extensions\": [\n    \"/path/to/local/extension.ts\",\n    \"/path/to/local/extension/dir\"\n  ]\n}\n```\n\n要通过 npm 或 git 将扩展共享为 pi 包，请参阅 [packages.md](packages.md)。\n\n## 可用进口\n\n| 包裹 | 目的 |\n|---------|---------|\n| `@earendil-works/pi-coding-agent` | 扩展类型（`ExtensionAPI`、`ExtensionContext`、事件） |\n| `typebox` | 工具参数的架构定义 |\n| `@earendil-works/pi-ai` | AI 实用程序（`StringEnum` 适用于 Google 兼容枚举） |\n| `@earendil-works/pi-tui` | 用于自定义渲染的TUI组件 |\n\nnpm 依赖关系也有效。在扩展旁边（或父目录中）添加 `package.json`，运行 `npm install`，然后从 `node_modules/` 导入会自动解析。\n\n对于使用`pi install`（npm或git）安装的分布式pi包，运行时依赖必须位于`dependencies`中。软件包安装默认使用生产安装（`npm install --omit=dev`），因此`devDependencies`在运行时不可用；当配置 `npmCommand` 时，git 包使用普通的 `install` 来与包装器兼容。\n\n还提供 Node.js 内置函数（`node:fs`、`node:path` 等）。\n\n## 编写扩展\n\n扩展导出一个接收 `ExtensionAPI` 的默认工厂函数。工厂可以是同步的或异步的：\n\n```typescript\nimport type { ExtensionAPI } from \"@earendil-works/pi-coding-agent\";\n\nexport default function (pi: ExtensionAPI) {\n  // Subscribe to events\n  pi.on(\"event_name\", async (event, ctx) => {\n    // ctx.ui for user interaction\n    const ok = await ctx.ui.confirm(\"Title\", \"Are you sure?\");\n    ctx.ui.notify(\"Done!\", \"info\");\n    ctx.ui.setStatus(\"my-ext\", \"Processing...\");  // Footer status\n    ctx.ui.setWidget(\"my-ext\", [\"Line 1\", \"Line 2\"]);  // Widget above editor (default)\n  });\n\n  // Register tools, commands, shortcuts, flags\n  pi.registerTool({ ... });\n  pi.registerCommand(\"name\", { ... });\n  pi.registerShortcut(\"ctrl+x\", { ... });\n  pi.registerFlag(\"my-flag\", { ... });\n}\n```\n\nExtensions 通过[jiti](https://github.com/unjs/jiti) 加载，因此TypeScript 无需编译即可工作。\n\n如果工厂返回`Promise`，pi 会在继续启动之前等待它。这意味着异步初始化在`session_start`之前、`resources_discover`之前以及通过`pi.registerProvider()`排​​队的提供者注册被刷新之前完成。\n\n### 异步工厂函数\n\n使用异步工厂进行一次性启动工作，例如获取远程配置或动态发现可用模型。\n\n```typescript\nimport type { ExtensionAPI } from \"@earendil-works/pi-coding-agent\";\n\nexport default async function (pi: ExtensionAPI) {\n  const response = await fetch(\"http://localhost:1234/v1/models\");\n  const payload = (await response.json()) as {\n    data: Array<{\n      id: string;\n      name?: string;\n      context_window?: number;\n      max_tokens?: number;\n    }>;\n  };\n\n  pi.registerProvider(\"local-openai\", {\n    baseUrl: \"http://localhost:1234/v1\",\n    apiKey: \"$LOCAL_OPENAI_API_KEY\",\n    api: \"openai-completions\",\n    models: payload.data.map((model) => ({\n      id: model.id,\n      name: model.name ?? model.id,\n      reasoning: false,\n      input: [\"text\"],\n      cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },\n      contextWindow: model.context_window ?? 128000,\n      maxTokens: model.max_tokens ?? 4096,\n    })),\n  });\n}\n```\n\n此模式使获取的模型在正常启动期间可用并达到 `pi --list-models`。\n\n### 长期资源和关闭\n\n扩展工厂可能在从不启动会话的调用中运行。不要从工厂启动后台资源，例如进程、套接字、文件观察程序或计时器。\n\n推迟后台资源启动，直到 `session_start` 或需要资源的命令/工具/事件。注册一个幂等 `session_shutdown` 处理程序来关闭您启动的任何会话范围的资源。\n\n### 扩展样式\n\n**单个文件** - 最简单，适用于小型扩展：\n\n```\n~/.pi/agent/extensions/\n└── my-extension.ts\n```\n\n**带有index.ts的目录** - 用于多文件扩展名：\n\n```\n~/.pi/agent/extensions/\n└── my-extension/\n    ├── index.ts        # Entry point (exports default function)\n    ├── tools.ts        # Helper module\n    └── utils.ts        # Helper module\n```\n\n**具有依赖项的包** - 对于需要 npm 包的扩展：\n\n```\n~/.pi/agent/extensions/\n└── my-extension/\n    ├── package.json    # Declares dependencies and entry points\n    ├── package-lock.json\n    ├── node_modules/   # After npm install\n    └── src/\n        └── index.ts\n```\n\n```json\n// package.json\n{\n  \"name\": \"my-extension\",\n  \"dependencies\": {\n    \"zod\": \"^3.0.0\",\n    \"chalk\": \"^5.0.0\"\n  },\n  \"pi\": {\n    \"extensions\": [\"./src/index.ts\"]\n  }\n}\n```\n\n在扩展目录中运行`npm install`，然​​后从`node_modules/`自动导入。\n\n## 活动\n\n### 生命周期概述\n\n```\npi starts\n  │\n  ├─► project_trust (user/global and CLI extensions only, before project resources load)\n  ├─► session_start { reason: \"startup\" }\n  └─► resources_discover { reason: \"startup\" }\n      │\n      ▼\nuser sends prompt ─────────────────────────────────────────┐\n  │                                                        │\n  ├─► (extension commands checked first, bypass if found)  │\n  ├─► input (can intercept, transform, or handle)          │\n  ├─► (skill/template expansion if not handled)            │\n  ├─► before_agent_start (can inject message, modify system prompt)\n  ├─► agent_start                                          │\n  ├─► message_start / message_update / message_end         │\n  │                                                        │\n  │   ┌─── turn (repeats while LLM calls tools) ───┐       │\n  │   │                                            │       │\n  │   ├─► turn_start                               │       │\n  │   ├─► context (can modify messages)            │       │\n  │   ├─► before_provider_headers (can mutate headers)     |\n  │   ├─► before_provider_request (can inspect or replace payload)\n  │   ├─► after_provider_response (status + headers, before stream consume)\n  │   │                                            │       │\n  │   │   LLM responds, may call tools:            │       │\n  │   │     ├─► tool_execution_start               │       │\n  │   │     ├─► tool_call (can block)              │       │\n  │   │     ├─► tool_execution_update              │       │\n  │   │     ├─► tool_result (can modify)           │       │\n  │   │     └─► tool_execution_end                 │       │\n  │   │                                            │       │\n  │   └─► turn_end                                 │       │\n  │                                                        │\n  ├─► agent_end                                            │\n  └─► agent_settled (no retry/compaction/follow-up left)   │\n                                                           │\nuser sends another prompt ◄────────────────────────────────┘\n\n/new (new session) or /resume (switch session)\n  ├─► session_before_switch (can cancel)\n  ├─► session_shutdown\n  ├─► session_start { reason: \"new\" | \"resume\", previousSessionFile? }\n  └─► resources_discover { reason: \"startup\" }\n\n/fork or /clone\n  ├─► session_before_fork (can cancel)\n  ├─► session_shutdown\n  ├─► session_start { reason: \"fork\", previousSessionFile }\n  └─► resources_discover { reason: \"startup\" }\n\n/name or pi.setSessionName()\n  └─► session_info_changed\n\n/compact or auto-compaction\n  ├─► session_before_compact (can cancel or customize)\n  └─► session_compact\n\n/tree navigation\n  ├─► session_before_tree (can cancel or customize)\n  └─► session_tree\n\n/model or Ctrl+P (model selection/cycling)\n  ├─► thinking_level_select (if model change changes/clamps thinking level)\n  └─► model_select\n\nthinking level changes (settings, keybinding, pi.setThinkingLevel())\n  └─► thinking_level_select\n\nexit (Ctrl+C, Ctrl+D, SIGHUP, SIGTERM)\n  └─► session_shutdown\n```\n\n### 启动活动\n\n#### 项目_信任\n\n在 pi 决定是否信任具有动态配置（`.pi` 或 `.agents/skills`）的项目之前触发。它在启动期间以及当会话替换（例如 `/resume`）进入当前进程中信任尚未解析的 cwd 时运行。仅用户/全局分机和CLI`-e`分机参与；直到信任解决后才会加载项目本地扩展。\n\n```typescript\npi.on(\"project_trust\", async (event, ctx) => {\n  // event.cwd - current working directory\n  // ctx has a limited trust context: cwd, mode, hasUI, and select/confirm/input/notify UI helpers\n  if (await ctx.ui.confirm(\"Trust project?\", event.cwd)) {\n    return { trusted: \"yes\", remember: true };\n  }\n  return { trusted: \"undecided\" };\n});\n```\n\n`project_trust` 处理程序必须返回 `{ trusted: \"yes\" | \"no\" | \"undecided\" }`。返回 `\"yes\"` 或 `\"no\"` 的用户/全局或 CLI 扩展拥有该决策；第一个是/否决定获胜并抑制内置信任提示。使用 `remember: true` 坚持是/否决定；否则它仅适用于当前进程。返回 `\"undecided\"` 让后续处理程序或内置信任流程决定。在提示之前检查`ctx.hasUI`。如果没有处理程序返回是/否，则继续正常的信任解析：首先应用保存的 `trust.json` 决策，然后`defaultProjectTrust` 控制 pi 默认情况下是否询问、信任或拒绝。\n\n### 资源事件\n\n#### 资源发现\n\n在 `session_start` 之后触发，因此扩展可以贡献额外的技能、提示和主题路径。\n启动路径使用`reason: \"startup\"`。重新加载使用`reason: \"reload\"`。\n\n```typescript\npi.on(\"resources_discover\", async (event, _ctx) => {\n  // event.cwd - current working directory\n  // event.reason - \"startup\" | \"reload\"\n  return {\n    skillPaths: [\"/path/to/skills\"],\n    promptPaths: [\"/path/to/prompts\"],\n    themePaths: [\"/path/to/themes\"],\n  };\n});\n```\n\n### 会议活动\n\n请参阅 [Session Format](session-format.md) 了解会话存储内部结构和 SessionManager API。\n\n#### 会话开始\n\n当会话启动、加载或重新加载时触发。\n\n```typescript\npi.on(\"session_start\", async (event, ctx) => {\n  // event.reason - \"startup\" | \"reload\" | \"new\" | \"resume\" | \"fork\"\n  // event.previousSessionFile - present for \"new\", \"resume\", and \"fork\"\n  ctx.ui.notify(`Session: ${ctx.sessionManager.getSessionFile() ?? \"ephemeral\"}`, \"info\");\n});\n```\n\n#### 会话信息已更改\n\n通过 `/name`、RPC 或 `pi.setSessionName()` 设置当前会话显示名称时触发。\n\n```typescript\npi.on(\"session_info_changed\", async (event, ctx) => {\n  // event.name - current normalized name, or undefined if cleared\n  ctx.ui.notify(`Session renamed: ${event.name ?? \"(none)\"}`, \"info\");\n});\n```\n\n#### 切换前的会话\n\n在开始新会话 (`/new`) 或切换会话 (`/resume`) 之前触发。\n\n```typescript\npi.on(\"session_before_switch\", async (event, ctx) => {\n  // event.reason - \"new\" or \"resume\"\n  // event.targetSessionFile - session we're switching to (only for \"resume\")\n\n  if (event.reason === \"new\") {\n    const ok = await ctx.ui.confirm(\"Clear?\", \"Delete all messages?\");\n    if (!ok) return { cancel: true };\n  }\n});\n```\n\n成功切换或新会话操作后，pi 为旧扩展实例发出 `session_shutdown`，为新会话重新加载并重新绑定扩展，然后发出 `session_start` 以及 `reason: \"new\" | \"resume\"` 和 `previousSessionFile`。\n在 `session_shutdown` 中进行清理工作，然后在 `session_start` 中重新建立任何内存中状态。\n\n#### 分叉前的会话\n\n通过 `/fork` 分叉或通过 `/clone` 克隆时触发。\n\n```typescript\npi.on(\"session_before_fork\", async (event, ctx) => {\n  // event.entryId - ID of the selected entry\n  // event.position - \"before\" for /fork, \"at\" for /clone\n  return { cancel: true }; // Cancel fork/clone\n  // OR\n  return { skipConversationRestore: true }; // Reserved for future conversation restore control\n});\n```\n\n成功分叉或克隆后，pi 为旧扩展实例发出 `session_shutdown`，为新会话重新加载并重新绑定扩展，然后发出 `session_start` 以及 `reason: \"fork\"` 和 `previousSessionFile`。\n在 `session_shutdown` 中进行清理工作，然后在 `session_start` 中重新建立任何内存中状态。\n\n#### session_before_compact / session_compact\n\n压实时发射。详情请参阅[compaction.md](compaction.md)。\n\n```typescript\npi.on(\"session_before_compact\", async (event, ctx) => {\n  const { preparation, branchEntries, customInstructions, reason, willRetry, signal } = event;\n\n  // reason - \"manual\" (/compact), \"threshold\", or \"overflow\"\n  // willRetry - whether the aborted turn is retried after compaction (overflow recovery)\n\n  // Cancel:\n  return { cancel: true };\n\n  // Custom summary:\n  return {\n    compaction: {\n      summary: \"...\",\n      firstKeptEntryId: preparation.firstKeptEntryId,\n      tokensBefore: preparation.tokensBefore,\n      // usage: summaryResponse.usage, // Optional; included in session totals\n    }\n  };\n});\n\npi.on(\"session_compact\", async (event, ctx) => {\n  // event.compactionEntry - the saved compaction\n  // event.fromExtension - whether extension provided it\n  // event.reason - \"manual\" (/compact), \"threshold\", or \"overflow\"\n  // event.willRetry - whether the aborted turn is retried after compaction (overflow recovery)\n});\n```\n\n#### session_before_tree / session_tree\n\n在 `/tree` 导航上触发。有关树导航概念，请参阅[Sessions](sessions.md)。\n\n```typescript\npi.on(\"session_before_tree\", async (event, ctx) => {\n  const { preparation, signal } = event;\n  return { cancel: true };\n  // OR provide custom summary:\n  return {\n    summary: {\n      summary: \"...\",\n      // usage: summaryResponse.usage, // Optional; included in session totals\n      details: {},\n    },\n  };\n});\n\npi.on(\"session_tree\", async (event, ctx) => {\n  // event.newLeafId, oldLeafId, summaryEntry, fromExtension\n});\n```\n\n#### 会话关闭\n\n在启动的会话运行时被拆除之前触发。使用它来清理从 `session_start` 或其他会话范围的挂钩打开的资源。\n\n```typescript\npi.on(\"session_shutdown\", async (event, ctx) => {\n  // event.reason - \"quit\" | \"reload\" | \"new\" | \"resume\" | \"fork\"\n  // event.targetSessionFile - destination session for session replacement flows\n  // Cleanup, save state, etc.\n});\n```\n\n### 代理活动\n\n#### 代理启动之前\n\n在用户提交提示后、代理循环之前触发。可以注入消息和/或修改系统提示。\n\n```typescript\npi.on(\"before_agent_start\", async (event, ctx) => {\n  // event.prompt - user's prompt text\n  // event.images - attached images (if any)\n  // event.systemPrompt - current chained system prompt for this handler\n  //   (includes changes from earlier before_agent_start handlers)\n  // event.systemPromptOptions - structured options used to build the system prompt\n  //   .customPrompt - any custom system prompt (from --system-prompt, SYSTEM.md, or custom templates)\n  //   .selectedTools - tools currently active in the prompt\n  //   .toolSnippets - one-line descriptions for each tool\n  //   .promptGuidelines - custom guideline bullets\n  //   .appendSystemPrompt - text from --append-system-prompt flags\n  //   .cwd - working directory\n  //   .contextFiles - AGENTS.md files and other loaded context files\n  //   .skills - loaded skills\n\n  return {\n    // Inject a persistent message (stored in session, sent to LLM)\n    message: {\n      customType: \"my-extension\",\n      content: \"Additional context for the LLM\",\n      display: true,\n    },\n    // Replace the system prompt for this turn (chained across extensions)\n    systemPrompt: event.systemPrompt + \"\\n\\nExtra instructions for this turn...\",\n  };\n});\n```\n\n`systemPromptOptions` 字段使扩展程序可以访问Pi 用于构建系统提示的相同结构化数据。这使您可以检查 Pi 已加载的内容 - 自定义提示、指南、工具片段、context files、技能 - 无需重新发现资源或重新解析标志。当您的扩展需要对系统提示进行深入、明智的更改，同时尊重用户提供的配置时，请使用它。\n\n`before_agent_start`、`event.systemPrompt`和`ctx.getSystemPrompt()`内部都反映了当前处理程序的链接系统提示符。以后`before_agent_start`处理程序仍然可以再次修改它。\n\n#### 代理开始/代理结束/代理结算\n\n当低级别代理运行开始时，`agent_start` 会触发。 `agent_end` 在运行结束时触发，但 Pi 仍可能自动重试、自动压缩并重试，或继续处理排队的后续消息。使用 `agent_settled` 进行需要知道 Pi 不会继续自动运行的状态集成。\n\n```typescript\npi.on(\"agent_start\", async (_event, ctx) => {});\n\npi.on(\"agent_end\", async (event, ctx) => {\n  // event.messages - messages from this low-level run\n});\n\npi.on(\"agent_settled\", async (_event, ctx) => {\n  // ctx.isIdle() is true here unless another extension started a new run.\n});\n```\n\n#### 转弯开始/转弯结束\n\n每回合触发（一个 LLM 响应 + 工具调用）。\n\n```typescript\npi.on(\"turn_start\", async (event, ctx) => {\n  // event.turnIndex, event.timestamp\n});\n\npi.on(\"turn_end\", async (event, ctx) => {\n  // event.turnIndex, event.message, event.toolResults\n});\n```\n\n#### 消息开始/消息更新/消息结束\n\n因消息生命周期更新而触发。\n\n- `message_start` 和 `message_end` 触发用户、助手和 toolResult 消息。\n- `message_update` 触发助理流式更新。\n- `message_end` 处理程序可以返回 `{ message }` 来替换最终确定的消息。替换者必须保持相同的`role`。\n\n```typescript\npi.on(\"message_start\", async (event, ctx) => {\n  // event.message\n});\n\npi.on(\"message_update\", async (event, ctx) => {\n  // event.message\n  // event.assistantMessageEvent (token-by-token stream event)\n});\n\npi.on(\"message_end\", async (event, ctx) => {\n  if (event.message.role !== \"assistant\") return;\n\n  return {\n    message: {\n      ...event.message,\n      usage: {\n        ...event.message.usage,\n        cost: {\n          ...event.message.usage.cost,\n          total: 0.123,\n        },\n      },\n    },\n  };\n});\n```\n\n#### 工具执行开始/工具执行更新/工具执行结束\n\n因工具执行生命周期更新而触发。\n\n在并行工具模式下：\n- `tool_execution_start` 在预检阶段按照辅助源顺序发出\n- `tool_execution_update` 事件可能会跨工具交错\n- 每个工具完成后，`tool_execution_end` 按工具完成顺序发出\n- 最终 `toolResult` 消息事件仍按助理源顺序稍后发出\n\n```typescript\npi.on(\"tool_execution_start\", async (event, ctx) => {\n  // event.toolCallId, event.toolName, event.args\n});\n\npi.on(\"tool_execution_update\", async (event, ctx) => {\n  // event.toolCallId, event.toolName, event.args, event.partialResult\n});\n\npi.on(\"tool_execution_end\", async (event, ctx) => {\n  // event.toolCallId, event.toolName, event.result, event.isError\n});\n```\n\n#### 语境\n\n在每次 LLM 通话之前触发。非破坏性地修改消息。消息类型请参见[Session Format](session-format.md)。\n\n```typescript\npi.on(\"context\", async (event, ctx) => {\n  // event.messages - deep copy, safe to modify\n  const filtered = event.messages.filter(m => !shouldPrune(m));\n  return { messages: filtered };\n});\n```\n\n#### before_provider_headers\n\n组装传出 HTTP 标头后触发。使用它来添加、覆盖或删除请求标头。\n\n处理程序就地突变`event.headers`。将键设置为字符串以添加或覆盖它，或设置为 `null` 来删除它。\n\n```typescript\npi.on(\"before_provider_headers\", (event, ctx) => {\n  // Add or override — e.g. a session id for gateway tracing/attribution\n  event.headers[\"x-session-id\"] = ctx.sessionManager.getSessionId();\n\n  // Drop a tracking header pi adds for this call\n  event.headers[\"X-OpenRouter-Title\"] = null;\n});\n```\n\n每个提供商请求运行一次；重试重用相同的标头而不是重新触发钩子。\n\n#### before_provider_request 之前\n\n在构建特定于提供者的有效负载之后、发送请求之前触发。处理程序按扩展加载顺序运行。返回 `undefined` 保持有效负载不变。返回任何其他值都会替换后续处理程序和实际请求的有效负载。\n\n该钩子可以重写提供者级别的系统指令或完全删除它们。这些有效负载级别的更改不会由 `ctx.getSystemPrompt()` 反映，它报告 Pi 的系统提示字符串，而不是最终的序列化提供程序有效负载。\n\n```typescript\npi.on(\"before_provider_request\", (event, ctx) => {\n  console.log(JSON.stringify(event.payload, null, 2));\n\n  // Optional: replace payload\n  // return { ...event.payload, temperature: 0 };\n});\n```\n\n这主要用于调试提供程序序列化和缓存行为。\n\n#### after_provider_response\n\n在收到 HTTP 响应之后且在使用其流主体之前触发。处理程序按扩展加载顺序运行。\n\n```typescript\npi.on(\"after_provider_response\", (event, ctx) => {\n  // event.status - HTTP status code\n  // event.headers - normalized response headers\n  if (event.status === 429) {\n    console.log(\"rate limited\", event.headers[\"retry-after\"]);\n  }\n});\n```\n\n标头可用性取决于提供商和传输。 Providers 抽象 HTTP 响应可能不会公开标头。\n\n### 模特活动\n\n#### 模型选择\n\n当模型通过 `/model` 命令、模型循环 (`Ctrl+P`) 或会话恢复更改时触发。\n\n```typescript\npi.on(\"model_select\", async (event, ctx) => {\n  // event.model - newly selected model\n  // event.previousModel - previous model (undefined if first selection)\n  // event.source - \"set\" | \"cycle\" | \"restore\"\n\n  const prev = event.previousModel\n    ? `${event.previousModel.provider}/${event.previousModel.id}`\n    : \"none\";\n  const next = `${event.model.provider}/${event.model.id}`;\n\n  ctx.ui.notify(`Model changed (${event.source}): ${prev} -> ${next}`, \"info\");\n});\n```\n\n使用它来更新 UI 元素（状态栏、页脚）或在活动模型更改时执行特定于模型的初始化。\n\n#### 思考级别选择\n\n当思维水平发生变化时被解雇。这仅用于通知；处理程序返回值将被忽略。\n\n```typescript\npi.on(\"thinking_level_select\", async (event, ctx) => {\n  // event.level - newly selected thinking level\n  // event.previousLevel - previous thinking level\n\n  ctx.ui.setStatus(\"thinking\", `thinking: ${event.level}`);\n});\n```\n\n当 `pi.setThinkingLevel()`、模型更改或内置思维级别控件更改活跃思维级别时，使用此更新扩展 UI。\n\n### 工具事件\n\n#### 工具调用\n\n在 `tool_execution_start` 之后、工具执行之前触发。 **可以阻止。** 使用 `isToolCallEventType` 缩小范围并获取键入的输入。\n\n在 `tool_call` 运行之前，pi 等待先前发出的代理事件以完成`AgentSession` 的排空。这意味着`ctx.sessionManager`通过当前辅助工具调用消息是最新的。\n\n在默认的并行工具执行模式下，来自同一辅助消息的同级工具调用将按顺序进行预检，然后并发执行。 `tool_call` 不保证能够从 `ctx.sessionManager` 中的同一助理消息中看到同级工具结果。\n\n`event.input` 是可变的。在执行之前对其进行适当修改以修补工具参数。\n\n行为保证：\n- `event.input` 的突变会影响实际的工具执行\n- 后来的`tool_call`处理程序看到了早期处理程序所做的突变\n- 突变后不会进行重新验证\n- 从`tool_call`返回值通过`{ block: true, reason?: string, terminate?: boolean }`控制阻塞\n- `terminate`仅适用于阻塞呼叫；仅当批次中的每个最终结果都终止时，代理才会提前停止\n\n```typescript\nimport { isToolCallEventType } from \"@earendil-works/pi-coding-agent\";\n\npi.on(\"tool_call\", async (event, ctx) => {\n  // event.toolName - \"bash\", \"read\", \"write\", \"edit\", etc.\n  // event.toolCallId\n  // event.input - tool parameters (mutable)\n\n  // Built-in tools: no type params needed\n  if (isToolCallEventType(\"bash\", event)) {\n    // event.input is { command: string; timeout?: number }\n    event.input.command = `source ~/.profile\\n${event.input.command}`;\n\n    if (event.input.command.includes(\"rm -rf\")) {\n      return { block: true, reason: \"Dangerous command\", terminate: true };\n    }\n  }\n\n  if (isToolCallEventType(\"read\", event)) {\n    // event.input is { path: string; offset?: number; limit?: number }\n    console.log(`Reading: ${event.input.path}`);\n  }\n});\n```\n\n#### 键入自定义工具输入\n\n自定义工具应导出其输入类型：\n\n```typescript\n// my-extension.ts\nexport type MyToolInput = Static<typeof myToolSchema>;\n```\n\n将 `isToolCallEventType` 与显式类型参数一起使用：\n\n```typescript\nimport { isToolCallEventType } from \"@earendil-works/pi-coding-agent\";\nimport type { MyToolInput } from \"my-extension\";\n\npi.on(\"tool_call\", (event) => {\n  if (isToolCallEventType<\"my_tool\", MyToolInput>(\"my_tool\", event)) {\n    event.input.action;  // typed\n  }\n});\n```\n\n#### 工具结果\n\n在工具执行完成后且在 `tool_execution_end` 加上最终工具结果消息事件发出之前触发。 **可以修改结果。**\n\n在并行工具模式下，`tool_result`和`tool_execution_end`可能会按照工具完成顺序交错，而最终的`toolResult`消息事件仍会按照辅助源顺序稍后发出。\n\n`tool_result` 处理程序链式中间件：\n- 处理程序按扩展加载顺序运行\n- 每个处理程序都会看到前一个处理程序更改后的最新结果\n- 处理程序可以返回部分补丁（`content`、`details`、`isError`或`usage`）；省略的字段保留其当前值\n\n使用 `ctx.signal` 进行处理程序内的嵌套异步工作。这允许 Esc 取消模型调用、`fetch()`以及扩展启动的其他中止感知操作。\n\n```typescript\nimport { isBashToolResult } from \"@earendil-works/pi-coding-agent\";\n\npi.on(\"tool_result\", async (event, ctx) => {\n  // event.toolName, event.toolCallId, event.input\n  // event.content, event.details, event.isError, event.usage\n\n  if (isBashToolResult(event)) {\n    // event.details is typed as BashToolDetails\n  }\n\n  const response = await fetch(\"https://example.com/summarize\", {\n    method: \"POST\",\n    body: JSON.stringify({ content: event.content }),\n    signal: ctx.signal,\n  });\n\n  // Modify result:\n  return { content: [...], details: {...}, isError: false, usage: nestedModelUsage };\n});\n```\n\n### 用户狂欢事件\n\n#### 用户_bash\n\n当用户执行 `!` 或 `!!` 命令时触发。 **可以拦截。**\n\n```typescript\nimport { createLocalBashOperations } from \"@earendil-works/pi-coding-agent\";\n\npi.on(\"user_bash\", (event, ctx) => {\n  // event.command - the bash command\n  // event.excludeFromContext - true if !! prefix\n  // event.cwd - working directory\n\n  // Option 1: Provide custom operations (e.g., SSH)\n  return { operations: remoteBashOps };\n\n  // Option 2: Wrap pi's built-in local bash backend\n  const local = createLocalBashOperations();\n  return {\n    operations: {\n      exec(command, cwd, options) {\n        return local.exec(`source ~/.profile\\n${command}`, cwd, options);\n      }\n    }\n  };\n\n  // Option 3: Full replacement - return result directly\n  return { result: { output: \"...\", exitCode: 0, cancelled: false, truncated: false } };\n});\n```\n\n### 输入事件\n\n#### 输入\n\n在检查扩展命令之后但在技能和模板扩展之前收到用户输入时触发。该事件看到原始输入文本，因此 `/skill:foo` 和 `/template` 尚未展开。\n\n**加工订单：**\n1. 首先检查扩展命令 (`/cmd`) - 如果找到，则运行处理程序并跳过输入事件\n2. `input` 事件触发 - 可以拦截、转换或处理\n3. 如果不处理：技能命令（`/skill:name`）扩展为技能内容\n4. 如果不处理：prompt templates(`/template`)扩展到模板内容\n5. 代理处理开始（`before_agent_start`等）\n\n```typescript\npi.on(\"input\", async (event, ctx) => {\n  // event.text - raw input (before skill/template expansion)\n  // event.images - attached images, if any\n  // event.source - \"interactive\" (typed), \"rpc\" (API), or \"extension\" (via sendUserMessage)\n  // event.streamingBehavior - \"steer\" | \"followUp\" | undefined\n  //   undefined when idle, \"steer\" for mid-stream interrupts,\n  //   \"followUp\" for messages queued until the agent finishes\n\n  // Transform: rewrite input before expansion\n  if (event.text.startsWith(\"?quick \"))\n    return { action: \"transform\", text: `Respond briefly: ${event.text.slice(7)}` };\n\n  // Handle: respond without LLM (extension shows its own feedback)\n  if (event.text === \"ping\") {\n    ctx.ui.notify(\"pong\", \"info\");\n    return { action: \"handled\" };\n  }\n\n  // Route by source: skip processing for extension-injected messages\n  if (event.source === \"extension\") return { action: \"continue\" };\n\n  // Intercept skill commands before expansion\n  if (event.text.startsWith(\"/skill:\")) {\n    // Could transform, block, or let pass through\n  }\n\n  return { action: \"continue\" };  // Default: pass through to expansion\n});\n```\n\n**结果：**\n- `continue` - 不变地传递（如果处理程序不返回任何内容，则默认）\n- `transform` - 修改文字/图像，然后继续扩展\n- `handled` - 完全跳过代理（第一个返回该代理的处理程序获胜）\n\n跨处理程序转换链。请参阅 [input-transform.ts](../examples/extensions/input-transform.ts) 和 [input-transform-streaming.ts](../examples/extensions/input-transform-streaming.ts) 了解 `streamingBehavior` 感知路由。\n\n## 扩展上下文\n\n所有处理程序都会收到 `ctx: ExtensionContext`。\n\n### ctx.ui\n\n用户交互的 UI 方法。有关完整详细信息，请参阅[Custom UI](#custom-ui)。\n\n### ctx模式\n\n当前运行模式：`\"tui\"`、`\"rpc\"`、`\"json\"`或`\"print\"`。使用 `ctx.mode === \"tui\"` 保护仅限终端的功能，例如 `custom()`、组件工厂、终端输入和直接 TUI 渲染。\n\n### ctx.hasUI\n\n`true` 在 TUI 和 RPC 模式下。打印模式 (`-p`) 和 JSON 模式下为`false`。使用它来保护在 TUI 和 TUI 和 TUI 中工作的对话框方法（`select`、`confirm`、`input`、`editor`）和即发即忘方法（`notify`、`setStatus`、`setWidget`、`setTitle`、`setEditorText`） RPC 模式。在RPC模式下，一些TUI特定的方法是无操作或返回默认值（请参阅[rpc.md](rpc.md#extension-ui-protocol)）。\n\n### ctx.cwd\n\n当前工作目录。\n\n构建项目本地配置路径时，使用 `CONFIG_DIR_NAME` 而不是硬编码 `.pi`。重新命名的发行版可以使用不同的配置目录名称。\n\n```typescript\nimport { CONFIG_DIR_NAME, type ExtensionAPI } from \"@earendil-works/pi-coding-agent\";\nimport { join } from \"node:path\";\n\nexport default function (pi: ExtensionAPI) {\n  pi.on(\"session_start\", (_event, ctx) => {\n    const projectConfigPath = join(ctx.cwd, CONFIG_DIR_NAME, \"my-extension.json\");\n    // ...\n  });\n}\n```\n\n### ctx.isProjectTrusted()\n\n返回项目本地信任对于当前会话上下文是否处于活动状态。这包括临时信任决策和CLI信任覆盖，而不仅仅是全局信任存储中保存的决策。\n\n在阅读项目本地扩展配置之前使用此配置，该配置仅适用于受信任的项目。\n\n### ctx.sessionManager\n\n对会话状态的只读访问。请参阅 [Session Format](session-format.md) 了解完整的 SessionManager API 和条目类型。\n\n对于`tool_call`，该状态在处理程序运行之前通过当前辅助消息进行同步。在并行工具执行模式下，仍然不能保证包含来自同一辅助消息的同级工具结果。\n\n```typescript\nctx.sessionManager.getEntries()             // All entries\nctx.sessionManager.getBranch()              // Current branch\nctx.sessionManager.buildContextEntries()    // Active branch entries with compaction applied\nctx.sessionManager.getLeafId()              // Current leaf entry ID\n```\n\n### ctx.modelRegistry / ctx.model / ctx.thinkingLevel / ctx.scopedModels\n\n访问模型、提供者和解析的身份验证。 `ctx.modelRegistry.getProvider(id)` 返回有效的 pi-ai 提供程序，而 `getProviderAuth(id)` 解析其当前的 API key、标头、基本 URL 和提供程序范围的环境，而无需加载模型。 `ctx.model`是活跃模型，`ctx.thinkingLevel`是当前有效思维水平。\n\n`ctx.scopedModels` 是当前会话范围内模型的只读列表 - 与 `/scoped-models` 命令显示的集合相同。它在会话开始时从 `--models` CLI 标志和 `enabledModels` 设置进行解析（与 `provider/modelId` 上的 minimatch 或裸的 `modelId` 上的可用目录进行匹配）。当未配置范围时它为空，这意味着每个可用模型都可用。每个条目都是 `{ model, thinkingLevel? }`，其中 `thinkingLevel` 仅当模式固定它时才设置（例如 `anthropic/*:high`）。使用它来填充模型选择器，该模型选择器镜像内置模型选择器，而不是通过 `ctx.modelRegistry.getAvailable()` 枚举整个目录。\n\n### ctx信号\n\n当前代理中止信号，或当没有代理轮次处于活动状态时为 `undefined`。\n\n将此用于由扩展处理程序启动的中止感知嵌套工作，例如：\n- `fetch(..., { signal: ctx.signal })`\n- 接受 `signal` 的模型调用\n- 接受 `AbortSignal` 的文件或进程助手\n\n`ctx.signal` 通常在活动转弯事件期间定义，例如 `tool_call`、`tool_result`、`message_update` 和 `turn_end`。\n在空闲或非轮流上下文中，例如会话事件、扩展命令和 pi 空闲时触发的快捷方式，它通常为 `undefined`。\n\n```typescript\npi.on(\"tool_result\", async (event, ctx) => {\n  const response = await fetch(\"https://example.com/api\", {\n    method: \"POST\",\n    body: JSON.stringify(event),\n    signal: ctx.signal,\n  });\n\n  const data = await response.json();\n  return { details: data };\n});\n```\n\n### ctx.isIdle() / ctx.abort() / ctx.hasPendingMessages()\n\n控制流程助手。当 Pi 正在处理代理运行、自动重试、自动压缩重试或排队延续时，`ctx.isIdle()` 为 false。\n\n### ctx.shutdown()\n\n请求正常关闭 pi。\n\n- **交互模式：** 推迟到代理变得空闲（处理完所有排队的转向和后续消息后）。\n- **RPC模式：**推迟到下一个空闲状态（完成当前命令响应后，等待下一个命令时）。\n- **打印模式：** 无操作。处理完所有提示后，该过程将自动退出。\n\n在退出之前向所有扩展发出 `session_shutdown` 事件。可用于所有上下文（事件处理程序、工具、命令、快捷方式）。\n\n```typescript\npi.on(\"tool_call\", (event, ctx) => {\n  if (isFatal(event.input)) {\n    ctx.shutdown();\n  }\n});\n```\n\n### ctx.getContextUsage()\n\n返回活动模型的当前上下文使用情况。使用最后一次助理使用情况（如果可用），然后估计跟踪消息的标记。\n\n```typescript\nconst usage = ctx.getContextUsage();\nif (usage && usage.tokens > 100_000) {\n  // ...\n}\n```\n\n### ctx.compact()\n\n触发压缩而不等待完成。使用`onComplete`和`onError`进行后续操作。\n\n```typescript\nctx.compact({\n  customInstructions: \"Focus on recent changes\",\n  onComplete: (result) => {\n    ctx.ui.notify(\"Compaction completed\", \"info\");\n  },\n  onError: (error) => {\n    ctx.ui.notify(`Compaction failed: ${error.message}`, \"error\");\n  },\n});\n```\n\n### ctx.getSystemPrompt()\n\n返回Pi当前的系统提示字符串。\n\n- 在`before_agent_start`期间，这反映了当前回合迄今为止所做的连锁系统提示更改。\n- 它不包括后来的`context`消息突变。\n- 它不包括 `before_provider_request` 有效负载重写。\n- 如果稍后加载的扩展程序在您的扩展程序之后运行，它们仍然可以更改最终发送的内容。\n\n```typescript\npi.on(\"before_agent_start\", (event, ctx) => {\n  const prompt = ctx.getSystemPrompt();\n  console.log(`System prompt length: ${prompt.length}`);\n});\n```\n\n## 扩展命令上下文\n\n命令处理程序接收`ExtensionCommandContext`，它使用会话控制方法扩展`ExtensionContext`。这些仅在命令中可用，因为如果从事件处理程序调用它们可能会死锁。\n\n### ctx.getSystemPromptOptions()\n\n返回当前用于构建系统提示的基本输入 Pi。\n\n```typescript\nconst options = ctx.getSystemPromptOptions();\nconst contextPaths = options.contextFiles?.map((file) => file.path) ?? [];\n```\n\n它与`before_agent_start``event.systemPromptOptions`具有相同的形状和可变性：自定义提示、活动工具、工具片段、提示指南、附加系统提示文本、cwd、加载context files和加载技能。它可能包含完整的上下文文件内容，因此将其视为敏感的扩展本地数据，并避免通过命令列表、日志或自动完成元数据公开它。\n\n这会报告当前的基本提示输入。它不包括每轮`before_agent_start`链式系统提示更改、后来的`context`事件消息突变或`before_provider_request`有效负载重写。\n\n### ctx.waitForIdle()\n\n等待代理完全解决，包括自动重试、自动压缩重试和排队延续：\n\n```typescript\npi.registerCommand(\"my-cmd\", {\n  handler: async (args, ctx) => {\n    await ctx.waitForIdle();\n    // Agent is now idle, safe to modify session\n  },\n});\n```\n\n### ctx.newSession（选项？）\n\n创建一个新会话：\n\n```typescript\nconst parentSession = ctx.sessionManager.getSessionFile();\nconst kickoff = \"Continue in the replacement session\";\n\nconst result = await ctx.newSession({\n  parentSession,\n  setup: async (sm) => {\n    sm.appendMessage({\n      role: \"user\",\n      content: [{ type: \"text\", text: \"Context from previous session...\" }],\n      timestamp: Date.now(),\n    });\n  },\n  withSession: async (ctx) => {\n    // Use only the replacement-session ctx here.\n    await ctx.sendUserMessage(kickoff);\n  },\n});\n\nif (result.cancelled) {\n  // An extension cancelled the new session\n}\n```\n\n选项：\n- `parentSession`：要记录在新会话标头中的父会话文件\n- `setup`：在`withSession`运行之前改变新会话的`SessionManager`\n- `withSession`：针对新的替换会话上下文运行切换后工作。不要使用捕获的旧`pi`/命令`ctx`；见[Session replacement lifecycle and footguns](#session-replacement-lifecycle-and-footguns)。\n\n### ctx.fork(entryId, 选项?)\n\n从特定条目分叉，创建一个新的会话文件：\n\n```typescript\nconst result = await ctx.fork(\"entry-id-123\", {\n  withSession: async (ctx) => {\n    // Use only the replacement-session ctx here.\n    ctx.ui.notify(\"Now in the forked session\", \"info\");\n  },\n});\nif (result.cancelled) {\n  // An extension cancelled the fork\n}\n\nconst cloneResult = await ctx.fork(\"entry-id-456\", { position: \"at\" });\nif (cloneResult.cancelled) {\n  // An extension cancelled the clone\n}\n```\n\n选项：\n- `position`：`\"before\"`（默认）在选定的用户消息之前分叉，将该提示恢复到编辑器中\n- `position`：`\"at\"` 通过所选条目复制活动路径，而不恢复编辑器文本\n- `withSession`：针对新的替换会话上下文运行切换后工作。不要使用捕获的旧`pi`/命令`ctx`；见[Session replacement lifecycle and footguns](#session-replacement-lifecycle-and-footguns)。\n\n### ctx.navigateTree(targetId, 选项?)\n\n导航到 session tree 中的不同点：\n\n```typescript\nconst result = await ctx.navigateTree(\"entry-id-456\", {\n  summarize: true,\n  customInstructions: \"Focus on error handling changes\",\n  replaceInstructions: false, // true = replace default prompt entirely\n  label: \"review-checkpoint\",\n});\n```\n\n选项：\n- `summarize`：是否生成废弃分支的摘要\n- `customInstructions`：摘要器的自定义指令\n- `replaceInstructions`：如果为 true，则`customInstructions` 替换默认提示而不是附加\n- `label`：附加到分支摘要条目的标签（如果不汇总，则附加到目标条目）\n\n### ctx.switchSession（会话路径，选项？）\n\n切换到不同的会话文件：\n\n```typescript\nconst result = await ctx.switchSession(\"/path/to/session.jsonl\", {\n  withSession: async (ctx) => {\n    await ctx.sendUserMessage(\"Resume work in the replacement session\");\n  },\n});\nif (result.cancelled) {\n  // An extension cancelled the switch via session_before_switch\n}\n```\n\n选项：\n- `withSession`：针对新的替换会话上下文运行切换后工作。不要使用捕获的旧`pi`/命令`ctx`；见[Session replacement lifecycle and footguns](#session-replacement-lifecycle-and-footguns)。\n\n要发现可用会话，请使用静态 `SessionManager.list()` 或 `SessionManager.listAll()` 方法：\n\n```typescript\nimport { SessionManager } from \"@earendil-works/pi-coding-agent\";\n\npi.registerCommand(\"switch\", {\n  description: \"Switch to another session\",\n  handler: async (args, ctx) => {\n    const sessions = await SessionManager.list(ctx.cwd);\n    if (sessions.length === 0) return;\n    const choice = await ctx.ui.select(\n      \"Pick session:\",\n      sessions.map(s => s.file),\n    );\n    if (choice) {\n      await ctx.switchSession(choice, {\n        withSession: async (ctx) => {\n          ctx.ui.notify(\"Switched session\", \"info\");\n        },\n      });\n    }\n  },\n});\n```\n\n### 会话替换生命周期和脚枪\n\n`withSession` 接收一个新的 `ReplacedSessionContext`，它使用绑定到替换会话的异步 `sendMessage()` 和 `sendUserMessage()` 帮助程序扩展 `ExtensionCommandContext`。\n\n生命周期和脚枪：\n- `withSession`仅在旧会话已发出`session_shutdown`、旧运行时已被拆除、替换会话已反弹并且新扩展实例已收到`session_start`之后运行。\n- 回调仍然在原始闭包中执行，而不是在新的扩展实例中执行。这意味着您的旧扩展实例可能已经在 `withSession` 启动之前运行了关闭清理。\n- 捕获的旧 `pi` / 旧命令 `ctx` 会话绑定对象在替换后已过时，如果使用将抛出。仅使用传递给 `withSession` 的 `ctx` 进行会话绑定工作。\n- 之前提取的原始对象仍然是您的责任。例如，如果您在替换之前捕获 `const sm = ctx.sessionManager`，则 `sm` 仍然是旧的 `SessionManager` 对象。更换后请勿重复使用。\n- `withSession` 中的代码应假定由 `session_shutdown` 处理程序无效的任何状态都已经消失。仅捕获在完全关闭后仍然存在的纯数据，例如字符串、ID 和序列化配置。\n\n安全模式：\n\n```typescript\npi.registerCommand(\"handoff\", {\n  handler: async (_args, ctx) => {\n    const kickoff = \"Continue from the replacement session\";\n    await ctx.newSession({\n      withSession: async (ctx) => {\n        await ctx.sendUserMessage(kickoff);\n      },\n    });\n  },\n});\n```\n\n不安全模式：\n\n```typescript\npi.registerCommand(\"handoff\", {\n  handler: async (_args, ctx) => {\n    const oldSessionManager = ctx.sessionManager;\n    await ctx.newSession({\n      withSession: async (_ctx) => {\n        // stale old objects: do not do this\n        oldSessionManager.getSessionFile();\n        pi.sendUserMessage(\"wrong\");\n      },\n    });\n  },\n});\n```\n\n### ctx.reload()\n\n运行与 `/reload` 相同的重新加载流程。\n\n```typescript\npi.registerCommand(\"reload-runtime\", {\n  description: \"Reload extensions, skills, prompts, themes, and context files\",\n  handler: async (_args, ctx) => {\n    await ctx.reload();\n    return;\n  },\n});\n```\n\n重要行为：\n- `await ctx.reload()` 为当前扩展运行时发出 `session_shutdown`\n- 然后它重新加载资源并发出 `session_start` 和 `reason: \"reload\"` 以及 `resources_discover` 和原因 `\"reload\"`\n- 当前运行的命令处理程序仍然在旧的调用框架中继续\n- `await ctx.reload()`之后的代码仍然从预重新加载版本运行\n- `await ctx.reload()` 之后的代码不得假设旧的内存扩展状态仍然有效\n- 处理程序返回后，未来的命令/事件/工具调用将使用新的扩展版本\n\n对于可预测的行为，请将重新加载视为该处理程序的终端 (`await ctx.reload(); return;`)。\n\n工具以`ExtensionContext`运行，因此无法直接调用`ctx.reload()`。使用命令作为重新加载入口点，然后公开一个将该命令作为后续用户消息排队的工具。\n\nLLM 可以调用来触发重新加载的示例工具：\n\n```typescript\nimport type { ExtensionAPI } from \"@earendil-works/pi-coding-agent\";\nimport { Type } from \"typebox\";\n\nexport default function (pi: ExtensionAPI) {\n  pi.registerCommand(\"reload-runtime\", {\n    description: \"Reload extensions, skills, prompts, themes, and context files\",\n    handler: async (_args, ctx) => {\n      await ctx.reload();\n      return;\n    },\n  });\n\n  pi.registerTool({\n    name: \"reload_runtime\",\n    label: \"Reload Runtime\",\n    description: \"Reload extensions, skills, prompts, themes, and context files\",\n    parameters: Type.Object({}),\n    async execute() {\n      pi.sendUserMessage(\"/reload-runtime\", { deliverAs: \"followUp\" });\n      return {\n        content: [{ type: \"text\", text: \"Queued /reload-runtime as a follow-up command.\" }],\n      };\n    },\n  });\n}\n```\n\n## 扩展API方法\n\n### pi.on（事件，处理程序）\n\n订阅活动。事件类型和返回值请参见[Events](#events)。\n\n### pi.registerTool(定义)\n\n注册一个可由法学硕士调用的自定义工具。有关完整详细信息，请参阅[Custom Tools](#custom-tools)。\n\n`pi.registerTool()` 在扩展加载期间和启动后都有效。您可以在 `session_start`、命令处理程序或其他事件处理程序中调用它。新工具会在同一个会话中立即刷新，因此它们出现在`pi.getAllTools()`中，并且可以由法学硕士调用，无需`/reload`。\n\n使用`pi.setActiveTools()`在运行时启用或禁用工具（包括动态添加的工具）。\n\n使用 `promptSnippet` 将自定义工具选择到 `Available tools` 中的单行条目中，并使用 `promptGuidelines` 在工具处于活动状态时将特定于工具的项目符号附加到默认的 `Guidelines` 部分。\n\n**重要提示：** `promptGuidelines` 项目符号平铺到 `Guidelines` 部分，没有工具名称前缀。每条指南必须命名它所引用的工具——避免“在......时使用此工具”，因为法学硕士无法分辨“这”意味着哪个工具。写“当...时使用 my_tool”。\n\n完整示例请参见[dynamic-tools.ts](../examples/extensions/dynamic-tools.ts)。\n\n```typescript\nimport { Type } from \"typebox\";\nimport { StringEnum } from \"@earendil-works/pi-ai\";\n\npi.registerTool({\n  name: \"my_tool\",\n  label: \"My Tool\",\n  description: \"What this tool does\",\n  promptSnippet: \"Summarize or transform text according to action\",\n  promptGuidelines: [\"Use my_tool when the user asks to summarize previously generated text.\"],\n  parameters: Type.Object({\n    action: StringEnum([\"list\", \"add\"] as const),\n    text: Type.Optional(Type.String()),\n  }),\n  prepareArguments(args) {\n    // Optional compatibility shim. Runs before schema validation.\n    // Return the current schema shape, for example to fold legacy fields\n    // into the modern parameter object.\n    return args;\n  },\n\n  async execute(toolCallId, params, signal, onUpdate, ctx) {\n    // Stream progress\n    onUpdate?.({ content: [{ type: \"text\", text: \"Working...\" }] });\n\n    return {\n      content: [{ type: \"text\", text: \"Done\" }],\n      details: { result: \"...\" },\n    };\n  },\n\n  // Optional: Custom rendering\n  renderCall(args, theme, context) { ... },\n  renderResult(result, options, theme, context) { ... },\n});\n```\n\n### pi.sendMessage（消息，选项？）\n\n将自定义消息注入会话中。自定义消息参与 LLM 上下文。对于不应发送至 LLM 的持久 TUI 内容，请将 [`pi.appendEntry()`](#piappendentrycustomtype-data) 与 [`pi.registerEntryRenderer()`](#piregisterentryrenderercustomtype-renderer) 结合使用。\n\n```typescript\npi.sendMessage({\n  customType: \"my-extension\",\n  content: \"Message text\",\n  display: true,\n  details: { ... },\n}, {\n  triggerTurn: true,\n  deliverAs: \"steer\",\n});\n```\n\n**选项：**\n- `deliverAs` - 交付方式：\n  - `\"steer\"`（默认）- 在流式传输时对消息进行排队。在当前助理轮次完成执行其工具调用后、下一次 LLM 调用之前交付。\n  - `\"followUp\"` - 等待代理完成。仅当代理不再有工具调用时才传送。\n  - `\"nextTurn\"` - 排队等待下一个用户提示。不会中断或触发任何事情。\n- `triggerTurn: true` - 如果代理空闲，立即触发 LLM 响应。仅适用于 `\"steer\"` 和 `\"followUp\"` 模式（`\"nextTurn\"` 忽略）。\n\n### pi.sendUserMessage（内容，选项？）\n\n向代理发送用户消息。与发送自定义消息的 `sendMessage()` 不同，它发送一条实际的用户消息，看起来就像是由用户键入的。总是触发转弯。\n\n```typescript\n// Simple text message\npi.sendUserMessage(\"What is 2+2?\");\n\n// With content array (text + images)\npi.sendUserMessage([\n  { type: \"text\", text: \"Describe this image:\" },\n  { type: \"image\", source: { type: \"base64\", mediaType: \"image/png\", data: \"...\" } },\n]);\n\n// During streaming - must specify delivery mode\npi.sendUserMessage(\"Focus on error handling\", { deliverAs: \"steer\" });\npi.sendUserMessage(\"And then summarize\", { deliverAs: \"followUp\" });\n```\n\n**选项：**\n- `deliverAs` - 代理流式传输时需要：\n  - `\"steer\"` - 在当前助手轮完成执行其工具调用后将消息排队等待传递\n  - `\"followUp\"` - 等待代理完成所有工具\n\n当不流式传输时，消息会立即发送并触发新一轮。当没有 `deliverAs` 的情况下进行流式传输时，会抛出错误。\n\n完整示例请参见[send-user-message.ts](../examples/extensions/send-user-message.ts)。\n\n### pi.appendEntry（自定义类型，数据？）\n\n保留扩展数据。自定义条目不参与 LLM 上下文。在交互模式下，当与 `pi.registerEntryRenderer()` 配对时，它们还可以在聊天记录中呈现。\n\n```typescript\npi.appendEntry(\"my-state\", { count: 42 });\npi.appendEntry(\"status-card\", { title: \"Indexed files\", count: 17 });\n\n// Restore on reload\npi.on(\"session_start\", async (_event, ctx) => {\n  for (const entry of ctx.sessionManager.getEntries()) {\n    if (entry.type === \"custom\" && entry.customType === \"my-state\") {\n      // Reconstruct from entry.data\n    }\n  }\n});\n```\n\n### pi.setSessionName(名称)\n\n设置会话显示名称（显示在会话选择器中而不是第一条消息中）。\n\n```typescript\npi.setSessionName(\"Refactor auth module\");\n```\n\n### pi.getSessionName()\n\n获取当前会话名称（如果已设置）。\n\n```typescript\nconst name = pi.getSessionName();\nif (name) {\n  console.log(`Session: ${name}`);\n}\n```\n\n### pi.setLabel(entryId, 标签)\n\n设置或清除条目上的标签。标签是用户定义的书签和导航标记（显示在 `/tree` 选择器中）。\n\n```typescript\n// Set a label\npi.setLabel(entryId, \"checkpoint-before-refactor\");\n\n// Clear a label\npi.setLabel(entryId, undefined);\n\n// Read labels via sessionManager\nconst label = ctx.sessionManager.getLabel(entryId);\n```\n\n标签在会话中保留并在重新启动后继续存在。使用它们来标记对话树中的重要点（回合、检查点）。\n\n### pi.registerCommand(名称, 选项)\n\n注册命令。\n\n如果多个扩展注册相同的命令名称，pi 会保留所有扩展并按加载顺序分配数字调用后缀，例如 `/review:1` 和 `/review:2`。\n\n```typescript\npi.registerCommand(\"stats\", {\n  description: \"Show session statistics\",\n  handler: async (args, ctx) => {\n    const count = ctx.sessionManager.getEntries().length;\n    ctx.ui.notify(`${count} entries`, \"info\");\n  }\n});\n```\n\n可选：为 `/command...` 添加参数自动完成：\n\n```typescript\nimport type { AutocompleteItem } from \"@earendil-works/pi-tui\";\n\npi.registerCommand(\"deploy\", {\n  description: \"Deploy to an environment\",\n  getArgumentCompletions: (prefix: string): AutocompleteItem[] | null => {\n    const envs = [\"dev\", \"staging\", \"prod\"];\n    const items = envs.map((e) => ({ value: e, label: e }));\n    const filtered = items.filter((i) => i.value.startsWith(prefix));\n    return filtered.length > 0 ? filtered : null;\n  },\n  handler: async (args, ctx) => {\n    ctx.ui.notify(`Deploying: ${args}`, \"info\");\n  },\n});\n```\n\n### pi.getCommands()\n\n在当前会话中通过`prompt`获取可调用的slash commands。包括扩展命令、prompt templates和技能命令。\n该列表符合 RPC `get_commands` 顺序：首先是扩展，然后是模板，最后是技能。\n\n```typescript\nconst commands = pi.getCommands();\nconst bySource = commands.filter((command) => command.source === \"extension\");\nconst userScoped = commands.filter((command) => command.sourceInfo.scope === \"user\");\n```\n\n每个条目都有这样的形状：\n\n```typescript\n{\n  name: string; // Invokable command name without the leading slash. May be suffixed like \"review:1\"\n  description?: string;\n  source: \"extension\" | \"prompt\" | \"skill\";\n  sourceInfo: {\n    path: string;\n    source: string;\n    scope: \"user\" | \"project\" | \"temporary\";\n    origin: \"package\" | \"top-level\";\n    baseDir?: string;\n  };\n}\n```\n\n使用 `sourceInfo` 作为规范来源字段。不要从命令名称或临时路径解析推断所有权。\n\n这里不包括内置的交互式命令（如`/model`和`/settings`）。它们仅在交互中处理\n模式，如果通过`prompt`发送则不会执行。\n\n### pi.registerMessageRenderer(customType, 渲染器)\n\n使用您的 `customType` 为自定义消息注册自定义 TUI 渲染器。自定义消息使用 `pi.sendMessage()` 创建并参与 LLM 上下文。参见[Custom UI](#custom-ui)。\n\n### pi.registerMarkdownTransformer(变压器)\n\n为普通用户文本、辅助文本和思维块中的Markdown注册一个转换器。变压器按照扩展负载顺序运行，每个变压器接收前一个变压器返回的Markdown。链完成后，Pi使用其内置渲染器渲染转换后的内容。\n\n转换器接收 Markdown 字符串和上下文：\n\n- `messageType` — `\"user\"`、`\"assistant\"` 或 `\"assistant-thinking\"`\n- `isStreaming` — `true` 用于部分助手更新； `false` 用户、最终确定的助手和恢复的消息\n- `availableWidth` — 可用于转换后的 Markdown 内容的精确终端列\n\n返回变换后的Markdown：\n\n```typescript\npi.registerMarkdownTransformer((markdown, { messageType, isStreaming }) => {\n  if (isStreaming || messageType === \"assistant-thinking\") return markdown;\n  return markdown.replaceAll(\"-->\", \"→\");\n});\n```\n\n如果变压器抛出异常，Pi 会保留到目前为止生成的 Markdown 并继续处理下一个变压器。该挂钩仅用于显示：原始消息在会话和模型上下文中保持不变。它运行新的用户消息、辅助流更新、恢复的会话消息和终端宽度变化，因此变压器应该保持同步且便宜。\n\n### pi.registerEntryRenderer(customType, 渲染器)\n\n使用您的 `customType` 为自定义条目注册自定义 TUI 渲染器。自定义条目是使用 `pi.appendEntry()` 创建的，不参与 LLM 上下文。\n\n```typescript\nimport { Box, Text } from \"@earendil-works/pi-tui\";\n\npi.registerEntryRenderer(\"status-card\", (entry, { expanded }, theme) => {\n  const data = entry.data as { title: string; count: number };\n  const box = new Box(1, 1, (text) => theme.bg(\"customMessageBg\", text));\n  box.addChild(new Text(`${theme.bold(data.title)}: ${data.count}`));\n  if (expanded) {\n    box.addChild(new Text(theme.fg(\"dim\", JSON.stringify(data, null, 2))));\n  }\n  return box;\n});\n\npi.appendEntry(\"status-card\", { title: \"Indexed files\", count: 17 });\n```\n\n### pi.registerShortcut（快捷方式，选项）\n\n注册键盘快捷键。请参阅 [keybindings.md](keybindings.md) 了解快捷方式格式和内置按键绑定。\n\n```typescript\npi.registerShortcut(\"ctrl+shift+p\", {\n  description: \"Toggle plan mode\",\n  handler: async (ctx) => {\n    ctx.ui.notify(\"Toggled!\");\n  },\n});\n```\n\n### pi.registerFlag(名称, 选项)\n\n注册一个CLI标志。\n\n```typescript\npi.registerFlag(\"plan\", {\n  description: \"Start in plan mode\",\n  type: \"boolean\",\n  default: false,\n});\n\n// Check value\nif (pi.getFlag(\"plan\")) {\n  // Plan mode enabled\n}\n```\n\n### pi.exec（命令、参数、选项？）\n\n执行外壳命令。\n\n```typescript\nconst result = await pi.exec(\"git\", [\"status\"], { signal, timeout: 5000 });\n// result.stdout, result.stderr, result.code, result.killed\n```\n\n### pi.getActiveTools() / pi.getAllTools() / pi.setActiveTools(names)\n\n管理活动工具。这适用于内置工具和动态注册工具。 `pi.getActiveTools()` 返回活动工具名称为 `string[]`； `pi.getAllTools()` 返回所有已配置工具的元数据。\n\n```typescript\nconst active = pi.getActiveTools(); // [\"read\", \"bash\", ...]\nconst all = pi.getAllTools();\n// all = [{\n//   name: \"read\",\n//   description: \"Read file contents...\",\n//   parameters: ...,\n//   promptGuidelines: [\"Use read to examine files instead of cat or sed.\"],\n//   sourceInfo: { path: \"<builtin:read>\", source: \"builtin\", scope: \"temporary\", origin: \"top-level\" }\n// }, ...]\nconst builtinTools = all.filter((t) => t.sourceInfo.source === \"builtin\");\nconst extensionTools = all.filter((t) => t.sourceInfo.source !== \"builtin\" && t.sourceInfo.source !== \"sdk\");\npi.setActiveTools([...new Set([...active, \"my_custom_tool\"])]); // Keep current tools and enable my_custom_tool\npi.setActiveTools([\"read\", \"bash\"]); // Switch to read-only\n```\n\n`pi.getAllTools()` 返回 `name`、`description`、`parameters`、`promptGuidelines` 和 `sourceInfo`。\n\n典型 `sourceInfo.source` 值：\n- `builtin` 用于内置工具\n- `sdk` 对于通过 `createAgentSession({ customTools })` 传递的工具\n- 由扩展注册的工具的扩展源元数据\n\n### pi.setModel(模型)\n\n设置当前模型。如果模型没有可用的 API key，则返回 `false`。请参阅 [models.md](models.md) 配置自定义模型。\n\n```typescript\nconst model = ctx.modelRegistry.find(\"anthropic\", \"claude-sonnet-4-5\");\nif (model) {\n  const success = await pi.setModel(model);\n  if (!success) {\n    ctx.ui.notify(\"No API key for this model\", \"error\");\n  }\n}\n```\n\n### pi.getThinkingLevel() / pi.setThinkingLevel(级别)\n\n获取或设置思维水平。级别受限于模型能力（非推理模型始终使用“off”）。变化发出`thinking_level_select`。\n\n```typescript\nconst current = pi.getThinkingLevel();  // \"off\" | \"minimal\" | \"low\" | \"medium\" | \"high\" | \"xhigh\" | \"max\"\npi.setThinkingLevel(\"high\");\n```\n\n### pi.事件\n\n用于扩展之间通信的共享事件总线：\n\n```typescript\npi.events.on(\"my:event\", (data) => { ... });\npi.events.emit(\"my:event\", { ... });\n```\n\n### pi.registerProvider(名称, 配置)\n\n动态注册或覆盖模型提供者。对于代理、自定义端点或团队范围的模型配置很有用。\n\n一旦运行程序初始化，扩展工厂函数期间进行的调用就会排队并应用。此后进行的调用（例如，从用户设置流程后的命令处理程序进行的调用）立即生效，无需 `/reload`。\n\n动态提供者可以实现`refreshModels`。 Pi 在模型刷新期间调用它，通过提供者同步发布返回的列表，并传递规范凭证/存储目录/网络/信号上下文。扩展通过生成检查`context.publish({ persist: entry })`决定是否持久化目录元数据；诸如 llama.cpp 之类的实时服务器可以返回模型而不保留它们。\n\n`context.signal` 始终是一个具体信号，提供者回调必须将其传递给阻塞 I/O。公共 `ModelRuntime.refresh()` 和 `ModelRegistry.refresh()` 调用接受可选信号，并且在省略时不受限制；延期和申请选择自己的截止日期。即使提供者忽略信号，取消也会让调用者停止等待，但仍然需要合作来停止底层工作。\n\n需要本机提供者身份验证、过滤、刷新或流行为的Extensions可以从`@earendil-works/pi-ai`注册完整的`Provider`。提供者成为组合基础，并且 `models.json` 覆盖仍然适用于其之上。\n\n```typescript\nimport { createProvider, openAICompletionsApi } from \"@earendil-works/pi-ai\";\n\nconst provider = createProvider({\n  id: \"local-server\",\n  name: \"Local Server\",\n  baseUrl: \"http://localhost:8080/v1\",\n  auth: {\n    apiKey: {\n      name: \"Local server setup\",\n      async login(interaction) {\n        return {\n          type: \"api_key\",\n          key: await interaction.prompt({ type: \"secret\", message: \"API key\" }),\n        };\n      },\n      async resolve({ credential }) {\n        return credential?.key\n          ? { auth: { apiKey: credential.key }, source: \"stored API key\" }\n          : undefined;\n      },\n    },\n  },\n  models: [],\n  api: openAICompletionsApi(),\n});\n\npi.registerProvider(provider);\n\n// Register a new provider with custom models\npi.registerProvider(\"my-proxy\", {\n  name: \"My Proxy\",\n  baseUrl: \"https://proxy.example.com\",\n  apiKey: \"$PROXY_API_KEY\",  // env var reference\n  api: \"anthropic-messages\",\n  models: [\n    {\n      id: \"claude-sonnet-4-20250514\",\n      name: \"Claude 4 Sonnet (proxy)\",\n      reasoning: false,\n      input: [\"text\", \"image\"],\n      cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },\n      contextWindow: 200000,\n      maxTokens: 16384\n    }\n  ]\n});\n\n// Register a live llama.cpp catalog without persisting discovered models\npi.registerProvider(\"llama.cpp\", {\n  baseUrl: \"http://localhost:8080/v1\",\n  apiKey: \"local\",\n  api: \"openai-completions\",\n  async refreshModels({ signal }) {\n    const response = await fetch(\"http://localhost:8080/v1/models\", { signal });\n    const { data } = await response.json();\n    return data.map(({ id }) => ({\n      id,\n      name: id,\n      reasoning: false,\n      input: [\"text\"],\n      cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },\n      contextWindow: 128000,\n      maxTokens: 16384\n    }));\n  }\n});\n\n// Override baseUrl for an existing provider (keeps all models)\npi.registerProvider(\"anthropic\", {\n  baseUrl: \"https://proxy.example.com\"\n});\n\n// Register provider with OAuth support for /login\npi.registerProvider(\"corporate-ai\", {\n  baseUrl: \"https://ai.corp.com\",\n  api: \"openai-responses\",\n  models: [...],\n  oauth: {\n    name: \"Corporate AI (SSO)\",\n    async login(callbacks) {\n      // Custom OAuth flow\n      callbacks.onAuth({ url: \"https://sso.corp.com/...\" });\n      const code = await callbacks.onPrompt({ message: \"Enter code:\" });\n      return { refresh: code, access: code, expires: Date.now() + 3600000 };\n    },\n    async refreshToken(credentials, signal) {\n      signal.throwIfAborted();\n      // Refresh logic\n      return credentials;\n    },\n    getApiKey(credentials) {\n      return credentials.access;\n    }\n  }\n});\n```\n\n对象形式接受完整的 pi-ai `Provider`，包括原生 `auth`、`getModels`、`refreshModels`、`filterModels`、`stream` 和 `streamSimple` 行为。\n\n**旧配置选项：**\n- `name` - UI 中提供者的显示名称，例如 `/login`。\n- `baseUrl` - API 端点 URL。定义模型时需要。\n- `apiKey` - API key 文字、环境插值（`$ENV_VAR` 或 `${ENV_VAR}`）或前导 `!command`。定义模型时必需的（除非提供了 `oauth`）。 `$`转义``apiKey` - API key 文字、环境插值（`$ENV_VAR` 或 `${ENV_VAR}`）或前导 `!command`。定义模型时必需的（除非提供了 `oauth`）。 `$`转义，`$!`转义文字`!`而不触发命令执行。\n- `api` - API 类型：`\"anthropic-messages\"`、`\"openai-completions\"`、`\"openai-responses\"` 等。\n- `headers` - 要包含在请求中的自定义标头。\n- `authHeader` - 如果为 true，则自动添加 `Authorization: Bearer` 标头。\n- `models` - 模型定义数组。如果提供，则替换该提供商的所有现有模型。模型定义可以设置 `baseUrl` 来覆盖该模型的提供者端点。\n- `refreshModels` - 异步动态发现回调。它返回的模型取代了扩展提供的模型。 `context.stored` 包含持久化提供者快照；仅当更新的目录数据应持续存在时才使用生成检查`context.publish({ persist: entry })`。使用 `persist: null` 删除该快照。\n- `oauth` - OAuth 支持 `/login` 的提供程序配置。提供后，提供商会出现在登录菜单中。\n- `streamSimple` - 非标准 API 的自定义流实现。\n\n请参阅 [custom-provider.md](custom-provider.md) 了解高级主题：自定义流式传输 API、OAuth 详细信息、模型定义参考。\n\n### pi.unregisterProvider(名称)\n\n删除先前注册的提供程序及其模型。被提供者覆盖的内置模型将被恢复。如果提供商未注册，则无效。\n\n与`registerProvider`一样，这在初始加载阶段后调用时立即生效，因此不需要`/reload`。\n\n```typescript\npi.registerCommand(\"my-setup-teardown\", {\n  description: \"Remove the custom proxy provider\",\n  handler: async (_args, _ctx) => {\n    pi.unregisterProvider(\"my-proxy\");\n  },\n});\n```\n\n## 状态管理\n\n具有状态的Extensions应将其存储在工具结果`details`中以获得正确的分支支持：\n\n```typescript\nexport default function (pi: ExtensionAPI) {\n  let items: string[] = [];\n\n  // Reconstruct state from session\n  pi.on(\"session_start\", async (_event, ctx) => {\n    items = [];\n    for (const entry of ctx.sessionManager.getBranch()) {\n      if (entry.type === \"message\" && entry.message.role === \"toolResult\") {\n        if (entry.message.toolName === \"my_tool\") {\n          items = entry.message.details?.items ?? [];\n        }\n      }\n    }\n  });\n\n  pi.registerTool({\n    name: \"my_tool\",\n    // ...\n    async execute(toolCallId, params, signal, onUpdate, ctx) {\n      items.push(\"new item\");\n      return {\n        content: [{ type: \"text\", text: \"Added\" }],\n        details: { items: [...items] },  // Store for reconstruction\n      };\n    },\n  });\n}\n```\n\n## 定制工具\n\n注册LLM可以通过`pi.registerTool()`调用的工具。工具出现在系统提示符中，并且可以进行自定义渲染。\n\n在默认系统提示符的 `Available tools` 部分中使用 `promptSnippet` 进行简短的一行输入。如果省略，自定义工具将不包含在该部分中。\n\n使用 `promptGuidelines` 将特定于工具的项目符号添加到默认系统提示`Guidelines` 部分。这些项目符号仅在工具处于活动状态时包含（例如，在 `pi.setActiveTools([...])` 之后）。\n\n**重要提示：** `promptGuidelines` 项目符号平铺到 `Guidelines` 部分，没有工具名称前缀或分组。每条指南必须命名它所引用的工具——避免“在......时使用此工具”，因为法学硕士无法分辨“这”意味着哪个工具。写“当...时使用 my_tool”。\n\n注意：有些模型是白痴，在工具路径参数中包含 @ 前缀。内置工具会在解析路径之前去除前导@。如果您的自定义工具接受路径，也请规范化前导@。\n\n如果您的自定义工具会改变文件，请使用 `withFileMutationQueue()`，以便它参与与内置 `edit` 和 `write` 相同的每个文件队列。这很重要，因为默认情况下工具调用是并行运行的。如果没有队列，两个工具可以读取相同的旧文件内容，计算不同的更新，然后最后写入的内容覆盖另一个。\n\n失败案例示例：您的自定义工具编辑 `foo.ts`，而内置 `edit` 也在同一个助手回合中更改 `foo.ts`。如果您的工具不参与队列，则两者都可以读取原始 `foo.ts`，应用单独的更改，并且其中一个更改会丢失。\n\n将真实的目标文件路径传递给 `withFileMutationQueue()`，而不是原始用户参数。首先将其解析为相对于 `ctx.cwd` 或工具工作目录的绝对路径。对于现有文件，帮助器通过 `realpath()` 进行规范化，因此同一文件的符号链接别名共享一个队列。对于新文件，它会回退到已解析的绝对路径，因为 `realpath()` 还没有任何内容。\n\n将整个突变窗口排队到该目标路径上。这包括读取-修改-写入逻辑，而不仅仅是最终写入。\n\n```typescript\nimport { withFileMutationQueue } from \"@earendil-works/pi-coding-agent\";\nimport { mkdir, readFile, writeFile } from \"node:fs/promises\";\nimport { dirname, resolve } from \"node:path\";\n\nasync execute(_toolCallId, params, _signal, _onUpdate, ctx) {\n  const absolutePath = resolve(ctx.cwd, params.path);\n\n  return withFileMutationQueue(absolutePath, async () => {\n    await mkdir(dirname(absolutePath), { recursive: true });\n    const current = await readFile(absolutePath, \"utf8\");\n    const next = current.replace(params.oldText, params.newText);\n    await writeFile(absolutePath, next, \"utf8\");\n\n    return {\n      content: [{ type: \"text\", text: `Updated ${params.path}` }],\n      details: {},\n    };\n  });\n}\n```\n\n### 工具定义\n\n```typescript\nimport { Type } from \"typebox\";\nimport { StringEnum } from \"@earendil-works/pi-ai\";\nimport { Text } from \"@earendil-works/pi-tui\";\n\npi.registerTool({\n  name: \"my_tool\",\n  label: \"My Tool\",\n  description: \"What this tool does (shown to LLM)\",\n  promptSnippet: \"List or add items in the project todo list\",\n  promptGuidelines: [\n    \"Use my_tool for todo planning instead of direct file edits when the user asks for a task list.\"\n  ],\n  parameters: Type.Object({\n    action: StringEnum([\"list\", \"add\"] as const),  // Use StringEnum for Google compatibility\n    text: Type.Optional(Type.String()),\n  }),\n  prepareArguments(args) {\n    if (!args || typeof args !== \"object\") return args;\n    const input = args as { action?: string; oldAction?: string };\n    if (typeof input.oldAction === \"string\" && input.action === undefined) {\n      return { ...input, action: input.oldAction };\n    }\n    return args;\n  },\n\n  async execute(toolCallId, params, signal, onUpdate, ctx) {\n    // Check for cancellation\n    if (signal?.aborted) {\n      return { content: [{ type: \"text\", text: \"Cancelled\" }] };\n    }\n\n    // Stream progress updates\n    onUpdate?.({\n      content: [{ type: \"text\", text: \"Working...\" }],\n      details: { progress: 50 },\n    });\n\n    // Run commands via pi.exec (captured from extension closure)\n    const result = await pi.exec(\"some-command\", [], { signal });\n\n    // Return result\n    return {\n      content: [{ type: \"text\", text: \"Done\" }],  // Sent to LLM\n      details: { data: result },                   // For rendering & state\n      // usage: nestedModelResponse.usage,          // Optional nested LLM usage\n      // Optional: stop after this tool batch when every finalized tool result\n      // in the batch also returns terminate: true.\n      terminate: true,\n    };\n  },\n\n  // Optional: Custom rendering\n  renderCall(args, theme, context) { ... },\n  renderResult(result, options, theme, context) { ... },\n});\n```\n\n**使用情况统计：** 如果工具进行嵌套 LLM 调用，则将其组合 `Usage` 返回为 `usage`。 Pi 将其保留在工具结果中，并将其包含在页脚、`/session` 和 RPC 会话总计中。 `tool_result` 处理程序可以检查或替换该值。\n\n**发出错误信号：** 要将工具执行标记为失败（在结果上设置 `isError: true` 并将其报告给 LLM），请从 `execute` 抛出错误。无论返回对象中包含哪些属性，返回值都不会设置错误标志。\n\n**提前终止：**从`execute()`返回`terminate: true`，以提示在当前工具批次之后应跳过自动后续LLM调用。仅当该批次中的每个最终工具结果都终止时，此操作才会生效。有关代理以最终结构化输出工具调用结束的最小示例，请参阅 [examples/extensions/structured-output.ts](../examples/extensions/structured-output.ts)。\n\n```typescript\n// Correct: throw to signal an error\nasync execute(toolCallId, params) {\n  if (!isValid(params.input)) {\n    throw new Error(`Invalid input: ${params.input}`);\n  }\n  return { content: [{ type: \"text\", text: \"OK\" }], details: {} };\n}\n```\n\n**重要提示：** 使用 `@earendil-works/pi-ai` 中的 `StringEnum` 作为字符串枚举。 `Type.Union`/`Type.Literal` 不适用于 Google 的 API。\n\n**参数准备：** `prepareArguments(args)` 是可选的。如果定义，它会在模式验证之前和 `execute()` 之前运行。当 pi 恢复其存储的工具调用参数不再与当前模式匹配的旧会话时，使用它来模仿旧的接受的输入形状。返回您想要针对 `parameters` 进行验证的对象。保持公共架构严格。不要仅仅为了保持旧的恢复会话正常工作而将已弃用的兼容性字段添加到 `parameters`。\n\n示例：旧会话可能包含具有顶级 `oldText` 和 `newText` 的 `edit` 工具调用，而当前架构仅接受 `edits: [{ oldText, newText }]`。\n\n```typescript\npi.registerTool({\n  name: \"edit\",\n  label: \"Edit\",\n  description: \"Edit a single file using exact text replacement\",\n  parameters: Type.Object({\n    path: Type.String(),\n    edits: Type.Array(\n      Type.Object({\n        oldText: Type.String(),\n        newText: Type.String(),\n      }),\n    ),\n  }),\n  prepareArguments(args) {\n    if (!args || typeof args !== \"object\") return args;\n\n    const input = args as {\n      path?: string;\n      edits?: Array<{ oldText: string; newText: string }>;\n      oldText?: unknown;\n      newText?: unknown;\n    };\n\n    if (typeof input.oldText !== \"string\" || typeof input.newText !== \"string\") {\n      return args;\n    }\n\n    return {\n      ...input,\n      edits: [...(input.edits ?? []), { oldText: input.oldText, newText: input.newText }],\n    };\n  },\n  async execute(toolCallId, params, signal, onUpdate, ctx) {\n    // params now matches the current schema\n    return {\n      content: [{ type: \"text\", text: `Applying ${params.edits.length} edit block(s)` }],\n      details: {},\n    };\n  },\n});\n```\n\n### 覆盖内置工具\n\nExtensions 可以通过注册同名工具来覆盖内置工具（`read`、`bash`、`edit`、`write`、`grep`、`find`、`ls`）。发生这种情况时，交互模式会显示警告。\n\n```bash\n# Extension's read tool replaces built-in read\npi -e ./tool-override.ts\n```\n\n或者，使用 `--no-builtin-tools` 在不使用任何内置工具的情况下启动，同时保持扩展工具启用：\n```bash\n# No built-in tools, only extension tools\npi --no-builtin-tools -e ./my-extension.ts\n```\n\n有关使用日志记录和访问控制覆盖 `read` 的完整示例，请参阅 [examples/extensions/tool-override.ts](../examples/extensions/tool-override.ts)。\n\n**渲染：** 内置渲染器继承是按插槽解析的。执行覆盖和渲染覆盖是独立的。如果您的覆盖省略 `renderCall`，则使用内置 `renderCall`。如果您的覆盖省略 `renderResult`，则使用内置 `renderResult`。如果您的覆盖忽略两者，则会自动使用内置渲染器（语法突出显示、差异等）。这使您可以封装用于日志记录或访问控制的内置工具，而无需重新实现 UI。\n\n**提示元数据：** `promptSnippet`和`promptGuidelines`不是从内置工具继承的。如果您的覆盖应保留这些提示说明，请在覆盖上明确定义它们。\n\n**您的实现必须与确切的结果形状匹配**，包括 `details` 类型。 UI 和会话逻辑依赖于这些形状来进行渲染和状态跟踪。\n\n内置工具实现：\n- [read.ts](https://github.com/earendil-works/pi-mono/blob/main/packages/coding-agent/src/core/tools/read.ts) - `ReadToolDetails`\n- [bash.ts](https://github.com/earendil-works/pi-mono/blob/main/packages/coding-agent/src/core/tools/bash.ts) - `BashToolDetails`\n- [edit.ts](https://github.com/earendil-works/pi-mono/blob/main/packages/coding-agent/src/core/tools/edit.ts)\n- [write.ts](https://github.com/earendil-works/pi-mono/blob/main/packages/coding-agent/src/core/tools/write.ts)\n- [grep.ts](https://github.com/earendil-works/pi-mono/blob/main/packages/coding-agent/src/core/tools/grep.ts) - `GrepToolDetails`\n- [find.ts](https://github.com/earendil-works/pi-mono/blob/main/packages/coding-agent/src/core/tools/find.ts) - `FindToolDetails`\n- [ls.ts](https://github.com/earendil-works/pi-mono/blob/main/packages/coding-agent/src/core/tools/ls.ts) - `LsToolDetails`\n\n### 远程执行\n\n内置工具支持可插拔操作以委托给远程系统（SSH、容器等）：\n\n```typescript\nimport { createReadTool, createBashTool, type ReadOperations } from \"@earendil-works/pi-coding-agent\";\n\n// Create tool with custom operations\nconst remoteRead = createReadTool(cwd, {\n  operations: {\n    readFile: (path) => sshExec(remote, `cat ${path}`),\n    access: (path) => sshExec(remote, `test -r ${path}`).then(() => {}),\n  }\n});\n\n// Register, checking flag at execution time\npi.registerTool({\n  ...remoteRead,\n  async execute(id, params, signal, onUpdate, _ctx) {\n    const ssh = getSshConfig();\n    if (ssh) {\n      const tool = createReadTool(cwd, { operations: createRemoteOps(ssh) });\n      return tool.execute(id, params, signal, onUpdate);\n    }\n    return localRead.execute(id, params, signal, onUpdate);\n  },\n});\n```\n\n**操作接口：** `ReadOperations`、`WriteOperations`、`EditOperations`、`BashOperations`、`LsOperations`、`GrepOperations`、`FindOperations`\n\n对于`user_bash`，扩展可以通过`createLocalBashOperations()`重用pi的本地shell后端，而不是重新实现本地进程生成、shell解析和进程树终止。\n\nbash工具还支持spawn hook，用于在执行前调整命令、cwd或env：\n\n```typescript\nimport { createBashTool } from \"@earendil-works/pi-coding-agent\";\n\nconst bashTool = createBashTool(cwd, {\n  spawnHook: ({ command, cwd, env }) => ({\n    command: `source ~/.profile\\n${command}`,\n    cwd: `/mnt/sandbox${cwd}`,\n    env: { ...env, CI: \"1\" },\n  }),\n});\n```\n\n`createBashTool()` 通过`PI_SESSION_ID`、`PI_SESSION_FILE`、`PI_PROVIDER`、`PI_MODEL` 和`PI_REASONING_LEVEL` 将当前会话公开给命令。注入发生在`spawnHook`之前，因此钩子在`env`中接收这些值，并在如上所述传播现有环境时保留它们。设置 `exposeSessionEnvironment: false` 禁用它们：\n\n```typescript\nconst bashTool = createBashTool(cwd, {\n  exposeSessionEnvironment: false,\n});\n```\n\n有关变量语义，请参阅[Bash tool session environment](environment-variables.md#bash-tool-session-environment)。有关带有 `--ssh` 标志的完整 SSH 示例，请参阅 [examples/extensions/ssh.ts](../examples/extensions/ssh.ts)。\n\n### 输出截断\n\n**工具必须截断其输出**以避免压垮 LLM 上下文。大输出可能会导致：\n- 上下文溢出错误（提示太长）\n- 压实失败\n- 模型性能下降\n\n内置限制为 **50KB**（约 10k 代币）和 **2000 行**，以先达到者为准。使用导出的截断实用程序：\n\n```typescript\nimport {\n  truncateHead,      // Keep first N lines/bytes (good for file reads, search results)\n  truncateTail,      // Keep last N lines/bytes (good for logs, command output)\n  truncateLine,      // Truncate a single line to maxBytes with ellipsis\n  formatSize,        // Human-readable size (e.g., \"50KB\", \"1.5MB\")\n  DEFAULT_MAX_BYTES, // 50KB\n  DEFAULT_MAX_LINES, // 2000\n} from \"@earendil-works/pi-coding-agent\";\n\nasync execute(toolCallId, params, signal, onUpdate, ctx) {\n  const output = await runCommand();\n\n  // Apply truncation\n  const truncation = truncateHead(output, {\n    maxLines: DEFAULT_MAX_LINES,\n    maxBytes: DEFAULT_MAX_BYTES,\n  });\n\n  let result = truncation.content;\n\n  if (truncation.truncated) {\n    // Write full output to temp file\n    const tempFile = writeTempFile(output);\n\n    // Inform the LLM where to find complete output\n    result += `\\n\\n[Output truncated: ${truncation.outputLines} of ${truncation.totalLines} lines`;\n    result += ` (${formatSize(truncation.outputBytes)} of ${formatSize(truncation.totalBytes)}).`;\n    result += ` Full output saved to: ${tempFile}]`;\n  }\n\n  return { content: [{ type: \"text\", text: result }] };\n}\n```\n\n**要点：**\n- 对于开头很重要的内容（搜索结果、文件读取）使用 `truncateHead`\n- 对于结尾重要的内容（日志、命令输出）使用 `truncateTail`\n- 当输出被截断时，务必通知法学硕士以及在哪里可以找到完整版本\n- 在工具描述中记录截断限制\n\n请参阅 [examples/extensions/truncated-tool.ts](../examples/extensions/truncated-tool.ts) 以获取使用适当截断包装 `rg` (ripgrep) 的完整示例。\n\n### 多种工具\n\n一个扩展可以注册多个具有共享状态的工具：\n\n```typescript\nexport default function (pi: ExtensionAPI) {\n  let connection = null;\n\n  pi.registerTool({ name: \"db_connect\", ... });\n  pi.registerTool({ name: \"db_query\", ... });\n  pi.registerTool({ name: \"db_close\", ... });\n\n  pi.on(\"session_shutdown\", async () => {\n    connection?.close();\n  });\n}\n```\n\n### 自定义渲染\n\n工具可以提供`renderCall`和`renderResult`用于自定义TUI显示。请参阅 [tui.md](tui.md) 了解完整组件 API 和 [tool-execution.ts](https://github.com/earendil-works/pi-mono/blob/main/packages/coding-agent/src/modes/interactive/components/tool-execution.ts) 了解工具行的组成方式。\n\n默认情况下，工具输出包装在处理填充和背景的 `Box` 中。定义的 `renderCall` 或 `renderResult` 必须返回 `Component`。如果未定义槽渲染器，则 `tool-execution.ts` 使用该槽的后备渲染。\n\n当工具应该渲染自己的 shell 而不是使用默认的 `Box` 时，设置 `renderShell: \"self\"`。这对于需要完全控制取景或背景行为的工具非常有用，例如在工具稳定后必须保持视觉稳定的大型预览。\n\n```typescript\npi.registerTool({\n  name: \"my_tool\",\n  label: \"My Tool\",\n  description: \"Custom shell example\",\n  parameters: Type.Object({}),\n  renderShell: \"self\",\n  async execute() {\n    return { content: [{ type: \"text\", text: \"ok\" }], details: undefined };\n  },\n  renderCall(args, theme, context) {\n    return new Text(theme.fg(\"accent\", \"my custom shell\"), 0, 0);\n  },\n});\n```\n\n`renderCall` 和 `renderResult` 各自接收一个 `context` 对象，其中：\n- `args` - 当前工具调用参数\n- `state` - 跨`renderCall`和`renderResult`共享行本地状态\n- `lastComponent` - 该插槽之前返回的组件（如果有）\n- `invalidate()` - 请求重新渲染此工具行\n- `toolCallId`, `cwd`, `executionStarted`, `argsComplete`, `isPartial`, `expanded`, `showImages`, `isError`\n\n使用 `context.state` 来实现跨槽共享状态。当您想要跨渲染重用和改变同一组件时，请在返回的组件实例上保留插槽本地缓存。\n\n#### 渲染调用\n\n呈现工具调用或标头：\n\n```typescript\nimport { Text } from \"@earendil-works/pi-tui\";\n\nrenderCall(args, theme, context) {\n  const text = (context.lastComponent as Text | undefined) ?? new Text(\"\", 0, 0);\n  let content = theme.fg(\"toolTitle\", theme.bold(\"my_tool \"));\n  content += theme.fg(\"muted\", args.action);\n  if (args.text) {\n    content += \" \" + theme.fg(\"dim\", `\"${args.text}\"`);\n  }\n  text.setText(content);\n  return text;\n}\n```\n\n#### 渲染结果\n\n呈现工具结果或输出：\n\n```typescript\nrenderResult(result, { expanded, isPartial }, theme, context) {\n  if (isPartial) {\n    return new Text(theme.fg(\"warning\", \"Processing...\"), 0, 0);\n  }\n\n  if (result.details?.error) {\n    return new Text(theme.fg(\"error\", `Error: ${result.details.error}`), 0, 0);\n  }\n\n  let text = theme.fg(\"success\", \"✓ Done\");\n  if (expanded && result.details?.items) {\n    for (const item of result.details.items) {\n      text += \"\\n  \" + theme.fg(\"dim\", item);\n    }\n  }\n  return new Text(text, 0, 0);\n}\n```\n\n如果槽故意没有可见内容，则返回空的 `Component`，例如空的 `Container`。\n\n#### 键绑定提示\n\n使用 `keyHint()` 显示遵循活动键绑定配置的键绑定提示：\n\n```typescript\nimport { keyHint } from \"@earendil-works/pi-coding-agent\";\n\nrenderResult(result, { expanded }, theme, context) {\n  let text = theme.fg(\"success\", \"✓ Done\");\n  if (!expanded) {\n    text += ` (${keyHint(\"app.tools.expand\", \"to expand\")})`;\n  }\n  return new Text(text, 0, 0);\n}\n```\n\n可用功能：\n- `keyHint(keybinding, description)` - 格式化配置的键绑定 ID，例如 `\"app.tools.expand\"` 或 `\"tui.select.confirm\"`\n- `keyText(keybinding)` - 返回按键绑定 id 的原始配置按键文本\n- `rawKeyHint(key, description)` - 格式化原始密钥字符串\n\n使用命名空间键绑定 ID：\n- 编码代理 ID 使用 `app.*` 命名空间，例如 `app.tools.expand`、`app.editor.external`、`app.session.rename`\n- 共享 TUI id 使用 `tui.*` 命名空间，例如 `tui.select.confirm`、`tui.select.cancel`、`tui.input.tab`\n\n有关键绑定 ID 和默认值的详尽列表，请参阅 [keybindings.md](keybindings.md)。 `keybindings.json` 使用相同的命名空间 id。\n\n自定义编辑器和 `ctx.ui.custom()` 组件接收 `keybindings: KeybindingsManager` 作为注入参数。他们应该直接使用注入的管理器，而不是调用 `getKeybindings()` 或 `setKeybindings()`。\n\n#### 最佳实践\n\n- 使用 `Text` 和填充 `(0, 0)`。默认的 Box 处理填充。\n- 使用 `\\n` 表示多行内容。\n- 处理 `isPartial` 以获取流式传输进度。\n- 支持`expanded`按需了解详情。\n- 保持默认视图紧凑。\n- 读取`renderResult`中的`context.args`，而不是将参数复制到`context.state`。\n- 仅对必须在调用和结果槽之间共享的数据使用`context.state`。\n- 当相同的组件实例可以就地更新时，重用`context.lastComponent`。\n- 仅当默认盒装 shell 妨碍时才使用 `renderShell: \"self\"`。在自外壳模式下，该工具负责其自己的框架、填充和背景。\n\n#### 倒退\n\n如果槽渲染器未定义或抛出：\n- `renderCall`：显示工具名称\n- `renderResult`：显示来自`content`的原始文本\n\n### 动态刀具加载\n\nExtensions 可以注册许多工具，同时仅保持一小部分初始集处于活动状态。然后，工具可以在执行期间使用 `pi.setActiveTools()` 添加更多工具。 Pi 检测纯粹的附加更改，记录该工具结果上新可用的工具名称，并在下一个模型请求之前应用更新的活动集。\n\n这适用于每个型号。 Models 具有本机延迟加载支持，保留稳定的提示前缀并在工具结果位置加载新定义。其他模型使用下面描述的后备。\n\n生命周期是：\n\n1. 将每个工具注册到`pi.registerTool()`，以便它出现在`pi.getAllTools()`中。\n2. 保持加载工具（例如 `search_tools`）处于活动状态，并使可搜索工具处于非活动状态。\n3. 在加载器执行期间，调用`pi.setActiveTools([...currentTools,...matchingTools])`。更改必须是附加的：不要在同一调用中删除当前活动的工具。\n4. Pi记录加载器的工具结果上添加了哪些工具。\n5. 在下一个模型响应之前，Pi 使用本机延迟加载（如果支持）公开添加的定义，否则使用正常的活动工具列表。\n\n您不需要返回特定于提供程序的工具引用或将加载程序标记为特殊搜索工具。主动换刀就是信号。传递给`pi.setActiveTools()`的名称必须已经注册；未知的名称将被忽略。\n\n#### Models 具有本机延迟加载\n\n- **人择**\n  - **Models:** Sonnet、Opus、Fable 版本 4.5 或更高版本（不含俳句）\n  - **原生表示：** 延迟定义使用 `defer_loading`；加载点使用 `tool_reference` 内容。\n- **开放人工智能**\n  - **Models:** `gpt-5.4` 及更新系列\n  - **本机表示：** Pi 在加载点添加已完成的客户端 `tool_search_call` 和 `tool_search_output` 项目。\n\n对于经过验证的自定义模型或代理，可以使用 `anthropic-messages` 的 `compat.supportsToolReferences: true` 或`openai-responses` 和 `openai-codex-responses` 的 `compat.supportsToolSearch: true` 启用本机处理。除非端点和模型接受相应的本机协议，否则将它们保持禁用状态。\n\n#### 回退行为\n\n对于所有其他模型和提供程序，动态激活仍然有效：Pi 通常在下一个请求时发送完整的当前活动工具列表。该模型可以调用新激活的工具，但添加它们的定义可能会使提供程序的缓存提示前缀无效。\n\n当活动集不是纯粹的累加性时（例如用一组工具替换另一组工具），Pi 也会使用这种安全回退。因此，工具删除可以工作，但它们不使用延迟加载。\n\n为了获得最佳缓存行为，请在整个会话中保持加载程序工具处于活动状态并添加工具而不是替换活动集。另请注意，使用`promptSnippet`或`promptGuidelines`激活工具会重建系统提示符；即使提供程序支持延迟模式，系统提示的更改也可能使前缀无效。延迟加载的工具通常应该依赖于它们的工具 `description` 并省略仅活动的提示元数据。\n\n#### 搜索工具示例\n\n以下扩展注册了两个可搜索工具，将它们从初始活动集中删除，并仅保留 `search_tools` 作为它们的加载器。该示例使用简单的关键字匹配，但搜索实现可以使用 BM25、嵌入、远程目录或特定于项目的路由。\n\n```typescript\nimport type { ExtensionAPI } from \"@earendil-works/pi-coding-agent\";\nimport { Type } from \"typebox\";\n\nconst SEARCHABLE_TOOL_NAMES = new Set([\"lookup_weather\", \"search_issues\"]);\n\nexport default function (pi: ExtensionAPI) {\n  pi.registerTool({\n    name: \"lookup_weather\",\n    label: \"Lookup Weather\",\n    description: \"Look up the current weather for a city\",\n    parameters: Type.Object({ city: Type.String() }),\n    async execute(_toolCallId, params) {\n      return {\n        content: [{ type: \"text\", text: `Weather for ${params.city}: sunny` }],\n        details: {},\n      };\n    },\n  });\n\n  pi.registerTool({\n    name: \"search_issues\",\n    label: \"Search Issues\",\n    description: \"Search project issues by keyword\",\n    parameters: Type.Object({ query: Type.String() }),\n    async execute(_toolCallId, params) {\n      return {\n        content: [{ type: \"text\", text: `No open issues matching ${params.query}` }],\n        details: {},\n      };\n    },\n  });\n\n  pi.registerTool({\n    name: \"search_tools\",\n    label: \"Search Tools\",\n    description: \"Search for and enable tools relevant to a task\",\n    promptSnippet: \"Search for additional tools when the active tools cannot perform the task\",\n    promptGuidelines: [\n      \"Use search_tools when a task requires a capability that is not currently available.\",\n    ],\n    parameters: Type.Object({\n      query: Type.String({ description: \"Capability or task to search for\" }),\n      limit: Type.Optional(Type.Integer({ minimum: 1, maximum: 10 })),\n    }),\n    async execute(_toolCallId, params) {\n      const terms = params.query.toLowerCase().split(/[^a-z0-9]+/).filter(Boolean);\n      const matches = pi.getAllTools()\n        .filter((tool) => SEARCHABLE_TOOL_NAMES.has(tool.name))\n        .map((tool) => ({\n          tool,\n          score: terms.reduce(\n            (score, term) =>\n              score + (`${tool.name} ${tool.description}`.toLowerCase().includes(term) ? 1 : 0),\n            0,\n          ),\n        }))\n        .filter((match) => match.score > 0)\n        .sort((a, b) => b.score - a.score)\n        .slice(0, params.limit ?? 3)\n        .map((match) => match.tool.name);\n\n      if (matches.length === 0) {\n        return {\n          content: [{ type: \"text\", text: `No tools found for: ${params.query}` }],\n          details: { matches: [] },\n        };\n      }\n\n      const active = pi.getActiveTools();\n      const added = matches.filter((name) => !active.includes(name));\n      pi.setActiveTools([...new Set([...active, ...added])]);\n\n      return {\n        content: [{\n          type: \"text\",\n          text: added.length > 0\n            ? `Loaded tools: ${added.join(\", \")}`\n            : `Matching tools already active: ${matches.join(\", \")}`,\n        }],\n        details: { matches, added },\n      };\n    },\n  });\n\n  pi.on(\"session_start\", () => {\n    // Keep searchable tools registered but initially inactive. Preserve built-ins\n    // and tools owned by other extensions, and keep the loader itself active.\n    const initialTools = pi.getActiveTools().filter(\n      (name) => !SEARCHABLE_TOOL_NAMES.has(name),\n    );\n    pi.setActiveTools([...new Set([...initialTools, \"search_tools\"])]);\n  });\n}\n```\n\n当 `search_tools` 添加匹配项时，模型会在紧随其后的请求中收到该定义。在支持本机的模型上，定义锚定在搜索结果之后，而不更改初始工具模式前缀。在其他型号上，它会根据相同的以下请求出现在正常工具列表中。\n\n## 自定义用户界面\n\nExtensions可以通过`ctx.ui`方法与用户交互并自定义消息/工具的呈现方式。\n\n**对于自定义组件，请参阅 [tui.md](tui.md)**，它具有以下复制粘贴模式：\n- 选择对话框（SelectList）\n- 带取消的异步操作 (BorderedLoader)\n- 设置切换（设置列表）\n- 状态指示器（setStatus）\n- 流媒体期间的工作消息、可见性和指示器（`setWorkingMessage`、`setWorkingVisible`、`setWorkingIndicator`）\n- 编辑器上方/下方的小部件 (setWidget)\n- 自动完成提供程序位于内置斜杠/路径完成之上 (addAutocompleteProvider)\n- 自定义页脚 (setFooter)\n\n### 对话框\n\n```typescript\n// Select from options\nconst choice = await ctx.ui.select(\"Pick one:\", [\"A\", \"B\", \"C\"]);\n\n// Confirm dialog\nconst ok = await ctx.ui.confirm(\"Delete?\", \"This cannot be undone\");\n\n// Text input\nconst name = await ctx.ui.input(\"Name:\", \"placeholder\");\n\n// Multi-line editor\nconst text = await ctx.ui.editor(\"Edit:\", \"prefilled text\");\n\n// Notification (non-blocking)\nctx.ui.notify(\"Done!\", \"info\");  // \"info\" | \"warning\" | \"error\"\n```\n\n#### 带倒计时的定时对话框\n\n对话框支持 `timeout` 选项，可通过实时倒计时显示自动关闭：\n\n```typescript\n// Dialog shows \"Title (5s)\" → \"Title (4s)\" → ... → auto-dismisses at 0\nconst confirmed = await ctx.ui.confirm(\n  \"Timed Confirmation\",\n  \"This dialog will auto-cancel in 5 seconds. Confirm?\",\n  { timeout: 5000 }\n);\n\nif (confirmed) {\n  // User confirmed\n} else {\n  // User cancelled or timed out\n}\n```\n\n**超时返回值：**\n- `select()` 返回 `undefined`\n- `confirm()` 返回 `false`\n- `input()` 返回 `undefined`\n\n#### 使用 AbortSignal 手动解雇\n\n要进行更多控制（例如，区分超时和用户取消），请使用 `AbortSignal`：\n\n```typescript\nconst controller = new AbortController();\nconst timeoutId = setTimeout(() => controller.abort(), 5000);\n\nconst confirmed = await ctx.ui.confirm(\n  \"Timed Confirmation\",\n  \"This dialog will auto-cancel in 5 seconds. Confirm?\",\n  { signal: controller.signal }\n);\n\nclearTimeout(timeoutId);\n\nif (confirmed) {\n  // User confirmed\n} else if (controller.signal.aborted) {\n  // Dialog timed out\n} else {\n  // User cancelled (pressed Escape or selected \"No\")\n}\n```\n\n完整示例请参见[examples/extensions/timed-confirm.ts](../examples/extensions/timed-confirm.ts)。\n\n### 小部件、状态和页脚\n\n```typescript\n// Status in footer (persistent until cleared)\nctx.ui.setStatus(\"my-ext\", \"Processing...\");\nctx.ui.setStatus(\"my-ext\", undefined);  // Clear\n\n// Working loader (shown during streaming)\nctx.ui.setWorkingMessage(\"Thinking deeply...\");\nctx.ui.setWorkingMessage();  // Restore default\nctx.ui.setWorkingVisible(false);  // Hide the built-in working loader row entirely\nctx.ui.setWorkingVisible(true);   // Show the built-in working loader row\n\n// Working indicator (shown during streaming)\nctx.ui.setWorkingIndicator({ frames: [ctx.ui.theme.fg(\"accent\", \"●\")] });  // Static dot\nctx.ui.setWorkingIndicator({\n  frames: [\n    ctx.ui.theme.fg(\"dim\", \"·\"),\n    ctx.ui.theme.fg(\"muted\", \"•\"),\n    ctx.ui.theme.fg(\"accent\", \"●\"),\n    ctx.ui.theme.fg(\"muted\", \"•\"),\n  ],\n  intervalMs: 120,\n});\nctx.ui.setWorkingIndicator({ frames: [] });  // Hide indicator\nctx.ui.setWorkingIndicator();  // Restore default spinner\n\n// Widget above editor (default)\nctx.ui.setWidget(\"my-widget\", [\"Line 1\", \"Line 2\"]);\n// Widget below editor\nctx.ui.setWidget(\"my-widget\", [\"Line 1\", \"Line 2\"], { placement: \"belowEditor\" });\nctx.ui.setWidget(\"my-widget\", (tui, theme) => new Text(theme.fg(\"accent\", \"Custom\"), 0, 0));\nctx.ui.setWidget(\"my-widget\", undefined);  // Clear\n\n// Custom footer (replaces built-in footer entirely)\nctx.ui.setFooter((tui, theme) => ({\n  render(width) { return [theme.fg(\"dim\", \"Custom footer\")]; },\n  invalidate() {},\n}));\nctx.ui.setFooter(undefined);  // Restore built-in footer\n\n// Terminal title\nctx.ui.setTitle(\"pi - my-project\");\n\n// Editor text\nctx.ui.setEditorText(\"Prefill text\");\nconst current = ctx.ui.getEditorText();\n\n// Paste into editor (triggers paste handling, including collapse for large content)\nctx.ui.pasteToEditor(\"pasted content\");\n\n// Stack custom autocomplete behavior on top of the built-in provider\nctx.ui.addAutocompleteProvider((current) => ({\n  triggerCharacters: [\"#\"],\n  async getSuggestions(lines, line, col, options) {\n    const beforeCursor = (lines[line] ?? \"\").slice(0, col);\n    const match = beforeCursor.match(/(?:^|[ \\t])#([^\\s#]*)$/);\n    if (!match) {\n      return current.getSuggestions(lines, line, col, options);\n    }\n\n    return {\n      prefix: `#${match[1] ?? \"\"}`,\n      items: [{ value: \"#2983\", label: \"#2983\", description: \"Extension API for autocomplete\" }],\n    };\n  },\n  applyCompletion(lines, line, col, item, prefix) {\n    return current.applyCompletion(lines, line, col, item, prefix);\n  },\n  shouldTriggerFileCompletion(lines, line, col) {\n    return current.shouldTriggerFileCompletion?.(lines, line, col) ?? true;\n  },\n}));\n\n// Tool output expansion\nconst wasExpanded = ctx.ui.getToolsExpanded();\nctx.ui.setToolsExpanded(true);\nctx.ui.setToolsExpanded(wasExpanded);\n\n// Custom editor (vim mode, emacs mode, etc.)\nctx.ui.setEditorComponent((tui, theme, keybindings) => new VimEditor(tui, theme, keybindings));\nconst currentEditor = ctx.ui.getEditorComponent();\nctx.ui.setEditorComponent((tui, theme, keybindings) =>\n  new WrappedEditor(tui, theme, keybindings, currentEditor?.(tui, theme, keybindings))\n);\nctx.ui.setEditorComponent(undefined);  // Restore default editor\n\n// Theme management (see themes.md for creating themes)\nconst themes = ctx.ui.getAllThemes();  // [{ name: \"dark\", path: \"/...\" | undefined }, ...]\nconst lightTheme = ctx.ui.getTheme(\"light\");  // Load without switching\nconst result = ctx.ui.setTheme(\"light\");  // Switch by name\nif (!result.success) {\n  ctx.ui.notify(`Failed: ${result.error}`, \"error\");\n}\nctx.ui.setTheme(lightTheme!);  // Or switch by Theme object\nctx.ui.theme.fg(\"accent\", \"styled text\");  // Access current theme\n```\n\n自定义工作指示器帧逐字​​呈现。如果您想要颜色，请自行将它们添加到框架字符串中，例如使用 `ctx.ui.theme.fg(...)`。\n\n### 自动完成Providers\n\n使用 `ctx.ui.addAutocompleteProvider()` 将自定义自动完成逻辑堆叠在内置斜杠命令和路径提供程序之上。为自定义自然触发器设置 `triggerCharacters`，例如 `使用 `ctx.ui.addAutocompleteProvider()` 将自定义自动完成逻辑堆叠在内置斜杠命令和路径提供程序之上。为自定义自然触发器设置 `triggerCharacters`，例如。\n\n典型模式：\n\n- 检查光标之前的文本\n- 当您的扩展特定语法匹配时返回您自己的建议\n- 否则委托给`current.getSuggestions(...)`\n- 委托 `applyCompletion(...)` 除非您需要自定义插入行为\n\n```typescript\npi.on(\"session_start\", (_event, ctx) => {\n  ctx.ui.addAutocompleteProvider((current) => ({\n    triggerCharacters: [\"#\"],\n    async getSuggestions(lines, cursorLine, cursorCol, options) {\n      const line = lines[cursorLine] ?? \"\";\n      const beforeCursor = line.slice(0, cursorCol);\n      const match = beforeCursor.match(/(?:^|[ \\t])#([^\\s#]*)$/);\n      if (!match) {\n        return current.getSuggestions(lines, cursorLine, cursorCol, options);\n      }\n\n      return {\n        prefix: `#${match[1] ?? \"\"}`,\n        items: [\n          { value: \"#2983\", label: \"#2983\", description: \"Extension API for registering custom @ autocomplete providers\" },\n          { value: \"#2753\", label: \"#2753\", description: \"Reload stale resource settings\" },\n        ],\n      };\n    },\n\n    applyCompletion(lines, cursorLine, cursorCol, item, prefix) {\n      return current.applyCompletion(lines, cursorLine, cursorCol, item, prefix);\n    },\n\n    shouldTriggerFileCompletion(lines, cursorLine, cursorCol) {\n      return current.shouldTriggerFileCompletion?.(lines, cursorLine, cursorCol) ?? true;\n    },\n  }));\n});\n```\n\n有关完整示例，请参阅 [github-issue-autocomplete.ts](../examples/extensions/github-issue-autocomplete.ts)，该示例使用 `gh issue list` 预加载最新开放的 GitHub 问题，并在本地过滤它们以快速完成 `#...`。它需要 GitHub CLI (`gh`) 和 GitHub 存储库签出。\n\n### 定制组件\n\n对于复杂的 UI，请使用 `ctx.ui.custom()`。这会暂时用您的组件替换编辑器，直到调用 `done()`：\n\n```typescript\nimport { Text, Component } from \"@earendil-works/pi-tui\";\n\nconst result = await ctx.ui.custom<boolean>((tui, theme, keybindings, done) => {\n  const text = new Text(\"Press Enter to confirm, Escape to cancel\", 1, 1);\n\n  text.onKey = (key) => {\n    if (key === \"return\") done(true);\n    if (key === \"escape\") done(false);\n    return true;\n  };\n\n  return text;\n});\n\nif (result) {\n  // User pressed Enter\n}\n```\n\n回调收到：\n- `tui` - TUI 实例（用于屏幕尺寸、焦点管理）\n- `theme` - 当前的样式主题\n- `keybindings` - 应用程序键绑定管理器（用于检查快捷方式）\n- `done(value)` - 调用关闭组件并返回值\n\n请参阅 [tui.md](tui.md) 了解完整组件 API。\n\n#### 叠加模式（实验性）\n\n传递 `{ overlay: true }` 将组件渲染为现有内容之上的浮动模式，而不清除屏幕：\n\n```typescript\nconst result = await ctx.ui.custom<string | null>(\n  (tui, theme, keybindings, done) => new MyOverlayComponent({ onClose: done }),\n  { overlay: true }\n);\n```\n\n对于高级定位（锚点、边距、百分比、响应式可见性），请传递 `overlayOptions`。使用 `onHandle` 以编程方式控制焦点或可见性：\n\n```typescript\nconst result = await ctx.ui.custom<string | null>(\n  (tui, theme, keybindings, done) => new MyOverlayComponent({ onClose: done }),\n  {\n    overlay: true,\n    overlayOptions: { anchor: \"top-right\", width: \"50%\", margin: 2 },\n    onHandle: (handle) => {\n      handle.focus(); // focus this overlay and bring it to the visual front\n      // handle.unfocus({ target: editorComponent }); // release input to a specific component\n      // handle.setHidden(true/false); // toggle visibility\n      // handle.hide(); // permanently remove\n    }\n  }\n);\n```\n\n临时非覆盖自定义 UI 关闭后，聚焦的可见覆盖可以回收输入。如果您有意希望另一个组件在覆盖层保持可见时保留输入，请调用 `handle.unfocus({ target })`。通过 `{ target: null }` 释放覆盖层而不聚焦另一个组件。\n\n有关完整的 `OverlayOptions` 和 `OverlayHandle` API 和 [overlay-qa-tests.ts](../examples/extensions/overlay-qa-tests.ts) 示例，请参阅 [tui.md](tui.md)。\n\n### 自定义编辑器\n\n将主输入编辑器替换为自定义实现（vim 模式、emacs 模式等）：\n\n```typescript\nimport { CustomEditor, type ExtensionAPI } from \"@earendil-works/pi-coding-agent\";\nimport { matchesKey } from \"@earendil-works/pi-tui\";\n\nclass VimEditor extends CustomEditor {\n  private mode: \"normal\" | \"insert\" = \"insert\";\n\n  handleInput(data: string): void {\n    if (matchesKey(data, \"escape\") && this.mode === \"insert\") {\n      this.mode = \"normal\";\n      return;\n    }\n    if (this.mode === \"normal\" && data === \"i\") {\n      this.mode = \"insert\";\n      return;\n    }\n    super.handleInput(data);  // App keybindings + text editing\n  }\n}\n\nexport default function (pi: ExtensionAPI) {\n  pi.on(\"session_start\", (_event, ctx) => {\n    ctx.ui.setEditorComponent((tui, theme, keybindings) =>\n      new VimEditor(tui, theme, keybindings)\n    );\n  });\n}\n```\n\n**要点：**\n- 扩展 `CustomEditor` （不是基础 `Editor`）以获取应用程序键绑定（转义以中止、ctrl+d、模型切换）\n- 对于您不处理的钥匙，请致电 `super.handleInput(data)`\n- Factory 从应用程序接收 `tui`、`theme` 和 `keybindings`\n- 在`setEditorComponent()`之前使用`ctx.ui.getEditorComponent()`来包装之前配置的自定义编辑器\n- 通过`undefined`恢复默认：`ctx.ui.setEditorComponent(undefined)`\n\n要与已替换编辑器的另一个扩展进行组合，请在设置您的工厂之前捕获以前的工厂：\n\n```typescript\nconst previous = ctx.ui.getEditorComponent();\nctx.ui.setEditorComponent((tui, theme, keybindings) =>\n  new MyEditor(tui, theme, keybindings, { base: previous?.(tui, theme, keybindings) })\n);\n```\n\n有关模式指示器的完整示例，请参阅[tui.md](tui.md) 模式 7。\n\n### 消息和条目渲染\n\n使用您的 `customType` 为消息注册自定义渲染器。对应参与 LLM 上下文的内容使用消息渲染器：\n\n```typescript\nimport { Text } from \"@earendil-works/pi-tui\";\n\npi.registerMessageRenderer(\"my-extension\", (message, options, theme) => {\n  const { expanded, outputPad } = options;\n  let text = theme.fg(\"accent\", `[${message.customType}] `);\n  text += message.content;\n\n  if (expanded && message.details) {\n    text += \"\\n\" + theme.fg(\"dim\", JSON.stringify(message.details, null, 2));\n  }\n\n  return new Text(text, outputPad, 0);\n});\n```\n\n消息通过`pi.sendMessage()`发送：\n\n```typescript\npi.sendMessage({\n  customType: \"my-extension\",  // Matches registerMessageRenderer\n  content: \"Status update\",\n  display: true,               // Show in TUI\n  details: { ... },            // Available in renderer\n});\n```\n\n对于不应发送至 LLM 的仅限 TUI 的内容，请改为呈现自定义条目：\n\n```typescript\npi.registerEntryRenderer(\"my-card\", (entry, options, theme) => {\n  return new Text(theme.fg(\"accent\", JSON.stringify(entry.data)));\n});\n\npi.appendEntry(\"my-card\", { status: \"done\" });\n```\n\n### 主题颜色\n\n所有渲染函数都会接收一个 `theme` 对象。请参阅 [themes.md](themes.md) 创建自定义主题和完整调色板。\n\n```typescript\n// Foreground colors\ntheme.fg(\"toolTitle\", text)   // Tool names\ntheme.fg(\"accent\", text)      // Highlights\ntheme.fg(\"success\", text)     // Success (green)\ntheme.fg(\"error\", text)       // Errors (red)\ntheme.fg(\"warning\", text)     // Warnings (yellow)\ntheme.fg(\"muted\", text)       // Secondary text\ntheme.fg(\"dim\", text)         // Tertiary text\n\n// Text styles\ntheme.bold(text)\ntheme.italic(text)\ntheme.strikethrough(text)\n```\n\n对于自定义工具渲染器中的语法突出显示：\n\n```typescript\nimport { highlightCode, getLanguageFromPath } from \"@earendil-works/pi-coding-agent\";\n\n// Highlight code with explicit language\nconst highlighted = highlightCode(\"const x = 1;\", \"typescript\", theme);\n\n// Auto-detect language from file path\nconst lang = getLanguageFromPath(\"/path/to/file.rs\");  // \"rust\"\nconst highlighted = highlightCode(code, lang, theme);\n```\n\n## 错误处理\n\n- 记录扩展错误，代理继续\n- `tool_call` 错误阻止工具（故障安全）\n- 工具`execute`错误必须通过抛出来表示；抛出的错误被捕获，并用`isError: true`报告给LLM，然后继续执行\n\n## 模式行为\n\n| 模式 | `ctx.mode` | `ctx.hasUI` | 笔记 |\n|------|------------|-------------|-------|\n| 交互的 | `\"tui\"` | `true` | 带有终端渲染的完整TUI |\n| RPC (`--mode rpc`) | `\"rpc\"` | `true` | 通过JSON协议进行对话和通知； `custom()` 返回`undefined`。见[rpc.md](rpc.md) |\n| JSON (`--mode json`) | `\"json\"` | `false` | 事件流到stdout； UI 方法是无操作的 |\n| 打印 (`-p`) | `\"print\"` | `false` | Extensions 运行但无法提示 |\n\n在TUI特定功能（`custom()`、组件工厂、终端输入）之前使用`ctx.mode === \"tui\"`。在同时适用于 TUI 和 RPC 模式的对话框和通知方法之前使用 `ctx.hasUI`。\n\n## 示例参考\n\n所有示例都在[examples/extensions/](../examples/extensions/)中。\n\n| 例子 | 描述 | 钥匙APIs |\n|---------|-------------|----------|\n| **工具** |  |  |\n| `hello.ts` | 最少的工具注册 | `registerTool` |\n| `question.ts` | 与用户交互的工具 | `registerTool`, `ui.select` |\n| `questionnaire.ts` | 多步骤向导工具 | `registerTool`, `ui.custom` |\n| `todo.ts` | 具有持久性的有状态工具 | `registerTool`、`appendEntry`、`renderResult`、会话事件 |\n| `dynamic-tools.ts` | 启动后和命令期间注册工具 | `registerTool`, `session_start`, `registerCommand` |\n| `structured-output.ts` | 最终的结构化输出工具，`terminate: true` | `registerTool`，终止工具结果 |\n| `truncated-tool.ts` | 输出截断示例 | `registerTool`, `truncateHead` |\n| `tool-override.ts` | 覆盖内置读取工具 | `registerTool`（与内置同名） |\n| **命令** |  |  |\n| `pirate.ts` | 修改每回合系统提示 | `registerCommand`, `before_agent_start` |\n| `summarize.ts` | 对话摘要命令 | `registerCommand`, `ui.custom` |\n| `handoff.ts` | 跨提供商模型切换 | `registerCommand`, `ui.editor`, `ui.custom` |\n| `qna.ts` | 带有自定义 UI 的问答 | `registerCommand`, `ui.custom`, `setEditorText` |\n| `send-user-message.ts` | 注入用户消息 | `registerCommand`, `sendUserMessage` |\n| `reload-runtime.ts` | 重新加载命令和LLM工具切换 | `registerCommand`, `ctx.reload()`, `sendUserMessage` |\n| `shutdown-command.ts` | 优雅的关机命令 | `registerCommand`, `shutdown()` |\n| **活动和大门** |  |  |\n| `permission-gate.ts` | 阻止危险命令 | `on(\"tool_call\")`, `ui.confirm` |\n| `project-trust.ts` | 决定或推迟来自用户/全局或CLI扩展的项目信任 | `on(\"project_trust\")`，信任UI，需要信任结果 |\n| `protected-paths.ts` | 阻止写入特定路径 | `on(\"tool_call\")` |\n| `confirm-destructive.ts` | 确认会话更改 | `on(\"session_before_switch\")`, `on(\"session_before_fork\")` |\n| `dirty-repo-guard.ts` | 警告肮脏的 git 仓库 | `on(\"session_before_*\")`, `exec` |\n| `input-transform.ts` | 转换用户输入 | `on(\"input\")` |\n| `input-transform-streaming.ts` | 流感知输入转换 | `on(\"input\")`, `streamingBehavior` |\n| `model-status.ts` | React 模型变更 | `on(\"model_select\")`, `setStatus` |\n| `provider-payload.ts` | 检查有效负载和提供者响应标头 | `on(\"before_provider_request\")`, `on(\"after_provider_response\")` |\n| `system-prompt-header.ts` | 显示系统提示信息 | `on(\"agent_start\")`, `getSystemPrompt` |\n| `claude-rules.ts` | 从文件加载规则 | `on(\"session_start\")`, `on(\"before_agent_start\")` |\n| `prompt-customizer.ts` | 使用 `systemPromptOptions` 添加上下文感知工具指导 | `on(\"before_agent_start\")`, `BuildSystemPromptOptions` |\n| `file-trigger.ts` | 文件观察器触发消息 | `sendMessage` |\n| **压实和会话** |  |  |\n| `custom-compaction.ts` | 自定义压缩摘要 | `on(\"session_before_compact\")` |\n| `trigger-compact.ts` | 手动触发压缩 | `compact()` |\n| `git-checkpoint.ts` | Git 回合藏匿 | `on(\"turn_start\")`, `on(\"session_before_fork\")`, `exec` |\n| `git-merge-and-resolve.ts` | 获取、合并和解决冲突 | `on(\"agent_end\")`, `exec`, `sendUserMessage` |\n| `auto-commit-on-exit.ts` | 关闭时提交 | `on(\"session_shutdown\")`, `exec` |\n| **用户界面组件** |  |  |\n| `status-line.ts` | 页脚状态指示灯 | `setStatus`，会话事件 |\n| `working-indicator.ts` | 自定义流媒体工作指示灯 | `setWorkingIndicator`, `registerCommand` |\n| `github-issue-autocomplete.ts` | 通过预加载 `gh issue list` 中最近未解决的问题，在内置自动完成之上添加 `#1234` 问题完成 | `addAutocompleteProvider`, `on(\"session_start\")`, `exec` |\n| `custom-footer.ts` | 完全替换页脚 | `registerCommand`, `setFooter` |\n| `custom-header.ts` | 替换启动头 | `on(\"session_start\")`, `setHeader` |\n| `modal-editor.ts` | Vim 风格的模态编辑器 | `setEditorComponent`, `CustomEditor` |\n| `rainbow-editor.ts` | 自定义编辑器样式 | `setEditorComponent` |\n| `widget-placement.ts` | 编辑器上方/下方的小部件 | `setWidget` |\n| `overlay-test.ts` | 覆盖组件 | `ui.custom` 带有叠加选项 |\n| `overlay-qa-tests.ts` | 综合覆盖测试 | `ui.custom`，所有叠加选项 |\n| `notify.ts` | 简单的通知 | `ui.notify` |\n| `timed-confirm.ts` | 超时对话框 | `ui.confirm` 有超时/信号 |\n| `mac-system-theme.ts` | 自动切换主题 | `setTheme`, `exec` |\n| **复杂Extensions** |  |  |\n| `plan-mode/` | 全计划模式实施 | 所有事件类型，`registerCommand`、`registerShortcut`、`registerFlag`、`setStatus`、`setWidget`、`sendMessage`、`setActiveTools` |\n| `preset.ts` | 可保存的预设（模型、工具、思维） | `registerCommand`, `registerShortcut`, `registerFlag`, `setModel`, `setActiveTools`, `setThinkingLevel`, `appendEntry` |\n| `tools.ts` | 打开/关闭 UI 工具 | `registerCommand`、`setActiveTools`、`SettingsList`、会话事件 |\n| **远程和沙箱** |  |  |\n| `ssh.ts` | SSH远程执行 | `registerFlag`、`on(\"user_bash\")`、`on(\"before_agent_start\")`、工具操作 |\n| `interactive-shell.ts` | 持久 shell 会话 | `on(\"user_bash\")` |\n| `sandbox/` | 沙盒工具执行 | 工具操作 |\n| `gondolin/` | 将内置工具和 `!` 命令路由到 Gondolin 微型虚拟机 | 工具操作、内置工具覆盖、`on(\"user_bash\")` |\n| `subagent/` | 生成子代理 | `registerTool`, `exec` |\n| **游戏** |  |  |\n| `snake.ts` | 贪吃蛇游戏 | `registerCommand`、`ui.custom`、键盘处理 |\n| `space-invaders.ts` | 太空侵略者游戏 | `registerCommand`, `ui.custom` |\n| `doom-overlay/` | 厄运叠加 | `ui.custom` 带覆盖 |\n| **Providers** |  |  |\n| `custom-provider-anthropic/` | 自定义人类代理 | `registerProvider` |\n| `custom-provider-gitlab-duo/` | GitLab Duo 集成 | `registerProvider` 与 OAuth |\n| **消息与通讯** |  |  |\n| `message-renderer.ts` | 自定义消息渲染 | `registerMessageRenderer`, `sendMessage` |\n| `entry-renderer.ts` | TUI-仅自定义入口渲染 | `registerEntryRenderer`, `appendEntry` |\n| `event-bus.ts` | 延伸间事件 | `pi.events` |\n| **会话元数据** |  |  |\n| `session-name.ts` | 为选择器命名会话 | `setSessionName`, `getSessionName` |\n| `bookmark.ts` | /tree 的书签条目 | `setLabel` |\n| **杂项** |  |  |\n| `inline-bash.ts` | 工具调用中的内联 bash | `on(\"tool_call\")` |\n| `bash-spawn-hook.ts` | 执行前调整bashcommand、cwd、env | `createBashTool`, `spawnHook` |\n| `with-deps/` | 具有 npm 依赖项的扩展 | `package.json`的封装结构 |","sourceFile":"extensions.md"},"index":{"title":"Pi 文档","markdown":"Pi 是最小的端子编码线束。它的设计目的是保持核心较小，同时通过 TypeScript 扩展、技能、prompt templates、主题和 pi 包进行扩展。\n\n## 快速启动\n\n安装 Pi 和 npm：\n\n```bash\nnpm install -g --ignore-scripts @earendil-works/pi-coding-agent\n```\n\n`--ignore-scripts` 在安装过程中禁用依赖生命周期脚本。 Pi 不需要正常安装npm 安装脚本。\n\n在 Linux 或 macOS 上，您还可以使用安装程序：\n\n```bash\ncurl -fsSL https://pi.dev/install.sh | sh\n```\n\n要卸载 pi 本身，请使用 npm 进行curl 并使用 npm 安装：\n\n```bash\nnpm uninstall -g @earendil-works/pi-coding-agent\n```\n\n对于 pnpm、Yarn 或 Bun 安装，请使用匹配的全局删除命令：`pnpm remove -g @earendil-works/pi-coding-agent`、`yarn global remove @earendil-works/pi-coding-agent` 或 `bun uninstall -g @earendil-works/pi-coding-agent`。\n\n然后在项目目录中运行它：\n\n```bash\npi\n```\n\n使用`/login`对subscription providers进行身份验证，或者在启动pi之前设置API key，例如`ANTHROPIC_API_KEY`。\n\n有关完整的首次运行流程，请参阅[Quickstart](quickstart.md)。\n\n## 从这里开始\n\n- [Quickstart](quickstart.md) - 安装、验证并运行第一个会话。\n- [Using Pi](usage.md) - 交互模式、slash commands、context files 和 CLI 参考。\n- [Providers](providers.md) - 内置提供商的订阅和API密钥设置。\n- [llama.cpp](llama-cpp.md) - 运行本地路由器并使用`/llama`管理模型。\n- [Security](security.md) - 项目信任、sandbox 边界和漏洞报告。\n- [Containerization](containerization.md) - sandbox pi 与 Gondolin、Docker 或 OpenShell。\n- [Settings](settings.md) - 全局和项目设置。\n- [Keybindings](keybindings.md) - 默认快捷键和自定义按键绑定。\n- [Sessions](sessions.md) - 会话管理、分支和树导航。\n- [Compaction](compaction.md) - context compaction 和 branch summarization。\n\n## 定制化\n\n- [Extensions](extensions.md) - TypeScript 工具、命令、事件和自定义 UI 模块。\n- [Skills](skills.md) - 代理Skills，用于可重用的按需功能。\n- [Prompt templates](prompt-templates.md) - 从 slash commands 扩展的可重复使用的提示。\n- [Themes](themes.md) - 内置和自定义terminal themes。\n- [Pi packages](packages.md) - 捆绑并共享扩展、技能、提示和主题。\n- [Custom models](models.md) - 为支持的提供者APIs 添加模型条目。\n- [Custom providers](custom-provider.md) - 实现自定义 API 和 OAuth 流程。\n\n## 程序化使用\n\n- [SDK](sdk.md) - 将 pi 嵌入到 Node.js 应用程序中。\n- [RPC mode](rpc.md) - 对 stdin/stdout JSONL 进行积分。\n- [JSON event stream mode](json.md) - 具有结构化事件的打印模式。\n- [TUI components](tui.md) - 为扩展构建自定义终端 UI。\n\n## 参考\n\n- [Environment variables](environment-variables.md) - Pi bash 工具可用的流程配置和会话元数据。\n- [Session format](session-format.md) - JSONL 会话文件格式、条目类型和 SessionManager API。\n\n## 平台设置\n\n- [Windows](windows.md)\n- [Termux on Android](termux.md)\n- [tmux](tmux.md)\n- [Terminal setup](terminal-setup.md)\n- [Shell aliases](shell-aliases.md)\n\n## 开发\n\n- [Development](development.md) - 本地设置、项目结构和调试。","sourceFile":"index.md"},"json":{"title":"JSON 事件流模式","markdown":"```bash\npi --mode json \"Your prompt\"\n```\n\n将所有会话事件输出为JSON行到stdout。对于将 pi 集成到其他工具或自定义 UI 中非常有用。\n\n## 事件类型\n\n连线事件使用`JsonAgentSessionEvent`。它匹配\n[`AgentSessionEvent`](https://github.com/earendil-works/pi-mono/blob/main/packages/coding-agent/src/core/agent-session.ts)\n除了流消息更新忽略累积快照之外：\n\n```typescript\ntype WithoutPartial<T> = T extends { partial: unknown } ? Omit<T, \"partial\"> : T;\n\ntype JsonAgentSessionEvent =\n  | Exclude<AgentSessionEvent, { type: \"message_update\" }>\n  | {\n      type: \"message_update\";\n      assistantMessageEvent: WithoutPartial<AssistantMessageEvent>;\n    };\n```\n\n每当发生变化时，`queue_update`都会发出完整的待处理转向和后续队列。 `compaction_start`和`compaction_end`涵盖手动和自动压实。\n\n其他基础事件来自\n[`AgentEvent`](https://github.com/earendil-works/pi-mono/blob/main/packages/agent/src/types.ts):\n\n```typescript\ntype AgentEvent =\n  // Agent lifecycle\n  | { type: \"agent_start\" }\n  | { type: \"agent_end\"; messages: AgentMessage[] }\n  // Turn lifecycle\n  | { type: \"turn_start\" }\n  | { type: \"turn_end\"; message: AgentMessage; toolResults: ToolResultMessage[] }\n  // Message lifecycle\n  | { type: \"message_start\"; message: AgentMessage }\n  | { type: \"message_update\"; message: AgentMessage; assistantMessageEvent: AssistantMessageEvent }\n  | { type: \"message_end\"; message: AgentMessage }\n  // Tool execution\n  | { type: \"tool_execution_start\"; toolCallId: string; toolName: string; args: any }\n  | { type: \"tool_execution_update\"; toolCallId: string; toolName: string; args: any; partialResult: any }\n  | { type: \"tool_execution_end\"; toolCallId: string; toolName: string; result: any; isError: boolean };\n```\n\n## 消息类型\n\n来自[`packages/ai/src/types.ts`](https://github.com/earendil-works/pi-mono/blob/main/packages/ai/src/types.ts#L134)的基本消息：\n- `UserMessage`（第134行）\n- `AssistantMessage`（第140行）\n- `ToolResultMessage`（第152行）\n\n来自[`packages/coding-agent/src/core/messages.ts`](https://github.com/earendil-works/pi-mono/blob/main/packages/coding-agent/src/core/messages.ts#L29)的扩展消息：\n- `BashExecutionMessage`（第29行）\n- `CustomMessage`（第 46 行）\n- `BranchSummaryMessage`（第 55 行）\n- `CompactionSummaryMessage`（第 62 行）\n\n## 输出格式\n\n每行都是一个 JSON 对象。第一行是会话头：\n\n```json\n{\"type\":\"session\",\"version\":3,\"id\":\"uuid\",\"timestamp\":\"...\",\"cwd\":\"/path\"}\n```\n\n接下来是事件发生时的情况：\n\n```json\n{\"type\":\"agent_start\"}\n{\"type\":\"turn_start\"}\n{\"type\":\"message_start\",\"message\":{\"role\":\"assistant\",\"content\":[],...}}\n{\"type\":\"message_update\",\"assistantMessageEvent\":{\"type\":\"text_delta\",\"contentIndex\":0,\"delta\":\"Hello\"}}\n{\"type\":\"message_end\",\"message\":{...}}\n{\"type\":\"turn_end\",\"message\":{...},\"toolResults\":[]}\n{\"type\":\"agent_end\",\"messages\":[...]}\n```\n\n`message_update` 记录仅包含增量。他们省略了累积 `message` 字段和\n`assistantMessageEvent.partial` 保持流大小线性。使用`contentIndex`和`delta`\n如果需要的话，可以组合实时文本、思考或工具调用参数。 `message_end` 包含\n最终的权威消息。\n\n## 例子\n\n```bash\npi --mode json \"List files\" 2>/dev/null | jq -c 'select(.type == \"message_end\")'\n```","sourceFile":"json.md"},"keybindings":{"title":"快捷键","markdown":"所有键盘快捷键都可以通过`~/.pi/agent/keybindings.json`自定义。每个动作都可以绑定到一个或多个键。\n\n配置文件使用与 pi 内部使用的命名空间键绑定 ID 以及扩展作者在 `keyHint()` 和注入的 `keybindings` 管理器中使用的相同的命名空间键绑定 ID。\n\n使用预命名空间 id（例如 `cursorUp` 或 `expandTools`）的旧配置会在启动时自动迁移到命名空间 id。\n\n编辑 `keybindings.json` 后，在 pi 中运行 `/reload` 以应用更改，而无需重新启动会话。\n\n## 密钥格式\n\n`modifier+key`，其中修饰符为 `ctrl`、`shift`、`alt`、`super`（可组合），键为：\n\n- **字母:** `a-z`\n- **数字:** `0-9`\n- **特殊键:** `escape`, `esc`, `enter`, `return`, `tab`, `space`, `backspace`, `delete`, `insert`, `clear`, `home`, `end`, `pageUp`, `pageDown`, `up`, `down`, `left`, `right`\n- **功能键:** `f1`-`f12`\n- **符号:** `` ` ``, `-`, `=`, `[`, `]`, `\\`, `;`, `'`, `,`, `.`, `/`, `!`, `@`, `#`, `$`, `%`, `^`, `&`, `*`, `(`, `)`, `_`, `+`, `|`, `~`, `{`, `}`, `:`, `<`, `>`, `?`\n\n修饰符组合：`ctrl+shift+x`、`alt+ctrl+x`、`ctrl+shift+alt+x`、`super+k`、`ctrl+super+k`、`ctrl+1`等。\n\n`super` 绑定需要一个单独报告修饰符的终端，通常通过 Kitty 键盘协议。如果没有这种支持，它们可能无法在终端中工作。\n\n## 所有动作\n\n### TUI 编辑器光标移动\n\n| 按键绑定 ID | 默认 | 描述 |\n|--------|---------|-------------|\n| `tui.editor.cursorUp` | `up` | 向上移动光标，在顶部浏览较旧的历史记录 |\n| `tui.editor.cursorDown` | `down` | 向下移动光标，在底部浏览较新的历史记录 |\n| `tui.editor.historyPrevious` | *（没有任何）* | 选择上一个提示历史记录条目 |\n| `tui.editor.historyNext` | *（没有任何）* | 选择下一个提示历史条目 |\n| `tui.editor.cursorLeft` | `left`, `ctrl+b` | 向左移动光标 |\n| `tui.editor.cursorRight` | `right`, `ctrl+f` | 向右移动光标 |\n| `tui.editor.cursorWordLeft` | `alt+left`, `ctrl+left`, `alt+b` | 向左移动光标单词 |\n| `tui.editor.cursorWordRight` | `alt+right`, `ctrl+right`, `alt+f` | 向右移动光标单词 |\n| `tui.editor.cursorLineStart` | `home`, `ctrl+home`, `ctrl+a` | 移至行开头 |\n| `tui.editor.cursorLineEnd` | `end`, `ctrl+end`, `ctrl+e` | 移至行尾 |\n| `tui.editor.jumpForward` | `ctrl+]` | 向前跳转到角色 |\n| `tui.editor.jumpBackward` | `ctrl+alt+]` | 向后跳转到字符 |\n| `tui.editor.pageUp` | `pageUp`, `ctrl+pageUp` | 按页向上滚动 |\n| `tui.editor.pageDown` | `pageDown`, `ctrl+pageDown` | 按页向下滚动 |\n\n无论多行提示中的光标位置如何，专用历史操作始终会更改历史条目。当主编辑器聚焦时，显式历史记录绑定优先于应用程序操作，因此将 `tui.editor.historyPrevious` 绑定到 `ctrl+p` 会覆盖该上下文中的模型循环，而不会更改选择器中的`Ctrl+P`。\n\n### TUI 编辑器删除\n\n| 按键绑定 ID | 默认 | 描述 |\n|--------|---------|-------------|\n| `tui.editor.deleteCharBackward` | `backspace` | 向后删除字符 |\n| `tui.editor.deleteCharForward` | `delete`, `ctrl+d` | 向前删除字符 |\n| `tui.editor.deleteWordBackward` | `ctrl+w`, `alt+backspace` | 向后删除单词 |\n| `tui.editor.deleteWordForward` | `alt+d`, `alt+delete` | 删除向前的单词 |\n| `tui.editor.deleteToLineStart` | `ctrl+u` | 删除至行首 |\n| `tui.editor.deleteToLineEnd` | `ctrl+k` | 删除到行尾 |\n\n### TUI 输入\n\n| 按键绑定 ID | 默认 | 描述 |\n|--------|---------|-------------|\n| `tui.input.newLine` | `shift+enter`, `ctrl+j` | 插入新行 |\n| `tui.input.submit` | `enter` | 提交意见 |\n| `tui.input.tab` | `tab` | 选项卡/自动完成 |\n\n### TUI 杀戒\n\n| 按键绑定 ID | 默认 | 描述 |\n|--------|---------|-------------|\n| `tui.editor.yank` | `ctrl+y` | 粘贴最近删除的文本 |\n| `tui.editor.yankPop` | `alt+y` | 猛拉后循环浏览已删除的文本 |\n| `tui.editor.undo` | `ctrl+-` | 撤消上次编辑 |\n\n### TUI 剪贴板和选择\n\n| 按键绑定 ID | 默认 | 描述 |\n|--------|---------|-------------|\n| `tui.input.copy` | `ctrl+c` | 复制选择 |\n| `tui.select.up` | `up` | 上移选择 |\n| `tui.select.down` | `down` | 向下移动选择 |\n| `tui.select.pageUp` | `pageUp` | 在列表中向上翻页 |\n| `tui.select.pageDown` | `pageDown` | 在列表中向下翻页 |\n| `tui.select.confirm` | `enter` | 确认选择 |\n| `tui.select.cancel` | `escape`, `ctrl+c` | 取消选择 |\n\n### TUI 全屏视口\n\n当交互模式使用 `--tui-mode fullscreen` 并定位主转录本滚动区域时，将应用这些操作。两指触控板和鼠标滚轮输入滚动指针下方的区域，回落到固定编辑器/状态/页脚停靠栏上的文字记录。单击 OSC 8 超链接将其在默认处理程序中打开。使用鼠标主按钮拖动选择文本并将其复制到剪贴板；按住记录的顶部或底部边缘会自动滚动到屏幕外的内容。\n\n全屏转录本绑定优先于编辑器绑定。因此，默认的未修改导航键控制全屏模式下的文字记录，而其 `ctrl` 变体继续控制编辑器。在全屏模式之外，两种变体都控制编辑器。\n\n| 钥匙 | 默认模式 | 全屏模式 |\n|-----|--------------|-----------------|\n| `home`, `end` | 编辑 | 成绩单 |\n| `ctrl+home`, `ctrl+end` | 编辑 | 编辑 |\n| `pageUp`, `pageDown` | 编辑 | 成绩单 |\n| `ctrl+pageUp`, `ctrl+pageDown` | 编辑 | 编辑 |\n\n该路由仍然可以通过普通操作绑定进行配置。例如，`\"tui.altScreen.pageUp\": \"ctrl+pageUp\"` 使`pageUp` 控制编辑器，`ctrl+pageUp` 控制全屏模式下的文字记录。绑定 `tui.altScreen.halfPageUp` 和 `tui.altScreen.halfPageDown` 以实现较小的转录步骤，同时保持全页绑定。设置 `\"tui.altScreen.pageUp\": []` 会完全禁用该转录快捷方式。用户绑定替换该操作的默认值。\n\n| 按键绑定 ID | 默认 | 描述 |\n|--------|---------|-------------|\n| `tui.altScreen.pageUp` | `pageUp` | 将文字记录向上滚动一页 |\n| `tui.altScreen.pageDown` | `pageDown` | 将文字记录向下滚动一页 |\n| `tui.altScreen.halfPageUp` | *（没有任何）* | 将文字记录向上滚动半页 |\n| `tui.altScreen.halfPageDown` | *（没有任何）* | 将文字记录向下滚动半页 |\n| `tui.altScreen.previousPrompt` | `ctrl+shift+up` | 跳转到上一条标记的消息 |\n| `tui.altScreen.nextPrompt` | `ctrl+shift+down` | 跳转到下一条标记的消息 |\n| `tui.altScreen.top` | `home` | 滚动到文字记录的开头 |\n| `tui.altScreen.bottom` | `end` | 滚动到转录结束并跟随新的输出 |\n\n### 应用\n\n| 按键绑定 ID | 默认 | 描述 |\n|--------|---------|-------------|\n| `app.interrupt` | `escape` | 取消/中止 |\n| `app.clear` | `ctrl+c` | 清除编辑器（第一）/退出（第二） |\n| `app.exit` | `ctrl+d` | 退出（当编辑器为空时） |\n| `app.suspend` | `ctrl+z`（Windows 上无） | 暂停到后台 |\n| `app.editor.external` | `ctrl+g` | 在外部编辑器中打开（`externalEditor`、`$VISUAL`、`$EDITOR`、Windows 上的记事本或其他地方的 `nano`） |\n| `app.clipboard.pasteImage` | `ctrl+v`（`alt+v` 在 Windows 上） | 从剪贴板粘贴图像或文本 |\n\n### 会话\n\n| 按键绑定 ID | 默认 | 描述 |\n|--------|---------|-------------|\n| `app.session.new` | *（没有任何）* | 开始新会话 (`/new`) |\n| `app.session.tree` | *（没有任何）* | 打开 session tree 导航器 (`/tree`) |\n| `app.session.fork` | *（没有任何）* | 分叉当前会话 (`/fork`) |\n| `app.session.resume` | *（没有任何）* | 打开会话恢复选择器 (`/resume`) |\n| `app.session.togglePath` | `ctrl+p` | 切换路径显示 |\n| `app.session.toggleSort` | `ctrl+s` | 切换排序模式 |\n| `app.session.toggleNamedFilter` | `ctrl+n` | 切换仅命名过滤器 |\n| `app.session.rename` | `ctrl+r` | 重命名会话 |\n| `app.session.delete` | `ctrl+d` | 删除会话 |\n| `app.session.deleteNoninvasive` | `ctrl+backspace` | 当查询为空时删除会话 |\n\n### Models与思考\n\n| 按键绑定 ID | 默认 | 描述 |\n|--------|---------|-------------|\n| `app.model.select` | `ctrl+l` | 打开模型选择器 |\n| `app.model.cycleForward` | `ctrl+p` | 循环到下一个模型 |\n| `app.model.cycleBackward` | `shift+ctrl+p` | 循环到之前的模型 |\n| `app.thinking.cycle` | `shift+tab` | 循环思维水平 |\n| `app.thinking.toggle` | `ctrl+t` | 折叠或扩展思维块 |\n\n### 显示和消息队列\n\n| 按键绑定 ID | 默认 | 描述 |\n|--------|---------|-------------|\n| `app.tools.expand` | `ctrl+o` | 折叠或展开工具输出 |\n| `app.message.copy` | `ctrl+x` | 复制最后一条助理消息，或`/tree`中选定的消息 |\n| `app.message.followUp` | `alt+enter` | 队列后续消息 |\n| `app.message.dequeue` | `alt+up` | 将排队消息恢复到编辑器 |\n\n### 树状导航\n\n| 按键绑定 ID | 默认 | 描述 |\n|--------|---------|-------------|\n| `app.tree.foldOrUp` | `ctrl+left`, `alt+left` | 折叠当前分支段，或跳转到上一个段开始 |\n| `app.tree.unfoldOrDown` | `ctrl+right`, `alt+right` | 展开当前分支段，或跳转到下一个段起点或分支终点 |\n| `app.tree.editLabel` | `shift+l` | 编辑所选树节点上的标签 |\n| `app.tree.toggleLabelTimestamp` | `shift+t` | 切换树中的标签时间戳 |\n| `app.tree.filter.default` | `ctrl+d` | 将树过滤器设置为默认视图 |\n| `app.tree.filter.noTools` | `ctrl+t` | 切换隐藏工具结果的树过滤器 |\n| `app.tree.filter.userOnly` | `ctrl+u` | 切换仅显示用户消息的树过滤器 |\n| `app.tree.filter.labeledOnly` | `ctrl+l` | 切换树过滤器仅显示带标签的条目 |\n| `app.tree.filter.all` | `ctrl+a` | 切换显示所有条目的树过滤器 |\n| `app.tree.filter.cycleForward` | `ctrl+o` | 循环树过滤器向前 |\n| `app.tree.filter.cycleBackward` | `shift+ctrl+o` | 向后循环树过滤 |\n\n### 作用域Models选择器\n\n在作用域模型选择器中使用（通过 `/scoped-models` 打开）。\n\n| 按键绑定 ID | 默认 | 描述 |\n|--------|---------|-------------|\n| `app.models.save` | `ctrl+s` | 将当前模型选择保存到设置 |\n| `app.models.enableAll` | `ctrl+a` | 启用所有模型（或所有匹配当前搜索的模型） |\n| `app.models.clearAll` | `ctrl+x` | 清除所有型号（或所有与当前搜索匹配的型号） |\n| `app.models.toggleProvider` | `ctrl+p` | 切换当前提供商的所有模型 |\n| `app.models.reorderUp` | `alt+up` | 将所选模型在循环顺序中上移 |\n| `app.models.reorderDown` | `alt+down` | 将所选模型按循环顺序向下移动 |\n\n## 自定义配置\n\n创建`~/.pi/agent/keybindings.json`：\n\n```json\n{\n  \"tui.editor.historyPrevious\": \"ctrl+p\",\n  \"tui.editor.historyNext\": \"ctrl+n\",\n  \"tui.editor.deleteWordBackward\": [\"ctrl+w\", \"alt+backspace\"]\n}\n```\n\n每个操作可以有一个键或一组键。用户配置覆盖默认值。\n\n在本机 Windows 上，`app.suspend` 没有默认绑定，因为 Windows 终端不支持 Unix 作业控制。如果您手动绑定它，pi 将显示状态消息而不是挂起。在 WSL 中，正常的 Linux `ctrl+z`/`fg` 行为仍然适用。\n\n### Emacs 示例\n\n```json\n{\n  \"tui.editor.historyPrevious\": \"ctrl+p\",\n  \"tui.editor.historyNext\": \"ctrl+n\",\n  \"tui.editor.cursorLeft\": [\"left\", \"ctrl+b\"],\n  \"tui.editor.cursorRight\": [\"right\", \"ctrl+f\"],\n  \"tui.editor.cursorWordLeft\": [\"alt+left\", \"alt+b\"],\n  \"tui.editor.cursorWordRight\": [\"alt+right\", \"alt+f\"],\n  \"tui.editor.deleteCharForward\": [\"delete\", \"ctrl+d\"],\n  \"tui.editor.deleteCharBackward\": [\"backspace\", \"ctrl+h\"],\n  \"tui.input.newLine\": [\"shift+enter\", \"ctrl+j\"]\n}\n```\n\n### Vim 示例\n\n```json\n{\n  \"tui.editor.cursorUp\": [\"up\", \"alt+k\"],\n  \"tui.editor.cursorDown\": [\"down\", \"alt+j\"],\n  \"tui.editor.cursorLeft\": [\"left\", \"alt+h\"],\n  \"tui.editor.cursorRight\": [\"right\", \"alt+l\"],\n  \"tui.editor.cursorWordLeft\": [\"alt+left\", \"alt+b\"],\n  \"tui.editor.cursorWordRight\": [\"alt+right\", \"alt+w\"]\n}\n```","sourceFile":"keybindings.md"},"llama-cpp":{"title":"llama.cpp","markdown":"Pi支持[llama.cpp](https://github.com/ggml-org/llama.cpp)路由器服务器。路由器发现多个GGUF模型并按需加载或卸载它们。\n\n使用具有路由器支持的当前 llama.cpp 版本。按照 [build instructions](https://github.com/ggml-org/llama.cpp/blob/master/docs/build.md) 操作或为您的平台安装 [prebuilt release](https://github.com/ggml-org/llama.cpp/releases)。\n\n## 启动路由器\n\n从 `llama-server` 开始，没有 `--model` 或 `-m`。传递模型会启动单模型模式而不是路由器模式。\n\n```bash\nllama-server \\\n  --models-dir ~/models \\\n  --no-models-autoload \\\n  --jinja \\\n  --host 127.0.0.1 \\\n  --port 8080 \\\n  -ngl 999 \\\n  -c 32768\n```\n\n重要选项：\n\n- `--models-dir ~/models` 发现本地GGUF 文件。\n- `--no-models-autoload` 通过`/llama` 保持显式加载。\n- `--jinja` 支持兼容的聊天模板和工具调用。\n- `-ngl 999` 将尽可能多的层卸载到 GPU。\n- `-c 32768` 设置每个加载模型的上下文窗口。省略它以使用模型的本机上下文，这可能需要更多的内存。\n\n单文件模型可以直接位于模型目录中。将多模式和多分片模型放在单独的子目录中：\n\n```text\n~/models/\n├── llama-3.2-1b-Q4_K_M.gguf\n├── gemma-3-4b-it-Q4_K_M/\n│   ├── gemma-3-4b-it-Q4_K_M.gguf\n│   └── mmproj-F16.gguf\n└── large-model-Q4_K_M/\n    ├── large-model-Q4_K_M-00001-of-00003.gguf\n    ├── large-model-Q4_K_M-00002-of-00003.gguf\n    └── large-model-Q4_K_M-00003-of-00003.gguf\n```\n\n手动添加文件后重启路由器。对于每个模型的上下文大小和其他选项，请使用 [llama.cpp model presets](https://github.com/ggml-org/llama.cpp/blob/master/tools/server/README.md#model-presets)。\n\n## 配置Pi\n\n启动 Pi 并配置提供程序：\n\n```text\n/login llama.cpp\n```\n\n输入路由器 URL 和可选的 API key。默认 URL 为 `http://127.0.0.1:8080`。\n\n环境变量可以在没有`/login`的情况下配置相同的值：\n\n```bash\nexport LLAMA_BASE_URL=http://127.0.0.1:8080\nexport LLAMA_API_KEY=optional-secret\npi\n```\n\n如果服务器使用 API key，则以匹配的 `--api-key` 值开始 `llama-server`。保留 `--host 127.0.0.1` 仅用于本地访问。\n\n## 管理模型\n\n跑步：\n\n```text\n/llama\n```\n\n- 选择一个已卸载的模型来加载它。\n- 选择已加载的模型将其卸载。\n- 选择 **下载模型...**，搜索 Hugging Face，然后选择存储库和量化。精确的 `owner/repository[:quant]` 值也有效。\n- 在加载或下载过程中按 Esc 键确认取消。\n\nHugging Face 搜索在设置时使用 `HF_TOKEN`，然​​后检查 `$HF_TOKEN_PATH`、`$HF_HOME/token`、`$XDG_CACHE_HOME/huggingface/token` 和 `~/.cache/huggingface/token`。搜索也无需身份验证即可运行，但受到较低的速率限制。 Pi 在下载封闭存储库及其访问页面的链接之前发出警告。 llama.cpp 服务器执行下载，因此当所选存储库需要访问时，其进程也必须具有 `HF_TOKEN`。\n\n如果加载了其他模型，Pi会询问是先卸载还是保持加载。 Pi 不会静默卸载模型，也不会删除模型文件。路由器可能与其他客户端共享，因此`/llama`始终显示路由器的当前状态。\n\n仅加载的模型出现在`/model`中。加载模型后，运行 `/model` 为当前 Pi 会话选择它。\n\n如果路由器断开连接，`/llama`会显示**重试**和**关闭**。重试重新连接并刷新模型状态，而不重播中断的操作。\n\n## 故障排除\n\n检查路由器是否可达：\n\n```bash\ncurl http://127.0.0.1:8080/health\ncurl http://127.0.0.1:8080/models\n```\n\n- **`/llama`中没有模型：**检查`--models-dir`目录布局，然后重新启动路由器。\n- **`/model` 中缺少模型：** 首先使用 `/llama` 加载它。\n- **加载失败或使用过多内存：**降低`-c`或卸载另一个模型。\n- **服务器未处于路由器模式：** 在没有 `--model`、`-m` 或 `-hf` 的情况下启动它。","sourceFile":"llama-cpp.md"},"models":{"title":"定制Models","markdown":"通过 `~/.pi/agent/models.json` 添加自定义提供程序和模型（Ollama、vLLM、LM Studio、代理）。\n\n## 目录\n\n- [Minimal Example](#minimal-example)\n- [Full Example](#full-example)\n- [Supported APIs](#supported-apis)\n- [Provider Configuration](#provider-configuration)\n- [Model Configuration](#model-configuration)\n- [Overriding Built-in Providers](#overriding-built-in-providers)\n- [Per-model Overrides](#per-model-overrides)\n- [Anthropic Messages Compatibility](#anthropic-messages-compatibility)\n- [OpenAI Compatibility](#openai-compatibility)\n\n## 最小的例子\n\n对于本地模型（Ollama、LM Studio、vLLM），每个模型仅需要 `id`：\n\n```json\n{\n  \"providers\": {\n    \"ollama\": {\n      \"baseUrl\": \"http://localhost:11434/v1\",\n      \"api\": \"openai-completions\",\n      \"apiKey\": \"ollama\",\n      \"models\": [\n        { \"id\": \"llama3.1:8b\" },\n        { \"id\": \"qwen2.5-coder:7b\" }\n      ]\n    }\n  }\n}\n```\n\n`apiKey` 值是一个占位符，因为 Ollama 会忽略它。 pi 仍然将模型视为需要身份验证才能出现在 `/model` 中，因此无密钥本地服务器应保留一个虚拟值，使用 `/login` 为该提供者保存密钥，或者在选择模型时传递 `--api-key`。\n\n一些 OpenAI 兼容服务器不理解用于推理模型的 `developer` 角色。对于这些提供程序，将 `compat.supportsDeveloperRole` 设置为 `false`，以便 pi 将系统提示作为 `system` 消息发送。如果服务器也不支持`reasoning_effort`，请将`compat.supportsReasoningEffort`也设置为`false`。\n\n您可以在提供程序级别设置 `compat` 以应用于所有模型，或在模型级别设置 `compat` 以覆盖特定模型。这通常适用于 Ollama、vLLM、SGLang 和类似的 OpenAI 兼容服务器。\n\n```json\n{\n  \"providers\": {\n    \"ollama\": {\n      \"baseUrl\": \"http://localhost:11434/v1\",\n      \"api\": \"openai-completions\",\n      \"apiKey\": \"ollama\",\n      \"compat\": {\n        \"supportsDeveloperRole\": false,\n        \"supportsReasoningEffort\": false\n      },\n      \"models\": [\n        {\n          \"id\": \"gpt-oss:20b\",\n          \"reasoning\": true\n        }\n      ]\n    }\n  }\n}\n```\n\n## 完整示例\n\n当您需要特定值时覆盖默认值：\n\n```json\n{\n  \"providers\": {\n    \"ollama\": {\n      \"baseUrl\": \"http://localhost:11434/v1\",\n      \"api\": \"openai-completions\",\n      \"apiKey\": \"ollama\",\n      \"models\": [\n        {\n          \"id\": \"llama3.1:8b\",\n          \"name\": \"Llama 3.1 8B (Local)\",\n          \"reasoning\": false,\n          \"input\": [\"text\"],\n          \"contextWindow\": 128000,\n          \"maxTokens\": 32000,\n          \"cost\": { \"input\": 0, \"output\": 0, \"cacheRead\": 0, \"cacheWrite\": 0 }\n        }\n      ]\n    }\n  }\n}\n```\n\n每次打开 `/model` 时，文件都会重新加载。在会议期间编辑；无需重新启动。\n\n## 谷歌AI工作室示例\n\n使用 `google-generative-ai` 和 `baseUrl` 从 Google AI Studio 添加模型，包括自定义 Gemma 4 条目：\n\n```json\n{\n  \"providers\": {\n    \"my-google\": {\n      \"baseUrl\": \"https://generativelanguage.googleapis.com/v1beta\",\n      \"api\": \"google-generative-ai\",\n      \"apiKey\": \"$GEMINI_API_KEY\",\n      \"models\": [\n        {\n          \"id\": \"gemma-4-31b-it\",\n          \"name\": \"Gemma 4 31B\",\n          \"input\": [\"text\", \"image\"],\n          \"contextWindow\": 262144,\n          \"reasoning\": true\n        }\n      ]\n    }\n  }\n}\n```\n\n将自定义模型添加到 `google-generative-ai` API 类型时需要`baseUrl`。\n\n## 支持APIs\n\n| API | 描述 |\n|-----|-------------|\n| `openai-completions` | OpenAI 聊天完成（最兼容） |\n| `openai-responses` | OpenAI 回应 API |\n| `anthropic-messages` | 人择信息API |\n| `google-generative-ai` | 谷歌生成人工智能 |\n\n在提供程序级别（所有模型的默认值）或模型级别（每个模型覆盖）设置 `api`。\n\n## 提供商配置\n\n| 场地 | 描述 |\n|-------|-------------|\n| `baseUrl` | API 端点 URL |\n| `api` | API类型（见上文） |\n| `apiKey` | 可选的 API key 配置（请参阅下面的值解析）。当 auth 由 `/login`/`auth.json` 或 CLI `--api-key` 提供时省略。 |\n| `oauth` | 动态 OAuth 提供者类型。目前支持`\"radius\"`；需要网关`baseUrl`。 |\n| `headers` | 自定义标头（请参阅下面的值解析） |\n| `authHeader` | 设置`true`自动添加`Authorization: Bearer <apiKey>` |\n| `models` | 模型配置数组 |\n| `modelOverrides` | 每个模型覆盖此提供程序上的内置或扩展注册模型 |\n\n对于具有 `models` 的提供程序，非内置提供程序配置需要提供程序或模型级别的 `baseUrl` 和 `api` 值。加载文件不需要`apiKey`：当通过`/login`/`auth.json`、CLI`--api-key`或提供者`apiKey`配置身份验证时，模型变得可用。如果未配置身份验证，则模型会加载，但在`/model`和`--list-models`中保持不可用。\n\n### 价值解析\n\n`apiKey` 和 `headers` 字段支持命令执行、环境插值和文字：\n\n- **Shell 命令：** 开头的 `\"!command\"` 将整个值作为命令执行并使用 stdout\n  ```json\n  \"apiKey\": \"!security find-generic-password -ws 'anthropic'\"\n  \"apiKey\": \"!op read 'op://vault/item/credential'\"\n  ```\n- **环境插值：** `\"$ENV_VAR\"` 或 `\"${ENV_VAR}\"` 使用命名变量的值。插值适用于较大的文字。\n  ```json\n  \"apiKey\": \"$MY_API_KEY\"\n  \"apiKey\": \"${KEY_PREFIX}_${KEY_SUFFIX}\"\n  ```\n  `$FOO_BAR`是变量`FOO_BAR`；当 `BAR` 是文字文本时，使用 `${FOO}_BAR`。缺少环境变量会导致该值无法解析。\n- **转义：** `\"$\"` 发出文字 `\"$\"`； `\"$!\"` 发出文字 `\"!\"` 而不触发命令执行。\n  ```json\n  \"apiKey\": \"$$literal-dollar-prefix\"\n  \"apiKey\": \"$!literal-bang-prefix\"\n  ```\n- **字面值：** 直接使用。普通大写字符串（例如 `MY_API_KEY`）是文字；使用 `$MY_API_KEY` 作为环境变量。\n  ```json\n  \"apiKey\": \"sk-...\"\n  ```\n\n对于 `models.json`，shell 命令在请求时解析。 pi 故意不对任意命令应用内置 TTL、过时重用或恢复逻辑。不同的命令需要不同的缓存和失败策略，并且 pi 无法推断出正确的策略。\n\n如果您的命令速度慢、成本高、速率受限，或者应该在暂时性故障时继续使用先前的值，请将其包装在您自己的脚本或命令中，以实现您想要的缓存或 TTL 行为。\n\n`/model` 可用性检查使用配置的身份验证存在并且不执行 shell 命令。\n\n### 自定义标头\n\n```json\n{\n  \"providers\": {\n    \"custom-proxy\": {\n      \"baseUrl\": \"https://proxy.example.com/v1\",\n      \"apiKey\": \"$MY_API_KEY\",\n      \"api\": \"anthropic-messages\",\n      \"headers\": {\n        \"x-portkey-api-key\": \"$PORTKEY_API_KEY\",\n        \"x-secret\": \"!op read 'op://vault/item/secret'\"\n      },\n      \"models\": [...]\n    }\n  }\n}\n```\n\n## 型号配置\n\n| 场地 | 必需的 | 默认 | 描述 |\n|-------|----------|---------|-------------|\n| `id` | 是的 | — | 型号标识符（传递给API） |\n| `name` | 不 | `id` | 人类可读的模型标签。用于匹配（`--model`模式）并显示为辅助模型详细信息文本。 |\n| `api` | 不 | 提供商的`api` | 覆盖此模型的提供商的 API |\n| `reasoning` | 不 | `false` | 支持扩展思维 |\n| `thinkingLevelMap` | 不 | 省略 | 将 pi 思维级别映射到提供者值并标记不支持的级别（见下文） |\n| `input` | 不 | `[\"text\"]` | 输入类型：`[\"text\"]`或`[\"text\", \"image\"]` |\n| `contextWindow` | 不 | `128000` | 上下文窗口大小（以标记为单位） |\n| `maxTokens` | 不 | `16384` | 最大输出令牌 |\n| `samplingParams` | 不 | 省略 | 采样参数逐字合并到每个请求正文中（见下文） |\n| `cost` | 不 | 全为零 | 每百万代币费率以及可选的请求范围输入定价层 |\n| `compat` | 不 | 提供者`compat` | 提供商兼容性覆盖。当两者都设置时，与提供者级别 `compat` 合并。 |\n\n成本层提供完整的替代费率集，并在总输入使用量 (`input + cacheRead + cacheWrite`) 超过 `inputTokensAbove` 时应用于完整请求。当多个级别匹配时，阈值最高的获胜。\n\n```json\n{\n  \"cost\": {\n    \"input\": 5,\n    \"output\": 30,\n    \"cacheRead\": 0.5,\n    \"cacheWrite\": 6.25,\n    \"tiers\": [\n      {\n        \"inputTokensAbove\": 272000,\n        \"input\": 10,\n        \"output\": 45,\n        \"cacheRead\": 1,\n        \"cacheWrite\": 12.5\n      }\n    ]\n  }\n}\n```\n\n当前行为：\n- `/model`、`--list-models` 和交互式页脚按模型`id` 显示条目。\n- 配置的`name`用于模型匹配和辅助模型详细文本。它不会替换页脚/状态栏模型 ID。\n\n### 采样参数\n\n`samplingParams` 是一个自由格式的对象，在字段 pi 设置自身之后，逐字合并到模型的每个请求主体中，因此它的键获胜。使用它发送 pi 不建模的采样参数 - 包括特定于服务器的参数，例如 llama.cpp 的 `min_p` 或 vLLM 的 `top_k`：\n\n```json\n{\n  \"id\": \"deepseek-v4-flash\",\n  \"samplingParams\": {\n    \"temperature\": 1.0,\n    \"top_p\": 0.95,\n    \"top_k\": 0,\n    \"min_p\": 0.0\n  }\n}\n```\n\n仅兼容 OpenAI 的 API 适用（`openai-completions`、`openai-responses`、`azure-openai-responses`）；其他API忽略它。键会覆盖 pi 的命名请求字段（例如，这里的 `temperature` 键击败了请求级别的温度），因此更喜欢将其作为模型采样事实的单一来源。在 `modelOverrides` 中，`samplingParams` 将每个键与基本模型的值合并。\n\n### 思维层次图\n\n在模型上使用`thinkingLevelMap`来描述特定于模型的思维控制。关键是 pi 思维级别：`off`、`minimal`、`low`、`medium`、`high`、`xhigh`、`max`。地图可能包含漏洞；例如，模型可以公开 `high` 和 `max`，而不公开 `xhigh`。\n\n值是三态的：\n\n| 价值 | 意义 |\n|-------|---------|\n| 省略 | 标准级别到`high`使用提供者的默认映射；不支持扩展 `xhigh` 和 `max` 级别 |\n| 细绳 | 支持级别并将该值发送给提供商 |\n| `null` | 水平仪不受支撑且隐藏/跳过/夹住 |\n\n仅支持 off、high 和 max 推理的模型示例：\n\n```json\n{\n  \"id\": \"deepseek-v4-pro\",\n  \"reasoning\": true,\n  \"thinkingLevelMap\": {\n    \"minimal\": null,\n    \"low\": null,\n    \"medium\": null,\n    \"high\": \"high\",\n    \"xhigh\": null,\n    \"max\": \"max\"\n  }\n}\n```\n\n思维不能被禁用的模型示例：\n\n```json\n{\n  \"id\": \"always-thinking-model\",\n  \"reasoning\": true,\n  \"thinkingLevelMap\": {\n    \"off\": null\n  }\n}\n```\n\n迁移：使用`compat.reasoningEffortMap`的旧配置应将该映射移动到模型级别`thinkingLevelMap`。对于不应出现在 UI 中的级别，请使用 `null`。\n\n## 覆盖内置 Providers\n\n通过代理路由内置提供者，无需重新定义模型：\n\n```json\n{\n  \"providers\": {\n    \"anthropic\": {\n      \"baseUrl\": \"https://my-proxy.example.com/v1\"\n    }\n  }\n}\n```\n\n所有内置 Anthropic 模型仍然可用。现有的 OAuth 或 API key 身份验证继续有效。\n\n要将自定义模型合并到内置提供程序中，请包含 `models` 数组：\n\n```json\n{\n  \"providers\": {\n    \"anthropic\": {\n      \"baseUrl\": \"https://my-proxy.example.com/v1\",\n      \"apiKey\": \"$ANTHROPIC_API_KEY\",\n      \"api\": \"anthropic-messages\",\n      \"models\": [...]\n    }\n  }\n}\n```\n\n合并语义：\n- 保留内置模型。\n- 自定义模型由 `id` 在提供程序中更新。\n- 如果自定义模型 `id` 与内置模型 `id` 匹配，则自定义模型将替换该内置模型。\n- 如果自定义模型 `id` 是新的，它将与内置模型一起添加。\n\n## 每个模型的覆盖\n\n使用 `modelOverrides` 自定义内置模型和匹配扩展注册的模型，而无需替换提供者的完整模型列表。\n\n```json\n{\n  \"providers\": {\n    \"openrouter\": {\n      \"modelOverrides\": {\n        \"anthropic/claude-sonnet-4\": {\n          \"name\": \"Claude Sonnet 4 (Bedrock Route)\",\n          \"compat\": {\n            \"openRouterRouting\": {\n              \"only\": [\"amazon-bedrock\"]\n            }\n          }\n        }\n      }\n    }\n  }\n}\n```\n\n`modelOverrides` 每个模型支持以下字段：`name`、`reasoning`、`thinkingLevelMap`、`input`、`cost`（部分）、`contextWindow`、`maxTokens`、`samplingParams`（每个键合并）、`headers`、`compat`。\n\nDirect OpenAI GPT-5.6 Sol、Terra 和 Luna 默认为 `272000` 上下文窗口，因此请求保留在 OpenAI 的短上下文定价层内。要选择 OpenAI 的 1.05M 上下文窗口，请为您使用的每个模型增加它：\n\n```json\n{\n  \"providers\": {\n    \"openai\": {\n      \"modelOverrides\": {\n        \"gpt-5.6-sol\": {\n          \"contextWindow\": 1050000\n        }\n      }\n    }\n  }\n}\n```\n\n覆盖保留内置定价元数据。总输入令牌超过 272K 的请求对整个请求使用 GPT-5.6 的长上下文速率。需要时，对 `gpt-5.6-terra` 或 `gpt-5.6-luna` 应用相同的覆盖。\n\n行为注意事项：\n- `modelOverrides` 适用于内置提供者模型和匹配的扩展注册提供者模型。\n- 未知的型号 ID 将被忽略。\n- 您可以将提供商级别 `baseUrl`/`headers` 与 `modelOverrides` 结合起来。\n- 覆盖`name`仅更改模型匹配和次要详细文本；页脚和主要型号列表继续显示型号 `id`。\n- 如果还为提供者定义了 `models`，则自定义模型将在内置覆盖后合并。具有相同 `id` 的自定义模型将替换覆盖的内置模型条目。\n\n## 人择消息兼容性\n\n对于使用`api: \"anthropic-messages\"`的提供者或代理，请使用`compat`来控制特定于人类的请求兼容性。\n\n默认情况下 pi 发送每个工具 `eager_input_streaming: true`。如果代理或与 Anthropic 兼容的后端拒绝该字段，请将 `supportsEagerToolInputStreaming` 设置为 `false`。 Pi 将省略 `tools[].eager_input_streaming` 并为支持工具的请求发送旧版 `fine-grained-tool-streaming-2025-05-14` beta 标头。\n\n一些人择模型需要适应性思维（`thinking.type: \"adaptive\"`加`output_config.effort`），而不是传统的基于预算的思维有效负载。内置模型会自动设置此项。对于路由到这些模型的自定义提供程序或别名，请将 `forceAdaptiveThinking` 设置为 `true`。\n\n一些与人类兼容的提供者发出带有空签名的思维块，并且仍然期望它们重播。仅针对这些提供商将 `allowEmptySignature` 设置为 `true`；真正的人择拒绝空洞的思维签名。\n\n内置人择模型在其模型元数据中启用 `supportsStrictTools`。当自定义 Anthropic 兼容模型的端点接受严格的 JSON 模式工具定义时，必须将其设置为 `true`。\n\n```json\n{\n  \"providers\": {\n    \"anthropic-proxy\": {\n      \"baseUrl\": \"https://proxy.example.com\",\n      \"api\": \"anthropic-messages\",\n      \"apiKey\": \"$ANTHROPIC_PROXY_KEY\",\n      \"compat\": {\n        \"supportsEagerToolInputStreaming\": false,\n        \"supportsLongCacheRetention\": true,\n        \"forceAdaptiveThinking\": true,\n        \"allowEmptySignature\": true\n      },\n      \"models\": [\n        {\n          \"id\": \"claude-opus-4-7\",\n          \"reasoning\": true,\n          \"input\": [\"text\", \"image\"]\n        }\n      ]\n    }\n  }\n}\n```\n\n| 场地 | 描述 |\n|-------|-------------|\n| `supportsEagerToolInputStreaming` | 提供者是否接受每个工具`eager_input_streaming`。默认值：`true`。设置为 `false` 可忽略该字段，并在启用工具的请求上使用旧版细粒度工具流式传输 Beta 标头。 |\n| `supportsLongCacheRetention` | 当缓存保留为 `long` 时，提供者是否接受 Anthropic 长缓存保留 (`cache_control.ttl: \"1h\"`)。默认值：`true`。 |\n| `sendSessionAffinityHeaders` | 启用缓存时是否从会话 ID 发送`x-session-affinity`。默认值：自动检测已知提供商。 |\n| `supportsCacheControlOnTools` | 提供者是否接受工具定义上的人类风格 `cache_control` 标记。默认值：`true`。 |\n| `forceAdaptiveThinking` | 是否为该模型发送自适应思维（`thinking.type: \"adaptive\"`加`output_config.effort`）。内置自适应模型会自动设置此值。默认值：`false`。 |\n| `allowEmptySignature` | 是否将空思维签名重播为`signature: \"\"`，而不是将思维转换为文本。默认值：`false`。 |\n| `supportsStrictTools` | 提供者是否接受严格的JSON-模式工具定义。默认值：`false`；内置的人择模型在生成的元数据中启用它。 |\n\n## OpenAI 兼容性\n\n对于具有部分 OpenAI 兼容性的提供商，请使用 `compat` 字段。\n\n- 提供者级别 `compat` 将默认值应用于该提供者下的所有模型。\n- 模型级别 `compat` 覆盖该模型的提供者级别值。\n\n```json\n{\n  \"providers\": {\n    \"local-llm\": {\n      \"baseUrl\": \"http://localhost:8080/v1\",\n      \"api\": \"openai-completions\",\n      \"compat\": {\n        \"supportsUsageInStreaming\": false,\n        \"maxTokensField\": \"max_tokens\"\n      },\n      \"models\": [...]\n    }\n  }\n}\n```\n\n| 场地 | 描述 |\n|-------|-------------|\n| `supportsStore` | 提供者支持`store`字段 |\n| `supportsDeveloperRole` | 使用 `developer` 与 `system` 角色 |\n| `supportsReasoningEffort` | 支持`reasoning_effort`参数 |\n| `supportsUsageInStreaming` | 支持`stream_options: { include_usage: true }`（默认：`true`） |\n| `supportsFinishReason` | 流式响应是否包含`finish_reason`。当 `false` 时，pi 在流结束时推断出 `stop` 或 `toolUse`。默认值：`true`。 |\n| `maxTokensField` | 使用 `max_completion_tokens` 或 `max_tokens` |\n| `requiresToolResultName` | 在工具结果消息中包含 `name` |\n| `requiresAssistantAfterToolResult` | 在工具结果之后的用户消息之前插入辅助消息 |\n| `requiresThinkingAsText` | 将思维块转换为纯文本 |\n| `requiresReasoningContentOnAssistantMessages` | 启用推理时，在所有重播的助手消息中包含空 `reasoning_content` |\n| `thinkingFormat` | 使用 `reasoning_effort`、`openrouter`、`deepseek`、`together`、`baseten`、`zai`、`qwen`、`chat-template` 或 `qwen-chat-template` 思维参数 |\n| `chatTemplateKwargs` | `chat_template_kwargs` `thinkingFormat: \"chat-template\"` 值；使用 `{ \"$var\": \"thinking.enabled\" }` 或 `{ \"$var\": \"thinking.effort\" }` 获取 pi 控制的思维值 |\n| `chatTemplateArgs` | `chat_template_args` `thinkingFormat: \"baseten\"` 值；使用 `{ \"$var\": \"thinking.enabled\" }` 或 `{ \"$var\": \"thinking.effort\" }` 获取 pi 控制的思维值 |\n| `cacheControlFormat` | 在系统提示、最后一个工具定义以及最后一个用户、助手或工具结果文本内容上使用人类风格的 `cache_control` 标记。目前仅支持`anthropic`。 |\n| `sendSessionAffinityHeaders` | 对于`openai-completions`，启用缓存时从会话 ID 发送会话亲和性标头。默认值：`false`。 |\n| `sessionAffinityFormat` | 对于`openai-completions`和`openai-responses`，会话亲和性标头格式：`openai`发送`session_id`/`x-client-request-id`（也完成`x-session-affinity`），`openai-nosession`省略包含下划线的`session_id`标头，`openrouter`发送`x-session-id`。不影响`prompt_cache_key`主体参数。默认：自动检测。 |\n| `supportsStrictMode` | 提供者是否接受严格的JSON-模式函数工具定义。默认值取决于 API；内置 OpenAI 模型携带明确的能力元数据。 |\n| `supportsOpenAIGrammarTools` | 兼容 OpenAI 的API是否发出自定义 Lark/regex 语法工具。当 `false` 时，语法约束工具回退到正常功能工具。默认值：`false`；内置模型目录支持 OpenAI、OpenAI Codex、Azure OpenAI、GitHub Copilot、opencode 和 Cloudflare AI Gateway 上的 GPT-5+ 模型。 |\n| `deferredToolsMode` | 使用特定于提供者的延迟工具序列化。 Kimi 的 OpenAI 兼容聊天完成格式目前仅支持 `\"kimi\"`。 |\n| `supportsLongCacheRetention` | 当缓存保留为`long`时，提供者是否接受长缓存保留：对于OpenAI提示缓存，`prompt_cache_retention: \"24h\"`，或者当`cacheControlFormat`为`anthropic`时，`cache_control.ttl: \"1h\"`。默认值：`true`。 |\n| `openRouterRouting` | OpenRouter 提供商的路由首选项。该对象按原样发送到 [OpenRouter API request](https://openrouter.ai/docs/guides/routing/provider-selection) 的 `provider` 字段中。 |\n| `vercelGatewayRouting` | 用于选择提供商的 Vercel AI 网关路由配置 (`only`、`order`) |\n\n`openrouter` 使用`reasoning: { effort }`。当启用 `supportsReasoningEffort` 时，`together` 使用`reasoning: { enabled }`，也使用`reasoning_effort`。 `qwen` 使用顶级`enable_thinking`。对于需要 `chat_template_kwargs.enable_thinking` 和 `preserve_thinking` 的本地 Qwen 兼容服务器，请使用 `qwen-chat-template`。对于需要可配置 `chat_template_kwargs` 的 vLLM/Hugging Face 聊天模板，请使用 `chat-template`，例如对于 DeepSeek V3.x 模板，请使用 `chatTemplateKwargs: { \"thinking\": { \"$var\": \"thinking.enabled\" } }`。对于通过 `chat_template_args` 公开切换控件并可选择支持顶级 `reasoning_effort` 的提供程序，将 `thinkingFormat: \"baseten\"` 与 `chatTemplateArgs` 结合使用。\n\n`cacheControlFormat: \"anthropic\"` 适用于与 OpenAI 兼容的提供程序，通过文本内容和工具定义上的 `cache_control` 标记公开人类风格的提示缓存。\n\n例子：\n\n```json\n{\n  \"providers\": {\n    \"openrouter\": {\n      \"baseUrl\": \"https://openrouter.ai/api/v1\",\n      \"apiKey\": \"$OPENROUTER_API_KEY\",\n      \"api\": \"openai-completions\",\n      \"models\": [\n        {\n          \"id\": \"openrouter/anthropic/claude-3.5-sonnet\",\n          \"name\": \"OpenRouter Claude 3.5 Sonnet\",\n          \"compat\": {\n            \"openRouterRouting\": {\n              \"allow_fallbacks\": true,\n              \"require_parameters\": false,\n              \"data_collection\": \"deny\",\n              \"zdr\": true,\n              \"enforce_distillable_text\": false,\n              \"order\": [\"anthropic\", \"amazon-bedrock\", \"google-vertex\"],\n              \"only\": [\"anthropic\", \"amazon-bedrock\"],\n              \"ignore\": [\"gmicloud\", \"friendli\"],\n              \"quantizations\": [\"fp16\", \"bf16\"],\n              \"sort\": {\n                \"by\": \"price\",\n                \"partition\": \"model\"\n              },\n              \"max_price\": {\n                \"prompt\": 10,\n                \"completion\": 20\n              },\n              \"preferred_min_throughput\": {\n                \"p50\": 100,\n                \"p90\": 50\n              },\n              \"preferred_max_latency\": {\n                \"p50\": 1,\n                \"p90\": 3,\n                \"p99\": 5\n              }\n            }\n          }\n        }\n      ]\n    }\n  }\n}\n```\n\nVercel AI 网关示例：\n\n```json\n{\n  \"providers\": {\n    \"vercel-ai-gateway\": {\n      \"baseUrl\": \"https://ai-gateway.vercel.sh/v1\",\n      \"apiKey\": \"$AI_GATEWAY_API_KEY\",\n      \"api\": \"openai-completions\",\n      \"models\": [\n        {\n          \"id\": \"moonshotai/kimi-k2.5\",\n          \"name\": \"Kimi K2.5 (Fireworks via Vercel)\",\n          \"reasoning\": true,\n          \"input\": [\"text\", \"image\"],\n          \"cost\": { \"input\": 0.6, \"output\": 3, \"cacheRead\": 0, \"cacheWrite\": 0 },\n          \"contextWindow\": 262144,\n          \"maxTokens\": 262144,\n          \"compat\": {\n            \"vercelGatewayRouting\": {\n              \"only\": [\"fireworks\", \"novita\"],\n              \"order\": [\"fireworks\", \"novita\"]\n            }\n          }\n        }\n      ]\n    }\n  }\n}\n```","sourceFile":"models.md"},"packages":{"title":"Pi Packages","markdown":"> pi 可以帮助您创建 pi 包。要求它捆绑您的扩展、技能、prompt templates或主题。\n\n\nPi 打包捆绑扩展、技能、prompt templates 和主题，以便您可以通过 npm 或 git 共享它们。包可以在 `pi` 键下的 `package.json` 中声明资源，或者使用常规目录。\n\n## 目录\n\n- [Install and Manage](#install-and-manage)\n- [Package Sources](#package-sources)\n- [Creating a Pi Package](#creating-a-pi-package)\n- [Package Structure](#package-structure)\n- [Dependencies](#dependencies)\n- [Package Filtering](#package-filtering)\n- [Enable and Disable Resources](#enable-and-disable-resources)\n- [Scope and Deduplication](#scope-and-deduplication)\n\n## 安装和管理\n\n> **安全性：** Pi 软件包以完全系统访问权限运行。 Extensions执行任意代码，技能可以指示模型执行任何操作，包括运行可执行文件。在安装第三方软件包之前检查源代码。\n\n```bash\npi install npm:@foo/bar@1.0.0\npi install git:github.com/user/repo@v1\npi install https://github.com/user/repo  # raw URLs work too\npi install /absolute/path/to/package\npi install ./relative/path/to/package\n\npi remove npm:@foo/bar\npi list                     # show installed packages from settings\npi update                   # update pi only\npi update --all             # update pi, update packages, and reconcile pinned git refs\npi update --extensions      # update packages and reconcile pinned git refs only\npi update --models          # refresh model catalogs only\npi update --self            # update pi only\npi update --self --force    # reinstall pi even if current\npi update npm:@foo/bar      # update one package\npi update --extension npm:@foo/bar\n```\n\n这些命令管理 pi 包，`pi update` 可以更新 pi CLI 安装。要卸载 pi 本身，请参阅 [Quickstart](quickstart.md#uninstall)。\n\n默认情况下，`install` 和 `remove` 写入用户设置 (`~/.pi/agent/settings.json`)。请使用 `-l` 写入项目设置 (`.pi/settings.json`)。项目设置可以与您的团队共享，并且在项目受信任后，pi 会在启动时自动安装任何缺少的软件包。\n\n要试用某个包而不安装它，请使用 `--extension` 或 `-e`。这将安装到仅用于当前运行的临时目录：\n\n```bash\npi -e npm:@foo/bar\npi -e git:github.com/user/repo\n```\n\n## 包来源\n\nPi 在设置和`pi install` 中接受三种源类型。\n\n### npm\n\n```\nnpm:@scope/pkg@1.2.3\nnpm:pkg\n```\n\n- 版本化规范由软件包更新固定和跳过（`pi update --extensions`、`pi update --all`）。\n- 用户安装位于 `~/.pi/agent/npm/` 下。\n- 项目安装量低于 `.pi/npm/`。\n- 将 `settings.json` 中的 `npmCommand` 设置为将 npm 包查找和安装操作固定到特定的包装器命令，例如 `mise` 或 `asdf`。\n\n例子：\n\n```json\n{\n  \"npmCommand\": [\"mise\", \"exec\", \"node@20\", \"--\", \"npm\"]\n}\n```\n\n### git\n\n```\ngit:github.com/user/repo@v1\ngit:git@github.com:user/repo@v1\nhttps://github.com/user/repo@v1\nssh://git@github.com/user/repo@v1\n```\n\n- 如果没有 `git:` 前缀，则仅接受协议 URL（`https://`、`http://`、`ssh://`、`git://`）。\n- 使用`git:`前缀，接受简写格式，包括`github.com/user/repo`和`git@github.com:user/repo`。\n- HTTPS 和 SSH URL 均受支持。\n- SSH URL 自动使用您配置的 SSH 键（尊重 `~/.ssh/config`）。\n- 对于非交互式运行（例如 CI），您可以设置 `GIT_TERMINAL_PROMPT=0` 以禁用凭据提示，并设置 `GIT_SSH_COMMAND`（例如 `ssh -o BatchMode=yes -o ConnectTimeout=5`）以快速失败。\n- 参考文献是固定标签或提交。 `pi update --extensions` 和 `pi update --all` 不会将它们移动到较新的引用，但它们确实会将现有克隆与配置的引用进行协调。\n- 使用 `pi install git:host/user/repo@new-ref` 更新设置并将现有包移动到新的固定引用。\n- 克隆到 `~/.pi/agent/git/<host>/<path>`（全局）或 `.pi/git/<host>/<path>`（项目）。\n- 当协调更改结帐时，pi 会重置并清理克隆，然后如果 `package.json` 存在则运行 `npm install`。\n\n**SSH 示例：**\n```bash\n# git@host:path shorthand (requires git: prefix)\npi install git:git@github.com:user/repo\n\n# ssh:// protocol format\npi install ssh://git@github.com/user/repo\n\n# With version ref\npi install git:git@github.com:user/repo@v1.0.0\n```\n\n### 本地路径\n\n```\n/absolute/path/to/package\n./relative/path/to/package\n```\n\n本地路径指向磁盘上的文件或目录，并且无需复制即可添加到设置中。相对路径根据它们出现的设置文件进行解析。如果路径是文件，它将作为单个扩展名加载。如果是目录，pi使用包规则加载资源。\n\n## 创建 Pi 包\n\n将 `pi` 清单添加到 `package.json` 或使用常规目录。包含 `pi-package` 关键字以提高可发现性。\n\n```json\n{\n  \"name\": \"my-package\",\n  \"keywords\": [\"pi-package\"],\n  \"pi\": {\n    \"extensions\": [\"./extensions\"],\n    \"skills\": [\"./skills\"],\n    \"prompts\": [\"./prompts\"],\n    \"themes\": [\"./themes\"]\n  }\n}\n```\n\n路径是相对于包根的。数组支持 glob 模式和 `!exclusions`。\n\n### 图库元数据\n\n[package gallery](https://pi.dev/packages) 显示标有 `pi-package` 的包。添加 `video` 或 `image` 字段以显示预览：\n\n```json\n{\n  \"name\": \"my-package\",\n  \"keywords\": [\"pi-package\"],\n  \"pi\": {\n    \"extensions\": [\"./extensions\"],\n    \"video\": \"https://example.com/demo.mp4\",\n    \"image\": \"https://example.com/screenshot.png\"\n  }\n}\n```\n\n- **视频**：仅限 MP4。在桌面上，悬停时自动播放。单击将打开全屏播放器。\n- **图像**：PNG、JPEG、GIF 或 WebP。显示为静态预览。\n\n如果两者均设置，则视频优先。\n\n## 封装结构\n\n### 会议目录\n\n如果不存在 `pi` 清单，pi 会自动从这些目录中发现资源：\n\n- `extensions/` 加载 `.ts` 和 `.js` 文件\n- `skills/`递归查找`SKILL.md`文件夹并加载顶级`.md`文件作为技能\n- `prompts/` 加载 `.md` 文件\n- `themes/` 加载 `.json` 文件\n\n## 依赖关系\n\n第三方运行时依赖项属于 `package.json` 中的 `dependencies`。不注册扩展、技能、prompt templates或主题的依赖项也属于`dependencies`。当 pi 从 npm 或 git 安装软件包时，它会运行 `npm install`，因此这些依赖项会自动安装。\n\nPi 捆绑扩展和技能的核心包。如果您导入其中任何一个，请在 `peerDependencies` 中以 `\"*\"` 范围列出它们，并且不要捆绑它们：`@earendil-works/pi-ai`、`@earendil-works/pi-agent-core`、`@earendil-works/pi-coding-agent`、`@earendil-works/pi-tui`、`typebox`。\n\n其他 pi 软件包必须捆绑在您的 tarball 中。将它们添加到`dependencies`和`bundledDependencies`，然后通过`node_modules/`路径引用它们的资源。 Pi 加载具有单独模块根的包，因此单独安装不会冲突或共享模块。\n\n例子：\n\n```json\n{\n  \"dependencies\": {\n    \"shitty-extensions\": \"^1.0.1\"\n  },\n  \"bundledDependencies\": [\"shitty-extensions\"],\n  \"pi\": {\n    \"extensions\": [\"extensions\", \"node_modules/shitty-extensions/extensions\"],\n    \"skills\": [\"skills\", \"node_modules/shitty-extensions/skills\"]\n  }\n}\n```\n\n## 包过滤\n\n使用设置中的对象形式过滤包加载的内容：\n\n```json\n{\n  \"packages\": [\n    \"npm:simple-pkg\",\n    {\n      \"source\": \"npm:my-package\",\n      \"extensions\": [\"extensions/*.ts\", \"!extensions/legacy.ts\"],\n      \"skills\": [],\n      \"prompts\": [\"prompts/review.md\"],\n      \"themes\": [\"+themes/legacy.json\"]\n    }\n  ]\n}\n```\n\n`+path` 和 `-path` 是相对于包根的精确路径。\n\n- 省略一个键即可加载所有该类型。\n- 使用 `[]` 不加载该类型。\n- `!pattern` 排除匹配项。\n- `+path`力-包括精确路径。\n- `-path` 强制排除精确路径。\n- 清单顶部的过滤层。他们缩小了已经允许的范围。\n\n## 启用和禁用资源\n\n使用 `pi config` 启用或禁用已安装软件包和本地目录中的扩展、技能、prompt templates 和主题。 `pi config` 在全局设置中启动（`~/.pi/agent/settings.json`）；按 T​​ab 键可在全局模式和项目本地模式之间切换。使用 `pi config -l` 在项目覆盖 (`.pi/settings.json`) 中启动，继承的全局资源变暗。\n\n## 范围和重复数据删除\n\n包可以出现在全局和项目设置中。如果相同的包出现在两者中，则项目条目获胜，除非项目条目具有 `autoload: false`，在这种情况下，它将作为全局条目的增量应用。身份由以下因素决定：\n\n- npm：包名\n- git：不带引用的存储库 URL\n- local：解析的绝对路径","sourceFile":"packages.md"},"prompt-templates":{"title":"Prompt Templates","markdown":"> pi 可以创建 prompt templates。要求它为您的工作流程构建一个。\n\n\n提示模板是 Markdown 片段，可扩展为完整提示。在编辑器中输入`/name`以调用模板，其中`name`是不带`.md`的文件名。\n\n## 地点\n\nPi 从以下位置加载 prompt templates：\n\n- 全球：`~/.pi/agent/prompts/*.md`\n- 项目：`.pi/prompts/*.md`（仅在项目被信任后）\n- 包：`prompts/`目录或`package.json`中的`pi.prompts`条目\n- 设置：`prompts`包含文件或目录的数组\n- CLI: `--prompt-template <path>`（可重复）\n\n使用 `--no-prompt-templates` 禁用发现。\n\n## 格式\n\n```markdown\n---\ndescription: Review staged git changes\n---\nReview the staged changes (`git diff --cached`). Focus on:\n- Bugs and logic errors\n- Security issues\n- Error handling gaps\n```\n\n- 文件名成为命令名。 `review.md` 变为 `/review`。\n- `description` 是可选的。如果丢失，则使用第一个非空行。\n- `argument-hint` 是可选的。设置后，提示将显示在自动完成下拉列表中的描述之前。\n\n### 论证提示\n\n在 frontmatter 中使用 `argument-hint` 来显示自动完成中的预期参数。使用 `<angle brackets>` 作为必需参数，使用 `[square brackets]` 作为可选参数：\n\n```markdown\n---\ndescription: Review PRs from URLs with structured issue and code analysis\nargument-hint: \"<PR-URL>\"\n---\n```\n\n这在自动完成下拉列表中呈现为：\n\n```\n→ pr   <PR-URL>       — Review PRs from URLs with structured issue and code analysis\n  is   <issue>        — Analyze GitHub issues (bugs or feature requests)\n  wr   [instructions] — Finish the current task end-to-end\n  cl   — Audit changelog entries before release\n```\n\n## 用法\n\n在编辑器中键入 `/`，后跟模板名称。自动完成显示可用模板和描述。\n\n```\n/review                           # Expands review.md\n/component Button                 # Expands with argument\n/component Button \"click handler\" # Multiple arguments\n```\n\n## 论据\n\n模板支持位置参数、默认值和简单切片：\n\n- `$1`, `$2`,... 位置参数\n- `$@` 或 `$ARGUMENTS` 对于所有加入的参数\n- 当存在/非空时，`${1:-default}`使用arg 1，否则`default`\n- `${@:-default}` 或 `${ARGUMENTS:-default}` 在存在/非空时使用所有参数，否则 `default`\n- `${@:N}` 对于第 N 个位置的参数（1-索引）\n- `${@:N:L}` 代表从 N 开始的 `L` 参数\n\n例子：\n\n```markdown\n---\ndescription: Create a component\n---\nCreate a React component named $1 with features: $@\n```\n\n默认值对于可选参数很有用：\n\n```markdown\nSummarize the current state in ${1:-7} bullet points.\n```\n\n用法：`/component Button \"onClick handler\" \"disabled support\"`\n\n## 加载规则\n\n- `prompts/` 中的模板发现是非递归的。\n- 如果您想要子目录中的模板，请通过 `prompts` 设置或包清单显式添加它们。","sourceFile":"prompt-templates.md"},"providers":{"title":"Providers","markdown":"Pi 通过环境变量或身份验证文件通过 OAuth 和 API key 提供程序支持基于订阅的提供程序。内置目录随 pi 一起提供；配置的提供程序可以刷新较新的目录并将其缓存在`~/.pi/agent/models-store.json`中以供离线使用。\n\n## 目录\n\n- [Subscriptions](#subscriptions)\n- [API Keys](#api-keys)\n- [Auth File](#auth-file)\n- [Cloud Providers](#cloud-providers)\n- [llama.cpp](#llamacpp)\n- [Custom Providers](#custom-providers)\n- [Resolution Order](#resolution-order)\n\n## 订阅\n\n在交互模式下使用`/login`，然​​后选择一个提供者：\n\n- ChatGPT Plus/Pro（法典）\n- 克劳德·普罗/麦克斯\n- GitHub 副驾驶\n- xAI（Grok/X 订阅）\n- OpenRouter（OAuth-铸造API key由OpenRouter积分计费）\n- 半径\n\n使用 `/logout` 清除凭证。令牌存储在`~/.pi/agent/auth.json`中，过期后自动刷新。 OpenRouter 相反会铸造一个用户控制的 API key，它不会自动过期。\n\n### OpenAI 法典\n\n- 需要 ChatGPT Plus 或 Pro 订阅\n- OpenAI官方认可：[Codex for OSS](https://developers.openai.com/community/codex-for-oss)\n\n### 克劳德·普罗/麦克斯\n\nAnthropic 订阅授权对于 Claude Pro/Max 帐户有效。第三方安全带使用量从 [extra usage](https://claude.ai/settings/usage) 开始，并按代币计费，不违反 Claude 计划限制。\n\n### GitHub 副驾驶\n\n- 按 Enter 键进入 github.com，或输入您的 GitHub Enterprise Server 域\n- 如果出现“型号不受支持”，请在 VS Code 中启用它：Copilot Chat → 型号选择器 → 选择型号 →“启用”\n\n### xAI（Grok/X 订阅）\n\n- 运行 `/login xai`，然​​后选择 **使用订阅**\n- `XAI_API_KEY` 通过 **使用 API key** 保持可用\n\n### 开放路由器\n\n- 运行`/login openrouter`，然​​后选择**使用OpenRouter登录**，开启OpenRouter PKCE授权流程\n- 授权创建一个用户控制的 OpenRouter API key，从您的 OpenRouter 积分中计费\n- 在远程/无头机器上（例如超过SSH），浏览器无法到达环回回调；将最终重定向 URL（或授权代码）粘贴到登录提示中\n- `OPENROUTER_API_KEY` 通过 **使用 API key** 保持可用\n\n### 半径\n\nRadius 是一个动态 `pi-messages` 网关。 `/login radius` 将OAuth 代币存储在`auth.json` 中；网关目录独立刷新并缓存在`models-store.json`中。自定义 Radius 网关可以在 `models.json` 和 `\"oauth\": \"radius\"` 以及网关 `baseUrl` 中声明。\n\n## API 按键\n\n### 环境变量或身份验证文件\n\n在交互模式下使用`/login`并选择一个提供者将API key存储在`auth.json`中，或通过环境变量设置凭据：\n\n```bash\nexport ANTHROPIC_API_KEY=sk-ant-...\npi\n```\n\n| 提供者 | 环境变量 | `auth.json`键 |\n|----------|----------------------|------------------|\n| 人择 | `ANTHROPIC_API_KEY` | `anthropic` |\n| 蚁灵 | `ANT_LING_API_KEY` | `ant-ling` |\n| Azure OpenAI 响应 | `AZURE_OPENAI_API_KEY` | `azure-openai-responses` |\n| 开放人工智能 | `OPENAI_API_KEY` | `openai` |\n| 深度搜索 | `DEEPSEEK_API_KEY` | `deepseek` |\n| 英伟达NIM | `NVIDIA_API_KEY` | `nvidia` |\n| 谷歌双子座 | `GEMINI_API_KEY` | `google` |\n| 亚马逊基岩 | `AWS_BEARER_TOKEN_BEDROCK` | `amazon-bedrock` |\n| 米斯特拉尔 | `MISTRAL_API_KEY` | `mistral` |\n| 格罗克 | `GROQ_API_KEY` | `groq` |\n| 大脑 | `CEREBRAS_API_KEY` | `cerebras` |\n| Cloudflare AI 网关 | `CLOUDFLARE_API_KEY` (+ `CLOUDFLARE_ACCOUNT_ID`, `CLOUDFLARE_GATEWAY_ID`) | `cloudflare-ai-gateway` |\n| Cloudflare Workers AI | `CLOUDFLARE_API_KEY` (+ `CLOUDFLARE_ACCOUNT_ID`) | `cloudflare-workers-ai` |\n| 人工智能 | `XAI_API_KEY` | `xai` |\n| 开放路由器 | `OPENROUTER_API_KEY` | `openrouter` |\n| Vercel人工智能网关 | `AI_GATEWAY_API_KEY` | `vercel-ai-gateway` |\n| ZAI 编码计划（全球） | `ZAI_API_KEY` | `zai` |\n| ZAI编码计划（中国） | `ZAI_CODING_CN_API_KEY` | `zai-coding-cn` |\n| 开放代码禅 | `OPENCODE_API_KEY` | `opencode` |\n| 开放代码Go | `OPENCODE_API_KEY` | `opencode-go` |\n| 半径 | `RADIUS_API_KEY` | `radius` |\n| Hugging Face | `HF_TOKEN` | `huggingface` |\n| 烟花 | `FIREWORKS_API_KEY` | `fireworks` |\n| 一起人工智能 | `TOGETHER_API_KEY` | `together` |\n| 巴斯坦 | `BASETEN_API_KEY` | `baseten` |\n| 基米编码 | `KIMI_API_KEY` | `kimi-coding` |\n| 最小最大 | `MINIMAX_API_KEY` | `minimax` |\n| 极小最大（中国） | `MINIMAX_CN_API_KEY` | `minimax-cn` |\n| Qwen 代币计划（现有目录） | `QWEN_TOKEN_PLAN_API_KEY` | `qwen-token-plan` |\n| Qwen 代币计划（个人） | `QWEN_TOKEN_PLAN_API_KEY` | `qwen-token-plan-individual` |\n| Qwen代币计划（中国） | `QWEN_TOKEN_PLAN_CN_API_KEY` | `qwen-token-plan-cn` |\n| 小米MiMo | `XIAOMI_API_KEY` | `xiaomi` |\n| 小米 MiMo 代币计划（中国） | `XIAOMI_TOKEN_PLAN_CN_API_KEY` | `xiaomi-token-plan-cn` |\n| 小米 MiMo 代币计划（阿姆斯特丹） | `XIAOMI_TOKEN_PLAN_AMS_API_KEY` | `xiaomi-token-plan-ams` |\n| 小米 MiMo 代币计划（新加坡） | `XIAOMI_TOKEN_PLAN_SGP_API_KEY` | `xiaomi-token-plan-sgp` |\n\n环境变量和`auth.json`键的参考：[`packages/ai/src/env-api-keys.ts`](https://github.com/earendil-works/pi-mono/blob/main/packages/ai/src/env-api-keys.ts)中的[`const envMap`](https://github.com/earendil-works/pi-mono/blob/main/packages/ai/src/env-api-keys.ts)。\n\n#### 验证文件\n\n将凭证存储在`~/.pi/agent/auth.json`中：\n\n```json\n{\n  \"anthropic\": { \"type\": \"api_key\", \"key\": \"sk-ant-...\" },\n  \"ant-ling\": { \"type\": \"api_key\", \"key\": \"...\" },\n  \"openai\": { \"type\": \"api_key\", \"key\": \"sk-...\" },\n  \"deepseek\": { \"type\": \"api_key\", \"key\": \"sk-...\" },\n  \"nvidia\": { \"type\": \"api_key\", \"key\": \"nvapi-...\" },\n  \"google\": { \"type\": \"api_key\", \"key\": \"...\" },\n  \"opencode\": { \"type\": \"api_key\", \"key\": \"...\" },\n  \"opencode-go\": { \"type\": \"api_key\", \"key\": \"...\" },\n  \"together\": { \"type\": \"api_key\", \"key\": \"...\" },\n  \"qwen-token-plan\":  { \"type\": \"api_key\", \"key\": \"sk-sp-...\" },\n  \"qwen-token-plan-individual\": { \"type\": \"api_key\", \"key\": \"sk-sp-...\" },\n  \"qwen-token-plan-cn\": { \"type\": \"api_key\", \"key\": \"sk-sp-...\" },\n  \"xiaomi\": { \"type\": \"api_key\", \"key\": \"...\" },\n  \"xiaomi-token-plan-cn\":  { \"type\": \"api_key\", \"key\": \"...\" },\n  \"xiaomi-token-plan-ams\": { \"type\": \"api_key\", \"key\": \"...\" },\n  \"xiaomi-token-plan-sgp\": { \"type\": \"api_key\", \"key\": \"...\" }\n}\n```\n\n`qwen-token-plan-individual` 使用相同的国际端点，`QWEN_TOKEN_PLAN_API_KEY` 与\n`qwen-token-plan`，但将选择器限制为个人订阅记录的模型。现有的\n提供商保留其更广泛的目录以实现向后兼容性。当使用`auth.json`时，存储\n您选择的提供商下的凭证；两个国际提供商共享一个环境变量。\n\n该文件是使用 `0600` 权限创建的（仅限用户读/写）。身份验证文件凭据优先于环境变量。\n\nAPI key 凭证还可以包含提供者范围内的环境值。在解析凭据密钥、提供程序/模型标头和提供程序配置（例如 Cloudflare 帐户 ID、Azure OpenAI 设置、Vertex 项目/位置、Bedrock 设置、`PI_CACHE_RETENTION` 和 `HTTP_PROXY`/`HTTPS_PROXY`）时，在处理环境变量之前使用这些值。\n\n```json\n{\n  \"cloudflare-ai-gateway\": {\n    \"type\": \"api_key\",\n    \"key\": \"$CLOUDFLARE_API_KEY\",\n    \"env\": {\n      \"CLOUDFLARE_API_KEY\": \"...\",\n      \"CLOUDFLARE_ACCOUNT_ID\": \"account-id\",\n      \"CLOUDFLARE_GATEWAY_ID\": \"gateway-id\"\n    }\n  }\n}\n```\n\n当 pi 应使用与项目 shell 环境不同的提供程序设置时，请使用此选项。\n\n### 关键解决方案\n\n`key`字段支持命令执行、环境插值和文字：\n\n- **Shell命令：** `\"!command\"`在开始时将整个值作为命令执行并使用stdout（在进程生命周期内缓存）\n  ```json\n  { \"type\": \"api_key\", \"key\": \"!security find-generic-password -ws 'anthropic'\" }\n  { \"type\": \"api_key\", \"key\": \"!op read 'op://vault/item/credential'\" }\n  ```\n- **环境插值：** `\"$ENV_VAR\"` 或 `\"${ENV_VAR}\"` 使用命名变量的值。插值适用于较大的文字。\n  ```json\n  { \"type\": \"api_key\", \"key\": \"$MY_ANTHROPIC_KEY\" }\n  { \"type\": \"api_key\", \"key\": \"${KEY_PREFIX}_${KEY_SUFFIX}\" }\n  ```\n  `$FOO_BAR`是变量`FOO_BAR`；当 `BAR` 是文字文本时，使用 `${FOO}_BAR`。缺少环境变量会导致该值无法解析。\n- **转义：** `\"$\"` 发出文字 `\"$\"`； `\"$!\"` 发出文字 `\"!\"` 而不触发命令执行。\n  ```json\n  { \"type\": \"api_key\", \"key\": \"$$literal-dollar-prefix\" }\n  { \"type\": \"api_key\", \"key\": \"$!literal-bang-prefix\" }\n  ```\n- **字面值：** 直接使用。普通大写字符串（例如 `MY_API_KEY`）是文字；使用 `$MY_API_KEY` 作为环境变量。\n  ```json\n  { \"type\": \"api_key\", \"key\": \"sk-ant-...\" }\n  { \"type\": \"api_key\", \"key\": \"public\" }\n  ```\n\nOAuth 凭证也存储在`/login`之后并自动管理。\n\n## 云Providers\n\n### Azure 开放人工智能\n\n```bash\nexport AZURE_OPENAI_API_KEY=...\nexport AZURE_OPENAI_BASE_URL=https://your-resource.ai.azure.com\n# also supported: https://your-resource.cognitiveservices.azure.com\n# also supported: https://your-resource.openai.azure.com\n# root endpoints are auto-normalized to /openai/v1\n# or use resource name instead of base URL\nexport AZURE_OPENAI_RESOURCE_NAME=your-resource\n\n# Optional\nexport AZURE_OPENAI_API_VERSION=2024-02-01\nexport AZURE_OPENAI_DEPLOYMENT_NAME_MAP=gpt-4=my-gpt4,gpt-4o=my-gpt4o\n```\n\n### 亚马逊基岩\n\n使用 `/login amazon-bedrock` 存储 Bedrock API key，或配置以下环境 AWS 凭证源之一：\n\n```bash\n# Option 1: AWS Profile\nexport AWS_PROFILE=your-profile\n\n# Option 2: IAM Keys\nexport AWS_ACCESS_KEY_ID=AKIA...\nexport AWS_SECRET_ACCESS_KEY=...\n\n# Option 3: Bearer Token\nexport AWS_BEARER_TOKEN_BEDROCK=...\n\n# Optional region (defaults to us-east-1)\nexport AWS_REGION=us-west-2\n```\n\n还支持 ECS 任务角色 (`AWS_CONTAINER_CREDENTIALS_*`) 和 IRSA (`AWS_WEB_IDENTITY_TOKEN_FILE`)。\n\n```bash\npi --provider amazon-bedrock --model us.anthropic.claude-sonnet-4-20250514-v1:0\n```\n\n对于 ID 包含可识别模型名称（基础模型和系统定义的推理配置文件）的 Claude 模型，会自动启用提示缓存。对于应用程序推理配置文件（其 ARN 不包含模型名称），设置 `AWS_BEDROCK_FORCE_CACHE=1` 以启用缓存点：\n\n```bash\nexport AWS_BEDROCK_FORCE_CACHE=1\npi --provider amazon-bedrock --model arn:aws:bedrock:us-east-1:123456789012:application-inference-profile/abc123\n```\n\n如果您要连接到 Bedrock API 代理，则可以使用以下环境变量：\n\n```bash\n# Set the URL for the Bedrock proxy (standard AWS SDK env var)\nexport AWS_ENDPOINT_URL_BEDROCK_RUNTIME=https://my.corp.proxy/bedrock\n\n# Set if your proxy does not require authentication\nexport AWS_BEDROCK_SKIP_AUTH=1\n\n# Set if your proxy only supports HTTP/1.1\nexport AWS_BEDROCK_FORCE_HTTP1=1\n```\n\n### Cloudflare AI 网关\n\n`CLOUDFLARE_API_KEY`可以通过`/login`设置。帐户 ID 和网关 slug 可以设置为环境变量，也可以设置在 `auth.json` 中的 API key 凭证的 `env` 对象中。\n\n```bash\nexport CLOUDFLARE_API_KEY=...           # or use /login\nexport CLOUDFLARE_ACCOUNT_ID=...\nexport CLOUDFLARE_GATEWAY_ID=...        # create at dash.cloudflare.com → AI → AI Gateway\npi --provider cloudflare-ai-gateway --model \"claude-sonnet-4-5\"\n```\n\n通过 Cloudflare AI Gateway 路由到 OpenAI、Anthropic 和 Workers AI。 Workers AI 使用统一 API (`/compat`) 和前缀模型 ID (`workers-ai/@cf/...`)。 OpenAI 使用 OpenAI 直通路由 (`/openai`) 和本机 OpenAI 模型 ID，例如`gpt-5.1`。 Anthropic 使用 Anthropic 直通路由 (`/anthropic`) 和本机 Anthropic 模型 ID，例如 `claude-sonnet-4-5`。\n\nAI网关认证使用`CLOUDFLARE_API_KEY`作为`cf-aig-authorization`。上游身份验证可以是以下之一：\n\n| 模式 | 请求授权 | 上游认证 |\n|------|--------------|---------------|\n| 工人人工智能 | 仅 Cloudflare 令牌 | Cloudflare-native |\n| 统一计费 | 仅 Cloudflare 令牌 | Cloudflare 处理上游身份验证并扣除积分 |\n| 存储的 BYOK | 仅 Cloudflare 令牌 | Cloudflare 注入存储在 AI Gateway 仪表板中的提供商密钥 |\n| 内嵌BYOK | Cloudflare 令牌加上上游 `Authorization` 标头 | 该请求提供上游提供商密钥 |\n\n对于正常的 pi 使用，更喜欢统一计费或存储 BYOK。内联 BYOK 需要为 Cloudflare AI Gateway 提供商配置额外的上游 `Authorization` 标头，例如通过 `models.json` 提供商/模型覆盖。\n\n### Cloudflare Workers AI\n\n`CLOUDFLARE_API_KEY`可以通过`/login`设置。 `CLOUDFLARE_ACCOUNT_ID` 可以设置为环境变量，也可以设置在 `auth.json` 中的 API key 凭证的 `env` 对象中。\n\n```bash\nexport CLOUDFLARE_API_KEY=...           # or use /login\nexport CLOUDFLARE_ACCOUNT_ID=...\npi --provider cloudflare-workers-ai --model \"@cf/moonshotai/kimi-k2.6\"\n```\n\nPi 自动设置`x-session-affinity` 以获得[prefix caching](https://developers.cloudflare.com/workers-ai/features/prompt-caching/) 折扣。\n\n### 谷歌顶点人工智能\n\n使用应用程序默认凭据：\n\n```bash\ngcloud auth application-default login\nexport GOOGLE_CLOUD_PROJECT=your-project\nexport GOOGLE_CLOUD_LOCATION=us-central1\n```\n\n或者将 `GOOGLE_APPLICATION_CREDENTIALS` 设置为服务帐户密钥文件。\n\n## llama.cpp\n\nPi支持llama.cpp路由器服务器。使用`/login llama.cpp`进行配置，使用`/llama`管理加载的模型，使用`/model`选择加载的模型。\n\n有关服务器设置、模型目录布局、环境变量和命令用法，请参阅 [llama.cpp](llama-cpp.md)。\n\n## 定制Providers\n\n**通过 models.json：** 添加 Ollama、LM Studio、vLLM 或任何支持 API 的提供商（OpenAI Completions、OpenAI Responses、Anthropic Messages、Google Generative AI）。参见[models.md](models.md)。\n\n**通过扩展：** 对于需要自定义 API 实现或 OAuth 流的提供商，请创建一个扩展。参见[custom-provider.md](custom-provider.md)和[examples/extensions/custom-provider-gitlab-duo](../examples/extensions/custom-provider-gitlab-duo/)。\n\n## 决议顺序\n\n解析提供商的凭据时：\n\n1. CLI `--api-key` 标志\n2. `auth.json`条目（API key或OAuth令牌）\n3. 环境变量\n4. 来自 `models.json` 的自定义提供商密钥","sourceFile":"providers.md"},"quickstart":{"title":"快速开始","markdown":"此页面将带您从安装到有用的第一个 pi 会话。\n\n## 安装\n\nPi 作为 npm 包分发：\n\n```bash\nnpm install -g --ignore-scripts @earendil-works/pi-coding-agent\n```\n\n`--ignore-scripts` 在安装过程中禁用依赖生命周期脚本。 Pi 不需要正常安装npm 安装脚本。\n\n### 卸载\n\n使用安装 pi 的包管理器。 curl 安装程序全局使用 npm，因此使用 npm 删除curl 和 npm 安装：\n\n```bash\n# curl installer or npm install -g\nnpm uninstall -g @earendil-works/pi-coding-agent\n\n# pnpm\npnpm remove -g @earendil-works/pi-coding-agent\n\n# Yarn\nyarn global remove @earendil-works/pi-coding-agent\n\n# Bun\nbun uninstall -g @earendil-works/pi-coding-agent\n```\n\n卸载 pi 会将设置、凭据、会话和已安装的 pi 软件包保留在 `~/.pi/agent/` 中。\n\n然后在您希望它运行的项目目录中启动 pi：\n\n```bash\ncd /path/to/project\npi\n```\n\n## 认证\n\nPi可以通过`/login`使用subscription providers，或通过环境变量或auth文件使用API密钥​​提供程序。\n\n### 选项一：订阅登录\n\n启动 pi 并运行：\n\n```text\n/login\n```\n\n然后选择一个提供商。内置订阅登录包括 Claude Pro/Max、ChatGPT Plus/Pro (Codex) 和 GitHub Copilot。\n\n### 选项2：API key\n\n在启动 pi 之前设置 API key：\n\n```bash\nexport ANTHROPIC_API_KEY=sk-ant-...\npi\n```\n\n您还可以运行 `/login` 并选择 API 密钥提供程序以将密钥存储在 `~/.pi/agent/auth.json` 中。\n\n请参阅 [Providers](providers.md) 了解所有支持的提供商、环境变量和云提供商设置。\n\n## 第一节\n\npi 启动后，输入请求并按 Enter：\n\n```text\nSummarize this repository and tell me how to run its checks.\n```\n\n默认情况下，pi 为模型提供了四种工具：\n\n- `read` - 读取文件\n- `write` - 创建或覆盖文件\n- `edit` - 补丁文件\n- `bash` - 运行 shell 命令\n\n其他内置只读工具（`grep`、`find`、`ls`）可通过工具选项使用。 Pi 在您当前的工作目录中运行并可以修改那里的文件。如果您想要轻松回滚，请使用 git 或其他检查点工作流程。\n\n## 给出 pi 项目说明\n\nPi 在启动时加载 context files。添加一个 `AGENTS.md` 文件来告诉它如何在项目中工作：\n\n```markdown\n# Project Instructions\n\n- Run `npm run check` after code changes.\n- Do not run production migrations locally.\n- Keep responses concise.\n```\n\nPi 负载：\n\n- `~/.pi/agent/AGENTS.md` 用于全局指令\n- 来自父目录和当前目录的`AGENTS.md`或`CLAUDE.md`\n\n如果目录包含 `AGENTS.override.md`，Pi 会从该目录加载它，而不是 `AGENTS.md` 或 `CLAUDE.md`。\n\n更改 context files 后，重新启动 pi，或运行 `/reload`。\n\n## 常见的尝试事项\n\n### 参考文件\n\n在编辑器中输入 `@` 来模糊搜索文件，或在命令行上传递文件：\n\n```bash\npi @README.md \"Summarize this\"\npi @src/app.ts @src/app.test.ts \"Review these together\"\n```\n\n可以使用 Ctrl+V（Windows 上为 Alt+V）粘贴图像或文本；图像也可以拖到支持的终端中。\n\n### 运行外壳命令\n\n在交互模式下：\n\n```text\n!npm run lint\n```\n\n命令输出发送到模型。使用 `!!command` 运行命令而不将其输出添加到模型上下文。\n\n### 切换型号\n\n使用 `/model` 或 Ctrl+L 选择模型。使用Shift+Tab循环思考级别。使用 Ctrl+P / Shift+Ctrl+P 循环浏览范围模型。\n\n### 稍后继续\n\n会话会自动保存：\n\n```bash\npi -c                  # Continue most recent session\npi -r                  # Browse previous sessions\npi --name \"my task\"    # Set session display name at startup\npi --session <path|id> # Open a specific session\n```\n\n在 pi 内部，使用 `/resume`、`/new`、`/tree`、`/fork` 和 `/clone` 来管理会话。\n\n### 非交互模式\n\n对于一次性提示：\n\n```bash\npi -p \"Summarize this codebase\"\ncat README.md | pi -p \"Summarize this text\"\npi -p @screenshot.png \"What's in this image?\"\n```\n\n使用 `--mode json` 进行 JSON 事件输出，或使用 `--mode rpc` 进行流程集成。\n\n## 后续步骤\n\n- [Using Pi](usage.md) - 交互模式、slash commands、会话、context files 和 CLI 参考。\n- [Providers](providers.md) - 身份验证和模型设置。\n- [Settings](settings.md) - 全局和项目配置。\n- [Keybindings](keybindings.md) - 快捷方式和自定义。\n- [Pi Packages](packages.md) - 安装共享扩展、技能、提示和主题。\n\n平台备注：[Windows](windows.md)、[Termux](termux.md)、[tmux](tmux.md)、[Terminal setup](terminal-setup.md)、[Shell aliases](shell-aliases.md)。","sourceFile":"quickstart.md"},"rpc":{"title":"RPC模式","markdown":"RPC 模式可通过stdin/stdout 上的JSON 协议实现编码代理的无头操作。这对于将代理嵌入其他应用程序、IDE 或自定义 UI 中非常有用。\n\n**Node.js/TypeScript 用户注意**：如果您正在构建 Node.js 应用程序，请考虑直接从 `@earendil-works/pi-coding-agent` 使用`AgentSession`，而不是生成子进程。请参阅[`src/core/agent-session.ts`](../src/core/agent-session.ts)了解API。对于基于子进程的TypeScript客户端，请参阅[`src/modes/rpc/rpc-client.ts`](../src/modes/rpc/rpc-client.ts)。\n\n## 启动RPC模式\n\n```bash\npi --mode rpc [options]\n```\n\n常用选项：\n- `--provider <name>`：设置LLM提供商（anthropic、openai、google等）\n- `--model <pattern>`：模型模式或ID（支持`provider/id`和可选的`:<thinking>`）\n- `--name <name>` / `-n <name>`：设置启动时的会话显示名称\n- `--no-session`：禁用会话持久化\n- `--session-dir <path>`：自定义会话存储目录\n\n## 协议概述\n\n- **命令**：JSON对象发送到stdin，每行一个\n- **响应**：JSON 对象，其中 `type: \"response\"` 指示命令成功/失败\n- **事件**：代理事件作为 JSON 行流式传输到 stdout\n\n所有命令都支持用于请求/响应关联的可选 `id` 字段。如果提供，相应的响应将包含相同的`id`。 `bash_execution_update` 事件还包括其原始 `bash` 命令的 `id`。\n\n### 取景\n\nRPC模式使用严格的JSONL语义，以LF（`\\n`）作为唯一的记录分隔符。\n\n这对客户很重要：\n- 仅在 `\\n` 上拆分记录\n- 通过剥离尾随 `\\r` 接受可选的 `\\r\\n` 输入\n- 不要使用将 Unicode 分隔符视为换行符的通用行读取器\n\n特别是，节点 `readline` 不符合 RPC 模式的协议，因为它也会在 `U+2028` 和 `U+2029` 上进行拆分，而这两个JSON 字符串在 JSON 字符串中有效。\n\n## 命令\n\n### 提示\n\n#### 迅速的\n\n向代理发送用户提示。在接受、排队或处理提示后，将发出命令响应。事件在接受后继续异步传输。\n\n```json\n{\"id\": \"req-1\", \"type\": \"prompt\", \"message\": \"Hello, world!\"}\n```\n\n有图像：\n```json\n{\"type\": \"prompt\", \"message\": \"What's in this image?\", \"images\": [{\"type\": \"image\", \"data\": \"base64-encoded-data\", \"mimeType\": \"image/png\"}]}\n```\n\n**流式传输期间**：如果代理已经在流式传输，则必须指定 `streamingBehavior` 来对消息进行排队：\n\n```json\n{\"type\": \"prompt\", \"message\": \"New instruction\", \"streamingBehavior\": \"steer\"}\n```\n\n- `\"steer\"`：代理运行时对消息进行排队。它在当前助理轮次完成执行其工具调用之后、下一个 LLM 调用之前交付。\n- `\"followUp\"`：等待代理完成。仅当代理停止时才会传递消息。\n\n如果代理正在流式传输并且未指定 `streamingBehavior`，则该命令将返回错误。\n\n**扩展命令**：如果消息是扩展命令（例如，`/mycommand`），则即使在流式传输期间也会立即执行。扩展命令通过 `pi.sendMessage()` 管理自己的 LLM 交互。\n\n**输入扩展**：技能命令（`/skill:name`）和prompt templates（`/template`）在发送/排队之前扩展。\n\n回复：\n```json\n{\"id\": \"req-1\", \"type\": \"response\", \"command\": \"prompt\", \"success\": true}\n```\n\n`success: true` 表示提示已被接受、排队或立即处理。 `success: false`表示提示在接受之前被拒绝。接受后的失败通过正常事件和消息流报告，而不是作为同一请求 ID 的第二个 `response`。\n\n`images` 字段是可选的。每个图像使用`ImageContent`格式：`{\"type\": \"image\", \"data\": \"base64-encoded-data\", \"mimeType\": \"image/png\"}`。\n\n#### 驾驶\n\n在代理运行时对转向消息进行排队。它在当前助理轮次完成执行其工具调用之后、下一个 LLM 调用之前交付。扩展了技能命令和prompt templates。不允许扩展命令（使用 `prompt` 代替）。\n\n```json\n{\"type\": \"steer\", \"message\": \"Stop and do this instead\"}\n```\n\n有图像：\n```json\n{\"type\": \"steer\", \"message\": \"Look at this instead\", \"images\": [{\"type\": \"image\", \"data\": \"base64-encoded-data\", \"mimeType\": \"image/png\"}]}\n```\n\n`images` 字段是可选的。每个图像都使用`ImageContent`格式（与`prompt`相同）。\n\n回复：\n```json\n{\"type\": \"response\", \"command\": \"steer\", \"success\": true}\n```\n\n请参阅[set_steering_mode](#set_steering_mode)来控制如何处理转向消息。\n\n#### 跟进\n\n将后续消息放入队列，以便在代理完成后进行处理。仅当代理不再有工具调用或转向消息时传送。扩展了技能命令和prompt templates。不允许扩展命令（使用 `prompt` 代替）。\n\n```json\n{\"type\": \"follow_up\", \"message\": \"After you're done, also do this\"}\n```\n\n有图像：\n```json\n{\"type\": \"follow_up\", \"message\": \"Also check this image\", \"images\": [{\"type\": \"image\", \"data\": \"base64-encoded-data\", \"mimeType\": \"image/png\"}]}\n```\n\n`images` 字段是可选的。每个图像都使用`ImageContent`格式（与`prompt`相同）。\n\n回复：\n```json\n{\"type\": \"response\", \"command\": \"follow_up\", \"success\": true}\n```\n\n请参阅[set_follow_up_mode](#set_follow_up_mode)以控制如何处理后续消息。\n\n#### 中止\n\n中止当前代理操作。\n\n```json\n{\"type\": \"abort\"}\n```\n\n回复：\n```json\n{\"type\": \"response\", \"command\": \"abort\", \"success\": true}\n```\n\n#### 新会话\n\n开始新的会话。可以通过 `session_before_switch` 扩展事件处理程序取消。\n\n```json\n{\"type\": \"new_session\"}\n```\n\n使用可选的父会话跟踪：\n```json\n{\"type\": \"new_session\", \"parentSession\": \"/path/to/parent-session.jsonl\"}\n```\n\n回复：\n```json\n{\"type\": \"response\", \"command\": \"new_session\", \"success\": true, \"data\": {\"cancelled\": false}}\n```\n\n如果延期取消：\n```json\n{\"type\": \"response\", \"command\": \"new_session\", \"success\": true, \"data\": {\"cancelled\": true}}\n```\n\n### 状态\n\n#### 获取状态\n\n获取当前会话状态。\n\n```json\n{\"type\": \"get_state\"}\n```\n\n回复：\n```json\n{\n  \"type\": \"response\",\n  \"command\": \"get_state\",\n  \"success\": true,\n  \"data\": {\n    \"model\": {...},\n    \"thinkingLevel\": \"medium\",\n    \"isStreaming\": false,\n    \"isCompacting\": false,\n    \"steeringMode\": \"all\",\n    \"followUpMode\": \"one-at-a-time\",\n    \"sessionFile\": \"/path/to/session.jsonl\",\n    \"sessionId\": \"abc123\",\n    \"sessionName\": \"my-feature-work\",\n    \"autoCompactionEnabled\": true,\n    \"messageCount\": 5,\n    \"pendingMessageCount\": 0\n  }\n}\n```\n\n`model`字段是完整的[Model](#model)对象或`null`。 `sessionName`字段是通过`set_session_name`设置的显示名称，如果未设置则省略。\n\n#### 获取消息\n\n获取对话中的所有消息。\n\n```json\n{\"type\": \"get_messages\"}\n```\n\n回复：\n```json\n{\n  \"type\": \"response\",\n  \"command\": \"get_messages\",\n  \"success\": true,\n  \"data\": {\"messages\": [...]}\n}\n```\n\n消息是 `AgentMessage` 对象（参见 [Message Types](#message-types)）。\n\n### 模型\n\n#### 设置模型\n\n切换到特定型号。\n\n```json\n{\"type\": \"set_model\", \"provider\": \"anthropic\", \"modelId\": \"claude-sonnet-4-20250514\"}\n```\n\n响应包含完整的 [Model](#model) 对象：\n```json\n{\n  \"type\": \"response\",\n  \"command\": \"set_model\",\n  \"success\": true,\n  \"data\": {...}\n}\n```\n\n#### 循环模型\n\n循环到下一个可用模型。如果只有一种模型可用，则返回 `null` 数据。\n\n```json\n{\"type\": \"cycle_model\"}\n```\n\n回复：\n```json\n{\n  \"type\": \"response\",\n  \"command\": \"cycle_model\",\n  \"success\": true,\n  \"data\": {\n    \"model\": {...},\n    \"thinkingLevel\": \"medium\",\n    \"isScoped\": false\n  }\n}\n```\n\n`model`字段是一个完整的[Model](#model)对象。\n\n#### 获取可用模型\n\n列出所有已配置的型号。\n\n```json\n{\"type\": \"get_available_models\"}\n```\n\n响应包含完整的 [Model](#model) 对象数组：\n```json\n{\n  \"type\": \"response\",\n  \"command\": \"get_available_models\",\n  \"success\": true,\n  \"data\": {\n    \"models\": [...]\n  }\n}\n```\n\n### 思维\n\n#### 设置思维级别\n\n为支持它的模型设置推理/思维水平。\n\n```json\n{\"type\": \"set_thinking_level\", \"level\": \"high\"}\n```\n\n级别：`\"off\"`、`\"minimal\"`、`\"low\"`、`\"medium\"`、`\"high\"`、`\"xhigh\"`、`\"max\"`\n\n仅当所选模型支持时，`\"xhigh\"` 和 `\"max\"` 才会暴露。包括 GPT-5.6 在内的某些模型同时暴露了两者。\n\n回复：\n```json\n{\"type\": \"response\", \"command\": \"set_thinking_level\", \"success\": true}\n```\n\n#### 循环思维水平\n\n循环浏览可用的思维水平。如果模型不支持思考，则返回`null`数据。\n\n```json\n{\"type\": \"cycle_thinking_level\"}\n```\n\n回复：\n```json\n{\n  \"type\": \"response\",\n  \"command\": \"cycle_thinking_level\",\n  \"success\": true,\n  \"data\": {\"level\": \"high\"}\n}\n```\n\n#### 获取可用思考级别\n\n列出当前模型支持的思维层次。对于没有推理支持的模型，返回 `[\"off\"]`。\n\n```json\n{\"type\": \"get_available_thinking_levels\"}\n```\n\n回复：\n```json\n{\n  \"type\": \"response\",\n  \"command\": \"get_available_thinking_levels\",\n  \"success\": true,\n  \"data\": {\n    \"levels\": [\"off\", \"minimal\", \"low\", \"medium\", \"high\"]\n  }\n}\n```\n\n### 队列模式\n\n#### 设置转向模式\n\n控制如何传递转向消息（来自`steer`）。\n\n```json\n{\"type\": \"set_steering_mode\", \"mode\": \"one-at-a-time\"}\n```\n\n模式：\n- `\"all\"`：在当前助手轮完成执行其工具调用后传递所有转向消息\n- `\"one-at-a-time\"`：助手每次完成转弯时传递一条转向消息（默认）\n\n回复：\n```json\n{\"type\": \"response\", \"command\": \"set_steering_mode\", \"success\": true}\n```\n\n#### 设置跟随模式\n\n控制后续消息（来自`follow_up`）的传递方式。\n\n```json\n{\"type\": \"set_follow_up_mode\", \"mode\": \"one-at-a-time\"}\n```\n\n模式：\n- `\"all\"`：代理完成后传递所有后续消息\n- `\"one-at-a-time\"`：每个代理完成后传递一条后续消息（默认）\n\n回复：\n```json\n{\"type\": \"response\", \"command\": \"set_follow_up_mode\", \"success\": true}\n```\n\n### 压缩\n\n#### 袖珍的\n\n手动压缩对话上下文以减少令牌使用。\n\n```json\n{\"type\": \"compact\"}\n```\n\n带有自定义说明：\n```json\n{\"type\": \"compact\", \"customInstructions\": \"Focus on code changes\"}\n```\n\n回复：\n```json\n{\n  \"type\": \"response\",\n  \"command\": \"compact\",\n  \"success\": true,\n  \"data\": {\n    \"summary\": \"Summary of conversation...\",\n    \"firstKeptEntryId\": \"abc123\",\n    \"tokensBefore\": 150000,\n    \"estimatedTokensAfter\": 32000,\n    \"usage\": {\n      \"input\": 32000,\n      \"output\": 1200,\n      \"cacheRead\": 0,\n      \"cacheWrite\": 0,\n      \"totalTokens\": 33200,\n      \"cost\": {\"input\": 0.01, \"output\": 0.02, \"cacheRead\": 0, \"cacheWrite\": 0, \"total\": 0.03}\n    },\n    \"details\": {}\n  }\n}\n```\n\n`estimatedTokensAfter` 是对压缩后立即重建的消息上下文的启发式估计，而不是提供者精确的令牌计数。 `usage` 报告生成摘要的 LLM 调用，并且可能会被自定义压缩处理程序省略。\n\n#### 设置自动压缩\n\n当上下文快满时启用或禁用自动压缩。\n\n```json\n{\"type\": \"set_auto_compaction\", \"enabled\": true}\n```\n\n回复：\n```json\n{\"type\": \"response\", \"command\": \"set_auto_compaction\", \"success\": true}\n```\n\n### 重试\n\n#### 设置自动重试\n\n启用或禁用瞬态错误（过载、速率限制、5xx）时的自动重试。\n\n```json\n{\"type\": \"set_auto_retry\", \"enabled\": true}\n```\n\n回复：\n```json\n{\"type\": \"response\", \"command\": \"set_auto_retry\", \"success\": true}\n```\n\n#### 中止重试\n\n中止正在进行的重试（取消延迟并停止重试）。\n\n```json\n{\"type\": \"abort_retry\"}\n```\n\n回复：\n```json\n{\"type\": \"response\", \"command\": \"abort_retry\", \"success\": true}\n```\n\n### 重击\n\n#### bash\n\n执行 shell 命令并将输出添加到对话上下文。命令运行时将输出流作为 `bash_execution_update` 事件；响应包含最终结果。\n\n```json\n{\"id\": \"req-1\", \"type\": \"bash\", \"command\": \"ls -la\"}\n```\n\n包含一个 `id` 将流式传输的 `bash_execution_update` 事件与此命令相关联。\n\n回复：\n```json\n{\n  \"id\": \"req-1\",\n  \"type\": \"response\",\n  \"command\": \"bash\",\n  \"success\": true,\n  \"data\": {\n    \"output\": \"total 48\\ndrwxr-xr-x ...\",\n    \"exitCode\": 0,\n    \"cancelled\": false,\n    \"truncated\": false\n  }\n}\n```\n\n如果输出被截断，则包括 `fullOutputPath`：\n```json\n{\n  \"type\": \"response\",\n  \"command\": \"bash\",\n  \"success\": true,\n  \"data\": {\n    \"output\": \"truncated output...\",\n    \"exitCode\": 0,\n    \"cancelled\": false,\n    \"truncated\": true,\n    \"fullOutputPath\": \"/tmp/pi-bash-abc123.log\"\n  }\n}\n```\n\n**bash结果如何达到法学硕士：**\n\n`bash` 命令立即执行并返回`BashResult`。在内部，会创建一个 `BashExecutionMessage` 并将其存储在代理的消息状态中。\n\n当发送下一个`prompt`命令时，所有消息（包括`BashExecutionMessage`）在发送到LLM之前都会进行转换。 `BashExecutionMessage` 转换为 `UserMessage`，格式如下：\n\n````\nRan `ls -la`\n```\n总计 48\ndrwxr-xr-x...\n```\n````\n\n这意味着：\n1. Bash 输出包含在**下一个提示**的 LLM 上下文中，而不是立即包含在内\n2. 在提示之前可以执行多个bash命令；所有输出都将包括在内\n\n#### 中止_bash\n\n中止正在运行的 bash 命令。\n\n```json\n{\"type\": \"abort_bash\"}\n```\n\n回复：\n```json\n{\"type\": \"response\", \"command\": \"abort_bash\", \"success\": true}\n```\n\n### 会议\n\n#### 获取会话统计信息\n\n获取令牌使用情况、成本统计信息和当前上下文窗口使用情况。\n\n```json\n{\"type\": \"get_session_stats\"}\n```\n\n回复：\n```json\n{\n  \"type\": \"response\",\n  \"command\": \"get_session_stats\",\n  \"success\": true,\n  \"data\": {\n    \"sessionFile\": \"/path/to/session.jsonl\",\n    \"sessionId\": \"abc123\",\n    \"userMessages\": 5,\n    \"assistantMessages\": 5,\n    \"toolCalls\": 12,\n    \"toolResults\": 12,\n    \"totalMessages\": 22,\n    \"tokens\": {\n      \"input\": 50000,\n      \"output\": 10000,\n      \"cacheRead\": 40000,\n      \"cacheWrite\": 5000,\n      \"total\": 105000\n    },\n    \"cost\": 0.45,\n    \"contextUsage\": {\n      \"tokens\": 60000,\n      \"contextWindow\": 200000,\n      \"percent\": 30\n    }\n  }\n}\n```\n\n`tokens`和`cost`包括辅助消息、工具报告的使用情况以及整个会话中的压缩/分支摘要生成。 `contextUsage` 包含用于压缩和页脚显示的实际当前上下文窗口估计。\n\n当没有模型或上下文窗口可用时，`contextUsage` 被省略。压缩后，`contextUsage.tokens` 和 `contextUsage.percent` 立即变为 `null`，直到新的压缩后助理响应提供有效的使用数据。\n\n#### 导出_html\n\n将会话导出到 HTML 文件。\n\n```json\n{\"type\": \"export_html\"}\n```\n\n使用自定义路径：\n```json\n{\"type\": \"export_html\", \"outputPath\": \"/tmp/session.html\"}\n```\n\n回复：\n```json\n{\n  \"type\": \"response\",\n  \"command\": \"export_html\",\n  \"success\": true,\n  \"data\": {\"path\": \"/tmp/session.html\"}\n}\n```\n\n#### 开关会话\n\n加载不同的会话文件。可以通过 `session_before_switch` 扩展事件处理程序取消。\n\n```json\n{\"type\": \"switch_session\", \"sessionPath\": \"/path/to/session.jsonl\"}\n```\n\n回复：\n```json\n{\"type\": \"response\", \"command\": \"switch_session\", \"success\": true, \"data\": {\"cancelled\": false}}\n```\n\n如果分机取消了切换：\n```json\n{\"type\": \"response\", \"command\": \"switch_session\", \"success\": true, \"data\": {\"cancelled\": true}}\n```\n\n#### 叉\n\n根据活动分支上的先前用户消息创建新分叉。可以通过 `session_before_fork` 扩展事件处理程序取消。返回分叉消息的文本。\n\n```json\n{\"type\": \"fork\", \"entryId\": \"abc123\"}\n```\n\n回复：\n```json\n{\n  \"type\": \"response\",\n  \"command\": \"fork\",\n  \"success\": true,\n  \"data\": {\"text\": \"The original prompt text...\", \"cancelled\": false}\n}\n```\n\n如果扩展取消了分叉：\n```json\n{\n  \"type\": \"response\",\n  \"command\": \"fork\",\n  \"success\": true,\n  \"data\": {\"text\": \"The original prompt text...\", \"cancelled\": true}\n}\n```\n\n#### 克隆\n\n将当前活动分支复制到当前位置的新会话中。可以通过 `session_before_fork` 扩展事件处理程序取消。\n\n```json\n{\"type\": \"clone\"}\n```\n\n回复：\n```json\n{\n  \"type\": \"response\",\n  \"command\": \"clone\",\n  \"success\": true,\n  \"data\": {\"cancelled\": false}\n}\n```\n\n如果扩展取消了克隆：\n```json\n{\n  \"type\": \"response\",\n  \"command\": \"clone\",\n  \"success\": true,\n  \"data\": {\"cancelled\": true}\n}\n```\n\n#### 获取分叉消息\n\n获取可用于分叉的用户消息。\n\n```json\n{\"type\": \"get_fork_messages\"}\n```\n\n回复：\n```json\n{\n  \"type\": \"response\",\n  \"command\": \"get_fork_messages\",\n  \"success\": true,\n  \"data\": {\n    \"messages\": [\n      {\"entryId\": \"abc123\", \"text\": \"First prompt...\"},\n      {\"entryId\": \"def456\", \"text\": \"Second prompt...\"}\n    ]\n  }\n}\n```\n\n#### 获取条目\n\n按附加顺序获取所有会话条目（不包括会话标头）。会话是具有稳定 id 的仅追加条目树，因此条目 id 可以用作持久游标：传递您看到的最后一个条目 id 作为 `since` 来严格仅获取其后的条目，即使在客户端重新启动时也是如此。与`get_messages`不同，这包括预压缩历史和废弃的分支。\n\n```json\n{\"type\": \"get_entries\"}\n```\n\n使用光标：\n```json\n{\"type\": \"get_entries\", \"since\": \"abc123\"}\n```\n\n回复：\n```json\n{\n  \"type\": \"response\",\n  \"command\": \"get_entries\",\n  \"success\": true,\n  \"data\": {\n    \"entries\": [\n      {\"type\": \"message\", \"id\": \"def456\", \"parentId\": \"abc123\", \"timestamp\": \"...\", \"message\": {\"role\": \"user\", \"...\": \"...\"}}\n    ],\n    \"leafId\": \"def456\"\n  }\n}\n```\n\n`leafId` 是当前叶条目的 id（`null` 表示空会话），因此客户端可以在一次往返中判断活动分支是否移动。如果`since`与任何条目id都不匹配，则响应为`success: false`。\n\n#### 获取树\n\n以条目树的形式获取会话。每个节点都是`{entry, children, label?, labelTimestamp?}`。一个格式良好的会话有一个根；孤立条目（断开的父链）也显示为根。\n\n```json\n{\"type\": \"get_tree\"}\n```\n\n回复：\n```json\n{\n  \"type\": \"response\",\n  \"command\": \"get_tree\",\n  \"success\": true,\n  \"data\": {\n    \"tree\": [\n      {\n        \"entry\": {\"type\": \"message\", \"id\": \"abc123\", \"parentId\": null, \"...\": \"...\"},\n        \"children\": [\n          {\"entry\": {\"type\": \"message\", \"id\": \"def456\", \"parentId\": \"abc123\", \"...\": \"...\"}, \"children\": []}\n        ]\n      }\n    ],\n    \"leafId\": \"def456\"\n  }\n}\n```\n\n#### 获取最后一个助手文本\n\n获取最后一条助理消息的文本内容。\n\n```json\n{\"type\": \"get_last_assistant_text\"}\n```\n\n回复：\n```json\n{\n  \"type\": \"response\",\n  \"command\": \"get_last_assistant_text\",\n  \"success\": true,\n  \"data\": {\"text\": \"The assistant's response...\"}\n}\n```\n\n如果不存在辅助消息，则返回`{\"text\": null}`。\n\n#### 设置会话名称\n\n设置当前会话的显示名称。该名称出现在会话列表中并有助于识别会话。\n\n```json\n{\"type\": \"set_session_name\", \"name\": \"my-feature-work\"}\n```\n\n回复：\n```json\n{\n  \"type\": \"response\",\n  \"command\": \"set_session_name\",\n  \"success\": true\n}\n```\n\n当前会话名称可通过 `sessionName` 字段中的 `get_state` 获得。要在启动RPC模式时设置初始名称，请将`--name <name>`或`-n <name>`传递给`pi --mode rpc`进程。\n\n### 命令\n\n#### 获取命令\n\n获取可用命令（扩展命令、prompt templates和技能）。这些可以通过 `prompt` 命令通过前缀 `/` 来调用。\n\n```json\n{\"type\": \"get_commands\"}\n```\n\n回复：\n```json\n{\n  \"type\": \"response\",\n  \"command\": \"get_commands\",\n  \"success\": true,\n  \"data\": {\n    \"commands\": [\n      {\"name\": \"session-name\", \"description\": \"Set or clear session name\", \"source\": \"extension\", \"path\": \"/home/user/.pi/agent/extensions/session.ts\"},\n      {\"name\": \"fix-tests\", \"description\": \"Fix failing tests\", \"source\": \"prompt\", \"location\": \"project\", \"path\": \"/home/user/myproject/.pi/agent/prompts/fix-tests.md\"},\n      {\"name\": \"skill:brave-search\", \"description\": \"Web search via Brave API\", \"source\": \"skill\", \"location\": \"user\", \"path\": \"/home/user/.pi/agent/skills/brave-search/SKILL.md\"}\n    ]\n  }\n}\n```\n\n每个命令都有：\n- `name`：命令名称（使用`/name`调用）\n- `description`：人类可读的描述（扩展命令可选）\n- `source`：什么样的命令：\n  - `\"extension\"`：通过扩展中的`pi.registerCommand()`注册\n  - `\"prompt\"`：从提示模板`.md`文件加载\n  - `\"skill\"`：从技能目录加载（名称以`skill:`为前缀）\n- `location`：从哪里加载（可选，对于扩展不存在）：\n  - `\"user\"`：用户级别（`~/.pi/agent/`）\n  - `\"project\"`：项目级别 (`./.pi/agent/`)\n  - `\"path\"`：通过CLI或设置的显式路径\n- `path`：命令源的绝对文件路径（可选）\n\n**注意**：不包括内置 TUI 命令（`/settings`、`/hotkeys` 等）。它们仅在交互模式下处理，如果通过 `prompt` 发送，则不会执行。\n\n## 活动\n\n在代理操作期间，事件将作为 JSON 行流式传输到 stdout。事件通常不包含 `id` 字段；当提供一个命令时，`bash_execution_update` 包括其原始 `bash` 命令的 `id`。\n\n### 事件类型\n\n| 事件 | 描述 |\n|-------|-------------|\n| `agent_start` | 代理开始处理 |\n| `agent_end` | 一次低级别代理运行完成（可能仍会重试、压缩或排队继续） |\n| `agent_settled` | 代理运行已全部解决；不保留自动重试、压缩重试或排队延续 |\n| `turn_start` | 新的转折开始了 |\n| `turn_end` | 转弯完成（包括辅助消息和工具结果） |\n| `message_start` | 消息开始 |\n| `message_update` | 流式更新（文本/思考/工具调用增量） |\n| `message_end` | 消息完成 |\n| `bash_execution_update` | 直接RPCbash命令输出chunk |\n| `tool_execution_start` | 工具开始执行 |\n| `tool_execution_update` | 工具执行进度（流式输出） |\n| `tool_execution_end` | 工具完成 |\n| `queue_update` | 待处理转向/后续队列已更改 |\n| `compaction_start` | 压实开始 |\n| `compaction_end` | 压实完成 |\n| `auto_retry_start` | 自动重试开始（瞬时错误后） |\n| `auto_retry_end` | 自动重试完成（成功或最终失败） |\n| `summarization_retry_scheduled` | 为瞬时压缩或分支摘要错误安排重试 |\n| `summarization_retry_attempt_start` | 重试汇总请求开始 |\n| `summarization_retry_finished` | 总结重试循环完成 |\n| `extension_error` | 扩展引发错误 |\n\n### 代理启动\n\n当代理开始处理提示时发出。\n\n```json\n{\"type\": \"agent_start\"}\n```\n\n### 代理结束\n\n当一个低级别代理运行完成时发出。包含本次运行期间生成的所有消息。如果`willRetry`为真，则会自动重试。\n\n```json\n{\n  \"type\": \"agent_end\",\n  \"messages\": [...],\n  \"willRetry\": false\n}\n```\n\n### 代理已解决\n\n完整会话级运行稳定后发出。此时Pi将不会通过重试、压缩重试或排队后续消息自动继续。\n\n```json\n{\"type\": \"agent_settled\"}\n```\n\n### 转弯开始/转弯结束\n\n一轮由一个助理响应以及任何由此产生的工具调用和结果组成。\n\n```json\n{\"type\": \"turn_start\"}\n```\n\n```json\n{\n  \"type\": \"turn_end\",\n  \"message\": {...},\n  \"toolResults\": [...]\n}\n```\n\n### 消息开始/消息结束\n\n当消息开始和完成时发出。 `message`字段包含`AgentMessage`。\n\n```json\n{\"type\": \"message_start\", \"message\": {...}}\n{\"type\": \"message_end\", \"message\": {...}}\n```\n\n### message_update（流式传输）\n\n在传输助理消息期间发出。包含增量事件，但没有累积消息快照。\n\n```json\n{\n  \"type\": \"message_update\",\n  \"assistantMessageEvent\": {\n    \"type\": \"text_delta\",\n    \"contentIndex\": 0,\n    \"delta\": \"Hello \"\n  }\n}\n```\n\n`assistantMessageEvent` 字段包含以下增量类型之一：\n\n| 类型 | 描述 |\n|------|-------------|\n| `text_start` | 文本内容块开始 |\n| `text_delta` | 文本内容块 |\n| `text_end` | 文本内容块结束 |\n| `thinking_start` | 思维块开始了 |\n| `thinking_delta` | 思考内容块 |\n| `thinking_end` | 思维障碍结束 |\n| `toolcall_start` | 工具调用开始 |\n| `toolcall_delta` | 工具调用参数块 |\n| `toolcall_end` | 工具调用结束（包括完整的`toolCall`对象） |\n\n流式传输文本响应的示例：\n```json\n{\"type\":\"message_update\",\"assistantMessageEvent\":{\"type\":\"text_start\",\"contentIndex\":0}}\n{\"type\":\"message_update\",\"assistantMessageEvent\":{\"type\":\"text_delta\",\"contentIndex\":0,\"delta\":\"Hello\"}}\n{\"type\":\"message_update\",\"assistantMessageEvent\":{\"type\":\"text_delta\",\"contentIndex\":0,\"delta\":\" world\"}}\n{\"type\":\"message_update\",\"assistantMessageEvent\":{\"type\":\"text_end\",\"contentIndex\":0,\"content\":\"Hello world\"}}\n```\n\n`message_update`故意省略了前面的累计`message`字段并且\n`assistantMessageEvent.partial`。需要实时部分消息的客户端必须组装它\n来自 `message_start` 以及使用 `contentIndex` 的后续事件。对待`message_end.message`\n作为权威。对于工具调用，缓冲区`toolcall_delta.delta`； `toolcall_end.toolCall`\n包含已完成的调用。\n\n### bash_execution_update\n\n从直接 `bash` 命令中为每个输出块发出一次。 `id` 与命令的 `id` 匹配，允许客户端将输出与正确的命令相关联。\n\n命令运行时事件会传输所有输出，即使最终 `bash` 响应的 `output` 被截断。\n\n```json\n{\n  \"type\": \"bash_execution_update\",\n  \"id\": \"req-1\",\n  \"delta\": \"total 48\\n\"\n}\n```\n\n### 工具执行开始/工具执行更新/工具执行结束\n\n当工具启动、传输进度并完成执行时发出。\n\n```json\n{\n  \"type\": \"tool_execution_start\",\n  \"toolCallId\": \"call_abc123\",\n  \"toolName\": \"bash\",\n  \"args\": {\"command\": \"ls -la\"}\n}\n```\n\n在执行期间，`tool_execution_update`事件会传输部分结果（例如，bash到达时输出）：\n\n```json\n{\n  \"type\": \"tool_execution_update\",\n  \"toolCallId\": \"call_abc123\",\n  \"toolName\": \"bash\",\n  \"args\": {\"command\": \"ls -la\"},\n  \"partialResult\": {\n    \"content\": [{\"type\": \"text\", \"text\": \"partial output so far...\"}],\n    \"details\": {\"truncation\": null, \"fullOutputPath\": null}\n  }\n}\n```\n\n完成后：\n\n```json\n{\n  \"type\": \"tool_execution_end\",\n  \"toolCallId\": \"call_abc123\",\n  \"toolName\": \"bash\",\n  \"result\": {\n    \"content\": [{\"type\": \"text\", \"text\": \"total 48\\n...\"}],\n    \"details\": {...}\n  },\n  \"isError\": false\n}\n```\n\n使用 `toolCallId` 关联事件。 `tool_execution_update`中的`partialResult`包含迄今为止累积的输出（不仅仅是增量），允许客户端在每次更​​新时简单地替换其显示。\n\n### 队列更新\n\n每当待处理的转向或后续队列发生更改时发出。\n\n```json\n{\n  \"type\": \"queue_update\",\n  \"steering\": [\"Focus on error handling\"],\n  \"followUp\": [\"After that, summarize the result\"]\n}\n```\n\n### 压缩开始/压缩结束\n\n压缩运行时发出，无论是手动还是自动。\n\n```json\n{\"type\": \"compaction_start\", \"reason\": \"threshold\"}\n```\n\n`reason`字段为`\"manual\"`、`\"threshold\"`或`\"overflow\"`。\n\n```json\n{\n  \"type\": \"compaction_end\",\n  \"reason\": \"threshold\",\n  \"result\": {\n    \"summary\": \"Summary of conversation...\",\n    \"firstKeptEntryId\": \"abc123\",\n    \"tokensBefore\": 150000,\n    \"estimatedTokensAfter\": 32000,\n    \"usage\": {\n      \"input\": 32000,\n      \"output\": 1200,\n      \"cacheRead\": 0,\n      \"cacheWrite\": 0,\n      \"totalTokens\": 33200,\n      \"cost\": {\"input\": 0.01, \"output\": 0.02, \"cacheRead\": 0, \"cacheWrite\": 0, \"total\": 0.03}\n    },\n    \"details\": {}\n  },\n  \"aborted\": false,\n  \"willRetry\": false\n}\n```\n\n如果 `reason` 为 `\"overflow\"` 并且压缩成功，则 `willRetry` 为 `true`，代理将自动重试提示。\n\n如果压缩被中止，`result`是`null`，`aborted`是`true`。\n\n如果压缩失败（例如超过API配额），则`result`为`null`，`aborted`为`false`，`errorMessage`包含错误描述。\n\n### 自动重试开始/自动重试结束\n\n当发生瞬时错误（过载、速率限制、5xx）后触发自动重试时发出。\n\n```json\n{\n  \"type\": \"auto_retry_start\",\n  \"attempt\": 1,\n  \"maxAttempts\": 3,\n  \"delayMs\": 2000,\n  \"errorMessage\": \"529 {\\\"type\\\":\\\"error\\\",\\\"error\\\":{\\\"type\\\":\\\"overloaded_error\\\",\\\"message\\\":\\\"Overloaded\\\"}}\"\n}\n```\n\n```json\n{\n  \"type\": \"auto_retry_end\",\n  \"success\": true,\n  \"attempt\": 2\n}\n```\n\n最终失败时（超出最大重试次数）：\n```json\n{\n  \"type\": \"auto_retry_end\",\n  \"success\": false,\n  \"attempt\": 3,\n  \"finalError\": \"529 overloaded_error: Overloaded\"\n}\n```\n\n### summarization_retry_scheduled / summarization_retry_attempt_start / summarization_retry_finished\n\n当压缩或分支摘要汇总在暂时提供程序错误后重试时发出。这些事件使用与自动助理轮次重试相同的重试设置。\n\n```json\n{\n  \"type\": \"summarization_retry_scheduled\",\n  \"attempt\": 1,\n  \"maxAttempts\": 3,\n  \"delayMs\": 2000,\n  \"errorMessage\": \"terminated\"\n}\n```\n\n```json\n{\n  \"type\": \"summarization_retry_attempt_start\",\n  \"source\": \"compaction\",\n  \"reason\": \"threshold\"\n}\n```\n\n对于分支摘要，`source` 是 `\"branchSummary\"`，并且不存在 `reason`。\n\n```json\n{\n  \"type\": \"summarization_retry_finished\"\n}\n```\n\n### 扩展错误\n\n当扩展抛出错误时发出。\n\n```json\n{\n  \"type\": \"extension_error\",\n  \"extensionPath\": \"/path/to/extension.ts\",\n  \"event\": \"tool_call\",\n  \"error\": \"Error message...\"\n}\n```\n\n## 扩展 UI 协议\n\nExtensions可以通过`ctx.ui.select()`、`ctx.ui.confirm()`等请求用户交互。在RPC模式下，这些被转换为基本命令/事件流之上的请求/响应子协议。\n\n扩展 UI 方法有两类：\n\n- **对话框方法** (`select`、`confirm`、`input`、`editor`)：在stdout上发出`extension_ui_request`并阻塞，直到客户端在stdin上发回与匹配的`id`相匹配的`extension_ui_response`。\n- **即发即忘方法**（`notify`、`setStatus`、`setWidget`、`setTitle`、`set_editor_text`）：在stdout上发出`extension_ui_request`，但不期望得到响应。客户端可以显示该信息或忽略它。\n\n如果对话方法包含 `timeout` 字段，则代理端将在超时到期时使用默认值自动解析。客户端不需要跟踪超时。\n\n某些 `ExtensionUIContext` 方法在 RPC 模式下不受支持或降级，因为它们需要直接 TUI 访问：\n- `custom()` 返回 `undefined`\n- `setWorkingMessage()`、`setWorkingIndicator()`、`setFooter()`、`setHeader()`、`setEditorComponent()`、`setToolsExpanded()` 为空操作\n- `getEditorText()` 返回 `\"\"`\n- `getToolsExpanded()` 返回 `false`\n- `pasteToEditor()` 委托给`setEditorText()`（无粘贴/折叠处理）\n- `getAllThemes()` 返回 `[]`\n- `getTheme()` 返回 `undefined`\n- `setTheme()` 返回 `{ success: false, error: \"...\" }`\n\n注意：在 RPC 模式下，`ctx.mode` 为 `\"rpc\"`，`ctx.hasUI` 为 `true`，因为对话框和即发即弃方法通过扩展 UI 子协议发挥作用。使用`ctx.mode === \"tui\"`来保护TUI特定功能，例如需要真实终端的`custom()`。\n\n### 扩展 UI 请求 (stdout)\n\n所有请求都有 `type: \"extension_ui_request\"`、唯一的 `id` 和 `method` 字段。\n\n#### 选择\n\n提示用户从列表中进行选择。带有 `timeout` 字段的对话框方法包括以毫秒为单位的超时；如果客户端没有及时响应，代理会自动解决`undefined`。\n\n```json\n{\n  \"type\": \"extension_ui_request\",\n  \"id\": \"uuid-1\",\n  \"method\": \"select\",\n  \"title\": \"Allow dangerous command?\",\n  \"options\": [\"Allow\", \"Block\"],\n  \"timeout\": 10000\n}\n```\n\n预期响应：`extension_ui_response` 与 `value`（所选选项字符串）或 `cancelled: true`。\n\n#### 确认\n\n提示用户进行是/否确认。\n\n```json\n{\n  \"type\": \"extension_ui_request\",\n  \"id\": \"uuid-2\",\n  \"method\": \"confirm\",\n  \"title\": \"Clear session?\",\n  \"message\": \"All messages will be lost.\",\n  \"timeout\": 5000\n}\n```\n\n预期响应：`extension_ui_response` 与 `confirmed: true/false` 或 `cancelled: true`。\n\n#### 输入\n\n提示用户输入自由格式的文本。\n\n```json\n{\n  \"type\": \"extension_ui_request\",\n  \"id\": \"uuid-3\",\n  \"method\": \"input\",\n  \"title\": \"Enter a value\",\n  \"placeholder\": \"type something...\"\n}\n```\n\n预期响应：`extension_ui_response` 和 `value`（输入的文本）或`cancelled: true`。\n\n#### 编辑\n\n打开带有可选预填充内容的多行文本编辑器。\n\n```json\n{\n  \"type\": \"extension_ui_request\",\n  \"id\": \"uuid-4\",\n  \"method\": \"editor\",\n  \"title\": \"Edit some text\",\n  \"prefill\": \"Line 1\\nLine 2\\nLine 3\"\n}\n```\n\n预期响应：`extension_ui_response` 与 `value`（编辑后的文本）或`cancelled: true`。\n\n#### 通知\n\n显示通知。即发即弃，预计不会有任何回应。\n\n```json\n{\n  \"type\": \"extension_ui_request\",\n  \"id\": \"uuid-5\",\n  \"method\": \"notify\",\n  \"message\": \"Command blocked by user\",\n  \"notifyType\": \"warning\"\n}\n```\n\n`notifyType`字段为`\"info\"`、`\"warning\"`或`\"error\"`。如果省略，则默认为 `\"info\"`。\n\n#### 设置状态\n\n设置或清除页脚/状态栏中的状态条目。一劳永逸。\n\n```json\n{\n  \"type\": \"extension_ui_request\",\n  \"id\": \"uuid-6\",\n  \"method\": \"setStatus\",\n  \"statusKey\": \"my-ext\",\n  \"statusText\": \"Turn 3 running...\"\n}\n```\n\n发送 `statusText: undefined` （或省略）以清除该键的状态条目。\n\n#### 设置小部件\n\n设置或清除编辑器上方或下方显示的小部件（文本行块）。一劳永逸。\n\n```json\n{\n  \"type\": \"extension_ui_request\",\n  \"id\": \"uuid-7\",\n  \"method\": \"setWidget\",\n  \"widgetKey\": \"my-ext\",\n  \"widgetLines\": [\"--- My Widget ---\", \"Line 1\", \"Line 2\"],\n  \"widgetPlacement\": \"aboveEditor\"\n}\n```\n\n发送 `widgetLines: undefined` （或省略）以清除小部件。 `widgetPlacement`字段为`\"aboveEditor\"`（默认）或`\"belowEditor\"`。 RPC模式仅支持字符串数组；组件工厂被忽略。\n\n#### 设置标题\n\n设置终端窗口/选项卡标题。一劳永逸。\n\n```json\n{\n  \"type\": \"extension_ui_request\",\n  \"id\": \"uuid-8\",\n  \"method\": \"setTitle\",\n  \"title\": \"pi - my project\"\n}\n```\n\n#### 设置编辑器文本\n\n在输入编辑器中设置文本。一劳永逸。\n\n```json\n{\n  \"type\": \"extension_ui_request\",\n  \"id\": \"uuid-9\",\n  \"method\": \"set_editor_text\",\n  \"text\": \"prefilled text for the user\"\n}\n```\n\n### 扩展 UI 响应 (stdin)\n\n仅针对对话方法发送响应（`select`、`confirm`、`input`、`editor`）。 `id` 必须符合请求。\n\n#### 值响应（选择、输入、编辑）\n\n```json\n{\"type\": \"extension_ui_response\", \"id\": \"uuid-1\", \"value\": \"Allow\"}\n```\n\n#### 确认回复（确认）\n\n```json\n{\"type\": \"extension_ui_response\", \"id\": \"uuid-2\", \"confirmed\": true}\n```\n\n#### 取消响应（任何对话框）\n\n关闭任何对话框方法。扩展接收`undefined`（用于选择/输入/编辑器）或`false`（用于确认）。\n\n```json\n{\"type\": \"extension_ui_response\", \"id\": \"uuid-3\", \"cancelled\": true}\n```\n\n## 错误处理\n\n失败的命令返回带有 `success: false` 的响应：\n\n```json\n{\n  \"type\": \"response\",\n  \"command\": \"set_model\",\n  \"success\": false,\n  \"error\": \"Model not found: invalid/model\"\n}\n```\n\n解析错误：\n\n```json\n{\n  \"type\": \"response\",\n  \"command\": \"parse\",\n  \"success\": false,\n  \"error\": \"Failed to parse command: Unexpected token...\"\n}\n```\n\n## 类型\n\n源文件：\n- [`packages/ai/src/types.ts`](../../ai/src/types.ts) - `Model`, `UserMessage`, `AssistantMessage`, `ToolResultMessage`\n- [`packages/agent/src/types.ts`](../../agent/src/types.ts) - `AgentMessage`, `AgentEvent`\n- [`src/core/messages.ts`](../src/core/messages.ts) - `BashExecutionMessage`\n- [`src/modes/json-event.ts`](../src/modes/json-event.ts) - `JsonAgentSessionEvent`\n- [`src/modes/rpc/rpc-types.ts`](../src/modes/rpc/rpc-types.ts) - RPC 命令/响应类型，扩展 UI 请求/响应类型\n\n### 模型\n\n```json\n{\n  \"id\": \"claude-sonnet-4-20250514\",\n  \"name\": \"Claude Sonnet 4\",\n  \"api\": \"anthropic-messages\",\n  \"provider\": \"anthropic\",\n  \"baseUrl\": \"https://api.anthropic.com\",\n  \"reasoning\": true,\n  \"input\": [\"text\", \"image\"],\n  \"contextWindow\": 200000,\n  \"maxTokens\": 16384,\n  \"cost\": {\n    \"input\": 3.0,\n    \"output\": 15.0,\n    \"cacheRead\": 0.3,\n    \"cacheWrite\": 3.75\n  }\n}\n```\n\n### 用户留言\n\n```json\n{\n  \"role\": \"user\",\n  \"content\": \"Hello!\",\n  \"timestamp\": 1733234567890,\n  \"attachments\": []\n}\n```\n\n`content`字段可以是字符串或`TextContent`/`ImageContent`块的数组。\n\n### 助理留言\n\n```json\n{\n  \"role\": \"assistant\",\n  \"content\": [\n    {\"type\": \"text\", \"text\": \"Hello! How can I help?\"},\n    {\"type\": \"thinking\", \"thinking\": \"User is greeting me...\"},\n    {\"type\": \"toolCall\", \"id\": \"call_123\", \"name\": \"bash\", \"arguments\": {\"command\": \"ls\"}}\n  ],\n  \"api\": \"anthropic-messages\",\n  \"provider\": \"anthropic\",\n  \"model\": \"claude-sonnet-4-20250514\",\n  \"usage\": {\n    \"input\": 100,\n    \"output\": 50,\n    \"cacheRead\": 0,\n    \"cacheWrite\": 0,\n    \"cost\": {\"input\": 0.0003, \"output\": 0.00075, \"cacheRead\": 0, \"cacheWrite\": 0, \"total\": 0.00105}\n  },\n  \"stopReason\": \"stop\",\n  \"timestamp\": 1733234567890\n}\n```\n\n停止原因：`\"stop\"`、`\"length\"`、`\"toolUse\"`、`\"error\"`、`\"aborted\"`\n\n### 工具结果消息\n\n```json\n{\n  \"role\": \"toolResult\",\n  \"toolCallId\": \"call_123\",\n  \"toolName\": \"bash\",\n  \"content\": [{\"type\": \"text\", \"text\": \"total 48\\ndrwxr-xr-x ...\"}],\n  \"usage\": {\n    \"input\": 100,\n    \"output\": 50,\n    \"cacheRead\": 0,\n    \"cacheWrite\": 0,\n    \"totalTokens\": 150,\n    \"cost\": {\"input\": 0.0003, \"output\": 0.00075, \"cacheRead\": 0, \"cacheWrite\": 0, \"total\": 0.00105}\n  },\n  \"isError\": false,\n  \"timestamp\": 1733234567890\n}\n```\n\n`usage` 是可选的，报告该工具执行的嵌套 LLM 工作。如果存在，它会影响会话令牌和成本总计。\n\n### Bash执行消息\n\n由 `bash` RPC 命令创建（不是由 LLM 工具调用）：\n\n```json\n{\n  \"role\": \"bashExecution\",\n  \"command\": \"ls -la\",\n  \"output\": \"total 48\\ndrwxr-xr-x ...\",\n  \"exitCode\": 0,\n  \"cancelled\": false,\n  \"truncated\": false,\n  \"fullOutputPath\": null,\n  \"timestamp\": 1733234567890\n}\n```\n\n### 依恋\n\n```json\n{\n  \"id\": \"img1\",\n  \"type\": \"image\",\n  \"fileName\": \"photo.jpg\",\n  \"mimeType\": \"image/jpeg\",\n  \"size\": 102400,\n  \"content\": \"base64-encoded-data...\",\n  \"extractedText\": null,\n  \"preview\": null\n}\n```\n\n## 示例：基本客户端 (Python)\n\n```python\nimport subprocess\nimport json\n\nproc = subprocess.Popen(\n    [\"pi\", \"--mode\", \"rpc\", \"--no-session\"],\n    stdin=subprocess.PIPE,\n    stdout=subprocess.PIPE,\n    text=True\n)\n\ndef send(cmd):\n    proc.stdin.write(json.dumps(cmd) + \"\\n\")\n    proc.stdin.flush()\n\ndef read_events():\n    for line in proc.stdout:\n        yield json.loads(line)\n\n# Send prompt\nsend({\"type\": \"prompt\", \"message\": \"Hello!\"})\n\n# Process events\nfor event in read_events():\n    if event.get(\"type\") == \"message_update\":\n        delta = event.get(\"assistantMessageEvent\", {})\n        if delta.get(\"type\") == \"text_delta\":\n            print(delta[\"delta\"], end=\"\", flush=True)\n    \n    if event.get(\"type\") == \"agent_end\":\n        print()\n        break\n```\n\n## 示例：交互式客户端 (Node.js)\n\n有关完整的交互式示例，请参阅 [`test/rpc-example.ts`](../test/rpc-example.ts)，或有关类型化客户端实现的 [`src/modes/rpc/rpc-client.ts`](../src/modes/rpc/rpc-client.ts)。\n\n有关处理扩展 UI 协议的完整示例，请参阅与 [`examples/extensions/rpc-demo.ts`](../examples/extensions/rpc-demo.ts) 扩展配对的 [`examples/rpc-extension-ui.ts`](../examples/rpc-extension-ui.ts)。\n\n```javascript\nconst { spawn } = require(\"child_process\");\nconst { StringDecoder } = require(\"string_decoder\");\n\nconst agent = spawn(\"pi\", [\"--mode\", \"rpc\", \"--no-session\"]);\n\nfunction attachJsonlReader(stream, onLine) {\n    const decoder = new StringDecoder(\"utf8\");\n    let buffer = \"\";\n\n    stream.on(\"data\", (chunk) => {\n        buffer += typeof chunk === \"string\" ? chunk : decoder.write(chunk);\n\n        while (true) {\n            const newlineIndex = buffer.indexOf(\"\\n\");\n            if (newlineIndex === -1) break;\n\n            let line = buffer.slice(0, newlineIndex);\n            buffer = buffer.slice(newlineIndex + 1);\n            if (line.endsWith(\"\\r\")) line = line.slice(0, -1);\n            onLine(line);\n        }\n    });\n\n    stream.on(\"end\", () => {\n        buffer += decoder.end();\n        if (buffer.length > 0) {\n            onLine(buffer.endsWith(\"\\r\") ? buffer.slice(0, -1) : buffer);\n        }\n    });\n}\n\nattachJsonlReader(agent.stdout, (line) => {\n    const event = JSON.parse(line);\n\n    if (event.type === \"message_update\") {\n        const { assistantMessageEvent } = event;\n        if (assistantMessageEvent.type === \"text_delta\") {\n            process.stdout.write(assistantMessageEvent.delta);\n        }\n    }\n});\n\n// Send prompt\nagent.stdin.write(JSON.stringify({ type: \"prompt\", message: \"Hello\" }) + \"\\n\");\n\n// Abort on Ctrl+C\nprocess.on(\"SIGINT\", () => {\n    agent.stdin.write(JSON.stringify({ type: \"abort\" }) + \"\\n\");\n});\n```","sourceFile":"rpc.md"},"sdk":{"title":"SDK","markdown":"> pi 可以帮助你使用SDK。要求它为您的用例构建集成。\n\n\nSDK 提供对 pi 代理功能的编程访问。使用它将 pi 嵌入其他应用程序、构建自定义界面或与自动化工作流程集成。\n\n**用例示例：**\n- 构建自定义 UI（Web、桌面、移动）\n- 将代理功能集成到现有应用程序中\n- 使用代理推理创建自动化管道\n- 构建生成子代理的自定义工具\n- 以编程方式测试代理行为\n\n有关从最小控制到完全控制的工作示例，请参阅[examples/sdk/](../examples/sdk/)。\n\n## 快速入门\n\n```typescript\nimport { createAgentSession, ModelRuntime, SessionManager } from \"@earendil-works/pi-coding-agent\";\n\nconst modelRuntime = await ModelRuntime.create();\nconst { session } = await createAgentSession({\n  sessionManager: SessionManager.inMemory(),\n  modelRuntime,\n});\n\nsession.subscribe((event) => {\n  if (event.type === \"message_update\" && event.assistantMessageEvent.type === \"text_delta\") {\n    process.stdout.write(event.assistantMessageEvent.delta);\n  }\n});\n\nawait session.prompt(\"What files are in the current directory?\");\n```\n\n## 安装\n\n```bash\nnpm install @earendil-works/pi-coding-agent\n```\n\nSDK 包含在主包中。无需单独安装。\n\n## 核心概念\n\n### 创建代理会话()\n\n单个`AgentSession`的主要工厂函数。\n\n`createAgentSession()` 使用`ResourceLoader` 提供扩展、技能、prompt templates、主题和context files。如果您不提供，它将使用 `DefaultResourceLoader` 进行标准发现。\n\n```typescript\nimport { createAgentSession, SessionManager } from \"@earendil-works/pi-coding-agent\";\n\n// Minimal: defaults with DefaultResourceLoader\nconst { session } = await createAgentSession();\n\n// Custom: override specific options\nconst { session } = await createAgentSession({\n  model: myModel,\n  tools: [\"read\", \"bash\"],\n  sessionManager: SessionManager.inMemory(),\n});\n```\n\n### 代理会话\n\n会话管理代理生命周期、消息历史记录、模型状态、压缩和事件流。\n\n```typescript\ninterface AgentSession {\n  // Send a prompt and wait for completion\n  prompt(text: string, options?: PromptOptions): Promise<void>;\n\n  // Queue messages during streaming\n  steer(text: string): Promise<void>;\n  followUp(text: string): Promise<void>;\n\n  // Subscribe to events (returns unsubscribe function)\n  subscribe(listener: (event: AgentSessionEvent) => void): () => void;\n\n  // Session info\n  sessionFile: string | undefined;\n  sessionId: string;\n\n  // Model control\n  setModel(model: Model): Promise<void>;\n  setThinkingLevel(level: ThinkingLevel): void;\n  cycleModel(): Promise<ModelCycleResult | undefined>;\n  cycleThinkingLevel(): ThinkingLevel | undefined;\n\n  // State access\n  agent: Agent;\n  model: Model | undefined;\n  thinkingLevel: ThinkingLevel;\n  messages: AgentMessage[];\n  isStreaming: boolean;\n\n  // In-place tree navigation within the current session file\n  navigateTree(targetId: string, options?: { summarize?: boolean; customInstructions?: string; replaceInstructions?: boolean; label?: string }): Promise<{ editorText?: string; cancelled: boolean }>;\n\n  // Compaction\n  compact(customInstructions?: string): Promise<CompactionResult>;\n  abortCompaction(): void;\n\n  // Abort current operation\n  abort(): Promise<void>;\n\n  // Cleanup\n  dispose(): void;\n}\n```\n\n会话替换 API，例如新会话、恢复、分叉和导入，在 `AgentSessionRuntime` 上实时进行，而不是在 `AgentSession` 上。\n\n### createAgentSessionRuntime() 和 AgentSessionRuntime\n\n当您需要替换活动会话并重建 cwd 绑定的运行时状态时，请使用运行时 API。\n这与内置交互、打印和 RPC 模式使用的层相同。\n\n`createAgentSessionRuntime()` 采用运行时工厂加上初始 cwd/会话目标。工厂关闭进程全局固定输入，为有效 cwd 重新创建 cwd 绑定服务，针对这些服务解析会话选项，并返回完整的运行时结果。\n\n```typescript\nimport {\n  type CreateAgentSessionRuntimeFactory,\n  createAgentSessionFromServices,\n  createAgentSessionRuntime,\n  createAgentSessionServices,\n  getAgentDir,\n  SessionManager,\n} from \"@earendil-works/pi-coding-agent\";\n\nconst createRuntime: CreateAgentSessionRuntimeFactory = async ({ cwd, sessionManager, sessionStartEvent }) => {\n  const services = await createAgentSessionServices({ cwd });\n  return {\n    ...(await createAgentSessionFromServices({\n      services,\n      sessionManager,\n      sessionStartEvent,\n    })),\n    services,\n    diagnostics: services.diagnostics,\n  };\n};\n\nconst runtime = await createAgentSessionRuntime(createRuntime, {\n  cwd: process.cwd(),\n  agentDir: getAgentDir(),\n  sessionManager: SessionManager.create(process.cwd()),\n});\n```\n\n`AgentSessionRuntime` 拥有跨以下活动运行时的替换：\n\n- `newSession()`\n- `switchSession()`\n- `fork()`\n- 克隆流经`fork(entryId, { position: \"at\" })`\n- `importFromJsonl()`\n\n重要行为：\n\n- 这些操作后`runtime.session`发生变化\n- 事件订阅附加到特定的`AgentSession`，因此替换后重新订阅\n- 如果您使用分机，请再次拨打 `runtime.session.bindExtensions(...)` 进行新会话\n- 创建返回`runtime.diagnostics`的诊断信息\n- 如果运行时创建或替换失败，该方法将抛出异常，调用者决定如何处理它\n\n```typescript\nlet session = runtime.session;\nlet unsubscribe = session.subscribe(() => {});\n\nawait runtime.newSession();\n\nunsubscribe();\nsession = runtime.session;\nunsubscribe = session.subscribe(() => {});\n```\n\n### 提示和消息队列\n\n`PromptOptions` 控制提示扩展、流式传输时的排队行为以及提示预检通知：\n\n```typescript\ninterface PromptOptions {\n  expandPromptTemplates?: boolean;\n  images?: ImageContent[];\n  streamingBehavior?: \"steer\" | \"followUp\";\n  source?: InputSource;\n  preflightResult?: (success: boolean) => void;\n}\n```\n\n每次 `prompt()` 调用都会调用一次 `preflightResult`：\n\n- `true` 当提示被接受、排队或立即处理时\n- `false` 当提示预检在接受之前被拒绝时\n\n它在 `prompt()` 结算之前触发。仅在完全接受的运行完成（包括重试）后，`prompt()` 仍会解析。接受后的失败通过正常事件和消息流报告，而不是通过`preflightResult(false)`。\n\n`prompt()`方法处理prompt templates、扩展命令和消息发送：\n\n```typescript\n// Basic prompt (when not streaming)\nawait session.prompt(\"What files are here?\");\n\n// With images\nawait session.prompt(\"What's in this image?\", {\n  images: [{ type: \"image\", source: { type: \"base64\", mediaType: \"image/png\", data: \"...\" } }]\n});\n\n// During streaming: must specify how to queue the message\nawait session.prompt(\"Stop and do this instead\", { streamingBehavior: \"steer\" });\nawait session.prompt(\"After you're done, also check X\", { streamingBehavior: \"followUp\" });\n```\n\n**行为：**\n- **扩展命令**（例如，`/mycommand`）：立即执行，即使在流式传输期间也是如此。他们通过 `pi.sendMessage()` 管理自己的法学硕士互动。\n- **基于文件的prompt templates**（来自`.md`文件）：在发送或排队之前扩展到其内容。\n- **在没有 `streamingBehavior`** 的情况下进行流式传输：抛出错误。直接使用`steer()`或`followUp()`，或指定选项。\n- **`preflightResult(true)`**：表示提示已被接受、排队或立即处理。\n- **`preflightResult(false)`**：表示在接受之前预检被拒绝。\n\n对于流式传输期间的显式排队：\n\n```typescript\n// Queue a steering message for delivery after the current assistant turn finishes its tool calls\nawait session.steer(\"New instruction\");\n\n// Wait for agent to finish (delivered only when agent stops)\nawait session.followUp(\"After you're done, also do this\");\n```\n\n`steer()`和`followUp()`都扩展了基于文件的prompt templates，但扩展命令出错（扩展命令无法排队）。\n\n### 代理和代理状态\n\n`Agent` 类（来自 `@earendil-works/pi-agent-core`）处理核心 LLM 交互。通过`session.agent`访问它。\n\n```typescript\n// Access current state\nconst state = session.agent.state;\n\n// state.messages: AgentMessage[] - conversation history\n// state.model: Model - current model\n// state.thinkingLevel: ThinkingLevel - current thinking level\n// state.systemPrompt: string - system prompt\n// state.tools: AgentTool[] - available tools\n// state.streamingMessage?: AgentMessage - current partial assistant message\n// state.errorMessage?: string - latest assistant error\n\n// Replace messages (useful for branching or restoration)\nsession.agent.state.messages = messages; // copies the top-level array\n\n// Replace tools\nsession.agent.state.tools = tools; // copies the top-level array\n\n// Wait for agent to finish processing\nawait session.agent.waitForIdle();\n```\n\n### 活动\n\n订阅事件以接收流输出和生命周期通知。\n\n```typescript\nsession.subscribe((event) => {\n  switch (event.type) {\n    // Streaming text from assistant\n    case \"message_update\":\n      if (event.assistantMessageEvent.type === \"text_delta\") {\n        process.stdout.write(event.assistantMessageEvent.delta);\n      }\n      if (event.assistantMessageEvent.type === \"thinking_delta\") {\n        // Thinking output (if thinking enabled)\n      }\n      break;\n    \n    // Tool execution\n    case \"tool_execution_start\":\n      console.log(`Tool: ${event.toolName}`);\n      break;\n    case \"tool_execution_update\":\n      // Streaming tool output\n      break;\n    case \"tool_execution_end\":\n      console.log(`Result: ${event.isError ? \"error\" : \"success\"}`);\n      break;\n    \n    // Message lifecycle\n    case \"message_start\":\n      // New message starting\n      break;\n    case \"message_end\":\n      // Message complete\n      break;\n    \n    // Agent lifecycle\n    case \"agent_start\":\n      // Agent started processing prompt\n      break;\n    case \"agent_end\":\n      // Agent finished (event.messages contains new messages)\n      break;\n    \n    // Turn lifecycle (one LLM response + tool calls)\n    case \"turn_start\":\n      break;\n    case \"turn_end\":\n      // event.message: assistant response\n      // event.toolResults: tool results from this turn\n      break;\n    \n    // Session events (queue, compaction, retry)\n    case \"queue_update\":\n      console.log(event.steering, event.followUp);\n      break;\n    case \"compaction_start\":\n    case \"compaction_end\":\n    case \"auto_retry_start\":\n    case \"auto_retry_end\":\n    case \"summarization_retry_scheduled\":\n    case \"summarization_retry_attempt_start\":\n    case \"summarization_retry_finished\":\n      break;\n  }\n});\n```\n\n## 选项参考\n\n### 目录\n\n```typescript\nconst { session } = await createAgentSession({\n  // Working directory for DefaultResourceLoader discovery\n  cwd: process.cwd(), // default\n  \n  // Global config directory\n  agentDir: \"~/.pi/agent\", // default (expands ~)\n});\n```\n\n`cwd` 由 `DefaultResourceLoader` 用于：\n- 项目扩展 (`.pi/extensions/`)\n- 项目技能：\n  - `.pi/skills/`\n  - `cwd` 和祖先目录中的 `.agents/skills/` （直至 git repo 根目录，或不在存储库中时的文件系统根目录）\n- 项目提示(`.pi/prompts/`)\n- 上下文文件（`AGENTS.md`从cwd向上走）\n- 会话目录命名\n\n`agentDir` 由 `DefaultResourceLoader` 用于：\n- 全局扩展 (`extensions/`)\n- 全球技能：\n  - `skills/` 位于 `agentDir` 之下（例如 `~/.pi/agent/skills/`）\n  - `~/.agents/skills/`\n- 全局提示 (`prompts/`)\n- 全局上下文文件 (`AGENTS.md`)\n- 设置（`settings.json`）\n- 定制型号 (`models.json`)\n- 凭证 (`auth.json`)\n- 会话 (`sessions/`)\n\n当您传递自定义的`ResourceLoader`时，`cwd`和`agentDir`不再控制资源发现。它们仍然影响会话命名和刀具路径解析。\n\n### 模型\n\n```typescript\nimport { getModel } from \"@earendil-works/pi-ai\";\nimport { ModelRuntime } from \"@earendil-works/pi-coding-agent\";\n\nconst modelRuntime = await ModelRuntime.create();\n\n// Find specific built-in model (doesn't check if API key exists)\nconst opus = getModel(\"anthropic\", \"claude-opus-4-5\");\nif (!opus) throw new Error(\"Model not found\");\n\n// Find any model by provider/id, including custom models from models.json\n// (doesn't check if API key exists)\nconst customModel = modelRuntime.getModel(\"my-provider\", \"my-model\");\n\n// Get only models that have valid authentication configured\nconst available = await modelRuntime.getAvailable();\n\nconst { session } = await createAgentSession({\n  model: opus,\n  thinkingLevel: \"medium\", // off, minimal, low, medium, high, xhigh, max\n  \n  // Models for cycling (Ctrl+P in interactive mode)\n  scopedModels: [\n    { model: opus, thinkingLevel: \"high\" },\n    { model: haiku, thinkingLevel: \"off\" },\n  ],\n  \n  modelRuntime,\n});\n```\n\n如果没有提供型号：\n1. 尝试从会话中恢复（如果继续）\n2. 使用设置中的默认值\n3. 回退到第一个可用模型\n\n要匹配 CLI 模型解析，请使用导出的解析器助手：\n\n```typescript\nimport {\n  resolveCliModel,\n  resolveModelScopeWithDiagnostics,\n} from \"@earendil-works/pi-coding-agent\";\n\nconst cliModel = resolveCliModel({\n  cliModel: \"anthropic/claude-opus-4-5:high\",\n  modelRuntime,\n});\nif (cliModel.error) throw new Error(cliModel.error);\nif (cliModel.warning) console.warn(cliModel.warning);\n\nconst { scopedModels, diagnostics } = await resolveModelScopeWithDiagnostics(\n  [\"anthropic/*:high\", \"gpt-5\"],\n  modelRuntime,\n);\nfor (const diagnostic of diagnostics) {\n  console.warn(diagnostic.message);\n}\n```\n\n`resolveCliModel()` 使用所有已注册的模型，因此 `--api-key` 样式首次设置可以在存储的身份验证存在之前解析模型。 `resolveModelScopeWithDiagnostics()` 匹配 `--models` 和 `enabledModels` 语义，同时返回警告而不是打印警告。\n\n> 见[examples/sdk/02-custom-model.ts](../examples/sdk/02-custom-model.ts)\n\n### API 钥匙和 OAuth\n\n认证解析优先级（由`ModelRuntime`处理）：\n1. 运行时覆盖（通过`setRuntimeApiKey`，不持久）\n2. 将凭证存储在`auth.json`（API key或OAuth令牌）中\n3. 环境变量（`ANTHROPIC_API_KEY`、`OPENAI_API_KEY`等）\n4. 后备解析器（适用于来自 `models.json` 的自定义提供程序密钥）\n\n```typescript\nimport { InMemoryCredentialStore } from \"@earendil-works/pi-ai\";\nimport { createAgentSession, ModelRuntime } from \"@earendil-works/pi-coding-agent\";\n\n// Default: uses ~/.pi/agent/auth.json and ~/.pi/agent/models.json\nconst modelRuntime = await ModelRuntime.create();\n\n// Provider-owned auth methods and current status\nfor (const provider of modelRuntime.getProviders()) {\n  const status = await modelRuntime.checkAuth(provider.id);\n  console.log(provider.name, provider.auth, status);\n}\n\n// Runtime API key override (not persisted to disk)\nawait modelRuntime.setRuntimeApiKey(\"anthropic\", \"sk-my-temp-key\");\n\n// Custom credential and model locations\nconst customRuntime = await ModelRuntime.create({\n  authPath: \"/my/app/auth.json\",\n  modelsPath: \"/my/app/models.json\",\n});\n\n// Or inject any pi-ai CredentialStore\nconst credentials = new InMemoryCredentialStore();\nconst inMemoryRuntime = await ModelRuntime.create({ credentials });\n\nconst { session } = await createAgentSession({\n  modelRuntime: customRuntime,\n});\n```\n\n在受影响的提供程序的缓存/内置目录、组合和可用性快照本地一致后，`login()`、`logout()`、`setRuntimeApiKey()` 和 `removeRuntimeApiKey()` 即可解决。他们不会等待远程目录的新鲜度。如果凭证已提交但本地同步失败，它们会拒绝导出的 `CredentialSynchronizationError`；检查其 `providerId`、`operation`、`credential` 和 `cause` 字段，而不是盲目地重试凭证突变。\n\n公共模型/验证操作和 `ModelRuntime.create({ signal })` 接受可选的中止信号，并且在省略时不受限制。 SDK 应用程序自己的远程目录新鲜度截止日期政策：\n\n```typescript\nconst signal = AbortSignal.timeout(15_000);\nconst result = await modelRuntime.refresh({\n  providers: [\"anthropic\"],\n  signal,\n});\nif (result.aborted) console.warn(\"Catalog refresh timed out; using cached models\");\nfor (const [providerId, error] of result.errors) {\n  console.warn(`Could not refresh ${providerId}:`, error);\n}\n```\n\n失败或超时的网络刷新不会撤消成功的凭据操作。 `refresh()` 启动新的提供程序生成，因此它不会等待旧的停滞刷新，并且过时的生成之后无法发布。\n\n> 见[examples/sdk/09-api-keys-and-oauth.ts](../examples/sdk/09-api-keys-and-oauth.ts)\n\n### 系统提示\n\n使用 `ResourceLoader` 覆盖系统提示：\n\n```typescript\nimport { createAgentSession, DefaultResourceLoader } from \"@earendil-works/pi-coding-agent\";\n\nconst loader = new DefaultResourceLoader({\n  systemPromptOverride: () => \"You are a helpful assistant.\",\n});\nawait loader.reload();\n\nconst { session } = await createAgentSession({ resourceLoader: loader });\n```\n\n> 见[examples/sdk/03-custom-prompt.ts](../examples/sdk/03-custom-prompt.ts)\n\n### 工具\n\n指定要启用的内置工具：\n\n- 内置工具名称：`read`、`bash`、`edit`、`write`、`grep`、`find`、`ls`\n- 默认内置：`read`、`bash`、`edit`、`write`\n- `noTools: \"all\"` 禁用所有工具\n- `noTools: \"builtin\"` 禁用默认内置程序，同时保持扩展和自定义工具启用\n- 应用任何 `tools` 允许列表后，`excludeTools` 禁用特定的内置、扩展或自定义工具名称\n\n`edit`工具为Pi的TUI显示返回`details.diff`，并为SDK消费者返回`details.patch`作为标准统一补丁。\n\n```typescript\nimport { createAgentSession } from \"@earendil-works/pi-coding-agent\";\n\n// Read-only mode\nconst { session } = await createAgentSession({\n  tools: [\"read\", \"grep\", \"find\", \"ls\"],\n});\n\n// Pick specific tools\nconst { session } = await createAgentSession({\n  tools: [\"read\", \"bash\", \"grep\"],\n});\n\n// Disable one tool while keeping the rest available\nconst { session } = await createAgentSession({\n  excludeTools: [\"ask_question\"],\n});\n```\n\n#### 带有自定义 cwd 的工具\n\n当您传递自定义 `cwd` 时，`createAgentSession()` 会为该 cwd 构建选定的内置工具。\n\n```typescript\nimport { createAgentSession, SessionManager } from \"@earendil-works/pi-coding-agent\";\n\nconst cwd = \"/path/to/project\";\n\n// Use default tools for custom cwd\nconst { session } = await createAgentSession({\n  cwd,\n  sessionManager: SessionManager.inMemory(cwd),\n});\n\n// Or pick specific tools for custom cwd\nconst { session } = await createAgentSession({\n  cwd,\n  tools: [\"read\", \"bash\", \"grep\"],\n  sessionManager: SessionManager.inMemory(cwd),\n});\n```\n\n> 见[examples/sdk/05-tools.ts](../examples/sdk/05-tools.ts)\n\n### 定制工具\n\n```typescript\nimport { Type } from \"typebox\";\nimport { createAgentSession, defineTool } from \"@earendil-works/pi-coding-agent\";\n\n// Inline custom tool\nconst myTool = defineTool({\n  name: \"my_tool\",\n  label: \"My Tool\",\n  description: \"Does something useful\",\n  parameters: Type.Object({\n    input: Type.String({ description: \"Input value\" }),\n  }),\n  execute: async (_toolCallId, params) => ({\n    content: [{ type: \"text\", text: `Result: ${params.input}` }],\n    details: {},\n  }),\n});\n\n// Pass custom tools directly\nconst { session } = await createAgentSession({\n  customTools: [myTool],\n});\n```\n\n使用 `defineTool()` 进行独立定义和数组，如 `customTools: [myTool]`。内联 `pi.registerTool({... })` 已经正确推断参数类型。\n\n通过 `customTools` 传递的自定义工具与扩展注册的工具相结合。 ResourceLoader加载的Extensions也可以通过`pi.registerTool()`注册工具。\n\n如果您传递 `tools`，请包含您想要启用的每个自定义或扩展工具名称，例如 `tools: [\"read\", \"bash\", \"my_tool\"]`。\n\n> 见[examples/sdk/05-tools.ts](../examples/sdk/05-tools.ts)\n\n### Extensions\n\nExtensions 由`ResourceLoader` 加载。 `DefaultResourceLoader` 从 `~/.pi/agent/extensions/`、`.pi/extensions/` 和 settings.json 扩展源发现扩展。\n\n```typescript\nimport { createAgentSession, DefaultResourceLoader } from \"@earendil-works/pi-coding-agent\";\n\nconst loader = new DefaultResourceLoader({\n  additionalExtensionPaths: [\"/path/to/my-extension.ts\"],\n  extensionFactories: [\n    (pi) => {\n      pi.on(\"agent_start\", () => {\n        console.log(\"[Inline Extension] Agent starting\");\n      });\n    },\n  ],\n});\nawait loader.reload();\n\nconst { session } = await createAgentSession({ resourceLoader: loader });\n```\n\nExtensions可以注册工具、订阅事件、添加命令等。完整的API请参见[extensions.md](extensions.md)。\n\n**命名内联扩展：** 默认情况下，内联工厂在启动 Extensions 列表中显示为 `<inline:1>`、`<inline:2>` 等。要显示描述性名称，请包装工厂：\n\n```typescript\nimport type { InlineExtension } from \"@earendil-works/pi-coding-agent\";\n\nconst myProvider: InlineExtension = {\n  name: \"my-provider\",\n  factory: (pi) => {\n    pi.on(\"agent_start\", () => {\n      console.log(\"[my-provider] Agent starting\");\n    });\n  },\n};\n\nconst loader = new DefaultResourceLoader({\n  extensionFactories: [myProvider],\n});\n```\n\n这显示为 `<inline:my-provider>` 而不是 `<inline:1>`。为了向后兼容，裸工厂函数仍然被接受。\n\n**事件总线：** Extensions可以通过`pi.events`进行通信。如果您需要从外部发出或监听，请将共享的 `eventBus` 传递给 `DefaultResourceLoader`：\n\n```typescript\nimport { createEventBus, DefaultResourceLoader } from \"@earendil-works/pi-coding-agent\";\n\nconst eventBus = createEventBus();\nconst loader = new DefaultResourceLoader({\n  eventBus,\n});\nawait loader.reload();\n\neventBus.on(\"my-extension:status\", (data) => console.log(data));\n```\n\n> 参见 [examples/sdk/06-extensions.ts](../examples/sdk/06-extensions.ts) 和 [docs/extensions.md](extensions.md)\n\n### Skills\n\n```typescript\nimport {\n  createAgentSession,\n  DefaultResourceLoader,\n  type Skill,\n} from \"@earendil-works/pi-coding-agent\";\n\nconst customSkill: Skill = {\n  name: \"my-skill\",\n  description: \"Custom instructions\",\n  filePath: \"/path/to/SKILL.md\",\n  baseDir: \"/path/to\",\n  source: \"custom\",\n};\n\nconst loader = new DefaultResourceLoader({\n  skillsOverride: (current) => ({\n    skills: [...current.skills, customSkill],\n    diagnostics: current.diagnostics,\n  }),\n});\nawait loader.reload();\n\nconst { session } = await createAgentSession({ resourceLoader: loader });\n```\n\n> 见[examples/sdk/04-skills.ts](../examples/sdk/04-skills.ts)\n\n### 上下文文件\n\n```typescript\nimport { createAgentSession, DefaultResourceLoader } from \"@earendil-works/pi-coding-agent\";\n\nconst loader = new DefaultResourceLoader({\n  agentsFilesOverride: (current) => ({\n    agentsFiles: [\n      ...current.agentsFiles,\n      { path: \"/virtual/AGENTS.md\", content: \"# Guidelines\\n\\n- Be concise\" },\n    ],\n  }),\n});\nawait loader.reload();\n\nconst { session } = await createAgentSession({ resourceLoader: loader });\n```\n\n> 见[examples/sdk/07-context-files.ts](../examples/sdk/07-context-files.ts)\n\n### 斜线命令\n\n```typescript\nimport {\n  createAgentSession,\n  DefaultResourceLoader,\n  type PromptTemplate,\n} from \"@earendil-works/pi-coding-agent\";\n\nconst customCommand: PromptTemplate = {\n  name: \"deploy\",\n  description: \"Deploy the application\",\n  source: \"(custom)\",\n  content: \"# Deploy\\n\\n1. Build\\n2. Test\\n3. Deploy\",\n};\n\nconst loader = new DefaultResourceLoader({\n  promptsOverride: (current) => ({\n    prompts: [...current.prompts, customCommand],\n    diagnostics: current.diagnostics,\n  }),\n});\nawait loader.reload();\n\nconst { session } = await createAgentSession({ resourceLoader: loader });\n```\n\n> 见[examples/sdk/08-prompt-templates.ts](../examples/sdk/08-prompt-templates.ts)\n\n### 会话管理\n\n会话使用具有 `id`/`parentId` 链接的树结构，从而实现就地分支。\n\n```typescript\nimport {\n  type CreateAgentSessionRuntimeFactory,\n  createAgentSession,\n  createAgentSessionFromServices,\n  createAgentSessionRuntime,\n  createAgentSessionServices,\n  getAgentDir,\n  SessionManager,\n} from \"@earendil-works/pi-coding-agent\";\n\n// In-memory (no persistence)\nconst { session } = await createAgentSession({\n  sessionManager: SessionManager.inMemory(),\n});\n\n// New persistent session\nconst { session: persisted } = await createAgentSession({\n  sessionManager: SessionManager.create(process.cwd()),\n});\n\n// Continue most recent\nconst { session: continued, modelFallbackMessage } = await createAgentSession({\n  sessionManager: SessionManager.continueRecent(process.cwd()),\n});\nif (modelFallbackMessage) {\n  console.log(\"Note:\", modelFallbackMessage);\n}\n\n// Open specific file\nconst { session: opened } = await createAgentSession({\n  sessionManager: SessionManager.open(\"/path/to/session.jsonl\"),\n});\n\n// List sessions\nconst currentProjectSessions = await SessionManager.list(process.cwd());\nconst allSessions = await SessionManager.listAll(process.cwd());\n\n// Session replacement API for /new, /resume, /fork, /clone, and import flows.\nconst createRuntime: CreateAgentSessionRuntimeFactory = async ({ cwd, sessionManager, sessionStartEvent }) => {\n  const services = await createAgentSessionServices({ cwd });\n  return {\n    ...(await createAgentSessionFromServices({\n      services,\n      sessionManager,\n      sessionStartEvent,\n    })),\n    services,\n    diagnostics: services.diagnostics,\n  };\n};\n\nconst runtime = await createAgentSessionRuntime(createRuntime, {\n  cwd: process.cwd(),\n  agentDir: getAgentDir(),\n  sessionManager: SessionManager.create(process.cwd()),\n});\n\n// Replace the active session with a fresh one\nawait runtime.newSession();\n\n// Replace the active session with another saved session\nawait runtime.switchSession(\"/path/to/session.jsonl\");\n\n// Replace the active session with a fork from a specific user entry\nawait runtime.fork(\"entry-id\");\n\n// Clone the active path through a specific entry\nawait runtime.fork(\"entry-id\", { position: \"at\" });\n```\n\n**SessionManager树API:**\n\n```typescript\nconst sm = SessionManager.open(\"/path/to/session.jsonl\");\n\n// Session listing\nconst currentProjectSessions = await SessionManager.list(process.cwd());\nconst allSessions = await SessionManager.listAll(process.cwd());\n\n// Tree traversal\nconst entries = sm.getEntries();        // All entries (excludes header)\nconst tree = sm.getTree();              // Full tree structure\nconst path = sm.getPath();              // Path from root to current leaf\nconst leaf = sm.getLeafEntry();         // Current leaf entry\nconst entry = sm.getEntry(id);          // Get entry by ID\nconst children = sm.getChildren(id);    // Direct children of entry\n\n// Labels\nconst label = sm.getLabel(id);          // Get label for entry\nsm.appendLabelChange(id, \"checkpoint\"); // Set label\n\n// Branching\nsm.branch(entryId);                     // Move leaf to earlier entry\nsm.branchWithSummary(id, \"Summary...\");  // Branch with context summary\nsm.createBranchedSession(leafId);       // Extract path to new file\n```\n\n> 参见 [examples/sdk/11-sessions.ts](../examples/sdk/11-sessions.ts) 和 [Session Format](session-format.md)\n\n### 设置管理\n\n```typescript\nimport { createAgentSession, SettingsManager, SessionManager } from \"@earendil-works/pi-coding-agent\";\n\n// Default: loads from files (global + project merged)\nconst { session } = await createAgentSession({\n  settingsManager: SettingsManager.create(),\n});\n\n// With overrides\nconst settingsManager = SettingsManager.create();\nsettingsManager.applyOverrides({\n  compaction: { enabled: false },\n  retry: { enabled: true, maxRetries: 5 },\n});\nconst { session } = await createAgentSession({ settingsManager });\n\n// In-memory (no file I/O, for testing)\nconst { session } = await createAgentSession({\n  settingsManager: SettingsManager.inMemory({ compaction: { enabled: false } }),\n  sessionManager: SessionManager.inMemory(),\n});\n\n// Custom directories\nconst { session } = await createAgentSession({\n  settingsManager: SettingsManager.create(\"/custom/cwd\", \"/custom/agent\"),\n});\n```\n\n**静态工厂：**\n- `SettingsManager.create(cwd?, agentDir?)` - 从文件加载\n- `SettingsManager.inMemory(settings?)` - 无文件 I/O\n\n**项目特定设置：**\n\n设置从两个位置加载并合并：\n1. 全球：`~/.pi/agent/settings.json`\n2. 项目：`<cwd>/.pi/settings.json`\n\n项目覆盖全局。嵌套对象合并键。默认情况下，设置者会修改全局设置。\n\n**持久性和错误处理语义：**\n\n- 设置 getter/setter 对于内存状态是同步的。\n- Setters 将持久化写入队列异步写入。\n- 当您需要持久性边界时（例如，在进程退出之前或在测试中断言文件内容之前），请调用`await settingsManager.flush()`。\n- `SettingsManager` 不打印设置 I/O 错误。使用 `settingsManager.drainErrors()` 并在您的应用程序层中报告它们。\n\n> 见[examples/sdk/10-settings.ts](../examples/sdk/10-settings.ts)\n\n## 资源加载器\n\n使用 `DefaultResourceLoader` 发现扩展、技能、提示、主题和 context files。\n\n```typescript\nimport {\n  DefaultResourceLoader,\n  getAgentDir,\n} from \"@earendil-works/pi-coding-agent\";\n\nconst loader = new DefaultResourceLoader({\n  cwd,\n  agentDir: getAgentDir(),\n});\nawait loader.reload();\n\nconst extensions = loader.getExtensions();\nconst skills = loader.getSkills();\nconst prompts = loader.getPrompts();\nconst themes = loader.getThemes();\nconst contextFiles = loader.getAgentsFiles().agentsFiles;\n```\n\n## 返回值\n\n`createAgentSession()` 返回：\n\n```typescript\ninterface CreateAgentSessionResult {\n  // The session\n  session: AgentSession;\n  \n  // Extensions result (for runner setup)\n  extensionsResult: LoadExtensionsResult;\n  \n  // Warning if session model couldn't be restored\n  modelFallbackMessage?: string;\n}\n\ninterface LoadExtensionsResult {\n  extensions: Extension[];\n  errors: Array<{ path: string; error: string }>;\n  runtime: ExtensionRuntime;\n}\n```\n\n## 完整示例\n\n```typescript\nimport { getModel } from \"@earendil-works/pi-ai\";\nimport { Type } from \"typebox\";\nimport {\n  createAgentSession,\n  DefaultResourceLoader,\n  defineTool,\n  ModelRuntime,\n  SessionManager,\n  SettingsManager,\n} from \"@earendil-works/pi-coding-agent\";\n\nconst modelRuntime = await ModelRuntime.create({\n  authPath: \"/custom/agent/auth.json\",\n  modelsPath: \"/custom/agent/models.json\",\n});\nif (process.env.MY_KEY) {\n  await modelRuntime.setRuntimeApiKey(\"anthropic\", process.env.MY_KEY);\n}\n\n// Inline tool\nconst statusTool = defineTool({\n  name: \"status\",\n  label: \"Status\",\n  description: \"Get system status\",\n  parameters: Type.Object({}),\n  execute: async () => ({\n    content: [{ type: \"text\", text: `Uptime: ${process.uptime()}s` }],\n    details: {},\n  }),\n});\n\nconst model = getModel(\"anthropic\", \"claude-opus-4-5\");\nif (!model) throw new Error(\"Model not found\");\n\n// In-memory settings with overrides\nconst settingsManager = SettingsManager.inMemory({\n  compaction: { enabled: false },\n  retry: { enabled: true, maxRetries: 2 },\n});\n\nconst loader = new DefaultResourceLoader({\n  cwd: process.cwd(),\n  agentDir: \"/custom/agent\",\n  settingsManager,\n  systemPromptOverride: () => \"You are a minimal assistant. Be concise.\",\n});\nawait loader.reload();\n\nconst { session } = await createAgentSession({\n  cwd: process.cwd(),\n  agentDir: \"/custom/agent\",\n\n  model,\n  thinkingLevel: \"off\",\n  modelRuntime,\n\n  tools: [\"read\", \"bash\", \"status\"],\n  customTools: [statusTool],\n  resourceLoader: loader,\n\n  sessionManager: SessionManager.inMemory(),\n  settingsManager,\n});\n\nsession.subscribe((event) => {\n  if (event.type === \"message_update\" && event.assistantMessageEvent.type === \"text_delta\") {\n    process.stdout.write(event.assistantMessageEvent.delta);\n  }\n});\n\nawait session.prompt(\"Get status and list files.\");\n```\n\n## 运行模式\n\nSDK 导出运行模式实用程序，用于在 `createAgentSession()` 之上构建自定义接口：\n\n### 交互模式\n\n完整的TUI交互模式，包含编辑器、聊天历史记录和所有内置命令：\n\n```typescript\nimport {\n  type CreateAgentSessionRuntimeFactory,\n  createAgentSessionFromServices,\n  createAgentSessionRuntime,\n  createAgentSessionServices,\n  getAgentDir,\n  InteractiveMode,\n  SessionManager,\n} from \"@earendil-works/pi-coding-agent\";\n\nconst createRuntime: CreateAgentSessionRuntimeFactory = async ({ cwd, sessionManager, sessionStartEvent }) => {\n  const services = await createAgentSessionServices({ cwd });\n  return {\n    ...(await createAgentSessionFromServices({ services, sessionManager, sessionStartEvent })),\n    services,\n    diagnostics: services.diagnostics,\n  };\n};\nconst runtime = await createAgentSessionRuntime(createRuntime, {\n  cwd: process.cwd(),\n  agentDir: getAgentDir(),\n  sessionManager: SessionManager.create(process.cwd()),\n});\n\nconst mode = new InteractiveMode(runtime, {\n  migratedProviders: [],\n  modelFallbackMessage: undefined,\n  initialMessage: \"Hello\",\n  initialImages: [],\n  initialMessages: [],\n});\n\nawait mode.run();\n```\n\n### 运行打印模式\n\n单次模式：发送提示、输出结果、退出：\n\n```typescript\nimport {\n  type CreateAgentSessionRuntimeFactory,\n  createAgentSessionFromServices,\n  createAgentSessionRuntime,\n  createAgentSessionServices,\n  getAgentDir,\n  runPrintMode,\n  SessionManager,\n} from \"@earendil-works/pi-coding-agent\";\n\nconst createRuntime: CreateAgentSessionRuntimeFactory = async ({ cwd, sessionManager, sessionStartEvent }) => {\n  const services = await createAgentSessionServices({ cwd });\n  return {\n    ...(await createAgentSessionFromServices({ services, sessionManager, sessionStartEvent })),\n    services,\n    diagnostics: services.diagnostics,\n  };\n};\nconst runtime = await createAgentSessionRuntime(createRuntime, {\n  cwd: process.cwd(),\n  agentDir: getAgentDir(),\n  sessionManager: SessionManager.create(process.cwd()),\n});\n\nawait runPrintMode(runtime, {\n  mode: \"text\",\n  initialMessage: \"Hello\",\n  initialImages: [],\n  messages: [\"Follow up\"],\n});\n```\n\n### 运行Rpc模式\n\n子流程集成的JSON-RPC模式：\n\n```typescript\nimport {\n  type CreateAgentSessionRuntimeFactory,\n  createAgentSessionFromServices,\n  createAgentSessionRuntime,\n  createAgentSessionServices,\n  getAgentDir,\n  runRpcMode,\n  SessionManager,\n} from \"@earendil-works/pi-coding-agent\";\n\nconst createRuntime: CreateAgentSessionRuntimeFactory = async ({ cwd, sessionManager, sessionStartEvent }) => {\n  const services = await createAgentSessionServices({ cwd });\n  return {\n    ...(await createAgentSessionFromServices({ services, sessionManager, sessionStartEvent })),\n    services,\n    diagnostics: services.diagnostics,\n  };\n};\nconst runtime = await createAgentSessionRuntime(createRuntime, {\n  cwd: process.cwd(),\n  agentDir: getAgentDir(),\n  sessionManager: SessionManager.create(process.cwd()),\n});\n\nawait runRpcMode(runtime);\n```\n\n请参阅[RPC documentation](rpc.md)了解JSON协议。\n\n## RPC 模式选择\n\n对于不使用 SDK 构建的基于子流程的集成，请直接使用 CLI：\n\n```bash\npi --mode rpc --no-session\n```\n\n请参阅[RPC documentation](rpc.md)了解JSON协议。\n\n在以下情况下，首选 SDK：\n- 你想要类型安全\n- 你们处于同一个Node.js流程中\n- 您需要直接访问代理状态\n- 您想要以编程方式自定义工具/扩展\n\n在以下情况下，首选RPC模式：\n- 您正在从另一种语言进行集成\n- 您想要进程隔离\n- 您正在构建一个与语言无关的客户端\n\n## 出口\n\n主要入口点导出：\n\n```typescript\n// Factory\ncreateAgentSession\ncreateAgentSessionRuntime\nAgentSessionRuntime\n\n// Auth and Models\nModelRuntime // implements pi-ai Models and owns credential storage\nModelRegistry // synchronous extension compatibility facade\nCredentialSynchronizationError\nresolveCliModel\nresolveModelScopeWithDiagnostics\n\n// Resource loading\nDefaultResourceLoader\ntype ResourceLoader\ncreateEventBus\n\n// Constants and helpers\nCONFIG_DIR_NAME\ndefineTool\ngetAgentDir\ngetPackageDir\ngetReadmePath\ngetDocsPath\ngetExamplesPath\n\n// Session management\nSessionManager\nSettingsManager\n\n// Tool factories\ncreateCodingTools\ncreateReadOnlyTools\ncreateReadTool, createBashTool, createEditTool, createWriteTool\ncreateGrepTool, createFindTool, createLsTool\n\n// Types\ntype CreateAgentSessionOptions\ntype CreateAgentSessionResult\ntype ExtensionFactory\ntype InlineExtension\ntype ExtensionAPI\ntype ToolDefinition\ntype Skill\ntype PromptTemplate\ntype Tool\n```\n\n对于扩展类型，请参阅 [extensions.md](extensions.md) 了解完整的 API。","sourceFile":"sdk.md"},"security":{"title":"安全","markdown":"Pi是本地编码代理。它以启动它的用户帐户的权限运行，并将该用户可写的文件视为在同一本地信任边界内。\n\n## 项目信托\n\n项目信任控制 pi 是否加载项目本地设置、资源、包和扩展。它不是 sandbox，并且它不限制模型在您开始在目录中工作后可以要求工具执行的操作。\n\n当Pi从当前工作目录中找到以下任何资源时，就认为项目具有需要信任的资源：\n\n- `.pi/settings.json`\n- `.pi/extensions`、`.pi/skills`、`.pi/prompts` 或 `.pi/themes`\n- `.pi/SYSTEM.md` 或 `.pi/APPEND_SYSTEM.md`\n- 当前目录或祖先目录中的项目`.agents/skills`\n\n裸露的 `.pi` 目录不算作需要信任的项目资源。\n\n当交互式会话在项目中启动且资源需要信任并且当前目录或父目录没有保存决策时，pi 遵循全局设置中的 `defaultProjectTrust`。默认值为`\"ask\"`，询问当UI可用时是否信任该项目。保存的决策按规范目录存储在`~/.pi/agent/trust.json`中，当前或父路径上最接近的保存决策在全局默认值之前应用。\n\n信任项目允许 pi 加载需要信任的项目资源，包括：\n\n- `.pi/settings.json`\n- `.pi` 扩展、技能、prompt templates、主题、系统提示文件等资源\n- 缺少通过项目设置配置的项目包\n- 项目本地扩展和项目包管理的扩展\n\n信任下降会跳过受保护的资源。无论项目信任如何，都会加载上下文文件，例如 `AGENTS.override.md`、`AGENTS.md` 和 `CLAUDE.md`，除非禁用上下文加载。在信任解决之前，pi 仅加载 context files、用户/全局扩展和 CLI `-e` 扩展。用户/全局和CLI扩展可以处理`project_trust`事件；返回是/否决策的第一个扩展拥有该决策。\n\n非交互模式（`-p`、`--mode json` 和 `--mode rpc`）不显示信任提示。如果没有适用的已保存信任决策，`defaultProjectTrust: \"ask\"`和`\"never\"`会忽略此类资源，而`\"always\"`则信任它们。使用 `--approve`/`-a` 或 `--no-approve`/`-na` 覆盖一次运行的项目信任。\n\n## 没有内置沙箱\n\nPi 不包括内置sandbox。内置工具可以使用 pi 进程的权限读取文件、写入文件、编辑文件以及运行 shell 命令。 Extensions 是TypeScript 模块，以相同的权限运行。包安装、shell 命令、语言服务器、测试命令和其他开发人员工具的行为与普通本地进程一样。\n\n这是故意的。 Pi旨在操作本地源代码树，调用项目工具链，并与用户现有的开发环境集成。部分进程内 sandbox 很容易被误解为安全边界，同时仍然依赖于主机 shell、文件系统、包管理器、凭据和扩展代码。真正的隔离需要来自操作系统或虚拟化/容器边界。\n\n项目信任只是一个输入加载防护。它可以防止存储库在您批准之前悄悄更改 pi 的设置或扩展。它不会使不受信任的代码、不受信任的提示或不受信任的模型输出变得安全。从存储库文件、注释、文档、context files或构建输出进行提示注入是预期的本地代理风险，并且 pi 无法可靠地阻止。\n\n## 运行不受信任或不受监控的工作\n\n对于不受信任的存储库、您不打算密切监视的生成代码或无人值守的自动化，请在封闭的环境中运行 pi。使用容器、虚拟机、微型虚拟机、远程 sandbox 或策略控制的 sandbox，仅使用任务所需的文件和凭据。\n\n常见模式记录在 [Containerization](containerization.md) 中：\n\n- 在容器内运行整个`pi`进程/sandbox\n- 运行主机 pi，同时将内置工具执行路由到 Gondolin 微型虚拟机\n- 仅挂载代理应访问的工作区路径\n- 避免挂载主机`~/.pi/agent`，除非容器应该访问主机会话、设置和凭据\n- 通过最低要求的 API keys 或使用短期凭证\n- 当任务不需要时限制网络访问\n- 在将结果复制回受信任的系统之前检查差异和输出\n\n如果您以读/写方式绑定挂载主机工作区，则来自容器或虚拟机内部的写入仍然可以修改主机文件。当您需要更强的保护以防止意外写入时，请使用只读挂载或将文件复制到 sandbox 或从 sandbox 复制文件。\n\n## 报告安全问题\n\n要报告安全问题，请关注存储库[Security Policy](https://github.com/earendil-works/pi-mono/blob/main/SECURITY.md)。不要针对安全敏感报告提出公开问题。\n\n预期的本地代理行为、缺乏内置 sandbox、来自不受信任内容的提示注入以及用户安装的扩展或技能的行为通常超出安全边界，除非报告演示了真正的特权边界绕过或显示 pi 如何授予本地用户尚未拥有的访问权限。","sourceFile":"security.md"},"session-format":{"title":"会话文件格式","markdown":"会话存储为 JSONL（JSON 行）文件。每行都是一个带有 `type` 字段的 JSON 对象。会话条目通过 `id`/`parentId` 字段形成树结构，无需创建新文件即可实现就地分支。\n\n## 文件位置\n\n```\n~/.pi/agent/sessions/--<path>--/<timestamp>_<uuid>.jsonl\n```\n\n其中`<path>`是工作目录，`/`替换为`-`。\n\n## 删除会话\n\n可以通过删除 `~/.pi/agent/sessions/` 下的 `.jsonl` 文件来删除会话。\n\nPi还支持从`/resume`交互式删除会话（选择会话并按`Ctrl+D`，然后确认）。如果可用，pi 使用 `trash` CLI 来避免永久删除。\n\n## 会话版本\n\n会话在标头中有一个版本字段：\n\n- **版本 1**：线性条目序列（旧版，加载时自动迁移）\n- **版本 2**：具有 `id`/`parentId` 连接的树结构\n- **版本 3**：将 `hookMessage` 角色重命名为 `custom`（扩展统一）\n\n现有会话在加载时会自动迁移到当前版本 (v3)。\n\n## 源文件\n\nGitHub ([pi-mono](https://github.com/earendil-works/pi-mono)) 来源：\n- [`packages/coding-agent/src/core/session-manager.ts`](https://github.com/earendil-works/pi-mono/blob/main/packages/coding-agent/src/core/session-manager.ts) - 会话条目类型和 SessionManager\n- [`packages/coding-agent/src/core/messages.ts`](https://github.com/earendil-works/pi-mono/blob/main/packages/coding-agent/src/core/messages.ts) - 扩展消息类型（BashExecutionMessage、CustomMessage 等）\n- [`packages/ai/src/types.ts`](https://github.com/earendil-works/pi-mono/blob/main/packages/ai/src/types.ts) - 基本消息类型（UserMessage、AssistantMessage、ToolResultMessage）\n- [`packages/agent/src/types.ts`](https://github.com/earendil-works/pi-mono/blob/main/packages/agent/src/types.ts) - AgentMessage 联合类型\n\n对于项目中的 TypeScript 定义，请检查 `node_modules/@earendil-works/pi-coding-agent/dist/` 和 `node_modules/@earendil-works/pi-ai/dist/`。\n\n## 消息类型\n\n会话条目包含`AgentMessage`个对象。理解这些类型对于解析会话和编写扩展至关重要。\n\n### 内容块\n\n消息包含类型化内容块的数组：\n\n```typescript\ninterface TextContent {\n  type: \"text\";\n  text: string;\n}\n\ninterface ImageContent {\n  type: \"image\";\n  data: string;      // base64 encoded\n  mimeType: string;  // e.g., \"image/jpeg\", \"image/png\"\n}\n\ninterface ThinkingContent {\n  type: \"thinking\";\n  thinking: string;\n}\n\ninterface ToolCall {\n  type: \"toolCall\";\n  id: string;\n  name: string;\n  arguments: Record<string, any>;\n}\n```\n\n### 基本消息类型（来自 pi-ai）\n\n```typescript\ninterface UserMessage {\n  role: \"user\";\n  content: string | (TextContent | ImageContent)[];\n  timestamp: number;  // Unix ms\n}\n\ninterface AssistantMessage {\n  role: \"assistant\";\n  content: (TextContent | ThinkingContent | ToolCall)[];\n  api: string;\n  provider: string;\n  model: string;\n  usage: Usage;\n  stopReason: \"stop\" | \"length\" | \"toolUse\" | \"error\" | \"aborted\";\n  errorMessage?: string;\n  timestamp: number;\n}\n\ninterface ToolResultMessage {\n  role: \"toolResult\";\n  toolCallId: string;\n  toolName: string;\n  content: (TextContent | ImageContent)[];\n  details?: any;      // Tool-specific metadata\n  usage?: Usage;      // Nested LLM work performed by the tool\n  isError: boolean;\n  timestamp: number;\n}\n\ninterface Usage {\n  input: number;\n  output: number;\n  cacheRead: number;\n  cacheWrite: number;\n  totalTokens: number;\n  cost: {\n    input: number;\n    output: number;\n    cacheRead: number;\n    cacheWrite: number;\n    total: number;\n  };\n}\n```\n\n导出的 pi-ai `StopReason` 类型还包括 `\"pending\"`，但该值是为流事件中的部分消息保留的。在 pi 保留助手消息之前，终端 `done`/`error` 消息将其替换为完成原因，因此 `\"pending\"` 永远不应出现在会话 JSONL 中。\n\n### 扩展消息类型（来自 pi-coding-agent）\n\n```typescript\ninterface BashExecutionMessage {\n  role: \"bashExecution\";\n  command: string;\n  output: string;\n  exitCode: number | undefined;\n  cancelled: boolean;\n  truncated: boolean;\n  fullOutputPath?: string;\n  excludeFromContext?: boolean;  // true for !! prefix commands\n  timestamp: number;\n}\n\ninterface CustomMessage {\n  role: \"custom\";\n  customType: string;            // Extension identifier\n  content: string | (TextContent | ImageContent)[];\n  display: boolean;              // Show in TUI\n  details?: any;                 // Extension-specific metadata\n  timestamp: number;\n}\n\ninterface BranchSummaryMessage {\n  role: \"branchSummary\";\n  summary: string;\n  fromId: string;                // Entry we branched from\n  timestamp: number;\n}\n\ninterface CompactionSummaryMessage {\n  role: \"compactionSummary\";\n  summary: string;\n  tokensBefore: number;\n  timestamp: number;\n}\n```\n\n### 代理消息联盟\n\n```typescript\ntype AgentMessage =\n  | UserMessage\n  | AssistantMessage\n  | ToolResultMessage\n  | BashExecutionMessage\n  | CustomMessage\n  | BranchSummaryMessage\n  | CompactionSummaryMessage;\n```\n\n## 入门基地\n\n所有条目（`SessionHeader`除外）都扩展`SessionEntryBase`：\n\n```typescript\ninterface SessionEntryBase {\n  type: string;\n  id: string;           // 8-char hex ID\n  parentId: string | null;  // Parent entry ID (null for first entry)\n  timestamp: string;    // ISO timestamp\n}\n```\n\n## 条目类型\n\n### 会话头\n\n文件的第一行。仅元数据，不是树的一部分（无`id`/`parentId`）。\n\n```json\n{\"type\":\"session\",\"version\":3,\"id\":\"uuid\",\"timestamp\":\"2024-12-03T14:00:00.000Z\",\"cwd\":\"/path/to/project\"}\n```\n\n对于与家长的会话（通过 `/fork`、`/clone` 或 `newSession({ parentSession })` 创建）：\n\n```json\n{\"type\":\"session\",\"version\":3,\"id\":\"uuid\",\"timestamp\":\"2024-12-03T14:00:00.000Z\",\"cwd\":\"/path/to/project\",\"parentSession\":\"/path/to/original/session.jsonl\"}\n```\n\n### 会话消息条目\n\n对话中的一条消息。 `message`字段包含`AgentMessage`。\n\n```json\n{\"type\":\"message\",\"id\":\"a1b2c3d4\",\"parentId\":\"prev1234\",\"timestamp\":\"2024-12-03T14:00:01.000Z\",\"message\":{\"role\":\"user\",\"content\":\"Hello\"}}\n{\"type\":\"message\",\"id\":\"b2c3d4e5\",\"parentId\":\"a1b2c3d4\",\"timestamp\":\"2024-12-03T14:00:02.000Z\",\"message\":{\"role\":\"assistant\",\"content\":[{\"type\":\"text\",\"text\":\"Hi!\"}],\"provider\":\"anthropic\",\"model\":\"claude-sonnet-4-5\",\"usage\":{...},\"stopReason\":\"stop\"}}\n{\"type\":\"message\",\"id\":\"c3d4e5f6\",\"parentId\":\"b2c3d4e5\",\"timestamp\":\"2024-12-03T14:00:03.000Z\",\"message\":{\"role\":\"toolResult\",\"toolCallId\":\"call_123\",\"toolName\":\"bash\",\"content\":[{\"type\":\"text\",\"text\":\"output\"}],\"isError\":false}}\n```\n\n### 模型更改条目\n\n当用户在会话中切换模型时发出。\n\n```json\n{\"type\":\"model_change\",\"id\":\"d4e5f6g7\",\"parentId\":\"c3d4e5f6\",\"timestamp\":\"2024-12-03T14:05:00.000Z\",\"provider\":\"openai\",\"modelId\":\"gpt-4o\"}\n```\n\n### 思维水平改变入口\n\n当用户改变思维/推理水平时发出。\n\n```json\n{\"type\":\"thinking_level_change\",\"id\":\"e5f6g7h8\",\"parentId\":\"d4e5f6g7\",\"timestamp\":\"2024-12-03T14:06:00.000Z\",\"thinkingLevel\":\"high\"}\n```\n\n### 压实入口\n\n压缩上下文时创建。存储早期消息的摘要。\n\n```json\n{\"type\":\"compaction\",\"id\":\"f6g7h8i9\",\"parentId\":\"e5f6g7h8\",\"timestamp\":\"2024-12-03T14:10:00.000Z\",\"summary\":\"User discussed X, Y, Z...\",\"firstKeptEntryId\":\"c3d4e5f6\",\"tokensBefore\":50000}\n```\n\n较新的线束生成的压缩将保留的压缩后上下文直接嵌入到条目上，而不是 `firstKeptEntryId`：\n\n```json\n{\"type\":\"compaction\",\"id\":\"f6g7h8i9\",\"parentId\":\"e5f6g7h8\",\"timestamp\":\"2024-12-03T14:10:00.000Z\",\"summary\":\"User discussed X, Y, Z...\",\"tokensBefore\":50000,\"retainedTail\":[{\"role\":\"user\",\"content\":\"latest request\"},{\"role\":\"assistant\",\"content\":[{\"type\":\"text\",\"text\":\"latest reply\"}],\"provider\":\"anthropic\",\"model\":\"claude-sonnet-4-5\",\"usage\":{...},\"stopReason\":\"stop\"}]}\n```\n\n可选字段：\n- `usage`：生成摘要时的LLM使用情况；包含在会话令牌和成本总计中\n- `retainedTail`：压实后保留的物化`AgentMessage[]`。这是可选的，只是为了向后兼容旧会话。较新的线束生成的压缩包含它，因此我们可以从此检查点重建上下文，而无需在压缩条目之前遍历旧条目。\n- `details`：特定于实现的数据（例如，`{ readFiles: string[], modifiedFiles: string[] }`表示默认值，或用于扩展的自定义数据）\n- `fromHook`：`true`（如果由扩展生成），`false`/`undefined`（如果由 pi 生成）（旧字段名称）\n- `firstKeptEntryId`：为了与旧的条目格式兼容。\n\n### 分支摘要条目\n\n当通过 `/tree` 切换分支时创建，并使用 LLM 生成的左分支到共同祖先的摘要。从废弃的路径捕获上下文。\n\n```json\n{\"type\":\"branch_summary\",\"id\":\"g7h8i9j0\",\"parentId\":\"a1b2c3d4\",\"timestamp\":\"2024-12-03T14:15:00.000Z\",\"fromId\":\"f6g7h8i9\",\"summary\":\"Branch explored approach A...\"}\n```\n\n可选字段：\n- `usage`：生成摘要时的LLM使用情况；包含在会话令牌和成本总计中\n- `details`：默认文件跟踪数据 (`{ readFiles: string[], modifiedFiles: string[] }`)，或扩展的自定义数据\n- `fromHook`：`true`（如果由扩展生成），`false`/`undefined`（如果由 pi 生成）（旧字段名称）\n\n### 自定义条目\n\n扩展状态持久性。不参与法学硕士背景。\n\n```json\n{\"type\":\"custom\",\"id\":\"h8i9j0k1\",\"parentId\":\"g7h8i9j0\",\"timestamp\":\"2024-12-03T14:20:00.000Z\",\"customType\":\"my-extension\",\"data\":{\"count\":42}}\n```\n\n使用 `customType` 来识别重新加载时的扩展条目。交互模式可以通过`pi.registerEntryRenderer(customType, renderer)`渲染自定义条目，但它们仍然不参与LLM上下文。\n\n### 自定义消息条目\n\n确实参与 LLM 上下文的扩展注入消息。\n\n```json\n{\"type\":\"custom_message\",\"id\":\"i9j0k1l2\",\"parentId\":\"h8i9j0k1\",\"timestamp\":\"2024-12-03T14:25:00.000Z\",\"customType\":\"my-extension\",\"content\":\"Injected context...\",\"display\":true}\n```\n\n领域：\n- `content`：字符串或`(TextContent | ImageContent)[]`（与 UserMessage 相同）\n- `display`: `true` = 在 TUI 中以独特的样式显示，`false` = 隐藏\n- `details`：可选的扩展特定元数据（不发送到LLM）\n\n### 标签条目\n\n条目上的用户定义书签/标记。\n\n```json\n{\"type\":\"label\",\"id\":\"j0k1l2m3\",\"parentId\":\"i9j0k1l2\",\"timestamp\":\"2024-12-03T14:30:00.000Z\",\"targetId\":\"a1b2c3d4\",\"label\":\"checkpoint-1\"}\n```\n\n将 `label` 设置为 `undefined` 以清除标签。\n\n### 会话信息条目\n\n会话元数据（例如，用户定义的显示名称）。通过扩展中的 `/name`、`--name` / `-n` 或 `pi.setSessionName()` 设置。\n\n```json\n{\"type\":\"session_info\",\"id\":\"k1l2m3n4\",\"parentId\":\"j0k1l2m3\",\"timestamp\":\"2024-12-03T14:35:00.000Z\",\"name\":\"Refactor auth module\"}\n```\n\n会话名称显示在会话选择器 (`/resume`) 中，而不是设置后的第一条消息。\n\n## 树结构\n\n条目形成树：\n- 第一个条目有 `parentId: null`\n- 每个后续条目通过 `parentId` 指向其父条目\n- 分支从较早的条目创建新的子项\n- “叶子”是树中的当前位置\n\n```\n[user msg] ─── [assistant] ─── [user msg] ─── [assistant] ─┬─ [user msg] ← current leaf\n                                                            │\n                                                            └─ [branch_summary] ─── [user msg] ← alternate branch\n```\n\n## 情境构建\n\n`buildContextEntries()` 从当前叶子走到根，在遵守压缩的同时生成活动条目列表：\n\n1. 收集路径上的所有条目\n2. 如果 `CompactionEntry` 在路径上：\n   - 首先包括压缩条目\n   - 如果存在`retainedTail`，则它充当独立的检查点，并包含压缩后的条目\n   - 否则包含从 `firstKeptEntryId` 到压缩的条目\n   - 然后包含压缩后的条目\n3. 保留选定范围内的非消息条目，以便交互模式可以呈现它们\n\n`buildSessionContext()` 以该条目列表为基础来生成 LLM 的消息列表：\n\n1. 从完整路径中提取当前模型和思维水平设置\n2. 将选定的条目转换为消息：\n   - `message` -> 已存储 `AgentMessage`\n   - `compaction` -> `compactionSummary` 加上 `retainedTail`（如果存在）\n   - `branch_summary` -> `branchSummary`\n   - `custom_message` -> `CustomMessage`\n   - `custom` -> 无上下文消息\n\n这使得新的压缩就像独立的检查点一样。 `retainedTail` 是可选的，以便仅存储 `firstKeptEntryId` 的旧会话继续正确加载。\n\n## 解析示例\n\n```typescript\nimport { readFileSync } from \"fs\";\n\nconst lines = readFileSync(\"session.jsonl\", \"utf8\").trim().split(\"\\n\");\n\nfor (const line of lines) {\n  const entry = JSON.parse(line);\n\n  switch (entry.type) {\n    case \"session\":\n      console.log(`Session v${entry.version ?? 1}: ${entry.id}`);\n      break;\n    case \"message\":\n      console.log(`[${entry.id}] ${entry.message.role}: ${JSON.stringify(entry.message.content)}`);\n      break;\n    case \"compaction\":\n      console.log(`[${entry.id}] Compaction: ${entry.tokensBefore} tokens summarized`);\n      break;\n    case \"branch_summary\":\n      console.log(`[${entry.id}] Branch from ${entry.fromId}`);\n      break;\n    case \"custom\":\n      console.log(`[${entry.id}] Custom (${entry.customType}): ${JSON.stringify(entry.data)}`);\n      break;\n    case \"custom_message\":\n      console.log(`[${entry.id}] Extension message (${entry.customType}): ${entry.content}`);\n      break;\n    case \"label\":\n      console.log(`[${entry.id}] Label \"${entry.label}\" on ${entry.targetId}`);\n      break;\n    case \"model_change\":\n      console.log(`[${entry.id}] Model: ${entry.provider}/${entry.modelId}`);\n      break;\n    case \"thinking_level_change\":\n      console.log(`[${entry.id}] Thinking: ${entry.thinkingLevel}`);\n      break;\n  }\n}\n```\n\n## 会话管理器API\n\n以编程方式处理会话的关键方法。\n\n### 静态创建方法\n- `SessionManager.create(cwd, sessionDir?)` - 新会话\n- `SessionManager.open(path, sessionDir?)` - 打开现有会话文件\n- `SessionManager.continueRecent(cwd, sessionDir?)` - 继续最近的或创建新的\n- `SessionManager.inMemory(cwd?)` - 无文件持久性\n- `SessionManager.forkFrom(sourcePath, targetCwd, sessionDir?)` - 从另一个项目分叉会话\n\n### 静态列表方法\n- `SessionManager.list(cwd, sessionDir?, onProgress?)` - 列出目录的会话\n- `SessionManager.listAll(onProgress?)` - 列出所有项目的所有会话\n\n### 实例方法 - 会话管理\n- `newSession(options?)` - 开始新会话（选项：`{ parentSession?: string }`）\n- `setSessionFile(path)` - 切换到不同的会话文件\n- `createBranchedSession(leafId)` - 将分支提取到新的会话文件\n\n### 实例方法-追加（全部返回条目ID）\n- `appendMessage(message)` - 添加消息\n- `appendThinkingLevelChange(level)` - 记录思维变化\n- `appendModelChange(provider, modelId)` - 记录模型变更\n- `appendCompaction(summary, firstKeptEntryId, tokensBefore, details?, fromHook?)` - 添加压缩\n- `appendCustomEntry(customType, data?)` - 扩展状态（不在上下文中）\n- `appendSessionInfo(name)` - 设置会话显示名称\n- `appendCustomMessageEntry(customType, content, display, details?)` - 扩展消息（在上下文中）\n- `appendLabelChange(targetId, label)` - 设置/清除标签\n\n### 实例方法 - 树导航\n- `getLeafId()` - 当前位置\n- `getLeafEntry()` - 获取当前叶条目\n- `getEntry(id)` - 通过ID获取条目\n- `getBranch(fromId?)` - 从入口走到根部\n- `getTree()` - 获取完整的树结构\n- `getChildren(parentId)` - 获取直系孩子\n- `getLabel(id)` - 获取条目标签\n- `branch(entryId)` - 将叶子移至较早的条目\n- `resetLeaf()` - 将叶子重置为空（在任何条目之前）\n- `branchWithSummary(entryId, summary, details?, fromHook?)` - 带有上下文摘要的分支\n\n### 实例方法 - 上下文和信息\n- `buildContextEntries()` - 获取应用压缩的活动分支条目\n- `buildSessionContext()` - 获取LLM的消息、思考水平和模型\n- `getEntries()` - 所有条目（不包括标题）\n- `getHeader()` - 会话标头元数据\n- `getSessionName()` - 从最新的 session_info 条目获取显示名称\n- `getCwd()` - 工作目录\n- `getSessionDir()` - 会话存储目录\n- `getSessionId()` - 会话 UUID\n- `getSessionFile()` - 会话文件路径（内存中未定义）\n- `isPersisted()` - 会话是否保存到磁盘","sourceFile":"session-format.md"},"sessions":{"title":"会话","markdown":"Pi 将对话保存为会话，以便您可以继续工作、从先前的回合分支并重新访问之前的路径。\n\n## 会话存储\n\n会话自动保存到`~/.pi/agent/sessions/`，按工作目录组织。每个会话都是一个具有树结构的JSONL文件。\n\n```bash\npi -c                  # Continue most recent session\npi -r                  # Browse and select from past sessions\npi --no-session        # Ephemeral mode; do not save\npi --name \"my task\"    # Set session display name at startup\npi --session <path|id> # Use a specific session file or partial session ID\npi --fork <path|id>    # Fork a session file or partial session ID into a new session\n```\n\n在交互模式下使用 `/session` 查看当前会话文件、会话 ID、消息计数、令牌和成本。\n\n对于JSONL文件格式和SessionManagerAPI，请参阅[Session Format](session-format.md)。\n\n## 会话命令\n\n| 命令 | 描述 |\n|---------|-------------|\n| `/resume` | 浏览并选择以前的会话 |\n| `/new` | 开始新会话 |\n| `/name <name>` | 设置当前会话显示名称 |\n| `/session` | 显示会话信息 |\n| `/tree` | 导航当前session tree |\n| `/fork` | 根据先前的用户消息创建新会话 |\n| `/clone` | 将当前活动分支复制到新会话中 |\n| `/compact [prompt]` | 总结旧的背景；见[Compaction](compaction.md) |\n| `/export [file]` | 将会话导出为 HTML |\n| `/share` | 上传为私有 GitHub 要点，并带有可共享的 HTML 链接 |\n\n## 恢复和删除会话\n\n`/resume` 打开当前项目的交互式会话选择器。 `pi -r` 在启动时打开相同的选择器。\n\n在选择器中您可以：\n\n- 通过输入搜索\n- 使用 Ctrl+P 切换路径显示\n- 使用 Ctrl+S 切换排序模式\n- 使用 Ctrl+N 过滤命名会话\n- 使用 Ctrl+R 重命名\n- 使用 Ctrl+D 删除，然后确认\n\n如果可用，pi 使用 `trash` CLI 进行删除，而不是永久删除文件。\n\n## 命名会话\n\n使用 `/name <name>` 设置人类可读的会话名称：\n\n```text\n/name Refactor auth module\n```\n\n使用 `--name` 或 `-n` 设置启动时的名称：\n\n```bash\npi --name \"Refactor auth module\"\npi --name \"CI audit\" -p \"Review this build failure\"\n```\n\n命名会话在 `/resume` 和 `pi -r` 中更容易找到。\n\n## 使用 `/tree` 进行分支\n\n会话存储为树。每个条目都有一个`id`和`parentId`，当前位置是活动叶子。 `/tree` 让您跳转到任何先前的点并从那里继续，而无需创建新文件。\n\n<p align=\"center\"><img src=\"images/tree-view.png\" alt=\"Tree View\" width=\"600\"></p>\n\n形状示例：\n\n```text\n├─ user: \"Hello, can you help...\"\n│  └─ assistant: \"Of course! I can...\"\n│     ├─ user: \"Let's try approach A...\"\n│     │  └─ assistant: \"For approach A...\"\n│     │     └─ user: \"That worked...\"  ← active\n│     └─ user: \"Actually, approach B...\"\n│        └─ assistant: \"For approach B...\"\n```\n\n### 树控件\n\n| 钥匙 | 行动 |\n|-----|--------|\n| ↑/↓ | 导航可见条目 |\n| ←/→ | 向上/向下翻页 |\n| Ctrl+←/Ctrl+→ 或 Alt+←/Alt+→ | 折叠/展开或在分支段之间跳跃 |\n| Shift+L | 设置或清除所选条目上的标签 |\n| Shift+T | 切换标签时间戳 |\n| 进入 | 选择条目 |\n| 退出/Ctrl+C | 取消 |\n| Ctrl+O | 循环过滤模式 |\n\n过滤器模式有：默认、无工具、仅用户、仅标记和全部。在 [Settings](settings.md) 中配置默认​​值 `treeFilterMode`。\n\n### 选择行为\n\n选择用户或自定义消息：\n\n1. 将叶移动到所选消息的父级。\n2. 将选定的消息文本放置在编辑器中。\n3. 允许您编辑并重新提交，创建新分支。\n\n选择助手、工具、压缩或其他非用户条目：\n\n1. 将叶子移动到该条目。\n2. 将编辑器留空。\n3. 让您从那一点继续。\n\n选择根用户消息会将叶重置为空对话，并将原始提示放置在编辑器中。\n\n## `/tree`、`/fork`、`/clone`\n\n| 特征 | `/tree` | `/fork` | `/clone` |\n|---------|---------|---------|----------|\n| 输出 | 相同的会话文件 | 新会话文件 | 新会话文件 |\n| 看法 | 满树 | 用户消息选择器 | 当前活动分支 |\n| 典型用途 | 探索现有的替代方案 | 从先前的提示开始新会话 | 在继续之前复制当前工作 |\n| 概括 | 可选分支摘要 | 没有任何 | 没有任何 |\n\n当您想将替代方案保留在一起时，请使用 `/tree`。当您需要单独的会话文件时，请使用 `/fork` 或 `/clone`。\n\n## 分支摘要\n\n当 `/tree` 从一个分支切换到另一个分支时，pi 可以总结废弃的分支并将该摘要附加到新位置。这可以保留您离开的路径中的重要上下文，而无需重播整个分支。\n\n出现提示时，选择以下选项之一：\n\n1. 没有总结\n2. 使用默认提示进行总结\n3. 使用自定义焦点说明进行总结\n\n请参阅 [Compaction](compaction.md) 了解 branch summarization 内部构件和延长钩。\n\n## 会话格式\n\n会话文件为JSONL，包含消息条目、模型更改、思维级别更改、标签、压缩、分支摘要和扩展条目。\n\n有关解析器、扩展、SDK用法和完整的 SessionManager API，请参阅[Session Format](session-format.md)。","sourceFile":"sessions.md"},"settings":{"title":"设置","markdown":"Pi 使用 JSON 设置文件，其中项目设置覆盖全局设置。\n\n| 地点 | 范围 |\n|----------|-------|\n| `~/.pi/agent/settings.json` | 全球（所有项目） |\n| `.pi/settings.json` | 项目（当前目录） |\n\n直接编辑或使用`/settings`作为常用选项。\n\n## 项目信托\n\n在交互式启动时，pi 在信任包含项目本地设置、资源或项目 `.agents/skills` 的项目文件夹之前会询问，并且在 `~/.pi/agent/trust.json` 中没有保存该文件夹或父文件夹的决定。信任项目允许 pi 加载 `.pi/settings.json` 和 `.pi` 资源、安装缺少的项目包以及执行项目扩展。\n\n非交互模式（`-p`、`--mode json` 和 `--mode rpc`）不显示信任提示。如果没有适用的已保存信任决策，他们将使用全局设置中的`defaultProjectTrust`：`ask`（默认）和`never`忽略这些项目资源，而`always`信任它们。通过 `--approve`/`-a` 或 `--no-approve`/`-na` 覆盖一次运行的项目信任。\n\n如果没有适用扩展或保存的决策，则`defaultProjectTrust`控制后备行为。将`~/.pi/agent/settings.json`中的`\"ask\"`、`\"always\"`或`\"never\"`设置为`\"ask\"`、`\"always\"`或`\"never\"`，或将其更改为`/settings`。\n\n`pi config` 和 package 命令使用相同的项目信任流程，但 `pi update` 从不提示。传递 `--approve` 以信任某个命令的项目本地设置，或传递 `--no-approve` 以忽略它们。\n\n在交互模式下使用 `/trust` 可以为将来的会话保存项目信任决策，包括对直接父文件夹的信任。只写`~/.pi/agent/trust.json`；当前会话不会重新加载，因此请重新启动 pi 以使更改生效。\n\n## 所有设置\n\n### 模型与思考\n\n| 环境 | 类型 | 默认 | 描述 |\n|---------|------|---------|-------------|\n| `defaultProvider` | 细绳 | - | 默认提供程序（例如，`\"anthropic\"`、`\"openai\"`） |\n| `defaultModel` | 细绳 | - | 默认型号 ID |\n| `defaultThinkingLevel` | 细绳 | - | `\"off\"`, `\"minimal\"`, `\"low\"`, `\"medium\"`, `\"high\"`, `\"xhigh\"`, `\"max\"` |\n| `hideThinkingBlock` | 布尔值 | `false` | 在输出中隐藏思维块 |\n| `showCacheMissNotices` | 布尔值 | `false` | 显示重大提示缓存未命中的记录通知 |\n| `thinkingBudgets` | 目的 | - | 每个思维级别的自定义代币预算 |\n\n#### 思考预算\n\n```json\n{\n  \"thinkingBudgets\": {\n    \"minimal\": 1024,\n    \"low\": 4096,\n    \"medium\": 10240,\n    \"high\": 32768\n  }\n}\n```\n\n### 用户界面与显示\n\n| 环境 | 类型 | 默认 | 描述 |\n|---------|------|---------|-------------|\n| `theme` | 细绳 | `\"dark\"` | 主题名称（`\"dark\"`、`\"light\"`或自定义） |\n| `externalEditor` | 细绳 | `$VISUAL`，然​​后`$EDITOR`，然后 Windows 上的记事本或 `nano` 其他地方 | Ctrl+G 外部编辑器的命令；优先于环境变量 |\n| `quietStartup` | 布尔值 | `false` | 隐藏启动标头 |\n| `defaultProjectTrust` | 细绳 | `\"ask\"` | 后备项目信任行为：`\"ask\"`、`\"always\"`或`\"never\"`。仅全局设置 |\n| `collapseChangelog` | 布尔值 | `false` | 更新后显示精简的变更日志 |\n| `enableInstallTelemetry` | 布尔值 | `true` | 首次安装或更改日志检测到的更新后发送匿名安装/更新版本 ping。这不控制更新检查 |\n| `enableAnalytics` | 布尔值 | `false` | 选择加入分析数据共享。目前仅在实验性首次设置期间要求 (`PI_EXPERIMENTAL=1`) |\n| `trackingId` | 细绳 | - | 分析跟踪标识符，在 `enableAnalytics` 打开时生成 |\n| `doubleEscapeAction` | 细绳 | `\"tree\"` | 双转义动作：`\"tree\"`、`\"fork\"`或`\"none\"` |\n| `treeFilterMode` | 细绳 | `\"default\"` | `/tree` 的默认过滤器：`\"default\"`、`\"no-tools\"`、`\"user-only\"`、`\"labeled-only\"`、`\"all\"` |\n| `editorPaddingX` | 数字 | `0` | 输入编辑器的水平填充（0-3） |\n| `outputPad` | 数字 | `1` | 用户消息、辅助消息和思考的水平填充（0 或 1） |\n| `autocompleteMaxVisible` | 数字 | `5` | 自动完成下拉列表中的最大可见项目数 (3-20) |\n| `showHardwareCursor` | 布尔值 | `false` | 显示终端光标，同时 TUI 定位它以支持 IME |\n| `tuiMode` | 细绳 | `\"regular\"` | 互动TUI模式：`\"regular\"`或实验性`\"fullscreen\"`。 `/settings` 的更改立即生效； `--tui-mode` 在启动时覆盖此设置 |\n| `fullscreenExitOutput` | 细绳 | `\"transcript\"` | 全屏退出输出：`\"transcript\"`打印最终成绩单和恢复提示，而`\"resume-hint\"`恢复前一屏幕并仅打印恢复提示。在常规TUI模式下没有效果 |\n| `fullscreenScrollbar` | 细绳 | `\"auto\"` | 全屏文字记录滚动条：`\"auto\"`在滚动时暂时显示它，`\"always\"`保留最右边的列并保持其可见，`\"hidden\"`隐藏它。在常规TUI模式下没有效果 |\n\n对于 VS Code，请包含 `--wait`，以便 pi 在编辑器退出后恢复：\n\n```json\n{\n  \"externalEditor\": \"code --wait\"\n}\n```\n\n### 遥测和更新检查\n\n`enableInstallTelemetry` 仅控制对`https://pi.dev/api/report-install` 的匿名安装/更新 ping。选择退出遥测不会禁用更新检查； Pi仍然可以获取`https://pi.dev/api/latest-version`来查找最新版本。\n\n设置`PI_SKIP_VERSION_CHECK=1`禁用Pi版本更新检查。使用 `--offline` 或 `PI_OFFLINE=1` 禁用此处描述的所有启动网络操作，包括更新检查、包更新检查和安装/更新遥测。\n\n### 网络\n\n| 环境 | 类型 | 默认 | 描述 |\n|---------|------|---------|-------------|\n| `httpProxy` | 细绳 | - | HTTP 代理 URL 应用为 `HTTP_PROXY` 和 `HTTPS_PROXY`。仅全局设置。 |\n\n```json\n{\n  \"httpProxy\": \"http://127.0.0.1:7890\"\n}\n```\n\n### 警告\n\n| 环境 | 类型 | 默认 | 描述 |\n|---------|------|---------|-------------|\n| `warnings.anthropicExtraUsage` | 布尔值 | `true` | 当 Anthropic 订阅身份验证可能使用付费额外使用时显示警告 |\n\n```json\n{\n  \"warnings\": {\n    \"anthropicExtraUsage\": false\n  }\n}\n```\n\n### 压缩\n\n| 环境 | 类型 | 默认 | 描述 |\n|---------|------|---------|-------------|\n| `compaction.enabled` | 布尔值 | `true` | 启用自动压缩 |\n| `compaction.reserveTokens` | 数字 | `16384` | 为 LLM 响应保留的令牌 |\n| `compaction.keepRecentTokens` | 数字 | `20000` | 最近要保留的令牌（未汇总） |\n\n```json\n{\n  \"compaction\": {\n    \"enabled\": true,\n    \"reserveTokens\": 16384,\n    \"keepRecentTokens\": 20000\n  }\n}\n```\n\n### 分行概要\n\n| 环境 | 类型 | 默认 | 描述 |\n|---------|------|---------|-------------|\n| `branchSummary.reserveTokens` | 数字 | `16384` | 为branch summarization保留的代币 |\n| `branchSummary.skipPrompt` | 布尔值 | `false` | 跳过“总结分支？” `/tree`导航提示（默认无摘要） |\n\n### 重试\n\n| 环境 | 类型 | 默认 | 描述 |\n|---------|------|---------|-------------|\n| `retry.enabled` | 布尔值 | `true` | 对暂时性错误启用自动代理级重试 |\n| `retry.maxRetries` | 数字 | `3` | 最大代理级别重试尝试次数 |\n| `retry.baseDelayMs` | 数字 | `2000` | 代理级指数退避的基本延迟（2s、4s、8s） |\n| `retry.provider.timeoutMs` | 数字 | SDK 默认 | Provider/SDK 请求超时（以毫秒为单位） |\n| `retry.provider.maxRetries` | 数字 | `0` | 提供商/SDK重试尝试 |\n| `retry.provider.maxRetryDelayMs` | 数字 | `60000` | 失败前服务器请求的最大延迟（60 秒） |\n\n当提供者请求重试延迟超过 `retry.provider.maxRetryDelayMs` 时，请求会立即失败并出现信息性错误，而不是静默等待。将其设置为 `0` 以禁用限制。\n\n将 `retry.provider.maxRetries` 保持在 `0` 除非明确需要提供者级别的重试。将其设置为高于 `0` 可以使 SDK/provider 重试在 Pi 看到超出使用限制的错误之前处理这些错误，这可能会阻止代理，直到在某些情况下提供程序配额重置。\n\n```json\n{\n  \"retry\": {\n    \"enabled\": true,\n    \"maxRetries\": 3,\n    \"baseDelayMs\": 2000,\n    \"provider\": {\n      \"timeoutMs\": 3600000,\n      \"maxRetries\": 0,\n      \"maxRetryDelayMs\": 60000\n    }\n  }\n}\n```\n\n### 消息传递\n\n| 环境 | 类型 | 默认 | 描述 |\n|---------|------|---------|-------------|\n| `steeringMode` | 细绳 | `\"one-at-a-time\"` | 如何发送转向消息：`\"all\"` 或 `\"one-at-a-time\"` |\n| `followUpMode` | 细绳 | `\"one-at-a-time\"` | 后续消息如何发送：`\"all\"` 或 `\"one-at-a-time\"` |\n| `transport` | 细绳 | `\"auto\"` | 支持多种传输的提供商的首选传输：`\"sse\"`、`\"websocket\"`、`\"websocket-cached\"` 或 `\"auto\"` |\n| `httpIdleTimeoutMs` | 数字 | `300000` | HTTP header/body 空闲超时（以毫秒为单位），也由具有显式流空闲超时的提供者使用。设置为 `0` 禁用。 |\n| `websocketConnectTimeoutMs` | 数字 | `15000` | 支持 WebSocket 传输的提供程序的 WebSocket 连接/打开握手超时（以毫秒为单位）。设置为 `0` 禁用。 |\n\n### 终端与图像\n\n| 环境 | 类型 | 默认 | 描述 |\n|---------|------|---------|-------------|\n| `terminal.showImages` | 布尔值 | `true` | 在终端中显示图像（如果支持） |\n| `terminal.imageWidthCells` | 数字 | `60` | 终端单元格中的首选内联图像宽度 |\n| `terminal.clearOnShrink` | 布尔值 | `false` | 内容缩小时清除空行（可能导致闪烁） |\n| `images.autoResize` | 布尔值 | `true` | 将图像大小调整为最大 2000x2000。适用于`@file`附件、`read`以及工具返回的图像 |\n| `images.blockImages` | 布尔值 | `false` | 阻止所有图像发送至 LLM |\n\n### 壳\n\n| 环境 | 类型 | 默认 | 描述 |\n|---------|------|---------|-------------|\n| `shellPath` | 细绳 | - | 自定义 shell 路径（例如，Windows 上的 Cygwin）；支持主目录前导 `~` |\n| `shellCommandPrefix` | 细绳 | - | 每个 bash 命令的前缀（例如，`\"shopt -s expand_aliases\"`） |\n| `npmCommand` | 细绳[] | - | 用于 npm 包查找/安装操作的命令 argv（例如，`[\"mise\", \"exec\", \"node@20\", \"--\", \"npm\"]`） |\n\n```json\n{\n  \"npmCommand\": [\"mise\", \"exec\", \"node@20\", \"--\", \"npm\"]\n}\n```\n\n`npmCommand` 用于所有 npm 包管理器操作，包括安装、卸载以及 git 包内的依赖项安装。用户范围的 npm 软件包安装在 `~/.pi/agent/npm/` 下；项目范围的 npm 软件包安装在 `.pi/npm/` 下。完全按照应启动的流程使用 argv 样式条目。配置 `npmCommand` 时，git 包依赖项安装使用普通 `install` 以避免包装器或备用包管理器中特定于 npm 的标志。\n\n### 会话\n\n| 环境 | 类型 | 默认 | 描述 |\n|---------|------|---------|-------------|\n| `sessionDir` | 细绳 | - | 存储会话文件的目录。接受绝对路径或相对路径，加上 `~`。 |\n\n```json\n{ \"sessionDir\": \".pi/sessions\" }\n```\n\n当多个源指定会话目录时，settings.json 中的优先级为 `--session-dir`、`PI_CODING_AGENT_SESSION_DIR`，然后是 `sessionDir`。\n\n### 模型自行车\n\n| 环境 | 类型 | 默认 | 描述 |\n|---------|------|---------|-------------|\n| `enabledModels` | 细绳[] | - | Ctrl+P 循环的模型模式（与 `--models` CLI 标志相同的格式） |\n\n```json\n{\n  \"enabledModels\": [\"claude-*\", \"gpt-4o\", \"gemini-2*\"]\n}\n```\n\n### Markdown\n\n| 环境 | 类型 | 默认 | 描述 |\n|---------|------|---------|-------------|\n| `markdown.codeBlockIndent` | 细绳 | `\"  \"` | 代码块的缩进 |\n| `markdown.mermaid` | 细绳 | `\"streaming\"` | 美人鱼渲染模式：`\"off\"`、`\"final\"`或`\"streaming\"` |\n\n### 资源\n\n这些设置定义从何处加载扩展、技能、提示和主题。\n\n`~/.pi/agent/settings.json` 中的路径相对于 `~/.pi/agent` 进行解析。 `.pi/settings.json` 中的路径相对于 `.pi` 进行解析。支持绝对路径和`~`。\n\n| 环境 | 类型 | 默认 | 描述 |\n|---------|------|---------|-------------|\n| `packages` | 大批 | `[]` | npm/git 包加载资源 |\n| `extensions` | 细绳[] | `[]` | 本地扩展文件路径或目录 |\n| `skills` | 细绳[] | `[]` | 本地技能文件路径或目录 |\n| `prompts` | 细绳[] | `[]` | 本地提示模板路径或目录 |\n| `themes` | 细绳[] | `[]` | 本地主题文件路径或目录 |\n| `enableSkillCommands` | 布尔值 | `true` | 将技能注册为`/skill:name`命令 |\n\n数组支持 glob 模式和排除。使用`!pattern`排除。使用 `+path` 强制包含精确路径，使用 `-path` 强制排除精确路径。\n\n#### 包\n\n字符串形式加载包中的所有资源：\n\n```json\n{\n  \"packages\": [\"pi-skills\", \"@org/my-extension\"]\n}\n```\n\n对象形式过滤要加载的资源：\n\n```json\n{\n  \"packages\": [\n    {\n      \"source\": \"pi-skills\",\n      \"skills\": [\"brave-search\", \"transcribe\"],\n      \"extensions\": []\n    }\n  ]\n}\n```\n\n有关包管理的详细信息，请参阅[packages.md](packages.md)。\n\n## 例子\n\n```json\n{\n  \"defaultProvider\": \"anthropic\",\n  \"defaultModel\": \"claude-sonnet-4-20250514\",\n  \"defaultThinkingLevel\": \"medium\",\n  \"theme\": \"dark\",\n  \"compaction\": {\n    \"enabled\": true,\n    \"reserveTokens\": 16384,\n    \"keepRecentTokens\": 20000\n  },\n  \"retry\": {\n    \"enabled\": true,\n    \"maxRetries\": 3\n  },\n  \"enabledModels\": [\"claude-*\", \"gpt-4o\"],\n  \"warnings\": {\n    \"anthropicExtraUsage\": true\n  },\n  \"packages\": [\"pi-skills\"]\n}\n```\n\n## 项目覆盖\n\n项目设置 (`.pi/settings.json`) 覆盖全局设置。嵌套对象被合并：\n\n```json\n// ~/.pi/agent/settings.json (global)\n{\n  \"theme\": \"dark\",\n  \"compaction\": { \"enabled\": true, \"reserveTokens\": 16384 }\n}\n\n// .pi/settings.json (project)\n{\n  \"compaction\": { \"reserveTokens\": 8192 }\n}\n\n// Result\n{\n  \"theme\": \"dark\",\n  \"compaction\": { \"enabled\": true, \"reserveTokens\": 8192 }\n}\n```","sourceFile":"settings.md"},"shell-aliases":{"title":"Shell Aliases","markdown":"Pi 以非交互模式 (`bash -c`) 运行 bash，默认情况下不扩展别名。\n\n要启用 shell 别名，请添加到 `~/.pi/agent/settings.json`：\n\n```json\n{\n  \"shellCommandPrefix\": \"shopt -s expand_aliases\\neval \\\"$(grep '^alias ' ~/.zshrc)\\\"\"\n}\n```\n\n调整路径（`~/.zshrc`、`~/.bashrc`等）以匹配您的 shell 配置。","sourceFile":"shell-aliases.md"},"skills":{"title":"Skills","markdown":"> pi 可以创造技能。要求它为您的用例构建一个。\n\n\nSkills 是代理按需加载的独立功能包。技能为特定任务提供专门的工作流程、设置说明、帮助脚本和参考文档。\n\nPi 实施[Agent Skills standard](https://agentskills.io/specification)，对大多数违规行为发出警告，但保持宽松。 Pi 允许技能名称与其父目录不同，即使标准不允许；对于跨多个代理工具使用的共享技能目录，该规则并不是最佳选择。\n\n## 目录\n\n- [Locations](#locations)\n- [How Skills Work](#how-skills-work)\n- [Skill Commands](#skill-commands)\n- [Skill Structure](#skill-structure)\n- [Frontmatter](#frontmatter)\n- [Validation](#validation)\n- [Example](#example)\n- [Skill Repositories](#skill-repositories)\n\n## 地点\n\n> **安全性：** Skills 可以指示模型执行任何操作，并且可能包括模型调用的可执行代码。使用前查看技能内容。\n\nPi 从以下位置加载技能：\n\n- 全球的：\n  - `~/.pi/agent/skills/`\n  - `~/.agents/skills/`\n- 项目（仅在项目被信任后）：\n  - `.pi/skills/`\n  - `cwd` 和祖先目录中的 `.agents/skills/` （直至 git repo 根目录，或不在存储库中时的文件系统根目录）\n- 包：`skills/`目录或`package.json`中的`pi.skills`条目\n- 设置：`skills`包含文件或目录的数组\n- CLI：`--skill <path>`（可重复，可与`--no-skills`相加）\n\n发现规则：\n- 在`~/.pi/agent/skills/`和`.pi/skills/`中，直接根`.md`文件被发现为个人技能\n- 在所有技能位置中，递归地发现包含`SKILL.md`的目录\n- 在`~/.agents/skills/`和项目`.agents/skills/`中，根`.md`文件被忽略\n\n使用 `--no-skills` 禁用发现（仍加载显式 `--skill` 路径）。\n\n### 使用其他线束中的 Skills\n\n要使用 Claude Code 或 OpenAI Codex 中的技能，请将其目录添加到设置中：\n\n```json\n{\n  \"skills\": [\n    \"~/.claude/skills\",\n    \"~/.codex/skills\"\n  ]\n}\n```\n\n对于项目级别的 Claude Code 技能，请添加到 `.pi/settings.json`：\n\n```json\n{\n  \"skills\": [\"../.claude/skills\"]\n}\n```\n\n## Skills 工作原理\n\n1. 启动时，pi 扫描技能位置并提取名称和描述\n2. 系统提示包含 XML 格式的可用技能，按照[specification](https://agentskills.io/integrate-skills)\n3. 当任务匹配时，代理使用`read`加载完整的SKILL.md（模型并不总是这样做；使用提示或`/skill:name`强制它）\n4. 代理按照说明操作，使用相对路径来引用脚本和资产\n\n这是渐进式披露：只有描述始终处于上下文中，完整的说明按需加载。\n\n## 技能命令\n\nSkills 注册为`/skill:name` 命令：\n\n```bash\n/skill:brave-search           # Load and execute the skill\n/skill:pdf-tools extract      # Load skill with arguments\n```\n\n命令后面的参数将作为`User: <args>`附加到技能内容中。\n\n在交互模式或`settings.json`下通过`/settings`切换技能命令：\n\n```json\n{\n  \"enableSkillCommands\": true\n}\n```\n\n## 技能结构\n\n技能是一个包含 `SKILL.md` 文件的目录。其他一切都是自由形式。\n\n```\nmy-skill/\n├── SKILL.md              # Required: frontmatter + instructions\n├── scripts/              # Helper scripts\n│   └── process.sh\n├── references/           # Detailed docs loaded on-demand\n│   └── api-reference.md\n└── assets/\n    └── template.json\n```\n\n### 技能.md 格式\n\n````markdown\n---\nname: my-skill\ndescription: What this skill does and when to use it. Be specific.\n---\n\n# My Skill\n\n## Setup\n\nRun once before first use:\n```bash\ncd /路径/到/技能 && npm 安装\n```\n\n## Usage\n\n```bash\n./scripts/process.sh <input>\n```\n````\n\n使用技能目录中的相对路径：\n\n```markdown\nSee [the reference guide](references/REFERENCE.md) for details.\n```\n\n## 前题\n\n根据 [Agent Skills specification](https://agentskills.io/specification#frontmatter-required)：\n\n| 场地 | 必需的 | 描述 |\n|-------|----------|-------------|\n| `name` | 是的 | 最多 64 个字符。小写 a-z、0-9、连字符。与标准不同，Pi 不要求它与父目录匹配，因为该标准要求对于共享技能目录来说不是最佳的。 |\n| `description` | 是的 | 最多 1024 个字符。该技能的作用是什么以及何时使用它。 |\n| `license` | 不 | 许可证名称或捆绑文件的引用。 |\n| `compatibility` | 不 | 最多 500 个字符。环境要求。 |\n| `metadata` | 不 | 任意键值映射。 |\n| `allowed-tools` | 不 | 以空格分隔的预先批准的工具列表（实验性）。 |\n| `disable-model-invocation` | 不 | 当`true`时，技能在系统提示中隐藏。用户必须使用`/skill:name`。 |\n\n### 命名规则\n\n- 1-64 个字符\n- 仅小写字母、数字、连字符\n- 没有前导/尾随连字符\n- 没有连续的连字符\nPi 不要求名称与父目录匹配。 Agent Skills 标准确实如此，但对于多个工具使用的共享技能目录来说，该要求并不是最优的。\n\n有效：`pdf-processing`、`data-analysis`、`code-review`\n无效：`PDF-Processing`、`-pdf`、`pdf--processing`\n\n### 描述 最佳实践\n\n描述决定代理何时加载技能。具体一点。\n\n好的：\n```yaml\ndescription: Extracts text and tables from PDF files, fills PDF forms, and merges multiple PDFs. Use when working with PDF documents.\n```\n\n贫穷的：\n```yaml\ndescription: Helps with PDFs.\n```\n\n## 验证\n\nPi 根据 Agent Skills 标准验证技能。大多数问题都会产生警告，但仍会加载技能：\n\n- 名称超过 64 个字符或包含无效字符\n- 名称以连字符开头/结尾或具有连续的连字符\n- 描述超过 1024 个字符\n\n未知的 frontmatter 字段将被忽略。\n\n**例外：** 缺少描述的 Skills 不会加载。\n\n名称冲突（不同位置的相同名称）会发出警告并保留找到的第一个技能。\n\n## 例子\n\n```\nbrave-search/\n├── SKILL.md\n├── search.js\n└── content.js\n```\n\n**技能.md:**\n````markdown\n---\nname: brave-search\ndescription: Web search and content extraction via Brave Search API. Use for searching documentation, facts, or any web content.\n---\n\n# Brave Search\n\n## Setup\n\n```bash\ncd /path/to/brave-search && npm install\n```\n\n## Search\n\n```bash\n./search.js \"query\" # 基本搜索\n./search.js \"query\" --content # 包含页面内容\n```\n\n## Extract Page Content\n\n```bash\n./content.js https://example.com\n```\n````\n\n## 技能库\n\n- [Anthropic Skills](https://github.com/anthropics/skills) - 文档处理（docx、pdf、pptx、xlsx）、Web 开发\n- [Pi Skills](https://github.com/badlogic/pi-skills) - 网络搜索、浏览器自动化、Google APIs、转录","sourceFile":"skills.md"},"terminal-setup":{"title":"终端设置","markdown":"Pi 使用 [Kitty keyboard protocol](https://sw.kovidgoyal.net/kitty/keyboard-protocol/) 进行可靠的修饰键检测。大多数现代终端都支持此协议，但有些需要配置。\n\n## 基蒂，iTerm2\n\n开箱即用。\n\n## 苹果终端\n\nPi 在可用时启用增强的关键报告。如果 Terminal.app 仍然发送 `Shift+Enter` 的普通 Return，则 pi 使用本地 macOS 修饰符后备将该 Return 视为 `Shift+Enter`。\n\n仅当 pi 与 Terminal.app 在同一台 Mac 上运行时，此后备才有效。它无法通过远程SSH检测到本地键盘。\n\n## 幽灵般的\n\n添加到您的 Ghostty 配置（macOS 上为 `~/Library/Application Support/com.mitchellh.ghostty/config`，Linux 上为 `~/.config/ghostty/config`）：\n\n```\nkeybind = alt+backspace=text:\\x1b\\x7f\n```\n\n较旧的克劳德代码版本可能添加了此 Ghostty 映射：\n\n```\nkeybind = shift+enter=text:\\n\n```\n\n该映射发送一个原始换行字节。在 pi 内部，它与 `Ctrl+J` 无法区分，因此 tmux 和 pi 不再看到真正的 `shift+enter` 按键事件。\n\n如果 Claude Code 2.x 或更新版本是您添加该映射的唯一原因，您可以将其删除，除非您想在 tmux 中使用 Claude Code，因为它仍然需要 Ghostty 映射。\n\nPi 将 `Ctrl+J` 绑定为默认换行符别名，因此 `Shift+Enter` 通过重新映射继续在 tmux 中工作，无需额外的 pi 配置。\n\n## 韦兹术语\n\nWezTerm 通常通过 xterm modifyOtherKeys 开箱即用地运行 `Shift+Enter`。要显式使用 Kitty 键盘协议，请创建 `~/.wezterm.lua`：\n\n```lua\nlocal wezterm = require 'wezterm'\nlocal config = wezterm.config_builder()\nconfig.enable_kitty_keyboard = true\nreturn config\n```\n\n在 macOS 上，WezTerm 默认将 `Option+Enter` 绑定到全屏。要使用 `Option+Enter` 进行 pi 后续队列，请添加此键覆盖：\n\n```lua\nlocal wezterm = require 'wezterm'\nlocal config = wezterm.config_builder()\nconfig.keys = {\n  {\n    key = 'Enter',\n    mods = 'ALT',\n    action = wezterm.action.SendString('\\x1b[13;3u'),\n  },\n}\nreturn config\n```\n\n如果您已有一个 `config.keys` 表，请向其中添加条目。\n\n在 WSL 上，WezTerm 可能需要可见的硬件光标来定位 IME 候选窗口。如果 CJK IME 候选项不跟随文本光标，请在运行 pi 之前设置 `PI_HARDWARE_CURSOR=1` 或在设置中将 `showHardwareCursor` 设置为 `true`。\n\n## 阿拉克里蒂\n\nAlacritty 通常开箱即用，适用于 `Shift+Enter`。在 macOS 上，`Option+Enter` 可能以普通的 `Enter` 形式出现。要使用 `Option+Enter` 进行 pi 后续队列，请添加到 `~/.config/alacritty/alacritty.toml`：\n\n```toml\n[[keyboard.bindings]]\nkey = \"Enter\"\nmods = \"Alt\"\nchars = \"\\u001b[13;3u\"\n```\n\n更改配置后重新启动 Alacritty。\n\n## VS Code（集成终端）\n\nVS Code 1.109.5 及更高版本默认在集成终端中启用 Kitty 键盘协议，因此 `Shift+Enter` 应该可以开箱即用。\n\n早于 1.109.5 的 VS Code 版本需要 `Shift+Enter` 的显式终端键绑定。\n\n`keybindings.json`地点：\n- 苹果系统：`~/Library/Application Support/Code/User/keybindings.json`\n- Linux：`~/.config/Code/User/keybindings.json`\n- 窗户：`%APPDATA%\\\\Code\\\\User\\\\keybindings.json`\n\n添加到`keybindings.json`：\n\n```json\n{\n  \"key\": \"shift+enter\",\n  \"command\": \"workbench.action.terminal.sendSequence\",\n  \"args\": { \"text\": \"\\u001b[13;2u\" },\n  \"when\": \"terminalFocus\"\n}\n```\n\n## Windows 终端\n\n添加到`settings.json`（Ctrl+Shift+，或设置→打开JSON文件）以转发修改后的Enter键pi使用：\n\n```json\n{\n  \"actions\": [\n    {\n      \"command\": { \"action\": \"sendInput\", \"input\": \"\\u001b[13;2u\" },\n      \"keys\": \"shift+enter\"\n    },\n    {\n      \"command\": { \"action\": \"sendInput\", \"input\": \"\\u001b[13;3u\" },\n      \"keys\": \"alt+enter\"\n    }\n  ]\n}\n```\n\n- `Shift+Enter` 插入新行。\n- 默认情况下，Windows 终端将 `Alt+Enter` 绑定到全屏。这会阻止 pi 接收 `Alt+Enter` 进行后续排队。\n- 将 `Alt+Enter` 重新映射到 `sendInput` 会将真正的调和弦转发到 pi。\n\n如果您已经有一个 `actions` 数组，请将对象添加到其中。如果旧的全屏行为仍然存在，请完全关闭并重新打开 Windows 终端。\n\n## xfce4-终端，终结者\n\n这些终端的转义序列支持有限。修改后的 Enter 键（如 `Ctrl+Enter` 和 `Shift+Enter`）无法与普通的 `Enter` 区分开来，从而阻止自定义键绑定（如 `submit: [\"ctrl+enter\"]`）工作。\n\n为了获得最佳体验，请使用支持 Kitty 键盘协议的终端：\n- [Kitty](https://sw.kovidgoyal.net/kitty/)\n- [Ghostty](https://ghostty.org/)\n- [WezTerm](https://wezfurlong.org/wezterm/)\n- [iTerm2](https://iterm2.com/)\n- [Alacritty](https://github.com/alacritty/alacritty)（需要带有Kitty协议支持的编译）\n\n## IntelliJ IDEA（集成终端）\n\n内置终端对转义序列的支持有限。 Shift+Enter 无法与 IntelliJ 终端中的 Enter 区分开。\n\n如果您希望硬件光标可见，请在运行 pi 之前设置 `PI_HARDWARE_CURSOR=1` （默认情况下禁用兼容性）。\n\n考虑使用专用的终端模拟器以获得最佳体验。","sourceFile":"terminal-setup.md"},"termux":{"title":"Termux（安卓）设置","markdown":"Pi 通过[Termux](https://termux.dev/)（适用于 Android 的终端模拟器和 Linux 环境）在 Android 上运行。\n\n## 先决条件\n\n1. 从 GitHub 或 F-Droid 安装 [Termux](https://github.com/termux/termux-app#installation)（不是 Google Play，该版本已弃用）\n2. 从 GitHub 或 F-Droid 安装 [Termux:API](https://github.com/termux/termux-api#installation) 以进行剪贴板和其他设备集成\n\n## 安装\n\n```bash\n# Update packages\npkg update && pkg upgrade\n\n# Install dependencies\npkg install nodejs termux-api git\n\n# Install pi\nnpm install -g --ignore-scripts @earendil-works/pi-coding-agent\n\n# Create config directory\nmkdir -p ~/.pi/agent\n\n# Run pi\npi\n```\n\n## 剪贴板支持\n\n在 Termux 中运行时，剪贴板操作使用 `termux-clipboard-set` 和 `termux-clipboard-get`。必须安装 Termux:API 应用程序才能正常工作。\n\nTermux 不支持图像剪贴板（`ctrl+v` 图像粘贴功能将不起作用）。\n\n## Termux 的 AGENTS.md 示例\n\n创建`~/.pi/agent/AGENTS.md`来帮助智能体了解Termux环境：\n\n````markdown\n# Agent Environment: Termux on Android\n\n## Location\n- **OS**: Android (Termux terminal emulator)\n- **Home**: `/data/data/com.termux/files/home`\n- **Prefix**: `/data/data/com.termux/files/usr`\n- **Shared storage**: `/storage/emulated/0` (Downloads, Documents, etc.)\n\n## Opening URLs\n```bash\ntermux-open-url“https://example.com”\n```\n\n## Opening Files\n```bash\ntermux-open file.pdf # 使用默认应用程序打开\ntermux-open --chooser image.jpg # 选择应用程序\n```\n\n## Clipboard\n```bash\ntermux-clipboard-set \"text\" # 复制\ntermux-clipboard-get # 粘贴\n```\n\n## Notifications\n```bash\ntermux-通知 -t“标题”-c“内容”\n```\n\n## Device Info\n```bash\ntermux-battery-status # 电池信息\ntermux-wifi-connectioninfo # WiFi 信息\ntermux-telephony-deviceinfo # 设备信息\n```\n\n## Sharing\n```bash\ntermux-share -a send file.txt # 共享文件\n```\n\n## Other Useful Commands\n```bash\ntermux-toast \"message\" # 快速 toast 弹出窗口\ntermux-vibrate # 振动设备\ntermux-tts-speak \"hello\" # 文本转语音\ntermux-camera-photo out.jpg # 拍照\n```\n\n## Notes\n- Termux:API app must be installed for `termux-*` commands\n- Use `pkg install termux-api` for the command-line tools\n- Storage permission needed for `/storage/emulated/0` access\n````\n\n## 局限性\n\n- **无图像剪贴板**：Termux剪贴板API仅支持文本\n- **没有本机二进制文件**：一些可选的本机依赖项（例如剪贴板模块）在 Android ARM64 上不可用，并且在安装过程中会被跳过\n- **存储访问**：要访问`/storage/emulated/0`中的文件（下载等），请运行一次`termux-setup-storage`以授予权限\n\n## 故障排除\n\n### 剪贴板不工作\n\n确保两个应用程序均已安装：\n1. Termux（来自 GitHub 或 F-Droid）\n2. Termux:API（来自GitHub或F-Droid）\n\n然后安装CLI工具：\n```bash\npkg install termux-api\n```\n\n### 共享存储的权限被拒绝\n\n运行一次以授予存储权限：\n```bash\ntermux-setup-storage\n```\n\n### Node.js安装问题\n\n如果npm失败，请尝试清除缓存：\n```bash\nnpm cache clean --force\n```","sourceFile":"termux.md"},"themes":{"title":"Themes","markdown":"> pi 可以创建主题。要求它为您的设置构建一个。\n\n\n主题是定义 TUI 颜色的 JSON 文件。\n\n## 目录\n\n- [Locations](#locations)\n- [Selecting a Theme](#selecting-a-theme)\n- [Creating a Custom Theme](#creating-a-custom-theme)\n- [Theme Format](#theme-format)\n- [Color Tokens](#color-tokens)\n- [Color Values](#color-values)\n- [Tips](#tips)\n\n## 地点\n\nPi 从以下位置加载主题：\n\n- 内置：`dark`、`light`\n- 全球：`~/.pi/agent/themes/*.json`\n- 项目：`.pi/themes/*.json`（仅在项目被信任后）\n- 包：`themes/`目录或`package.json`中的`pi.themes`条目\n- 设置：`themes`包含文件或目录的数组\n- CLI: `--theme <path>`（可重复）\n\n使用 `--no-themes` 禁用发现。\n\n## 选择主题\n\n通过 `/settings` 或 `settings.json` 选择主题：\n\n```json\n{\n  \"theme\": \"my-theme\"\n}\n```\n\n首次运行时，pi 会检测您的终端背景并默认为 `dark` 或 `light`。\n\n## 创建自定义主题\n\n1. 创建主题文件：\n\n```bash\nmkdir -p ~/.pi/agent/themes\nvim ~/.pi/agent/themes/my-theme.json\n```\n\n2. 使用所有必需的颜色定义主题（参见[Color Tokens](#color-tokens)）：\n\n```json\n{\n  \"$schema\": \"https://raw.githubusercontent.com/earendil-works/pi/main/packages/coding-agent/src/modes/interactive/theme/theme-schema.json\",\n  \"name\": \"my-theme\",\n  \"vars\": {\n    \"primary\": \"#00aaff\",\n    \"secondary\": 242\n  },\n  \"colors\": {\n    \"accent\": \"primary\",\n    \"border\": \"primary\",\n    \"borderAccent\": \"#00ffff\",\n    \"borderMuted\": \"secondary\",\n    \"success\": \"#00ff00\",\n    \"error\": \"#ff0000\",\n    \"warning\": \"#ffff00\",\n    \"muted\": \"secondary\",\n    \"dim\": 240,\n    \"text\": \"\",\n    \"thinkingText\": \"secondary\",\n    \"selectedBg\": \"#2d2d30\",\n    \"scrollbarThumb\": \"#555566\",\n    \"userMessageBg\": \"#2d2d30\",\n    \"userMessageText\": \"\",\n    \"customMessageBg\": \"#2d2d30\",\n    \"customMessageText\": \"\",\n    \"customMessageLabel\": \"primary\",\n    \"toolPendingBg\": \"#1e1e2e\",\n    \"toolSuccessBg\": \"#1e2e1e\",\n    \"toolErrorBg\": \"#2e1e1e\",\n    \"toolTitle\": \"primary\",\n    \"toolOutput\": \"\",\n    \"mdHeading\": \"#ffaa00\",\n    \"mdLink\": \"primary\",\n    \"mdLinkUrl\": \"secondary\",\n    \"mdCode\": \"#00ffff\",\n    \"mdCodeBlock\": \"\",\n    \"mdCodeBlockBorder\": \"secondary\",\n    \"mdQuote\": \"secondary\",\n    \"mdQuoteBorder\": \"secondary\",\n    \"mdHr\": \"secondary\",\n    \"mdListBullet\": \"#00ffff\",\n    \"toolDiffAdded\": \"#00ff00\",\n    \"toolDiffRemoved\": \"#ff0000\",\n    \"toolDiffContext\": \"secondary\",\n    \"syntaxComment\": \"secondary\",\n    \"syntaxKeyword\": \"primary\",\n    \"syntaxFunction\": \"#00aaff\",\n    \"syntaxVariable\": \"#ffaa00\",\n    \"syntaxString\": \"#00ff00\",\n    \"syntaxNumber\": \"#ff00ff\",\n    \"syntaxType\": \"#00aaff\",\n    \"syntaxOperator\": \"primary\",\n    \"syntaxPunctuation\": \"secondary\",\n    \"thinkingOff\": \"secondary\",\n    \"thinkingMinimal\": \"primary\",\n    \"thinkingLow\": \"#00aaff\",\n    \"thinkingMedium\": \"#00ffff\",\n    \"thinkingHigh\": \"#ff00ff\",\n    \"thinkingXhigh\": \"#ff0000\",\n    \"thinkingMax\": \"#ff0088\",\n    \"bashMode\": \"#ffaa00\"\n  }\n}\n```\n\n3. 通过`/settings`选择主题。\n\n**热重载：** 当您编辑当前活动的自定义主题文件时，pi 会自动重新加载它以获得即时视觉反馈。\n\n## 主题格式\n\n```json\n{\n  \"$schema\": \"https://raw.githubusercontent.com/earendil-works/pi/main/packages/coding-agent/src/modes/interactive/theme/theme-schema.json\",\n  \"name\": \"my-theme\",\n  \"vars\": {\n    \"blue\": \"#0066cc\",\n    \"gray\": 242\n  },\n  \"colors\": {\n    \"accent\": \"blue\",\n    \"muted\": \"gray\",\n    \"text\": \"\",\n    ...\n  }\n}\n```\n\n- `name` 是必需的，必须是唯一的，并且不能包含`/`。\n- `vars` 是可选的。在这里定义可重复使用的颜色，然后在`colors`中引用它们。\n- `colors` 必须定义所有 51 个必需的标记。 `thinkingMax` 是可选的，并回落到 `thinkingXhigh`； `scrollbarThumb` 是可选的，并回落到 `selectedBg`。\n\n`$schema` 字段支持编辑器自动完成和验证。\n\n## 颜色标记\n\n每个主题必须定义所有 51 个必需的颜色标记。 `thinkingMax`和`scrollbarThumb`是可选的，以兼容现有主题；省略时，它们分别使用 `thinkingXhigh` 和 `selectedBg`。\n\n### 核心 UI（11 种颜色）\n\n| 代币 | 目的 |\n|-------|---------|\n| `accent` | 主要强调（徽标、所选项目、光标） |\n| `border` | 正常边框 |\n| `borderAccent` | 突出显示的边框 |\n| `borderMuted` | 微妙的边界（编辑） |\n| `success` | 成功状态 |\n| `error` | 错误状态 |\n| `warning` | 警告状态 |\n| `muted` | 次要文本 |\n| `dim` | 第三级文本 |\n| `text` | 默认文本（通常为`\"\"`） |\n| `thinkingText` | 思维块文本 |\n\n### 背景和内容（11 个必需，1 个可选）\n\n| 代币 | 目的 |\n|-------|---------|\n| `selectedBg` | 选定的线条背景 |\n| `scrollbarThumb` | 全屏滚动条拇指背景；可选，回落到 `selectedBg` |\n| `userMessageBg` | 用户留言背景 |\n| `userMessageText` | 用户消息文本 |\n| `customMessageBg` | 分机消息背景 |\n| `customMessageText` | 扩展消息文本 |\n| `customMessageLabel` | 扩展消息标签 |\n| `toolPendingBg` | 工具箱（待定） |\n| `toolSuccessBg` | 工具箱（成功） |\n| `toolErrorBg` | 工具箱（错误） |\n| `toolTitle` | 工具标题 |\n| `toolOutput` | 工具输出文本 |\n\n### Markdown（10种颜色）\n\n| 代币 | 目的 |\n|-------|---------|\n| `mdHeading` | 标题 |\n| `mdLink` | 链接文字 |\n| `mdLinkUrl` | 链接网址 |\n| `mdCode` | 内联代码 |\n| `mdCodeBlock` | 代码块内容 |\n| `mdCodeBlockBorder` | 代码块围栏 |\n| `mdQuote` | 块引用文本 |\n| `mdQuoteBorder` | 块引用边框 |\n| `mdHr` | 水平尺 |\n| `mdListBullet` | 列出项目符号 |\n\n### 工具差异（3 种颜色）\n\n| 代币 | 目的 |\n|-------|---------|\n| `toolDiffAdded` | 已添加线路 |\n| `toolDiffRemoved` | 删除的行 |\n| `toolDiffContext` | 上下文线 |\n\n### 语法突出显示（9 种颜色）\n\n| 代币 | 目的 |\n|-------|---------|\n| `syntaxComment` | 评论 |\n| `syntaxKeyword` | 关键词 |\n| `syntaxFunction` | 函数名称 |\n| `syntaxVariable` | 变量 |\n| `syntaxString` | 弦乐 |\n| `syntaxNumber` | 数字 |\n| `syntaxType` | 类型 |\n| `syntaxOperator` | 运营商 |\n| `syntaxPunctuation` | 标点 |\n\n### 思维水平边界（6 个必需，1 个可选）\n\n编辑器边框颜色表示思维水平（视觉层次从微妙到突出）：\n\n| 代币 | 目的 |\n|-------|---------|\n| `thinkingOff` | 思考 |\n| `thinkingMinimal` | 最少的思考 |\n| `thinkingLow` | 低思维 |\n| `thinkingMedium` | 中等思维 |\n| `thinkingHigh` | 高思想 |\n| `thinkingXhigh` | 超高思维 |\n| `thinkingMax` | 最大限度的思考；可选，回落到 `thinkingXhigh` |\n\n### 重击模式（1 种颜色）\n\n| 代币 | 目的 |\n|-------|---------|\n| `bashMode` | bash 模式下的编辑器边框（`!` 前缀） |\n\n### HTML 导出（可选）\n\n`export` 部分控制 `/export` HTML 输出的颜色。如果省略，则颜色源自 `userMessageBg`。\n\n```json\n{\n  \"export\": {\n    \"pageBg\": \"#18181e\",\n    \"cardBg\": \"#1e1e24\",\n    \"infoBg\": \"#3c3728\"\n  }\n}\n```\n\n## 颜色值\n\n支持四种格式：\n\n| 格式 | 例子 | 描述 |\n|--------|---------|-------------|\n| 十六进制 | `\"#ff0000\"` | 6 位十六进制 RGB |\n| 256色 | `39` | xterm 256 色调色板索引 (0-255) |\n| 多变的 | `\"primary\"` | 引用 `vars` 条目 |\n| 默认 | `\"\"` | 终端的默认颜色 |\n\n### 256 调色板\n\n- `0-15`：基本 ANSI 颜色（取决于终端）\n- `16-231`：6×6×6 RGB 立方体（`16 + 36×R + 6×G + B`，其中 R、G、B 为 0-5）\n- `232-255`：灰度渐变\n\n### 终端兼容性\n\nPi 使用 24 位 RGB 颜色。大多数现代终端都支持此功能（iTerm2、Kitty、WezTerm、Windows Terminal、VS Code）。对于仅支持 256 色的旧终端，pi 会回落到最接近的近似值。\n\n检查真彩色支持：\n\n```bash\necho $COLORTERM  # Should output \"truecolor\" or \"24bit\"\n```\n\n## 尖端\n\n**深色终端：** 使用明亮、饱和且对比度较高的颜色。\n\n**灯终端：** 使用较暗、柔和的颜色和较低的对比度。\n\n**色彩和谐：** 从基础调色板（Nord、Gruvbox、Tokyo Night）开始，在 `vars` 中定义它，并一致地引用。\n\n**测试：** 使用不同的消息类型、工具状态、Markdown 内容和长换行文本检查您的主题。\n\n**VS Code：** 将 `terminal.integrated.minimumContrastRatio` 设置为 `1` 以获得准确的颜色。\n\n## 示例\n\n查看内置主题：\n- [dark.json](../src/modes/interactive/theme/dark.json)\n- [light.json](../src/modes/interactive/theme/light.json)","sourceFile":"themes.md"},"tmux":{"title":"tmux 设置","markdown":"Pi 在 tmux 内部工作，但 tmux 默认情况下会从某些键中删除修饰符信息。如果没有配置，`Shift+Enter`和`Ctrl+Enter`通常与普通的`Enter`无法区分。\n\n## 推荐配置\n\n添加到`~/.tmux.conf`：\n\n```tmux\nset -g extended-keys on\nset -g extended-keys-format csi-u\n```\n\n然后完全重新启动tmux：\n\n```bash\ntmux kill-server\ntmux\n```\n\n当 Kitty 键盘协议不可用时，Pi 自动请求扩展按键报告。与`extended-keys-format csi-u`一起，tmux以CSI-u格式转发修改后的密钥，这是最可靠的配置。 `extended-keys-format` 选项需要 tmux 3.5 或更高版本。\n\n## 为什么推荐`csi-u`\n\n仅与：\n\n```tmux\nset -g extended-keys on\n```\n\ntmux 默认为 `extended-keys-format xterm`。当应用程序请求扩展密钥报告时，修改后的密钥将以 xterm `modifyOtherKeys` 格式转发，例如：\n\n- `Ctrl+C` → `\\x1b[27;5;99~`\n- `Ctrl+D` → `\\x1b[27;5;100~`\n- `Ctrl+Enter` → `\\x1b[27;5;13~`\n\n使用`extended-keys-format csi-u`，相同的密钥将转发为：\n\n- `Ctrl+C` → `\\x1b[99;5u`\n- `Ctrl+D` → `\\x1b[100;5u`\n- `Ctrl+Enter` → `\\x1b[13;5u`\n\nPi 支持两种格式，但`csi-u` 是推荐的tmux 设置。\n\n## 这修复了什么\n\n如果没有 tmux 扩展键，修改后的 Enter 键会折叠为旧序列：\n\n| 钥匙 | 没有外接键 | 与 `csi-u` |\n|-----|-----------------|--------------|\n| 进入 | `\\r` | `\\r` |\n| Shift+Enter | `\\r` | `\\x1b[13;2u` |\n| Ctrl+Enter | `\\r` | `\\x1b[13;5u` |\n| Alt/Option+Enter | `\\x1b\\r` | `\\x1b[13;3u` |\n\n这会影响默认的键绑定（`Enter`用于提交，`Shift+Enter`用于换行）以及使用修改后的 Enter 的任何自定义键绑定。\n\n## 要求\n\n- `extended-keys-format csi-u` tmux 3.5 或更高版本（运行 `tmux -V` 进行检查）\n- 支持扩展键的终端模拟器（Ghostty、Kitty、iTerm2、WezTerm、Windows Terminal）\n\n对于 tmux 3.2 到 3.4，省略 `extended-keys-format csi-u`； Pi 仍然支持 tmux 的默认 xterm `modifyOtherKeys` 格式。","sourceFile":"tmux.md"},"tui":{"title":"TUI 组件","markdown":"> pi 可以创建 TUI 个组件。要求它为您的用例构建一个。\n\n\nExtensions 和自定义工具可以为交互式用户界面渲染自定义 TUI 组件。本页介绍了组件系统和可用的构建块。\n\n**来源：** [`@earendil-works/pi-tui`](https://github.com/earendil-works/pi-mono/tree/main/packages/tui)\n\n## 组件接口\n\n所有组件均实现：\n\n```typescript\ninterface Component {\n  render(width: number): string[];\n  handleInput?(data: string): void;\n  wantsKeyRelease?: boolean;\n  invalidate(): void;\n}\n```\n\n| 方法 | 描述 |\n|--------|-------------|\n| `render(width)` | 返回字符串数组（每行一个）。每行**不得超过`width`**。 |\n| `handleInput?(data)` | 当组件获得焦点时接收键盘输入。 |\n| `wantsKeyRelease?` | 如果为 true，组件会接收按键释放事件（Kitty 协议）。默认值：假。 |\n| `invalidate()` | 清除缓存的渲染状态。呼吁改变主题。 |\n\nTUI 在每个渲染行的末尾附加完整的 SGR 重置和 OSC 8 重置。风格不跨界。如果您发出带有样式的多行文本，请重新应用每行样式或使用 `wrapTextWithAnsi()`，以便为每个换行行保留样式。\n\n## 可聚焦界面（IME 支持）\n\n显示文本光标并需要 IME（输入法编辑器）支持的组件应实现 `Focusable` 接口：\n\n```typescript\nimport { CURSOR_MARKER, type Component, type Focusable } from \"@earendil-works/pi-tui\";\n\nclass MyInput implements Component, Focusable {\n  focused: boolean = false;  // Set by TUI when focus changes\n  \n  render(width: number): string[] {\n    const marker = this.focused ? CURSOR_MARKER : \"\";\n    // Emit marker right before the fake cursor\n    return [`> ${beforeCursor}${marker}\\x1b[7m${atCursor}\\x1b[27m${afterCursor}`];\n  }\n}\n```\n\n当 `Focusable` 组件获得焦点时，TUI：\n1. 在组件上设置 `focused = true`\n2. 扫描渲染输出中的 `CURSOR_MARKER`（零宽度 APC 转义序列）\n3. 将硬件终端光标定位在该位置\n4. 仅当启用 `showHardwareCursor` 时才显示硬件光标\n\n默认情况下，光标保持隐藏状态。这保留了假光标渲染，同时仍然为使用隐藏光标跟踪 IME 候选窗口的终端定位硬件光标。某些终端需要可见的硬件光标来进行 IME 定位；使用 `showHardwareCursor`、`setShowHardwareCursor(true)` 或 `PI_HARDWARE_CURSOR=1` 启用它。 `Editor`和`Input`内置组件已经实现了这个接口。\n\n### 具有嵌入式输入的容器组件\n\n当容器组件（对话框、选择器等）包含 `Input` 或 `Editor` 子组件时，容器必须实现 `Focusable` 并将焦点状态传播到子组件。否则，硬件光标将无法正确定位以进行 IME 输入。\n\n```typescript\nimport { Container, type Focusable, Input } from \"@earendil-works/pi-tui\";\n\nclass SearchDialog extends Container implements Focusable {\n  private searchInput: Input;\n\n  // Focusable implementation - propagate to child input for IME cursor positioning\n  private _focused = false;\n  get focused(): boolean {\n    return this._focused;\n  }\n  set focused(value: boolean) {\n    this._focused = value;\n    this.searchInput.focused = value;\n  }\n\n  constructor() {\n    super();\n    this.searchInput = new Input();\n    this.addChild(this.searchInput);\n  }\n}\n```\n\n如果没有这种传播，使用 IME（中文、日文、韩文等）键入将在屏幕上的错误位置显示候选窗口。\n\n## 使用组件\n\n**在扩展中**通过 `ctx.ui.custom()`：\n\n```typescript\npi.on(\"session_start\", async (_event, ctx) => {\n  const result = await ctx.ui.custom<string | null>((tui, theme, keybindings, done) =>\n    new MyComponent({\n      theme,\n      keybindings,\n      onChange: () => tui.requestRender(),\n      onSelect: (value) => done(value),\n      onCancel: () => done(null),\n    })\n  );\n});\n```\n\n**在自定义工具中**通过 `ctx.ui.custom()`：\n\n```typescript\nasync execute(toolCallId, params, signal, onUpdate, ctx) {\n  const result = await ctx.ui.custom<string | null>((tui, theme, keybindings, done) =>\n    new MyComponent({\n      theme,\n      keybindings,\n      onChange: () => tui.requestRender(),\n      onSelect: (value) => done(value),\n      onCancel: () => done(null),\n    })\n  );\n  // Use result...\n}\n```\n\n## 叠加层\n\n将渲染组件叠加在现有内容之上，而无需清除屏幕。将 `{ overlay: true }` 传递到 `ctx.ui.custom()`：\n\n```typescript\nconst result = await ctx.ui.custom<string | null>(\n  (tui, theme, keybindings, done) => new MyDialog({ onClose: done }),\n  { overlay: true }\n);\n```\n\n对于定位和调整大小，请使用 `overlayOptions`：\n\n```typescript\nconst result = await ctx.ui.custom<string | null>(\n  (tui, theme, keybindings, done) => new SidePanel({ onClose: done }),\n  {\n    overlay: true,\n    overlayOptions: {\n      // Size: number or percentage string\n      width: \"50%\",          // 50% of terminal width\n      minWidth: 40,          // minimum 40 columns\n      maxHeight: \"80%\",      // max 80% of terminal height\n\n      // Position: anchor-based (default: \"center\")\n      anchor: \"right-center\", // 9 positions: center, top-left, top-center, etc.\n      offsetX: -2,            // offset from anchor\n      offsetY: 0,\n\n      // Or percentage/absolute positioning\n      row: \"25%\",            // 25% from top\n      col: 10,               // column 10\n\n      // Margins\n      margin: 2,             // all sides, or { top, right, bottom, left }\n\n      // Responsive: hide on narrow terminals\n      visible: (termWidth, termHeight) => termWidth >= 80,\n    },\n    // Get handle for programmatic focus and visibility control\n    onHandle: (handle) => {\n      // handle.focus() - focus this overlay and bring it to the visual front\n      // handle.unfocus() - release input to normal fallback\n      // handle.unfocus({ target }) - release input to a specific component or null\n      // handle.setHidden(true/false) - toggle visibility\n      // handle.hide() - permanently remove\n    },\n  }\n);\n```\n\n### 叠加焦点\n\n集中可见的叠加层可在临时非叠加 UI 中保留输入所有权。如果覆盖层打开另一个没有 `{ overlay: true }` 的 `ctx.ui.custom()` 组件，则替换 UI 在活动时接收输入；当它关闭时，聚焦的覆盖层可以回收输入。\n\n当可见叠加层应停止拥有输入并让 TUI 回退到另一个可见捕获叠加层或前一个焦点目标时，请使用 `handle.unfocus()`。当特定组件应在覆盖层保持可见时接收输入时，请使用`handle.unfocus({ target })`。故意传递 `{ target: null }` 不会留下任何焦点组件，直到再次设置焦点。\n\n### 覆盖生命周期\n\n覆盖组件在关闭时被丢弃。不要重复使用引用 - 创建新实例：\n\n```typescript\n// Wrong - stale reference\nlet menu: MenuComponent;\nawait ctx.ui.custom((_, __, ___, done) => {\n  menu = new MenuComponent(done);\n  return menu;\n}, { overlay: true });\nsetActiveComponent(menu);  // Disposed\n\n// Correct - re-call to re-show\nconst showMenu = () => ctx.ui.custom((_, __, ___, done) => \n  new MenuComponent(done), { overlay: true });\n\nawait showMenu();  // First show\nawait showMenu();  // \"Back\" = just call again\n```\n\n请参阅 [overlay-qa-tests.ts](../examples/extensions/overlay-qa-tests.ts) 了解涵盖锚点、边距、堆叠、响应式可见性和动画的综合示例。\n\n## 内置组件\n\n从 `@earendil-works/pi-tui` 导入：\n\n```typescript\nimport { Text, Box, Container, Spacer, Markdown } from \"@earendil-works/pi-tui\";\n```\n\n### 文本\n\n带自动换行功能的多行文本。\n\n```typescript\nconst text = new Text(\n  \"Hello World\",    // content\n  1,                // paddingX (default: 1)\n  1,                // paddingY (default: 1)\n  (s) => bgGray(s)  // optional background function\n);\ntext.setText(\"Updated\");\n```\n\n### 盒子\n\n具有填充和背景颜色的容器。\n\n```typescript\nconst box = new Box(\n  1,                // paddingX\n  1,                // paddingY\n  (s) => bgGray(s)  // background function\n);\nbox.addChild(new Text(\"Content\", 0, 0));\nbox.setBgFn((s) => bgBlue(s));\n```\n\n### 容器\n\n垂直分组子组件。\n\n```typescript\nconst container = new Container();\ncontainer.addChild(component1);\ncontainer.addChild(component2);\ncontainer.removeChild(component1);\n```\n\n### 垫片\n\n空的垂直空间。\n\n```typescript\nconst spacer = new Spacer(2);  // 2 empty lines\n```\n\n### Markdown\n\n使用语法突出显示呈现 Markdown。\n\n```typescript\nconst md = new Markdown(\n  \"# Title\\n\\nSome **bold** text\",\n  1,        // paddingX\n  1,        // paddingY\n  theme     // MarkdownTheme (see below)\n);\nmd.setText(\"Updated markdown\");\n```\n\n### 图像\n\n在支持的终端（Kitty、iTerm2、Ghostty、WezTerm、Warp）中渲染图像。\n\n```typescript\nconst image = new Image(\n  base64Data,   // base64-encoded image\n  \"image/png\",  // MIME type\n  theme,        // ImageTheme\n  { maxWidthCells: 80, maxHeightCells: 24 }\n);\n```\n\n## 键盘输入\n\n使用 `matchesKey()` 进行按键检测：\n\n```typescript\nimport { matchesKey, Key } from \"@earendil-works/pi-tui\";\n\nhandleInput(data: string) {\n  if (matchesKey(data, Key.up)) {\n    this.selectedIndex--;\n  } else if (matchesKey(data, Key.enter)) {\n    this.onSelect?.(this.selectedIndex);\n  } else if (matchesKey(data, Key.escape)) {\n    this.onCancel?.();\n  } else if (matchesKey(data, Key.ctrl(\"c\"))) {\n    // Ctrl+C\n  }\n}\n```\n\n**关键标识符**（使用 `Key.*` 进行自动完成，或字符串文字）：\n- 基本按键：`Key.enter`、`Key.escape`、`Key.tab`、`Key.space`、`Key.backspace`、`Key.delete`、`Key.home`、`Key.end`\n- 方向键：`Key.up`、`Key.down`、`Key.left`、`Key.right`\n- 带修饰符：`Key.ctrl(\"c\")`、`Key.shift(\"tab\")`、`Key.alt(\"left\")`、`Key.ctrlShift(\"p\")`\n- 字符串格式也适用：`\"enter\"`、`\"ctrl+c\"`、`\"shift+tab\"`、`\"ctrl+shift+p\"`\n\n## 线宽\n\n**关键：** 从 `render()` 开始的每一行都不能超过 `width` 参数。\n\n```typescript\nimport { visibleWidth, truncateToWidth } from \"@earendil-works/pi-tui\";\n\nrender(width: number): string[] {\n  // Truncate long lines\n  return [truncateToWidth(this.text, width)];\n}\n```\n\n公用事业：\n- `visibleWidth(str)` - 获取显示宽度（忽略 ANSI 代码）\n- `truncateToWidth(str, width, ellipsis?)` - 使用可选省略号截断\n- `wrapTextWithAnsi(str, width)` - 保留 ANSI 代码的自动换行\n\n## 创建自定义组件\n\n示例：交互式选择器\n\n```typescript\nimport {\n  matchesKey, Key,\n  truncateToWidth, visibleWidth\n} from \"@earendil-works/pi-tui\";\n\nclass MySelector {\n  private items: string[];\n  private selected = 0;\n  private cachedWidth?: number;\n  private cachedLines?: string[];\n  \n  public onSelect?: (item: string) => void;\n  public onCancel?: () => void;\n\n  constructor(items: string[]) {\n    this.items = items;\n  }\n\n  handleInput(data: string): void {\n    if (matchesKey(data, Key.up) && this.selected > 0) {\n      this.selected--;\n      this.invalidate();\n    } else if (matchesKey(data, Key.down) && this.selected < this.items.length - 1) {\n      this.selected++;\n      this.invalidate();\n    } else if (matchesKey(data, Key.enter)) {\n      this.onSelect?.(this.items[this.selected]);\n    } else if (matchesKey(data, Key.escape)) {\n      this.onCancel?.();\n    }\n  }\n\n  render(width: number): string[] {\n    if (this.cachedLines && this.cachedWidth === width) {\n      return this.cachedLines;\n    }\n\n    this.cachedLines = this.items.map((item, i) => {\n      const prefix = i === this.selected ? \"> \" : \"  \";\n      return truncateToWidth(prefix + item, width);\n    });\n    this.cachedWidth = width;\n    return this.cachedLines;\n  }\n\n  invalidate(): void {\n    this.cachedWidth = undefined;\n    this.cachedLines = undefined;\n  }\n}\n```\n\n在扩展中的用法：\n\n```typescript\npi.registerCommand(\"pick\", {\n  description: \"Pick an item\",\n  handler: async (_args, ctx) => {\n    const items = [\"Option A\", \"Option B\", \"Option C\"];\n    const selected = await ctx.ui.custom<string | null>((tui, _theme, _keybindings, done) => {\n      const selector = new MySelector(items);\n      selector.onSelect = done;\n      selector.onCancel = () => done(null);\n\n      return {\n        render: (width) => selector.render(width),\n        handleInput: (data) => {\n          selector.handleInput(data);\n          tui.requestRender();\n        },\n        invalidate: () => selector.invalidate(),\n      };\n    });\n\n    if (selected !== null) {\n      ctx.ui.notify(`Selected: ${selected}`, \"info\");\n    }\n  }\n});\n```\n\n## 主题化\n\n组件接受主题对象来设置样式。\n\n**在`renderCall`/`renderResult`**中，使用`theme`参数：\n\n```typescript\nrenderResult(result, options, theme, context) {\n  // Use theme.fg() for foreground colors\n  return new Text(theme.fg(\"success\", \"Done!\"), 0, 0);\n  \n  // Use theme.bg() for background colors\n  const styled = theme.bg(\"toolPendingBg\", theme.fg(\"accent\", \"text\"));\n}\n```\n\n**前景色** (`theme.fg(color, text)`):\n\n| 类别 | 颜色 |\n|----------|--------|\n| 一般的 | `text`, `accent`, `muted`, `dim` |\n| 地位 | `success`, `error`, `warning` |\n| 边框 | `border`, `borderAccent`, `borderMuted` |\n| 留言 | `userMessageText`, `customMessageText`, `customMessageLabel` |\n| 工具 | `toolTitle`, `toolOutput` |\n| 差异 | `toolDiffAdded`, `toolDiffRemoved`, `toolDiffContext` |\n| Markdown | `mdHeading`, `mdLink`, `mdLinkUrl`, `mdCode`, `mdCodeBlock`, `mdCodeBlockBorder`, `mdQuote`, `mdQuoteBorder`, `mdHr`, `mdListBullet` |\n| 句法 | `syntaxComment`, `syntaxKeyword`, `syntaxFunction`, `syntaxVariable`, `syntaxString`, `syntaxNumber`, `syntaxType`, `syntaxOperator`, `syntaxPunctuation` |\n| 思维 | `thinkingOff`, `thinkingMinimal`, `thinkingLow`, `thinkingMedium`, `thinkingHigh`, `thinkingXhigh`, `thinkingMax` |\n| 模式 | `bashMode` |\n\n**背景颜色** (`theme.bg(color, text)`):\n\n`selectedBg`, `userMessageBg`, `customMessageBg`, `toolPendingBg`, `toolSuccessBg`, `toolErrorBg`\n\n**对于 Markdown**，使用 `getMarkdownTheme()`：\n\n```typescript\nimport { getMarkdownTheme } from \"@earendil-works/pi-coding-agent\";\nimport { Markdown } from \"@earendil-works/pi-tui\";\n\nrenderResult(result, options, theme, context) {\n  const mdTheme = getMarkdownTheme();\n  return new Markdown(result.details.markdown, 0, 0, mdTheme);\n}\n```\n\n**对于自定义组件**，定义您自己的主题界面：\n\n```typescript\ninterface MyTheme {\n  selected: (s: string) => string;\n  normal: (s: string) => string;\n}\n```\n\n## 调试日志记录\n\n设置 `PI_TUI_WRITE_LOG` 以捕获写入stdout 的原始 ANSI 流。\n\n```bash\nPI_TUI_WRITE_LOG=/tmp/tui-ansi.log npx tsx packages/tui/test/chat-simple.ts\n```\n\n## 表现\n\n尽可能缓存渲染的输出：\n\n```typescript\nclass CachedComponent {\n  private cachedWidth?: number;\n  private cachedLines?: string[];\n\n  render(width: number): string[] {\n    if (this.cachedLines && this.cachedWidth === width) {\n      return this.cachedLines;\n    }\n    // ... compute lines ...\n    this.cachedWidth = width;\n    this.cachedLines = lines;\n    return lines;\n  }\n\n  invalidate(): void {\n    this.cachedWidth = undefined;\n    this.cachedLines = undefined;\n  }\n}\n```\n\n当状态改变时调用`invalidate()`，然​​后使用注入的`tui.requestRender()`触发重新渲染。\n\n## 失效和主题变更\n\n当主题改变时，TUI在所有组件上调用`invalidate()`来清除它们的缓存。组件必须正确实现`invalidate()`以确保主题更改生效。\n\n### 问题\n\n如果组件将主题颜色预烘焙为字符串（通过 `theme.fg()`、`theme.bg()` 等）并缓存它们，则缓存的字符串包含旧主题中的 ANSI 转义码。如果组件单独存储主题内容，那么仅仅清除渲染缓存是不够的。\n\n**错误的方法**（主题颜色不会更新）：\n\n```typescript\nclass BadComponent extends Container {\n  private content: Text;\n\n  constructor(message: string, theme: Theme) {\n    super();\n    // Pre-baked theme colors stored in Text component\n    this.content = new Text(theme.fg(\"accent\", message), 1, 0);\n    this.addChild(this.content);\n  }\n  // No invalidate override - parent's invalidate only clears\n  // child render caches, not the pre-baked content\n}\n```\n\n### 解决方案\n\n使用主题颜色构建内容的组件必须在调用 `invalidate()` 时重建该内容：\n\n```typescript\nclass GoodComponent extends Container {\n  private message: string;\n  private content: Text;\n\n  constructor(message: string) {\n    super();\n    this.message = message;\n    this.content = new Text(\"\", 1, 0);\n    this.addChild(this.content);\n    this.updateDisplay();\n  }\n\n  private updateDisplay(): void {\n    // Rebuild content with current theme\n    this.content.setText(theme.fg(\"accent\", this.message));\n  }\n\n  override invalidate(): void {\n    super.invalidate();  // Clear child caches\n    this.updateDisplay(); // Rebuild with new theme\n  }\n}\n```\n\n### 模式：无效时重建\n\n对于内容复杂的组件：\n\n```typescript\nclass ComplexComponent extends Container {\n  private data: SomeData;\n\n  constructor(data: SomeData) {\n    super();\n    this.data = data;\n    this.rebuild();\n  }\n\n  private rebuild(): void {\n    this.clear();  // Remove all children\n\n    // Build UI with current theme\n    this.addChild(new Text(theme.fg(\"accent\", theme.bold(\"Title\")), 1, 0));\n    this.addChild(new Spacer(1));\n\n    for (const item of this.data.items) {\n      const color = item.active ? \"success\" : \"muted\";\n      this.addChild(new Text(theme.fg(color, item.label), 1, 0));\n    }\n  }\n\n  override invalidate(): void {\n    super.invalidate();\n    this.rebuild();\n  }\n}\n```\n\n### 当这很重要时\n\n在以下情况下需要此模式：\n\n1. **预烘焙主题颜色** - 使用 `theme.fg()` 或 `theme.bg()` 创建存储在子组件中的样式字符串\n2. **语法突出显示** - 使用 `highlightCode()` 应用基于主题的语法颜色\n3. **复杂布局** - 构建嵌入主题颜色的子组件树\n\n在以下情况下不需要此模式：\n\n1. **使用主题回调** - 传递渲染期间调用的函数，例如 `(text) => theme.fg(\"accent\", text)`\n2. **简单容器** - 只需对其他组件进行分组，而不添加主题内容\n3. **无状态渲染** - 在每个 `render()` 调用中计算新鲜的主题输出（无缓存）\n\n## 常见模式\n\n这些模式涵盖了扩展中最常见的 UI 需求。 **复制这些模式而不是从头开始构建。**\n\n### 模式 1：选择对话框（SelectList）\n\n用于让用户从选项列表中进行选择。使用`@earendil-works/pi-tui`中的`SelectList`和`DynamicBorder`进行取景。\n\n```typescript\nimport type { ExtensionAPI } from \"@earendil-works/pi-coding-agent\";\nimport { DynamicBorder } from \"@earendil-works/pi-coding-agent\";\nimport { Container, type SelectItem, SelectList, Text } from \"@earendil-works/pi-tui\";\n\npi.registerCommand(\"pick\", {\n  handler: async (_args, ctx) => {\n    const items: SelectItem[] = [\n      { value: \"opt1\", label: \"Option 1\", description: \"First option\" },\n      { value: \"opt2\", label: \"Option 2\", description: \"Second option\" },\n      { value: \"opt3\", label: \"Option 3\" },  // description is optional\n    ];\n\n    const result = await ctx.ui.custom<string | null>((tui, theme, _kb, done) => {\n      const container = new Container();\n\n      // Top border\n      container.addChild(new DynamicBorder((s: string) => theme.fg(\"accent\", s)));\n\n      // Title\n      container.addChild(new Text(theme.fg(\"accent\", theme.bold(\"Pick an Option\")), 1, 0));\n\n      // SelectList with theme\n      const selectList = new SelectList(items, Math.min(items.length, 10), {\n        selectedPrefix: (t) => theme.fg(\"accent\", t),\n        selectedText: (t) => theme.fg(\"accent\", t),\n        description: (t) => theme.fg(\"muted\", t),\n        scrollInfo: (t) => theme.fg(\"dim\", t),\n        noMatch: (t) => theme.fg(\"warning\", t),\n      });\n      selectList.onSelect = (item) => done(item.value);\n      selectList.onCancel = () => done(null);\n      container.addChild(selectList);\n\n      // Help text\n      container.addChild(new Text(theme.fg(\"dim\", \"↑↓ navigate • enter select • esc cancel\"), 1, 0));\n\n      // Bottom border\n      container.addChild(new DynamicBorder((s: string) => theme.fg(\"accent\", s)));\n\n      return {\n        render: (w) => container.render(w),\n        invalidate: () => container.invalidate(),\n        handleInput: (data) => { selectList.handleInput(data); tui.requestRender(); },\n      };\n    });\n\n    if (result) {\n      ctx.ui.notify(`Selected: ${result}`, \"info\");\n    }\n  },\n});\n```\n\n**示例：** [preset.ts](../examples/extensions/preset.ts)、[tools.ts](../examples/extensions/tools.ts)\n\n### 模式 2：带取消的异步操作 (BorderedLoader)\n\n对于需要时间并且应该可以取消的操作。 `BorderedLoader` 显示一个旋转器并处理转义以取消。\n\n```typescript\nimport { BorderedLoader } from \"@earendil-works/pi-coding-agent\";\n\npi.registerCommand(\"fetch\", {\n  handler: async (_args, ctx) => {\n    const result = await ctx.ui.custom<string | null>((tui, theme, _kb, done) => {\n      const loader = new BorderedLoader(tui, theme, \"Fetching data...\");\n      loader.onAbort = () => done(null);\n\n      // Do async work\n      fetchData(loader.signal)\n        .then((data) => done(data))\n        .catch(() => done(null));\n\n      return loader;\n    });\n\n    if (result === null) {\n      ctx.ui.notify(\"Cancelled\", \"info\");\n    } else {\n      ctx.ui.setEditorText(result);\n    }\n  },\n});\n```\n\n**示例：** [qna.ts](../examples/extensions/qna.ts)、[handoff.ts](../examples/extensions/handoff.ts)\n\n### 模式 3：设置/切换（SettingsList）\n\n用于切换多个设置。将 `@earendil-works/pi-tui` 中的 `SettingsList` 与 `getSettingsListTheme()` 结合使用。\n\n```typescript\nimport { getSettingsListTheme } from \"@earendil-works/pi-coding-agent\";\nimport { Container, type SettingItem, SettingsList, Text } from \"@earendil-works/pi-tui\";\n\npi.registerCommand(\"settings\", {\n  handler: async (_args, ctx) => {\n    const items: SettingItem[] = [\n      { id: \"verbose\", label: \"Verbose mode\", currentValue: \"off\", values: [\"on\", \"off\"] },\n      { id: \"color\", label: \"Color output\", currentValue: \"on\", values: [\"on\", \"off\"] },\n    ];\n\n    await ctx.ui.custom((_tui, theme, _kb, done) => {\n      const container = new Container();\n      container.addChild(new Text(theme.fg(\"accent\", theme.bold(\"Settings\")), 1, 1));\n\n      const settingsList = new SettingsList(\n        items,\n        Math.min(items.length + 2, 15),\n        getSettingsListTheme(),\n        (id, newValue) => {\n          // Handle value change\n          ctx.ui.notify(`${id} = ${newValue}`, \"info\");\n        },\n        () => done(undefined),  // On close\n        { enableSearch: true }, // Optional: enable fuzzy search by label\n      );\n      container.addChild(settingsList);\n\n      return {\n        render: (w) => container.render(w),\n        invalidate: () => container.invalidate(),\n        handleInput: (data) => settingsList.handleInput?.(data),\n      };\n    });\n  },\n});\n```\n\n**示例：** [tools.ts](../examples/extensions/tools.ts)\n\n### 模式 4：持续状态指示器\n\n在页脚中显示在渲染过程中持续存在的状态。适用于模式指示器。\n\n```typescript\n// Set status (shown in footer)\nctx.ui.setStatus(\"my-ext\", ctx.ui.theme.fg(\"accent\", \"● active\"));\n\n// Clear status\nctx.ui.setStatus(\"my-ext\", undefined);\n```\n\n**示例：** [status-line.ts](../examples/extensions/status-line.ts)、[plan-mode/index.ts](../examples/extensions/plan-mode/index.ts)、[preset.ts](../examples/extensions/preset.ts)\n\n### 模式 4b：工作指标定制\n\n自定义 pi 传输响应时显示的内联工作指示器。\n\n```typescript\n// Static indicator\nctx.ui.setWorkingIndicator({ frames: [ctx.ui.theme.fg(\"accent\", \"●\")] });\n\n// Custom animated indicator\nctx.ui.setWorkingIndicator({\n  frames: [\n    ctx.ui.theme.fg(\"dim\", \"·\"),\n    ctx.ui.theme.fg(\"muted\", \"•\"),\n    ctx.ui.theme.fg(\"accent\", \"●\"),\n    ctx.ui.theme.fg(\"muted\", \"•\"),\n  ],\n  intervalMs: 120,\n});\n\n// Hide the indicator entirely\nctx.ui.setWorkingIndicator({ frames: [] });\n\n// Restore pi's default spinner\nctx.ui.setWorkingIndicator();\n```\n\n这只影响正常的流媒体工作指标。压实和重试加载器保持其内置样式。自定义框架逐字渲染，因此扩展必须在需要时添加自己的颜色。\n\n**示例：** [working-indicator.ts](../examples/extensions/working-indicator.ts)\n\n### 模式 5：编辑器上方/下方的小部件\n\n在输入编辑器上方或下方显示持久内容。适合待办事项列表、进度。\n\n```typescript\n// Simple string array (above editor by default)\nctx.ui.setWidget(\"my-widget\", [\"Line 1\", \"Line 2\"]);\n\n// Render below the editor\nctx.ui.setWidget(\"my-widget\", [\"Line 1\", \"Line 2\"], { placement: \"belowEditor\" });\n\n// Or with theme\nctx.ui.setWidget(\"my-widget\", (_tui, theme) => {\n  const lines = items.map((item, i) =>\n    item.done\n      ? theme.fg(\"success\", \"✓ \") + theme.fg(\"muted\", item.text)\n      : theme.fg(\"dim\", \"○ \") + item.text\n  );\n  return {\n    render: () => lines,\n    invalidate: () => {},\n  };\n});\n\n// Clear\nctx.ui.setWidget(\"my-widget\", undefined);\n```\n\n**示例：** [plan-mode/index.ts](../examples/extensions/plan-mode/index.ts)\n\n### 模式 6：自定义页脚\n\n更换页脚。 `footerData` 公开扩展无法访问的数据。\n\n```typescript\nctx.ui.setFooter((tui, theme, footerData) => ({\n  invalidate() {},\n  render(width: number): string[] {\n    // footerData.getGitBranch(): string | null\n    // footerData.getExtensionStatuses(): ReadonlyMap<string, string>\n    return [`${ctx.model?.id} (${footerData.getGitBranch() || \"no git\"})`];\n  },\n  dispose: footerData.onBranchChange(() => tui.requestRender()), // reactive\n}));\n\nctx.ui.setFooter(undefined); // restore default\n```\n\n可通过 `ctx.sessionManager.getBranch()` 和 `ctx.model` 获取代币统计信息。\n\n**示例：** [custom-footer.ts](../examples/extensions/custom-footer.ts)\n\n### 模式7：自定义编辑器（vim模式等）\n\n用自定义实现替换主输入编辑器。对于模式编辑 (vim)、不同的键绑定 (emacs) 或专门的输入处理很有用。\n\n```typescript\nimport { CustomEditor, type ExtensionAPI } from \"@earendil-works/pi-coding-agent\";\nimport { matchesKey, truncateToWidth } from \"@earendil-works/pi-tui\";\n\ntype Mode = \"normal\" | \"insert\";\n\nclass VimEditor extends CustomEditor {\n  private mode: Mode = \"insert\";\n\n  handleInput(data: string): void {\n    // Escape: switch to normal mode, or pass through for app handling\n    if (matchesKey(data, \"escape\")) {\n      if (this.mode === \"insert\") {\n        this.mode = \"normal\";\n        return;\n      }\n      // In normal mode, escape aborts agent (handled by CustomEditor)\n      super.handleInput(data);\n      return;\n    }\n\n    // Insert mode: pass everything to CustomEditor\n    if (this.mode === \"insert\") {\n      super.handleInput(data);\n      return;\n    }\n\n    // Normal mode: vim-style navigation\n    switch (data) {\n      case \"i\": this.mode = \"insert\"; return;\n      case \"h\": super.handleInput(\"\\x1b[D\"); return; // Left\n      case \"j\": super.handleInput(\"\\x1b[B\"); return; // Down\n      case \"k\": super.handleInput(\"\\x1b[A\"); return; // Up\n      case \"l\": super.handleInput(\"\\x1b[C\"); return; // Right\n    }\n    // Pass unhandled keys to super (ctrl+c, etc.), but filter printable chars\n    if (data.length === 1 && data.charCodeAt(0) >= 32) return;\n    super.handleInput(data);\n  }\n\n  render(width: number): string[] {\n    const lines = super.render(width);\n    // Add mode indicator to bottom border (use truncateToWidth for ANSI-safe truncation)\n    if (lines.length > 0) {\n      const label = this.mode === \"normal\" ? \" NORMAL \" : \" INSERT \";\n      const lastLine = lines[lines.length - 1]!;\n      // Pass \"\" as ellipsis to avoid adding \"...\" when truncating\n      lines[lines.length - 1] = truncateToWidth(lastLine, width - label.length, \"\") + label;\n    }\n    return lines;\n  }\n}\n\nexport default function (pi: ExtensionAPI) {\n  pi.on(\"session_start\", (_event, ctx) => {\n    // Factory receives the TUI, theme, and keybindings from the app\n    ctx.ui.setEditorComponent((tui, theme, keybindings) =>\n      new VimEditor(tui, theme, keybindings)\n    );\n  });\n}\n```\n\n**要点：**\n\n- **扩展 `CustomEditor`** （不是基础 `Editor`）以获取应用程序键绑定（转义以中止、ctrl+d 退出、模型切换等）\n- **对于您不处理的钥匙，请致电 `super.handleInput(data)`**\n- **工厂模式**：`setEditorComponent`接收一个获取`tui`、`theme`和`keybindings`的工厂函数\n- **通过`undefined`**恢复默认编辑器：`ctx.ui.setEditorComponent(undefined)`\n\n**示例：** [modal-editor.ts](../examples/extensions/modal-editor.ts)\n\n## 关键规则\n\n1. **始终使用回调中的主题** - 不要直接导入主题。使用 `ctx.ui.custom((tui, theme, keybindings, done) =>...)` 回调中的 `theme`。\n\n2. **始终输入 DynamicBorder 颜色参数** - 写入 `(s: string) => theme.fg(\"accent\", s)`，而不是 `(s) => theme.fg(\"accent\", s)`。\n\n3. **状态改变后调用tui.requestRender()** - 在`handleInput`中，更新状态后调用`tui.requestRender()`。\n\n4. **返回三方法对象** - 自定义组件需要`{ render, invalidate, handleInput }`。\n\n5. **使用现有组件** - `SelectList`、`SettingsList`、`BorderedLoader`覆盖 90% 的情况。不要重建它们。\n\n## 示例\n\n- **选择 UI**：[examples/extensions/preset.ts](../examples/extensions/preset.ts) - 具有 DynamicBorder 框架的 SelectList\n- **与取消异步**：[examples/extensions/qna.ts](../examples/extensions/qna.ts) - 用于 LLM 调用的 BorderedLoader\n- **设置切换**：[examples/extensions/tools.ts](../examples/extensions/tools.ts) - 用于工具启用/禁用的设置列表\n- **状态指示器**：[examples/extensions/plan-mode/index.ts](../examples/extensions/plan-mode/index.ts) - setStatus 和 setWidget\n- **工作指示器**：[examples/extensions/working-indicator.ts](../examples/extensions/working-indicator.ts) - setWorkingIndicator\n- **自定义页脚**：[examples/extensions/custom-footer.ts](../examples/extensions/custom-footer.ts) - 带统计信息的 setFooter\n- **自定义编辑器**：[examples/extensions/modal-editor.ts](../examples/extensions/modal-editor.ts) - 类似 Vim 的模式编辑\n- **贪吃蛇游戏**：[examples/extensions/snake.ts](../examples/extensions/snake.ts) - 带键盘输入、游戏循环的完整游戏\n- **自定义工具渲染**：[examples/extensions/todo.ts](../examples/extensions/todo.ts) - renderCall 和 renderResult","sourceFile":"tui.md"},"usage":{"title":"使用Pi","markdown":"此页面收集不适合快速入门页面的日常使用详细信息。\n\n## 互动模式\n\n<p align=\"center\"><img src=\"images/interactive-mode.png\" alt=\"Interactive Mode\" width=\"600\"></p>\n\n该界面有四个主要区域：\n\n- **启动标题** - 快捷方式、加载的context files、prompt templates、技能和扩展\n- **消息** - 用户消息、助手响应、工具调用、工具结果、通知、错误和扩展 UI\n- **编辑器** - 您输入的位置；边框颜色表示当前思维水平\n- **页脚** - 工作目录、会话名称、令牌/缓存使用情况、成本、上下文使用情况和当前模型。总计包括助理响应、工具报告的使用情况以及摘要生成。\n\n编辑器可以暂时替换为内置 UI（例如 `/settings`）或自定义扩展 UI。\n\n### 编辑器功能\n\n| 特征 | 如何 |\n|---------|-----|\n| 文件参考 | 输入 `@` 模糊搜索项目文件 |\n| 路径补全 | 按 T​​ab 键完成路径 |\n| 多行输入 | Shift+Enter，或 Windows 终端上的 Ctrl+Enter |\n| 复制回复 | Ctrl+X 复制最后一条助手消息；在`/tree`中，它复制所选消息 |\n| 图片 | 在 Windows 上使用 Ctrl+V、Alt+V 粘贴，或拖到终端中 |\n| 外壳命令 | `!command` 运行并将输出发送到模型 |\n| 隐藏的 shell 命令 | `!!command` 运行而不将输出发送到模型 |\n| 外部编辑 | Ctrl+G 在 Windows 上打开 `externalEditor`、`$VISUAL`、`$EDITOR`、记事本，或在其他地方打开 `nano` |\n\n有关所有快捷方式和自定义，请参阅 [Keybindings](keybindings.md)。\n\n## 斜线命令\n\n在编辑器中输入 `/` 打开命令补全。 Extensions可以注册自定义命令，技能与`/skill:name`相同，prompt templates通过`/templatename`扩展。\n\n| 命令 | 描述 |\n|---------|-------------|\n| `/login`, `/logout` | 管理 OAuth 或 API 密钥凭证 |\n| [`/llama`](llama-cpp.md) | 下载、加载和卸载 llama.cpp 路由器模型 |\n| `/model` | 切换型号 |\n| `/scoped-models` | 启用/禁用 Ctrl+P 循环模型 |\n| `/settings` | 思维层次、主题、信息传递、传输 |\n| `/resume` | Pick 之前的会议 |\n| `/new` | 开始新会话 |\n| `/name <name>` | 设置会话显示名称 |\n| `/session` | 显示会话文件、ID、消息、令牌和成本 |\n| `/tree` | 跳转到会话中的任意一点并从那里继续 |\n| `/trust` | 保存项目信任决策以供未来会议使用 |\n| `/fork` | 根据先前的用户消息创建新会话 |\n| `/clone` | 将当前活动分支复制到新会话中 |\n| `/compact [prompt]` | 手动压缩上下文，可选择使用自定义指令 |\n| `/copy` | 将最后一条助理消息复制到剪贴板 |\n| `/export [file]` | 将会话导出为 HTML 或 JSONL |\n| `/import <file>` | 从 JSONL 文件导入并恢复会话 |\n| `/share` | 上传为私有 GitHub 要点，并带有可共享的 HTML 链接 |\n| `/reload` | 重新加载按键绑定、扩展、技能、提示、主题和 context files |\n| `/hotkeys` | 显示所有键盘快捷键 |\n| `/changelog` | 显示版本历史记录 |\n| `/quit` | 退出圆周率 |\n\n## 消息队列\n\n您可以在代理仍在工作时提交消息：\n\n- **Enter** 将转向消息排队，在当前助手轮完成执行其工具调用后传递。\n- **Alt+Enter** 将后续消息排队，在代理完成所有工作后发送。\n- **Escape** 中止排队消息并将其恢复到编辑器。\n- **Alt+Up** 将排队的消息检索回编辑器。\n\n在 Windows 终端上，Alt+Enter 默认为全屏。如果您希望 pi 接收快捷方式，请按照 [Terminal setup](terminal-setup.md) 中的说明重新映射它。\n\n使用`steeringMode`和`followUpMode`配置[Settings](settings.md)中的交付。\n\n## 会话\n\n会话自动保存到`~/.pi/agent/sessions/`，按工作目录组织。\n\n```bash\npi -c                  # Continue most recent session\npi -r                  # Browse and select a session\npi --no-session        # Ephemeral mode; do not save\npi --name \"my task\"    # Set session display name at startup\npi --session <path|id> # Use a specific session file or session ID\npi --fork <path|id>    # Fork a session into a new session file\n```\n\n有用的会话命令：\n\n- `/session` 显示当前会话文件和ID。\n- `/tree` 导航文件内session tree 并可以总结废弃的分支。\n- `/fork` 根据较早的用户消息创建新会话。\n- `/clone` 将当前活动分支复制到新的会话文件中。\n- `/compact` 将旧消息总结为自由上下文。\n\n详情请参阅[Sessions](sessions.md)和[Compaction](compaction.md)。\n\n## 上下文文件\n\nPi 在启动时加载 `AGENTS.md` 或 `CLAUDE.md`：\n\n- `~/.pi/agent/AGENTS.md` 用于全局指令\n- 父目录，从当前工作目录向上走\n- 当前目录\n\n如果目录包含 `AGENTS.override.md`，Pi 会从该目录加载它，而不是 `AGENTS.md` 或 `CLAUDE.md`。其他目录中的上下文文件仍然正常分层。\n\n使用 context files 表示项目约定、命令、安全规则和首选项。使用 `--no-context-files` 或 `-nc` 禁用加载。\n\n### 系统提示文件\n\n将默认的系统提示替换为：\n\n- `.pi/SYSTEM.md` 对于一个项目\n- 全球`~/.pi/agent/SYSTEM.md`\n\n附加到默认提示，而不在任一位置将其替换为 `APPEND_SYSTEM.md`。\n\n### 项目信托\n\n在交互式启动时，pi 在信任包含项目本地设置、资源或项目 `.agents/skills` 的项目文件夹之前会询问，并且在 `~/.pi/agent/trust.json` 中没有保存该文件夹或父文件夹的决定。信任项目允许 pi 加载 `.pi/settings.json` 和 `.pi` 资源、安装缺少的项目包以及执行项目扩展。\n\n在做出信任决定之前，pi 仅加载 context files、用户/全局扩展和 CLI `-e` 扩展，以便它们可以处理 `project_trust` 事件。仅在项目受信任后才会加载项目本地扩展、项目包管理的扩展和项目设置。当从当前进程中尚未解析信任的不同 cwd 切换到会话时，此分割也适用。\n\n非交互模式（`-p`、`--mode json` 和 `--mode rpc`）不显示信任提示。如果没有适用的已保存信任决策，他们将使用全局设置中的`defaultProjectTrust`：`ask`（默认）和`never`忽略这些项目资源，而`always`信任它们。通过 `--approve`/`-a` 或 `--no-approve`/`-na` 覆盖一次运行的项目信任。\n\n如果没有适用扩展或保存的决策，则`defaultProjectTrust`控制后备行为。将`~/.pi/agent/settings.json`中的`\"ask\"`、`\"always\"`或`\"never\"`设置为`\"ask\"`、`\"always\"`或`\"never\"`，或将其更改为`/settings`。\n\n`pi config` 和 package 命令使用相同的项目信任流程，但 `pi update` 从不提示。传递 `--approve` 以信任某个命令的项目本地设置，或传递 `--no-approve` 以忽略它们。\n\n在交互模式下使用 `/trust` 可以为将来的会话保存项目信任决策，包括对直接父文件夹的信任。只写`~/.pi/agent/trust.json`；当前会话不会重新加载，因此请重新启动 pi 以使更改生效。\n\n\n## 导出和共享会话\n\n使用 `/export [file]` 将会话写入 HTML。\n\n使用 `/share` 上传带有可共享 HTML 链接的私有 GitHub 要点。\n\n如果您使用 pi 进行开源工作，并希望发布模型、提示、工具和评估研究的会话，请参阅[`badlogic/pi-share-hf`](https://github.com/badlogic/pi-share-hf)。它将会话发布到 Hugging Face 数据集。\n\n## CLI 参考\n\n```bash\npi [options] [@files...] [messages...]\n```\n\n### 包命令\n\n```bash\npi install <source> [-l]     # Install package, -l for project-local\npi remove <source> [-l]      # Remove package\npi uninstall <source> [-l]   # Alias for remove\npi update [source|self|pi]   # Update pi only, or one package source\npi update --all              # Update pi and packages; reconcile pinned git refs\npi update --extensions       # Update packages only; reconcile pinned git refs\npi update --models           # Refresh model catalogs only\npi update --self             # Update pi only\npi update --extension <src>  # Update one package\npi list                      # List installed packages\npi config                    # Enable/disable package resources\n```\n\n这些命令管理 pi 包，`pi update` 可以更新 pi CLI 安装。要卸载 pi 本身，请参阅 [Quickstart](quickstart.md#uninstall)。 `pi config` 和项目包命令接受 `--approve`/`--no-approve` 以信任或忽略一个命令的项目本地设置。 `pi update`从不提示项目信任。\n\n有关软件包来源和安全说明，请参阅[Pi Packages](packages.md)。\n\n### 模式\n\n| 旗帜 | 描述 |\n|------|-------------|\n| 默认 | 交互模式 |\n| `-p`, `--print` | 打印响应并退出 |\n| `--mode json` | 将所有事件输出为JSON行；见[JSON mode](json.md) |\n| `--mode rpc` | RPC模式超过stdin/stdout；见[RPC mode](rpc.md) |\n| `--export <in> [out]` | 将会话导出为 HTML |\n\n在打印模式下，pi 还会读取管道 stdin 并将其合并到初始提示中：\n\n```bash\ncat README.md | pi -p \"Summarize this text\"\n```\n\n### 型号选项\n\n| 选项 | 描述 |\n|--------|-------------|\n| `--provider <name>` | 提供者，例如 `anthropic`、`openai` 或 `google` |\n| `--model <pattern>` | 型号图案或 ID；支持`provider/id`和可选的`:<thinking>` |\n| `--api-key <key>` | API key，覆盖环境变量 |\n| `--thinking <level>` | `off`, `minimal`, `low`, `medium`, `high`, `xhigh`, `max` |\n| `--models <patterns>` | 用于 Ctrl+P 循环的逗号分隔模式 |\n| `--list-models [search]` | 列出可用型号 |\n\n### 会话选项\n\n| 选项 | 描述 |\n|--------|-------------|\n| `-c`, `--continue` | 继续最近的会话 |\n| `-r`, `--resume` | 浏览并选择一个会话 |\n| `--会话<路径\\ | id>` | 使用特定的会话文件或部分UUID |\n| `--fork <路径\\ | id>` | 将会话文件或部分 UUID 分叉到新会话中 |\n| `--session-dir <dir>` | 自定义会话存储目录 |\n| `--no-session` | 短暂模式；不保存 |\n| `--name <name>`, `-n <name>` | 设置启动时的会话显示名称 |\n\n### 工具选项\n\n| 选项 | 描述 |\n|--------|-------------|\n| `--tools <list>`, `-t <list>` | 将特定内置、扩展和自定义工具列入白名单 |\n| `--exclude-tools <list>`, `-xt <list>` | 禁用特定的内置、扩展和自定义工具 |\n| `--no-builtin-tools`, `-nbt` | 禁用内置工具但保持扩展/自定义工具启用 |\n| `--no-tools`, `-nt` | 禁用所有工具 |\n\n内置工具：`read`、`bash`、`edit`、`write`、`grep`、`find`、`ls`。\n\n### 资源选项\n\n| 选项 | 描述 |\n|--------|-------------|\n| `-e`, `--extension <source>` | 从路径、npm或git加载扩展；可重复的 |\n| `--no-extensions` | 禁用扩展发现 |\n| `--skill <path>` | 加载技能；可重复的 |\n| `--no-skills` | 禁用技能发现 |\n| `--prompt-template <path>` | 加载提示模板；可重复的 |\n| `--no-prompt-templates` | 禁用提示模板发现 |\n| `--theme <path>` | 加载主题；可重复的 |\n| `--no-themes` | 禁用主题发现 |\n| `--no-context-files`, `-nc` | 禁用 `AGENTS.md` 和 `CLAUDE.md` 发现 |\n\n将 `--no-*` 与显式标志结合起来即可准确加载您需要的内容，忽略设置。例子：\n\n```bash\npi --no-extensions -e ./my-extension.ts\n```\n\n### 其他选项\n\n| 选项 | 描述 |\n|--------|-------------|\n| `--system-prompt <text>` | 替换默认提示； context files 技能仍附加 |\n| `--append-system-prompt <text>` | 附加到系统提示符 |\n| `--tui-mode <mode>` | TUI模式：`regular`（默认）或实验性`fullscreen` |\n| `--verbose` | 强制详细启动 |\n| `-a`, `--approve` | 信任本次运行的项目本地文件 |\n| `-na`, `--no-approve` | 忽略本次运行的项目本地文件 |\n| `-h`, `--help` | 显示帮助 |\n| `-v`, `--version` | 显示版本 |\n\n在 `fullscreen` 模式下，记录在终端视口内滚动，而排队消息、工作状态、扩展小部件、编辑器和页脚保持固定在底部。鼠标/触控板输入滚动指针下方的区域；键盘视口操作始终保持可用。内联图像可在支持 Kitty 图形协议（包括 Kitty 和 Ghostty）的终端中工作。在 iTerm2 中，它们呈现为文本占位符，因为其内联图像协议无法在应用程序拥有的滚动期间删除或裁剪位置。在`regular`模式下，pi使用主屏幕和终端拥有的回滚，iTerm2内联图像继续正常渲染。\n\n在`/settings`中设置**TUI模式**可立即在`regular`和`fullscreen`之间切换，并为将来的会话选择默认值。 **全屏退出输出** 控制退出全屏是否打印最终记录或恢复前一屏幕并仅打印会话恢复提示。\n\n### 文件参数\n\n使用 `@` 为文件添加前缀以将其包含在消息中：\n\n```bash\npi @prompt.md \"Answer this\"\npi -p @screenshot.png \"What's in this image?\"\npi @code.ts @test.ts \"Review these files\"\n```\n\n### 示例\n\n```bash\n# Interactive with initial prompt\npi \"List all .ts files in src/\"\n\n# Non-interactive\npi -p \"Summarize this codebase\"\n\n# Non-interactive with piped stdin\ncat README.md | pi -p \"Summarize this text\"\n\n# Named one-shot session\npi --name \"release audit\" -p \"Audit this repository\"\n\n# Different model\npi --provider openai --model gpt-4o \"Help me refactor\"\n\n# Model with provider prefix\npi --model openai/gpt-4o \"Help me refactor\"\n\n# Model with thinking level shorthand\npi --model sonnet:high \"Solve this complex problem\"\n\n# Limit model cycling\npi --models \"claude-*,gpt-4o\"\n\n# Read-only mode\npi --tools read,grep,find,ls -p \"Review the code\"\n\n# Disable one extension or built-in tool while keeping the rest available\npi --exclude-tools ask_question\n```\n\n## 设计原则\n\nPi 保持核心较小，并将特定于工作流的行为推送到扩展、技能、prompt templates 和包中。\n\n它故意不包含内置MCP、子代理、权限弹出窗口、计划模式、待办事项或背景bash。您可以将这些工作流程构建或安装为扩展或包，或者使用容器和tmux等外部工具。\n\n要了解完整的原理，请阅读[blog post](https://mariozechner.at/posts/2025-11-30-pi-coding-agent/)。","sourceFile":"usage.md"},"windows":{"title":"Windows 设置","markdown":"Pi 需要 Windows 上的 bash shell。检查地点（按顺序）：\n\n1. 从 `~/.pi/agent/settings.json` 开始的自定义路径\n2. Git 猛击 (`C:\\Program Files\\Git\\bin\\bash.exe`)\n3. 路径上的`bash.exe`（Cygwin、MSYS2、WSL）\n\n对于大多数用户来说，[Git for Windows](https://git-scm.com/download/win)就足够了。\n\n## 自定义外壳路径\n\n```json\n{\n  \"shellPath\": \"C:\\\\cygwin64\\\\bin\\\\bash.exe\"\n}\n```","sourceFile":"windows.md"}}},"navigation":{"zh-CN":[{"title":"从这里开始","items":[{"title":"Pi 文档","path":"/docs/latest","slug":"index"},{"title":"快速开始","path":"/docs/latest/quickstart","slug":"quickstart"},{"title":"使用Pi","path":"/docs/latest/usage","slug":"usage"},{"title":"Providers","path":"/docs/latest/providers","slug":"providers"},{"title":"安全","path":"/docs/latest/security","slug":"security"},{"title":"容器化","path":"/docs/latest/containerization","slug":"containerization"},{"title":"设置","path":"/docs/latest/settings","slug":"settings"},{"title":"快捷键","path":"/docs/latest/keybindings","slug":"keybindings"},{"title":"会话","path":"/docs/latest/sessions","slug":"sessions"},{"title":"压缩和分支汇总","path":"/docs/latest/compaction","slug":"compaction"}]},{"title":"自定义","items":[{"title":"Extensions","path":"/docs/latest/extensions","slug":"extensions"},{"title":"Skills","path":"/docs/latest/skills","slug":"skills"},{"title":"Prompt Templates","path":"/docs/latest/prompt-templates","slug":"prompt-templates"},{"title":"Themes","path":"/docs/latest/themes","slug":"themes"},{"title":"Pi Packages","path":"/docs/latest/packages","slug":"packages"},{"title":"定制Models","path":"/docs/latest/models","slug":"models"},{"title":"定制Providers","path":"/docs/latest/custom-provider","slug":"custom-provider"}]},{"title":"参考","items":[{"title":"会话文件格式","path":"/docs/latest/session-format","slug":"session-format"}]},{"title":"编程式使用","items":[{"title":"SDK","path":"/docs/latest/sdk","slug":"sdk"},{"title":"RPC模式","path":"/docs/latest/rpc","slug":"rpc"},{"title":"JSON 事件流模式","path":"/docs/latest/json","slug":"json"},{"title":"TUI 组件","path":"/docs/latest/tui","slug":"tui"}]},{"title":"平台设置","items":[{"title":"Windows 设置","path":"/docs/latest/windows","slug":"windows"},{"title":"Termux（安卓）设置","path":"/docs/latest/termux","slug":"termux"},{"title":"tmux 设置","path":"/docs/latest/tmux","slug":"tmux"},{"title":"终端设置","path":"/docs/latest/terminal-setup","slug":"terminal-setup"},{"title":"Shell Aliases","path":"/docs/latest/shell-aliases","slug":"shell-aliases"}]},{"title":"开发","items":[{"title":"开发","path":"/docs/latest/development","slug":"development"}]}]}}
