{"locale":"zh-TW","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-TW":{"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}` 會插入環境變數，`$$` 會輸出字面 `$`，`$!` 會輸出字面 `!`。\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` | Google 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` in TUI and RPC modes. `false` in print mode (`-p`) and JSON mode. Use this to guard dialog methods (`select`, `confirm`, `input`, `editor`) and fire-and-forget methods (`notify`, `setStatus`, `setWidget`, `setTitle`) that work in TUI and RPC modes. In RPC mode, some TUI-specific methods are no-ops or return default values (see [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(name, 配置)\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`（completions 也會傳送 `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`）；按 Tab 鍵可在全域模式和專案本地模式之間切換。使用 `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":"提示模板","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| Google雙子座 | `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（網路、桌面、行動裝置）\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":"外殼別名","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":"主題","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-TW":[{"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":"提示模板","path":"/docs/latest/prompt-templates","slug":"prompt-templates"},{"title":"主題","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":"外殼別名","path":"/docs/latest/shell-aliases","slug":"shell-aliases"}]},{"title":"開發","items":[{"title":"發展","path":"/docs/latest/development","slug":"development"}]}]}}
