{"locale":"vi","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":{"vi":{"compaction":{"title":"Tóm tắt nén và phân nhánh","markdown":"LLM có cửa sổ ngữ cảnh hạn chế. Khi các cuộc trò chuyện quá dài, Pi sử dụng tính năng nén để tóm tắt nội dung cũ hơn trong khi vẫn giữ nguyên tác phẩm gần đây. Trang này bao gồm cả tự động nén và branch summarization.\n\n**Tệp nguồ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) - Logic tự động nén\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) - Tổng hợp chi nhánh\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) - Tiện ích chia sẻ (theo dõi tập tin, tuần tự hóa)\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) - Loại mục nhập (`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) - Các loại sự kiện mở rộng\n\nĐể biết các định nghĩa TypeScript trong dự án của bạn, hãy kiểm tra `node_modules/@earendil-works/pi-coding-agent/dist/`.\n\n## Tổng quan\n\nPi có hai cơ chế tóm tắt:\n\n| Cơ chế | Cò súng | Mục đích |\n|-----------|---------|---------|\n| nén chặt | Ngữ cảnh vượt quá ngưỡng hoặc `/compact` | Tóm tắt tin nhắn cũ để giải phóng ngữ cảnh |\n| Tóm tắt chi nhánh | `/tree` điều hướng | Bảo toàn bối cảnh khi chuyển nhánh |\n\nCả hai đều sử dụng cùng một định dạng tóm tắt có cấu trúc và theo dõi các hoạt động tập tin một cách tích lũy. Yêu cầu nén và tóm tắt nhánh sử dụng ID phiên định tuyến mới và, nếu được nhà cung cấp hỗ trợ, sẽ vô hiệu hóa tính năng ghi vào bộ nhớ đệm vì những lời nhắc một lần này khó có thể được sử dụng lại.\n\n## nén chặt\n\n### Khi nó kích hoạt\n\nKích hoạt tự động nén khi:\n\n```\ncontextTokens > contextWindow - reserveTokens\n```\n\nTheo mặc định, `reserveTokens` là 16384 mã thông báo (có thể định cấu hình trong `~/.pi/agent/settings.json` hoặc `<project-dir>/.pi/settings.json`). Điều này nhường chỗ cho phản hồi của LLM.\n\nBạn cũng có thể kích hoạt thủ công bằng `/compact [instructions]`, trong đó các hướng dẫn tùy chọn tập trung vào phần tóm tắt.\n\n### Nó hoạt động như thế nào\n\n1. **Tìm điểm cắt**: Đi lùi từ tin nhắn mới nhất, tích lũy ước tính mã thông báo cho đến khi đạt đến `keepRecentTokens` (20k mặc định, có thể định cấu hình trong `~/.pi/agent/settings.json` hoặc `<project-dir>/.pi/settings.json`)\n2. **Trích xuất tin nhắn**: Thu thập tin nhắn từ ranh giới được giữ trước đó (hoặc bắt đầu phiên) cho đến điểm cắt\n3. **Tạo bản tóm tắt**: Gọi LLM để tóm tắt bằng định dạng có cấu trúc, chuyển bản tóm tắt trước đó dưới dạng ngữ cảnh lặp lại khi có\n4. **Nối mục nhập**: Lưu `CompactionEntry` với bản tóm tắt và `firstKeptEntryId`\n5. **Tải lại**: Tải lại phiên, sử dụng tóm tắt + tin nhắn từ `firstKeptEntryId` trở đi\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\nTrong các lần nén lặp lại, khoảng tóm tắt bắt đầu ở ranh giới được giữ lại của lần nén trước đó (`firstKeptEntryId`), chứ không phải ở chính mục nhập nén, quay trở lại mục nhập sau lần nén trước đó nếu không thể tìm thấy mục nhập được lưu giữ đó trong đường dẫn. Điều này bảo tồn các thông báo còn sót lại sau quá trình nén trước đó bằng cách đưa chúng vào lần tóm tắt tiếp theo. Pi cũng tính toán lại `tokensBefore` từ bối cảnh phiên được xây dựng lại trước khi viết `CompactionEntry` mới, do đó số lượng mã thông báo phản ánh bối cảnh thực tế trước khi nén trước khi được thay thế.\n\n### Chia lượt\n\nMột \"lượt\" bắt đầu bằng tin nhắn của người dùng và bao gồm tất cả các phản hồi của trợ lý và lệnh gọi công cụ cho đến tin nhắn tiếp theo của người dùng. Thông thường, độ nén cắt ở ranh giới rẽ.\n\nKhi một lượt vượt quá `keepRecentTokens`, điểm cắt sẽ xuất hiện ở giữa lượt với thông báo trợ lý. Đây là một \"lượt rẽ\":\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Đối với các lượt chia tách, Pi tạo hai bản tóm tắt và hợp nhất chúng:\n1. **Tóm tắt lịch sử**: Bối cảnh trước đó (nếu có)\n2. **Tóm tắt tiền tố lượt**: Phần đầu của lượt chia\n\n### Quy tắc điểm cắt\n\nĐiểm cắt hợp lệ là:\n- Tin nhắn của người dùng\n- Tin nhắn trợ lý\n- Thông báo thực thi Bash\n- Tin nhắn tùy chỉnh (custom_message, Branch_summary)\n\nKhông bao giờ cắt ở kết quả công cụ (họ phải tuân theo lệnh gọi công cụ của mình).\n\n### Cấu trúc nén chặt\n\nĐược xác định trong [`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 có ​​thể lưu trữ bất kỳ dữ liệu có thể tuần tự hóa JSON nào trong `details`. Việc nén mặc định theo dõi các hoạt động của tệp, nhưng việc triển khai tiện ích mở rộng tùy chỉnh có thể sử dụng cấu trúc riêng của chúng. Các bản tóm tắt được tạo và do tiện ích mở rộng cung cấp sẽ lưu trữ LLM `usage` của chúng khi có sẵn để tổng số phiên bao gồm cả công việc tóm tắt.\n\nXem [`prepareCompaction()`](https://github.com/earendil-works/pi-mono/blob/main/packages/coding-agent/src/core/compaction/compaction.ts) và [`compact()`](https://github.com/earendil-works/pi-mono/blob/main/packages/coding-agent/src/core/compaction/compaction.ts) để biết cách triển khai. Đối với tóm tắt trực tiếp theo chương trình, `generateSummary()` trả về văn bản tóm tắt và `generateSummaryWithUsage()` trả về `{ text, usage }`.\n\n## Tóm tắt chi nhánh\n\n### Khi nó kích hoạt\n\nKhi bạn sử dụng `/tree` để điều hướng đến một nhánh khác, Pi đề xuất tóm tắt công việc bạn sắp hoàn thành. Thao tác này sẽ chèn ngữ cảnh từ nhánh bên trái vào nhánh mới.\n\n### Nó hoạt động như thế nào\n\n1. **Tìm tổ tiên chung**: Nút sâu nhất được chia sẻ bởi các vị trí cũ và mới\n2. **Sưu tầm**: Đi từ lá già về tổ chung\n3. **Chuẩn bị ngân sách**: Bao gồm các tin nhắn tối đa ngân sách mã thông báo (mới nhất trước)\n4. **Tạo tóm tắt**: Gọi LLM với định dạng có cấu trúc\n5. **Nối mục nhập**: Lưu `BranchSummaryEntry` tại điểm điều hướng\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### Theo dõi tệp tích lũy\n\nCả tệp nén và tệp theo dõi branch summarization đều được tích lũy. Khi tạo bản tóm tắt, pi trích xuất các thao tác tệp từ:\n- Cuộc gọi công cụ trong các tin nhắn đang được tóm tắt\n- Việc nén trước hoặc tóm tắt nhánh `details` (nếu có)\n\nĐiều này có nghĩa là việc theo dõi tệp được tích lũy qua nhiều lần nén hoặc tóm tắt nhánh lồng nhau, lưu giữ toàn bộ lịch sử của các tệp đã đọc và sửa đổi.\n\n### Chi nhánhTóm tắtCấu trúc đầu vào\n\nĐược xác định trong [`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\nTương tự như nén, tiện ích mở rộng có thể lưu trữ dữ liệu tùy chỉnh trong `details`.\n\nXem [`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) và [`generateBranchSummary()`](https://github.com/earendil-works/pi-mono/blob/main/packages/coding-agent/src/core/compaction/branch-summarization.ts) để biết cách triển khai.\n\n## Định dạng tóm tắt\n\nCả nén và branch summarization đều sử dụng cùng một định dạng có cấu trúc:\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### Tuần tự hóa tin nhắn\n\nTrước khi tóm tắt, tin nhắn được tuần tự hóa thành văn bản qua [`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Điều này ngăn mô hình coi nó như một cuộc trò chuyện để tiếp tục.\n\nKết quả của công cụ bị cắt ngắn còn 2000 ký tự trong quá trình tuần tự hóa. Nội dung vượt quá giới hạn đó sẽ được thay thế bằng điểm đánh dấu cho biết số lượng ký tự bị cắt bớt. Điều này giúp duy trì các yêu cầu tóm tắt trong phạm vi ngân sách mã thông báo hợp lý, vì kết quả của công cụ (đặc biệt là từ `read` và `bash`) thường là yếu tố đóng góp lớn nhất cho quy mô ngữ cảnh.\n\n## Tóm tắt tùy chỉnh qua Extensions\n\nExtensions có thể chặn và tùy chỉnh cả nén và branch summarization. Xem [`extensions/types.ts`](https://github.com/earendil-works/pi-mono/blob/main/packages/coding-agent/src/core/extensions/types.ts) để biết định nghĩa loại sự kiện.\n\n### phiên_trước_compact\n\nĐược kích hoạt trước khi tự động nén hoặc `/compact`. Có thể hủy bỏ hoặc cung cấp bản tóm tắt tùy chỉnh. Xem `SessionBeforeCompactEvent` và `CompactionPreparation` trong tệp loại.\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#### Chuyển tin nhắn thành văn bản\n\nĐể tạo bản tóm tắt bằng mô hình của riêng bạn, hãy chuyển tin nhắn thành văn bản bằng cách sử dụng `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\nXem [custom-compaction.ts](../examples/extensions/custom-compaction.ts) để biết ví dụ hoàn chỉnh sử dụng mô hình khác.\n\n### phiên_trước_tree\n\nĐã kích hoạt trước `/tree` điều hướng. Luôn kích hoạt bất kể người dùng có chọn tóm tắt hay không. Có thể hủy điều hướng hoặc cung cấp tóm tắt tùy chỉnh.\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\nXem `SessionBeforeTreeEvent` và `TreePreparation` trong tệp loại.\n\n## Cài đặt\n\nĐịnh cấu hình nén trong `~/.pi/agent/settings.json` hoặc `<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| Cài đặt | Mặc định | Sự miêu tả |\n|---------|---------|-------------|\n| `enabled` | `true` | Bật tính năng tự động nén |\n| `reserveTokens` | `16384` | Mã thông báo để dự trữ cho phản hồi LLM |\n| `keepRecentTokens` | `20000` | Mã thông báo gần đây cần giữ (không được tóm tắt) |\n\nTắt tính năng tự động nén bằng `\"enabled\": false`. Bạn vẫn có thể nén thủ công bằng `/compact`.","sourceFile":"compaction.md"},"containerization":{"title":"Container hóa","markdown":"Pi chạy với tất cả các quyền theo mặc định, nhưng trong một số trường hợp, bạn sẽ muốn có nhiều quyền kiểm soát hơn đối với những thư mục mà Pi có thể ghi vào và những thư mục nào có quyền truy cập.\n\nCó hai lựa chọn chung. Bạn có thể\n1. chạy toàn bộ quy trình `pi` bên trong một môi trường biệt lập hoặc\n2. chạy `pi` trên máy chủ và định tuyến thực thi công cụ vào một môi trường biệt lập.\n\n## Chọn một mẫu\n\n| Mẫu | Cái gì bị cô lập | Tốt nhất cho | Ghi chú |\n| --- | --- | --- | --- |\n| Gondolin tiện ích mở rộng | Các công cụ tích hợp và lệnh `!` | Cách ly micro-VM cục bộ trong khi vẫn giữ xác thực trên máy chủ | Xem [`examples/extensions/gondolin/`](../examples/extensions/gondolin/). |\n| Đồng bằng Docker | Toàn bộ quá trình `pi` trong vùng chứa cục bộ | Cách ly cục bộ đơn giản | Nhà cung cấp API keys vào thùng chứa. |\n| OpenShell | Toàn bộ quy trình `pi` trong quy trình sandbox được kiểm soát chính sách | Được quản lý cục bộ hoặc từ xa sandbox | Yêu cầu cổng OpenShell |\n\nExtensions chạy ở bất cứ nơi nào quá trình `pi` chạy. Nếu bạn chạy máy chủ `pi` với tiện ích mở rộng định tuyến công cụ, các công cụ tiện ích mở rộng tùy chỉnh khác vẫn chạy trên máy chủ trừ khi chúng cũng ủy quyền hoạt động của chúng.\n\n## Gondolin\n\n[Gondolin](https://github.com/earendil-works/gondolin) là một máy ảo Linux cục bộ.\nSử dụng [example extension](../examples/extensions/gondolin) khi bạn muốn `pi` trên máy chủ nhưng tất cả các công cụ tích hợp đều được chuyển vào VM.\n\nCài đặt:\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\nChạy từ dự án bạn muốn gắn kết:\n\n```bash\ncd /path/to/project\npi -e ~/.pi/agent/extensions/gondolin\n```\n\nTiện ích mở rộng gắn cwd máy chủ tại `/workspace` trong VM và ghi đè `read`, `write`, `edit`, `bash`, `grep`, `find` và `ls`.\nCác lệnh của người dùng `!` cũng được chuyển vào VM.\nCác thay đổi về tệp trong `/workspace` được ghi thông qua máy chủ.\n\nYêu cầu: Node.js >= 23.6.0 cho `@earendil-works/gondolin`, cộng với QEMU (yêu cầu cài đặt thông qua trình quản lý gói của bạn).\n\n## Đồng bằng Docker\n\nChạy toàn bộ quy trình `pi` trong Docker khi bạn muốn ranh giới vùng chứa cục bộ đơn giản nhất.\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\nXây dựng và chạy:\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\"` gắn thư mục hiện tại của bạn vào vùng chứa tại /workspace sao cho việc đọc và ghi vào `/workspace` bên trong Docker ảnh hưởng trực tiếp đến các tệp máy chủ của bạn, như trong ví dụ Gondolin.\n\nSử dụng âm lượng được đặt tên cho `/root/.pi/agent` nếu bạn muốn các phiên và cài đặt cục bộ vùng chứa. Việc gắn máy chủ của bạn `~/.pi/agent` sẽ hiển thị các tệp phiên và xác thực máy chủ vào vùng chứa.\n\n## OpenShell\n\nSử dụng [NVIDIA OpenShell](https://docs.nvidia.com/openshell/about/overview) khi bạn muốn sandbox được kiểm soát chính sách với các điều khiển hệ thống tệp, quy trình, mạng, thông tin xác thực và suy luận.\nOpenShell có thể chạy sandboxes thông qua cổng cục bộ được hỗ trợ bởi Docker, Podman hoặc thời gian chạy VM hoặc thông qua cổng Kubernetes từ xa.\n\nMọi sandbox đều yêu cầu một cổng hoạt động.\nĐăng ký và chọn một cái trước khi tạo sandbox:\n\n```bash\nopenshell gateway add <gateway-url> --name <name>\nopenshell gateway select <name>\n```\n\nKhởi chạy `pi` bên trong OpenShell sandbox:\n\n```bash\nopenshell sandbox create --name pi-sandbox --from pi -- pi\n```\n\nTrong mẫu này, toàn bộ quá trình `pi` chạy bên trong sandbox.\nCác công cụ tích hợp, lệnh `!` và công cụ mở rộng thực thi bên trong ranh giới OpenShell.\n\nNếu cổng ở xa, các tệp dự án không được liên kết với máy chủ, nghĩa là việc ghi vào sandbox không được phản ánh trên máy của bạn.\nSao chép kho lưu trữ bên trong sandbox hoặc sử dụng lệnh truyền tệp OpenShell:\n\n```bash\nopenshell sandbox upload pi-sandbox ./repo /workspace\nopenshell sandbox download pi-sandbox /workspace/repo ./repo-out\n```\n\nOpenShell nhà cung cấp có thể giữ mô hình thô API key bên ngoài sandbox.\nKhi định tuyến suy luận được định cấu hình, mã bên trong sandbox có thể gọi `https://inference.local` và cổng sẽ đưa ngược dòng thông tin xác thực của nhà cung cấp đã định cấu hình.\nĐịnh cấu hình Pi để sử dụng điểm cuối tương thích với OpenAI hoặc Anthropic tương ứng nếu bạn muốn lưu lượng truy cập mô hình sử dụng tuyến đường này.","sourceFile":"containerization.md"},"custom-provider":{"title":"Tùy chỉnh Providers","markdown":"Extensions có thể đăng ký nhà cung cấp mô hình tùy chỉnh thông qua `pi.registerProvider()`. Điều này cho phép:\n\n- **Proxies** - Định tuyến yêu cầu thông qua proxy công ty hoặc cổng API\n- **Điểm cuối tùy chỉnh** - Sử dụng triển khai mô hình riêng tư hoặc tự lưu trữ\n- **OAuth/SSO** - Thêm luồng xác thực cho nhà cung cấp doanh nghiệp\n- **APIs tùy chỉnh** - Triển khai phát trực tuyến cho LLM APIs không chuẩn\n\n## Ví dụ Extensions\n\nXem các ví dụ về nhà cung cấp hoàn chỉnh sau:\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## Mục lục\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## Tham khảo nhanh\n\nExtensions có ​​thể đăng ký một pi-ai `Provider` hoàn chỉnh hoặc sử dụng biểu mẫu cấu hình nhà cung cấp cũ. Ưu tiên một nhà cung cấp hoàn chỉnh khi yêu cầu hành vi xác thực, lọc, làm mới hoặc phát trực tuyến tùy chỉnh. Pi soạn `models.json` ghi đè lên trên các nhà cung cấp bản địa đã đăng ký.\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\nNhà máy mở rộng cũng có thể là `async`. Để khám phá mô hình động, hãy tìm nạp và đăng ký các mô hình trong nhà máy thay vì `session_start`. pi đợi nhà máy trước khi tiếp tục khởi động, vì vậy nhà cung cấp có sẵn trong quá trình khởi động tương tác và `pi --list-models`.\n\n## Ghi đè nhà cung cấp hiện tại\n\nTrường hợp sử dụng đơn giản nhất: chuyển hướng nhà cung cấp hiện có thông qua proxy.\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\nKhi chỉ cung cấp `baseUrl` và/hoặc `headers` (không có `models`), tất cả các mô hình hiện có cho nhà cung cấp đó sẽ được giữ nguyên với điểm cuối mới.\n\n## Đăng ký nhà cung cấp mới\n\nĐể thêm nhà cung cấp hoàn toàn mới, hãy chỉ định `models` cùng với cấu hình được yêu cầu.\n\nNếu danh sách mô hình đến từ điểm cuối từ xa, hãy sử dụng nhà máy tiện ích mở rộng không đồng bộ:\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\nViệc này sẽ đăng ký các mô hình được tìm nạp trước khi quá trình khởi động kết thúc.\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\nKhi `models` được cung cấp, nó **thay thế** tất cả các mô hình hiện có cho nhà cung cấp đó.\n\n`apiKey` và các giá trị tiêu đề tùy chỉnh sử dụng cú pháp giá trị cấu hình giống như `models.json`: `!command` khi bắt đầu thực thi lệnh cho toàn bộ giá trị, `$ENV_VAR` và `${ENV_VAR}` nội suy các biến môi trường, `$` phát ra một chữ ``apiKey` và các giá trị tiêu đề tùy chỉnh sử dụng cú pháp giá trị cấu hình giống như `models.json`: `!command` khi bắt đầu thực thi lệnh cho toàn bộ giá trị, `$ENV_VAR` và `${ENV_VAR}` nội suy các biến môi trường, `$` phát ra một chữ  và `$!` phát ra một chữ `!`.\n\n## Hủy đăng ký nhà cung cấp\n\nSử dụng `pi.unregisterProvider(name)` để xóa nhà cung cấp đã được đăng ký trước đó qua `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\nViệc hủy đăng ký sẽ xóa các mô hình động, dự phòng API key, đăng ký nhà cung cấp OAuth và đăng ký trình xử lý luồng tùy chỉnh của nhà cung cấp đó. Mọi mô hình tích hợp hoặc hành vi của nhà cung cấp đã bị ghi đè đều được khôi phục.\n\nCác cuộc gọi được thực hiện sau giai đoạn tải tiện ích mở rộng ban đầu sẽ được áp dụng ngay lập tức, do đó không cần `/reload`.\n\n### API Các loại\n\nTrường `api` xác định cách triển khai phát trực tuyến nào được sử dụng:\n\n| API | Sử dụng cho |\n|-----|---------|\n| `anthropic-messages` | Claude nhân loại API và những người tương thích |\n| `openai-completions` | Số lần hoàn thành trò chuyện OpenAI API và tương thích |\n| `openai-responses` | Phản hồi OpenAI API |\n| `azure-openai-responses` | Phản hồi của Azure OpenAI API |\n| `openai-codex-responses` | Phản hồi Codex của OpenAI API |\n| `mistral-conversations` | Phát trực tuyến hoàn thành trò chuyện Mistral bản địa |\n| `google-generative-ai` | AI sáng tạo của Google API |\n| `google-vertex` | Google Vertex AI API |\n| `bedrock-converse-stream` | Converse Amazon Bedrock API |\n\nHầu hết các nhà cung cấp tương thích với OpenAI đều hoạt động với `openai-completions`. Sử dụng cấp độ mô hình `thinkingLevelMap` cho các cấp độ tư duy dành riêng cho mô hình và `compat` cho các yêu cầu riêng của nhà cung cấp. Các cấp độ `xhigh` và `max` được chọn tham gia, yêu cầu các mục nhập bản đồ không có giá trị rỗng và có thể được phân tách bằng các lỗ không được hỗ trợ:\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\nSử dụng `openrouter` cho các điều khiển `reasoning: { effort }` kiểu OpenRouter. Sử dụng `together` cho các điều khiển kiểu Together `reasoning: { enabled }`; với `supportsReasoningEffort`, nó cũng gửi `reasoning_effort`. Sử dụng `qwen-chat-template` cho các máy chủ tương thích với Qwen cục bộ đọc `chat_template_kwargs.enable_thinking` và cần `preserve_thinking`.\nSử dụng `cacheControlFormat: \"anthropic\"` cho các nhà cung cấp tương thích với OpenAI hiển thị bộ đệm ẩn lời nhắc kiểu Anthropic thông qua `cache_control` trên lời nhắc hệ thống, định nghĩa công cụ cuối cùng và nội dung văn bản kết quả công cụ, người dùng cuối cùng hoặc trợ lý.\n\nĐối với các nhà cung cấp tương thích với Con người đang sử dụng `api: \"anthropic-messages\"`, hãy đặt `compat.forceAdaptiveThinking: true` trên các mô hình hoặc nhà cung cấp có mô hình ngược dòng yêu cầu tư duy thích ứng (`thinking.type: \"adaptive\"` cộng với `output_config.effort`). Các mô hình Claude thích ứng tích hợp sẽ tự động thiết lập điều này. Chỉ đặt `compat.allowEmptySignature: true` cho các nhà cung cấp phát ra các chữ ký suy nghĩ trống rỗng và mong đợi `signature: \"\"` khi phát lại.\n\n> Ghi chú di chuyển: Mistral đã chuyển từ `openai-completions` sang `mistral-conversations`.\n> Sử dụng `mistral-conversations` cho các mẫu Mistral bản địa.\n> Nếu bạn cố tình định tuyến các điểm cuối tùy chỉnh/tương thích với Mistral thông qua `openai-completions`, hãy đặt cờ `compat` một cách rõ ràng nếu cần.\n\n### Tiêu đề xác thực\n\nNếu nhà cung cấp của bạn mong đợi `Authorization: Bearer <key>` nhưng không sử dụng API tiêu chuẩn, hãy đặt `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\nChìa khóa được giải quyết cho mỗi yêu cầu. Tiêu đề yêu cầu rõ ràng `Authorization` được ưu tiên hơn giá trị được tạo.\n\n## OAuth Hỗ trợ\n\nThêm xác thực OAuth/SSO tích hợp với `/login`:\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\nSau khi đăng ký, người dùng có thể xác thực qua `/login corporate-ai`.\n\n### OAuthĐăng nhậpGọi lại\n\nĐối tượng `callbacks` cung cấp các tương tác trung lập với giao diện người dùng cho luồng do nhà cung cấp sở hữu:\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### OAuthThông tin xác thực\n\nThông tin xác thực được duy trì trong `~/.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## Phát trực tuyến tùy chỉnh API\n\nĐối với các nhà cung cấp có API không chuẩn, hãy triển khai `streamSimple`. Nghiên cứu cách triển khai của nhà cung cấp hiện có trước khi viết bài của riêng bạn:\n\n**Triển khai tham khảo:**\n- [anthropic.ts](https://github.com/earendil-works/pi-mono/blob/main/packages/ai/src/providers/anthropic.ts) - Thông điệp nhân loại API\n- [mistral.ts](https://github.com/earendil-works/pi-mono/blob/main/packages/ai/src/providers/mistral.ts) - Cuộc trò chuyện của Mistral API\n- [openai-completions.ts](https://github.com/earendil-works/pi-mono/blob/main/packages/ai/src/providers/openai-completions.ts) - Hoàn thành trò chuyện OpenAI\n- [openai-responses.ts](https://github.com/earendil-works/pi-mono/blob/main/packages/ai/src/providers/openai-responses.ts) - Phản hồi của OpenAI API\n- [google.ts](https://github.com/earendil-works/pi-mono/blob/main/packages/ai/src/providers/google.ts) - Trí tuệ nhân tạo của Google\n- [amazon-bedrock.ts](https://github.com/earendil-works/pi-mono/blob/main/packages/ai/src/providers/amazon-bedrock.ts) - AWS Bedrock\n\n### Mẫu luồng\n\nTất cả các nhà cung cấp đều theo cùng một mẫu:\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### Các loại sự kiện\n\nĐẩy các sự kiện qua `stream.push()` theo thứ tự sau:\n\n1. `{ type: \"start\", partial: output }` - Đã bắt đầu phát trực tiếp\n\n2. Sự kiện nội dung (có thể lặp lại, theo dõi `contentIndex` cho mỗi khối):\n   - `{ type: \"text_start\", contentIndex, partial }` - Khối văn bản đã bắt đầu\n   - `{ type: \"text_delta\", contentIndex, delta, partial }` - Đoạn văn bản\n   - `{ type: \"text_end\", contentIndex, content, partial }` - Khối văn bản đã kết thúc\n   - `{ type: \"thinking_start\", contentIndex, partial }` - Bắt đầu suy nghĩ\n   - `{ type: \"thinking_delta\", contentIndex, delta, partial }` - Đoạn suy nghĩ\n   - `{ type: \"thinking_end\", contentIndex, content, partial }` - Suy nghĩ kết thúc\n   - `{ type: \"toolcall_start\", contentIndex, partial }` - Cuộc gọi công cụ đã bắt đầu\n   - `{ type: \"toolcall_delta\", contentIndex, delta, partial }` - Lệnh gọi công cụ JSON chunk\n   - `{ type: \"toolcall_end\", contentIndex, toolCall, partial }` - Cuộc gọi công cụ đã kết thúc\n\n3. `{ type: \"done\", reason, message }` hoặc `{ type: \"error\", reason, error }` - Đã kết thúc luồng\n\nTrường `partial` trong mỗi sự kiện chứa trạng thái `AssistantMessage` hiện tại. Cập nhật `output.content` khi bạn nhận được dữ liệu, sau đó đưa `output` làm `partial`.\n\n### Khối nội dung\n\nThêm khối nội dung vào `output.content` khi chúng xuất hiện:\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### Cuộc gọi công cụ\n\nLệnh gọi công cụ yêu cầu tích lũy JSON và phân tích cú pháp:\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### Cách sử dụng và chi phí\n\nCập nhật mức sử dụng từ phản hồi API và tính chi phí:\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### Lỗi tràn ngữ cảnh\n\nKhi một yêu cầu vượt quá cửa sổ ngữ cảnh của mô hình, pi có thể tự động phục hồi bằng cách thu gọn cuộc hội thoại và thử lại. Sự phục hồi này chỉ bắt đầu nếu pi nhận ra lỗi là tràn.\n\nQuá trình phát hiện sẽ chạy trên thông báo trợ lý cuối cùng:\n\n- `stopReason === \"error\"`\n- `errorMessage` khớp với một trong các kiểu tràn đã biết của pi (xem [`packages/ai/src/utils/overflow.ts`](https://github.com/earendil-works/pi-mono/blob/main/packages/ai/src/utils/overflow.ts))\n\nNếu nhà cung cấp của bạn trả về lỗi tràn kèm theo thông báo pi không nhận ra, hãy bình thường hóa lỗi từ cùng một tiện ích mở rộng đã đăng ký nhà cung cấp. Sử dụng trình xử lý `message_end` để viết lại tin nhắn trợ lý sao cho `errorMessage` của nó bắt đầu bằng cụm từ pi nhận dạng. Dự phòng chung `context_length_exceeded` là lựa chọn an toàn nhất.\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` chạy trước khi pi theo dõi thông báo trợ lý để tự động nén, do đó, `errorMessage` được viết lại là những gì pi kiểm tra. Với điều này, pi sẽ:\n\n1. Phát hiện tràn từ `errorMessage`.\n2. Loại bỏ thông báo trợ lý không thành công khỏi ngữ cảnh trực tiếp.\n3. Chạy nén.\n4. Hãy thử lại yêu cầu một lần.\n\nBảo vệ việc viết lại cẩn thận:\n\n- Phạm vi áp dụng cho nhà cung cấp của bạn (`message.provider` và `ctx.model?.provider`) để không xử lý các lỗi không liên quan từ các nhà cung cấp khác.\n- Khớp với mẫu dành riêng cho nhà cung cấp, không phải mẫu tràn chung của pi. Việc viết lại các lỗi giới hạn tốc độ hoặc điều chỉnh (`rate limit`, `too many requests`) sẽ kích hoạt quá trình nén sai thay vì đường dẫn thử lại với bước lùi thông thường của pi.\n- Bỏ qua khi `errorMessage` đã bao gồm `context_length_exceeded` nên trình xử lý không có hiệu lực.\n\n### Sự đăng ký\n\nĐăng ký chức năng truyền phát của bạ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## Kiểm tra việc triển khai của bạn\n\nKiểm tra nhà cung cấp của bạn dựa trên các bộ thử nghiệm tương tự được sử dụng bởi các nhà cung cấp tích hợp. Sao chép và điều chỉnh các tệp thử nghiệm này từ [packages/ai/test/](https://github.com/earendil-works/pi-mono/tree/main/packages/ai/test):\n\n| Bài kiểm tra | Mục đích |\n|------|---------|\n| `stream.test.ts` | Truyền phát cơ bản, xuất văn bản |\n| `tokens.test.ts` | Đếm và sử dụng mã thông báo |\n| `abort.test.ts` | Hủy bỏXử lý tín hiệu |\n| `empty.test.ts` | Phản hồi trống/tối thiểu |\n| `context-overflow.test.ts` | Giới hạn cửa sổ ngữ cảnh |\n| `image-limits.test.ts` | Xử lý đầu vào hình ảnh |\n| `unicode-surrogate.test.ts` | Trường hợp cạnh Unicode |\n| `tool-call-without-result.test.ts` | Các trường hợp cạnh gọi công cụ |\n| `image-tool-result.test.ts` | Hình ảnh trong kết quả công cụ |\n| `total-tokens.test.ts` | Tính toán tổng số token |\n| `cross-provider-handoff.test.ts` | Chuyển giao bối cảnh giữa các nhà cung cấp |\n\nChạy thử nghiệm với các cặp nhà cung cấp/mô hình của bạn để xác minh tính tương thích.\n\n## Tham khảo cấu hình\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## Tham chiếu định nghĩa mô hình\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` gửi `reasoning: { effort }`. `deepseek` gửi `thinking: { type: \"enabled\" | \"disabled\" }` và `reasoning_effort` khi được bật. `together` gửi `reasoning: { enabled }` và cả `reasoning_effort` khi `supportsReasoningEffort` được bật. `qwen` dành cho cấp cao nhất theo phong cách DashScope `enable_thinking`. Sử dụng `qwen-chat-template` cho các máy chủ tương thích với Qwen cục bộ đọc `chat_template_kwargs.enable_thinking` và cần `preserve_thinking`. Sử dụng `chat-template` cho `chat_template_kwargs` có thể định cấu hình, ví dụ: DeepSeek V3.x đằng sau vLLM với `chatTemplateKwargs: { \"thinking\": { \"$var\": \"thinking.enabled\" } }`. Sử dụng `thinkingFormat: \"baseten\"` với `chatTemplateArgs` khi nhà cung cấp mong muốn chuyển đổi các giá trị dưới `chat_template_args` và tùy chọn hỗ trợ cấp cao nhất `reasoning_effort`.\n`cacheControlFormat: \"anthropic\"` áp dụng các điểm đánh dấu `cache_control` kiểu Anthropic cho lời nhắc hệ thống, định nghĩa công cụ cuối cùng và nội dung văn bản kết quả công cụ, trợ lý hoặc người dùng cuối cùng.","sourceFile":"custom-provider.md"},"development":{"title":"Phát triển","markdown":"Xem [AGENTS.md](https://github.com/earendil-works/pi-mono/blob/main/AGENTS.md) để biết thêm hướng dẫn.\n\n## Cài đặt\n\n```bash\ngit clone https://github.com/earendil-works/pi-mono\ncd pi-mono\nnpm install\nnpm run build\n```\n\nChạy từ nguồn:\n\n```bash\n/path/to/pi-mono/pi-test.sh\n```\n\nTập lệnh có thể được chạy từ bất kỳ thư mục nào. Pi giữ thư mục làm việc hiện tại của người gọi.\n\n## Forking/Đổi thương hiệu\n\nĐịnh cấu hình qua `package.json`:\n\n```json\n{\n  \"piConfig\": {\n    \"name\": \"pi\",\n    \"configDir\": \".pi\"\n  }\n}\n```\n\nThay đổi trường `name`, `configDir` và `bin` cho ngã ba của bạn. Ảnh hưởng đến biểu ngữ CLI, đường dẫn cấu hình và tên biến môi trường.\n\n## Độ phân giải đường dẫn\n\nBa chế độ thực thi: npm cài đặt, nhị phân độc lập, tsx từ nguồn.\n\n**Luôn sử dụng `src/config.ts`** cho nội dung gói:\n\n```typescript\nimport { getPackageDir, getThemeDir } from \"./config.js\";\n```\n\nKhông bao giờ sử dụng trực tiếp `__dirname` cho nội dung gói.\n\n## Lệnh gỡ lỗi\n\n`/debug` (ẩn) ghi vào `~/.pi/agent/pi-debug.log`:\n- Đã hiển thị các dòng TUI có mã ANSI\n- Tin nhắn cuối cùng được gửi đến LLM\n\n## Kiểm tra\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## Cấu trúc dự á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":"Biến môi trường","markdown":"Pi sử dụng biến môi trường theo ba cách:\n\n- Các biến như `PI_OFFLINE` định cấu hình quy trình Pi.\n- Pi đặt `PI_CODING_AGENT` để các tiến trình con có thể phát hiện rằng chúng chạy bên trong Pi.\n- Các lệnh được chạy bởi công cụ bash có thể gọi LLM nhận các biến `PI_*` mô tả phiên hiện tại.\n\nCác biến khóa API của nhà cung cấp được ghi lại riêng biệt trong [Providers](providers.md#environment-variables-or-auth-file).\n\n## Điểm đánh dấu quy trình\n\nĐiểm vào CLI và RPC được đặt `PI_CODING_AGENT=true`. Các tiến trình con kế thừa nó và có thể sử dụng nó để phát hiện xem chúng có chạy bên trong Pi hay không. Nó không dành riêng cho phiên và không được đặt tự động khi Pi được nhúng thông qua SDK.\n\n## Môi trường phiên công cụ Bash\n\nCác lệnh do công cụ bash chạy sẽ nhận trạng thái phiên Pi hiện tại:\n\n| Biến | Sự miêu tả |\n|----------|-------------|\n| `PI_SESSION_ID` | ID phiên hiện tại |\n| `PI_SESSION_FILE` | Đường dẫn tuyệt đối đến tệp JSONL phiên hiện tại; không được đặt cho các phiên tạm thời |\n| `PI_PROVIDER` | Nhà cung cấp mô hình hiện được chọn |\n| `PI_MODEL` | ID mẫu hiện được chọn |\n| `PI_REASONING_LEVEL` | Mức lý luận hiệu quả hiện tại: `off`, `minimal`, `low`, `medium`, `high`, `xhigh` hoặc `max` |\n\nCác giá trị được giải quyết khi mỗi lệnh bắt đầu. Do đó, việc chuyển đổi mô hình hoặc thay đổi mức lý luận sẽ ảnh hưởng đến lệnh bash tiếp theo mà không cần khởi động lại Pi. `PI_PROVIDER` và `PI_MODEL` xác định mô hình Pi đã chọn, không phải mô hình ngược tuyến khác mà bộ định tuyến có thể chọn nội bộ.\n\nKhi được hỏi mô hình hoặc nhà cung cấp nào đang chạy, hãy kiểm tra các biến này thay vì suy ra câu trả lời từ lời nhắc hệ thống:\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\nTệp phiên có thể được kiểm tra trực tiếp khi phiên liên tục:\n\n```bash\nif [ -n \"$PI_SESSION_FILE\" ]; then\n  tail -n 1 \"$PI_SESSION_FILE\"\nfi\n```\n\nCác biến này được đưa vào công cụ bash có ​​thể gọi LLM. Chúng không được đưa vào các lệnh `!` hoặc `!!` do người dùng nhập.\n\n### Công cụ Bash tùy chỉnh\n\nCác công cụ Bash được tạo bằng `createBashTool()` hiển thị môi trường phiên theo mặc định khi đăng ký với Pi. Việc tiêm xảy ra trước `spawnHook`, do đó, hook nhận các biến trong `ctx.env`:\n\n```typescript\nconst bashTool = createBashTool(cwd, {\n  spawnHook: (ctx) => ({\n    ...ctx,\n    env: { ...ctx.env, CI: \"1\" },\n  }),\n});\n```\n\nTắt siêu dữ liệu phiên độc lập với hook sinh sản:\n\n```typescript\nconst bashTool = createBashTool(cwd, {\n  exposeSessionEnvironment: false,\n  spawnHook: (ctx) => ctx,\n});\n```\n\nKhi bị tắt, Pi sẽ xóa các giá trị kế thừa cho các biến này để các quy trình Pi lồng nhau không hiển thị siêu dữ liệu phiên gốc cũ.\n\n## Pi Cấu hình quy trình\n\nCác biến này được đọc bởi chính Pi:\n\n| Biến | Sự miêu tả |\n|----------|-------------|\n| `PI_CODING_AGENT_DIR` | Ghi đè thư mục cấu hình; mặc định là `~/.pi/agent` |\n| `PI_CODING_AGENT_SESSION_DIR` | Ghi đè lưu trữ phiên; bị ghi đè bởi `--session-dir` |\n| `PI_PACKAGE_DIR` | Ghi đè thư mục gói, hữu ích cho đường dẫn lưu trữ Nix/Guix |\n| `PI_OFFLINE` | Vô hiệu hóa các hoạt động mạng khởi động, bao gồm kiểm tra cập nhật, cập nhật gói và cài đặt/cập nhật đo từ xa |\n| `PI_SKIP_VERSION_CHECK` | Tắt yêu cầu phiên bản mới nhất `pi.dev` |\n| `PI_TELEMETRY` | Ghi đè tiêu đề cài đặt/cập nhật đo từ xa và tiêu đề phân bổ nhà cung cấp: `1`/`true`/`yes` hoặc `0`/`false`/`no` |\n| `PI_CACHE_RETENTION` | Đặt thành `long` để lưu vào bộ đệm ẩn lời nhắc của nhà cung cấp mở rộng nếu được hỗ trợ |\n| `PI_SHARE_VIEWER_URL` | Ghi đè URL cơ sở được sử dụng bởi `/share` |\n| `PI_HARDWARE_CURSOR` | Đặt thành `1` để hiển thị con trỏ phần cứng; xem [Terminal setup](terminal-setup.md) |\n| `VISUAL`, `EDITOR` | Dự phòng trình chỉnh sửa bên ngoài khi `externalEditor` không được đặt |\n| `HTTP_PROXY`, `HTTPS_PROXY` | Yêu cầu HTTP gửi đi qua proxy |\n\nThông tin xác thực của nhà cung cấp như `ANTHROPIC_API_KEY`, `OPENAI_API_KEY` và cấu hình nhà cung cấp đám mây được liệt kê trong [Providers](providers.md#environment-variables-or-auth-file).","sourceFile":"environment-variables.md"},"extensions":{"title":"Extensions","markdown":"> pi có thể tạo tiện ích mở rộng. Yêu cầu nó xây dựng một cái cho trường hợp sử dụng của bạn.\n\n\nExtensions là TypeScript mô-đun mở rộng hành vi của pi. Họ có thể đăng ký các sự kiện trong vòng đời, đăng ký các công cụ tùy chỉnh mà LLM có thể gọi, thêm lệnh, v.v.\n\n> **Vị trí dành cho /tải lại:** Đặt tiện ích mở rộng ở `~/.pi/agent/extensions/` (toàn cầu) hoặc `.pi/extensions/` (dự án cục bộ) để tự động khám phá. Chỉ sử dụng `pi -e./path.ts` cho các bài kiểm tra nhanh. Extensions ở các vị trí được tự động phát hiện có thể được tải lại nóng bằng `/reload`.\n\n**Các khả năng chính:**\n- **Công cụ tùy chỉnh** - Đăng ký công cụ mà LLM có thể gọi qua `pi.registerTool()`\n- **Chặn sự kiện** - Chặn hoặc sửa đổi lệnh gọi công cụ, chèn ngữ cảnh, tùy chỉnh nén\n- **Tương tác người dùng** - Nhắc người dùng qua `ctx.ui` (chọn, xác nhận, nhập, thông báo)\n- **Thành phần giao diện người dùng tùy chỉnh** - Thành phần TUI đầy đủ với đầu vào bàn phím qua `ctx.ui.custom()` cho các tương tác phức tạp\n- **Lệnh tùy chỉnh** - Đăng ký các lệnh như `/mycommand` qua `pi.registerCommand()`\n- **Tính bền bỉ của phiên** - Lưu trữ trạng thái vẫn tồn tại khi khởi động lại thông qua `pi.appendEntry()`\n- **Hiển thị tùy chỉnh** - Kiểm soát cách các lệnh gọi/kết quả và thông báo của công cụ xuất hiện trong TUI\n\n**Các trường hợp sử dụng ví dụ:**\n- Cổng cấp phép (xác nhận trước `rm -rf`, `sudo`, v.v.)\n- Git điểm kiểm tra (lưu trữ ở mỗi lượt, khôi phục trên nhánh)\n- Bảo vệ đường dẫn (chặn ghi vào `.env`, `node_modules/`)\n- Thu gọn tùy chỉnh (tóm tắt cuộc trò chuyện theo cách của bạn)\n- Tóm tắt cuộc hội thoại (xem ví dụ `summarize.ts`)\n- Các công cụ tương tác (câu hỏi, hướng dẫn, hộp thoại tùy chỉnh)\n- Các công cụ trạng thái (danh sách việc cần làm, nhóm kết nối)\n- Tích hợp bên ngoài (trình theo dõi tệp, webhooks, trình kích hoạt CI)\n- Trò chơi trong khi chờ đợi (xem ví dụ `snake.ts`)\n\nXem [examples/extensions/](../examples/extensions/) để biết cách triển khai hoạt động.\n\n## Mục lục\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## Bắt đầu nhanh\n\nTạo `~/.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\nKiểm tra bằng cờ `--extension` (hoặc `-e`):\n\n```bash\npi -e ./my-extension.ts\n```\n\n## Vị trí tiện ích mở rộng\n\n> **Bảo mật:** Extensions chạy với toàn bộ quyền hệ thống của bạn và có thể thực thi mã tùy ý. Chỉ cài đặt từ các nguồn mà bạn tin tưởng.\n\nExtensions được tự động phát hiện từ các vị trí đáng tin cậy. Các mục nhập dự án `.pi/extensions` cục bộ chỉ tải sau khi dự án được tin cậy.\n\n| Vị trí | Phạm vi |\n|----------|-------|\n| `~/.pi/agent/extensions/*.ts` | Toàn cầu (tất cả các dự án) |\n| `~/.pi/agent/extensions/*/index.ts` | Toàn cầu (thư mục con) |\n| `.pi/extensions/*.ts` | Dự án-địa phương |\n| `.pi/extensions/*/index.ts` | Dự án-cục bộ (thư mục con) |\n\nĐường dẫn bổ sung qua `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Để chia sẻ tiện ích mở rộng qua npm hoặc git dưới dạng gói pi, hãy xem [packages.md](packages.md).\n\n## Nhập khẩu có sẵn\n\n| Bưu kiện | Mục đích |\n|---------|---------|\n| `@earendil-works/pi-coding-agent` | Các loại tiện ích mở rộng (`ExtensionAPI`, `ExtensionContext`, sự kiện) |\n| `typebox` | Định nghĩa lược đồ cho các tham số công cụ |\n| `@earendil-works/pi-ai` | Tiện ích AI (`StringEnum` dành cho các enum tương thích với Google) |\n| `@earendil-works/pi-tui` | TUI thành phần để hiển thị tùy chỉnh |\n\nnpm phần phụ thuộc cũng hoạt động. Thêm `package.json` bên cạnh tiện ích mở rộng của bạn (hoặc trong thư mục mẹ), chạy `npm install` và quá trình nhập từ `node_modules/` được giải quyết tự động.\n\nĐối với các gói pi phân tán được cài đặt bằng `pi install` (npm hoặc git), thời gian chạy phải ở `dependencies`. Cài đặt gói sử dụng cài đặt sản xuất (`npm install --omit=dev`) theo mặc định, vì vậy `devDependencies` không có sẵn trong thời gian chạy; khi `npmCommand` được định cấu hình, các gói git sử dụng `install` đơn giản để tương thích với các trình bao bọc.\n\nNode.js tích hợp sẵn (`node:fs`, `node:path`, v.v.) cũng có sẵn.\n\n## Viết một phần mở rộng\n\nTiện ích mở rộng xuất hàm xuất xưởng mặc định nhận `ExtensionAPI`. Nhà máy có thể đồng bộ hoặc không đồng bộ:\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 được tải qua [jiti](https://github.com/unjs/jiti), vì vậy TypeScript hoạt động mà không cần biên dịch.\n\nNếu nhà máy trả về `Promise`, pi sẽ đợi nó trước khi tiếp tục khởi động. Điều đó có nghĩa là quá trình khởi tạo không đồng bộ hoàn tất trước `session_start`, trước `resources_discover` và trước khi đăng ký nhà cung cấp xếp hàng qua `pi.registerProvider()` bị xóa.\n\n### Chức năng của nhà máy không đồng bộ\n\nSử dụng nhà máy không đồng bộ cho công việc khởi động một lần, chẳng hạn như tìm nạp cấu hình từ xa hoặc tự động khám phá các mô hình có sẵ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\nMẫu này làm cho các mô hình được tìm nạp có sẵn trong quá trình khởi động bình thường và tới `pi --list-models`.\n\n### Tài nguyên tồn tại lâu dài và tắt máy\n\nCác nhà máy mở rộng có thể chạy trong các lệnh gọi không bao giờ bắt đầu phiên. Không khởi động các tài nguyên nền như quy trình, ổ cắm, trình theo dõi tệp hoặc bộ hẹn giờ từ nhà máy.\n\nTrì hoãn khởi động tài nguyên nền cho đến `session_start` hoặc lệnh/công cụ/sự kiện cần tài nguyên. Đăng ký trình xử lý `session_shutdown` bình thường để đóng mọi tài nguyên trong phạm vi phiên mà bạn bắt đầu.\n\n### Kiểu mở rộng\n\n**Tệp đơn** - đơn giản nhất, dành cho các phần mở rộng nhỏ:\n\n```\n~/.pi/agent/extensions/\n└── my-extension.ts\n```\n\n**Thư mục có index.ts** - dành cho phần mở rộng nhiều tệp:\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**Gói có phần phụ thuộc** - dành cho các tiện ích mở rộng cần gói 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\nChạy `npm install` trong thư mục tiện ích mở rộng, sau đó nhập từ `node_modules/` sẽ tự động hoạt động.\n\n## Sự kiện\n\n### Tổng quan về vòng đời\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### Sự kiện khởi nghiệp\n\n#### dự án_trust\n\nĐược kích hoạt trước khi pi quyết định có tin cậy một dự án có cấu hình động hay không (`.pi` hoặc `.agents/skills`). Nó chạy trong khi khởi động và khi thay thế phiên (ví dụ `/resume`) nhập vào một cwd mà sự tin cậy chưa được giải quyết trong quy trình hiện tại. Chỉ có tiện ích mở rộng người dùng/toàn cầu và tiện ích mở rộng CLI `-e` mới tham gia; tiện ích mở rộng dự án cục bộ không được tải cho đến khi giải quyết được sự tin cậy.\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\nTrình xử lý `project_trust` phải trả về `{ trusted: \"yes\" | \"no\" | \"undecided\" }`. Tiện ích mở rộng người dùng/toàn cầu hoặc CLI trả về `\"yes\"` hoặc `\"no\"` sở hữu quyết định; quyết định có/không đầu tiên sẽ thắng và loại bỏ lời nhắc tin cậy tích hợp. Sử dụng `remember: true` để duy trì quyết định có/không; nếu không nó chỉ áp dụng cho quy trình hiện tại. Trả về `\"undecided\"` để cho phép các trình xử lý sau hoặc luồng tin cậy tích hợp quyết định. Kiểm tra `ctx.hasUI` trước khi nhắc. Nếu không có trình xử lý nào trả về có/không, quá trình phân giải tin cậy thông thường sẽ tiếp tục: các quyết định `trust.json` đã lưu được áp dụng trước, sau đó `defaultProjectTrust` kiểm soát xem pi yêu cầu, tin tưởng hay từ chối theo mặc định.\n\n### Sự kiện tài nguyên\n\n#### tài nguyên_khám phá\n\nĐược kích hoạt sau `session_start` để tiện ích mở rộng có thể đóng góp thêm đường dẫn kỹ năng, lời nhắc và chủ đề.\nĐường dẫn khởi động sử dụng `reason: \"startup\"`. Tải lại sử dụng `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### Sự kiện phiên\n\nXem [Session Format](session-format.md) để biết nội dung lưu trữ phiên và Trình quản lý phiên API.\n\n#### phiên_bắt đầu\n\nĐược kích hoạt khi một phiên được bắt đầu, tải hoặc tải lại.\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#### phiên_info_changed\n\nĐược kích hoạt khi tên hiển thị phiên hiện tại được đặt qua `/name`, RPC hoặc `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#### phiên_trước_switch\n\nĐược kích hoạt trước khi bắt đầu phiên mới (`/new`) hoặc chuyển đổi phiên (`/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\nSau khi chuyển đổi thành công hoặc hành động phiên mới, pi phát ra `session_shutdown` cho phiên bản tiện ích mở rộng cũ, tải lại và liên kết lại các tiện ích mở rộng cho phiên mới, sau đó phát ra `session_start` với `reason: \"new\" | \"resume\"` và `previousSessionFile`.\nThực hiện công việc dọn dẹp trong `session_shutdown`, sau đó thiết lập lại mọi trạng thái trong bộ nhớ trong `session_start`.\n\n#### phiên_trước_fork\n\nĐược kích hoạt khi phân nhánh qua `/fork` hoặc sao chép qua `/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\nSau khi phân nhánh hoặc sao chép thành công, pi phát ra `session_shutdown` cho phiên bản tiện ích mở rộng cũ, tải lại và liên kết lại các tiện ích mở rộng cho phiên mới, sau đó phát ra `session_start` với `reason: \"fork\"` và `previousSessionFile`.\nThực hiện công việc dọn dẹp trong `session_shutdown`, sau đó thiết lập lại mọi trạng thái trong bộ nhớ trong `session_start`.\n\n#### session_b Before_compact / session_compact\n\nBắn vào nén. Xem [compaction.md](compaction.md) để biết chi tiết.\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#### phiên_trước_tree / session_tree\n\nĐã kích hoạt trên điều hướng `/tree`. Xem [Sessions](sessions.md) để biết các khái niệm điều hướng cây.\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#### phiên_tắt máy\n\nĐược kích hoạt trước khi thời gian chạy phiên bắt đầu bị phá bỏ. Sử dụng tính năng này để dọn sạch các tài nguyên được mở từ `session_start` hoặc các hook trong phạm vi phiên khác.\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### Sự kiện đại lý\n\n#### before_agent_start\n\nĐược kích hoạt sau khi người dùng gửi lời nhắc, trước vòng lặp tác nhân. Có thể đưa vào một tin nhắn và/hoặc sửa đổi lời nhắc hệ thống.\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\nTrường `systemPromptOptions` cấp cho tiện ích mở rộng quyền truy cập vào cùng dữ liệu có cấu trúc Pi sử dụng để xây dựng lời nhắc hệ thống. Điều này cho phép bạn kiểm tra những gì Pi đã tải — lời nhắc tùy chỉnh, nguyên tắc, đoạn mã công cụ, context files, kỹ năng — mà không cần khám phá lại tài nguyên hoặc phân tích lại cờ. Hãy sử dụng tính năng này khi tiện ích mở rộng của bạn cần thực hiện các thay đổi sâu sắc và đầy đủ thông tin đối với lời nhắc của hệ thống trong khi vẫn tôn trọng cấu hình do người dùng cung cấp.\n\nBên trong `before_agent_start`, `event.systemPrompt` và `ctx.getSystemPrompt()` đều phản ánh lời nhắc hệ thống được xâu chuỗi như của trình xử lý hiện tại. Trình xử lý `before_agent_start` sau này vẫn có thể sửa đổi lại.\n\n#### đại lý_bắt đầu / đại lý_end / đại lý_settled\n\n`agent_start` kích hoạt khi quá trình chạy tác nhân cấp thấp bắt đầu. `agent_end` kích hoạt khi quá trình chạy đó kết thúc, nhưng Pi vẫn có thể tự động thử lại, tự động nén và thử lại hoặc tiếp tục với các tin nhắn tiếp theo được xếp hàng đợi. Sử dụng `agent_settled` để tích hợp trạng thái cần biết Pi sẽ không tự động chạy tiếp.\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#### lượt_bắt đầu / lượt_kết thúc\n\nĐược kích hoạt cho mỗi lượt (một phản hồi LLM + lệnh gọi công cụ).\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#### tin nhắn_bắt đầu / tin nhắn_update / tin nhắn_end\n\nĐược kích hoạt để cập nhật vòng đời tin nhắn.\n\n- `message_start` và `message_end` kích hoạt thông báo kết quả của người dùng, trợ lý và công cụ.\n- `message_update` kích hoạt các bản cập nhật phát trực tuyến của trợ lý.\n- Trình xử lý `message_end` có thể trả về `{ message }` để thay thế thông báo cuối cùng. Việc thay thế phải giữ nguyên `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#### tool_execution_start / tool_execution_update / tool_execution_end\n\nĐược kích hoạt để cập nhật vòng đời thực thi công cụ.\n\nỞ chế độ công cụ song song:\n- `tool_execution_start` được phát ra theo thứ tự nguồn trợ lý trong giai đoạn chuẩn bị chiếu sáng\n- `tool_execution_update` sự kiện có thể xen kẽ giữa các công cụ\n- `tool_execution_end` được phát ra theo thứ tự hoàn thành công cụ sau khi mỗi công cụ được hoàn thiện\n- sự kiện tin nhắn `toolResult` cuối cùng vẫn được phát sau theo thứ tự nguồn trợ lý\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#### bối cảnh\n\nĐược kích hoạt trước mỗi cuộc gọi LLM. Sửa đổi tin nhắn không phá hủy. Xem [Session Format](session-format.md) để biết các loại tin nhắn.\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Được kích hoạt sau khi các tiêu đề HTTP gửi đi được tập hợp. Sử dụng nó để thêm, ghi đè hoặc xóa tiêu đề yêu cầu.\n\nTrình xử lý biến đổi `event.headers` tại chỗ. Đặt khóa thành một chuỗi để thêm hoặc ghi đè chuỗi đó hoặc thành `null` để xóa chuỗi đó.\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\nChạy một lần theo yêu cầu của nhà cung cấp; thử lại sử dụng lại các tiêu đề tương tự thay vì kích hoạt lại hook.\n\n#### before_provider_request\n\nĐược kích hoạt sau khi tải trọng dành riêng cho nhà cung cấp được tạo, ngay trước khi yêu cầu được gửi. Trình xử lý chạy theo thứ tự tải mở rộng. Việc trả về `undefined` giữ cho tải trọng không thay đổi. Trả về bất kỳ giá trị nào khác sẽ thay thế tải trọng cho các trình xử lý sau này và cho yêu cầu thực tế.\n\nMóc này có thể viết lại hướng dẫn hệ thống cấp nhà cung cấp hoặc xóa chúng hoàn toàn. Những thay đổi ở cấp tải trọng đó không được phản ánh bởi `ctx.getSystemPrompt()`, báo cáo chuỗi lời nhắc hệ thống của Pi thay vì tải trọng cuối cùng của nhà cung cấp được tuần tự hóa.\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Điều này chủ yếu hữu ích để gỡ lỗi hành vi tuần tự hóa và bộ đệm của nhà cung cấp.\n\n#### after_provider_response\n\nĐược kích hoạt sau khi nhận được phản hồi HTTP và trước khi nội dung luồng của nó được sử dụng. Trình xử lý chạy theo thứ tự tải mở rộng.\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\nTính khả dụng của tiêu đề phụ thuộc vào nhà cung cấp và phương tiện vận chuyển. Providers rằng phản hồi HTTP trừu tượng có thể không hiển thị tiêu đề.\n\n### Sự kiện mẫu\n\n#### model_select\n\nĐược kích hoạt khi mô hình thay đổi thông qua lệnh `/model`, xoay vòng mô hình (`Ctrl+P`) hoặc khôi phục phiên.\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\nSử dụng tính năng này để cập nhật các thành phần giao diện người dùng (thanh trạng thái, chân trang) hoặc thực hiện khởi tạo theo mô hình cụ thể khi mô hình hoạt động thay đổi.\n\n#### suy nghĩ_level_select\n\nBắn khi mức độ suy nghĩ thay đổi. Đây chỉ là thông báo; giá trị trả về của trình xử lý bị bỏ qua.\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\nSử dụng tính năng này để cập nhật giao diện người dùng mở rộng khi `pi.setThinkingLevel()`, thay đổi mô hình hoặc các điều khiển cấp độ tư duy tích hợp thay đổi cấp độ tư duy tích cực.\n\n### Sự kiện công cụ\n\n#### tool_call\n\nĐược kích hoạt sau `tool_execution_start`, trước khi công cụ thực thi. **Có thể chặn.** Sử dụng `isToolCallEventType` để thu hẹp và nhận dữ liệu nhập đã nhập.\n\nTrước khi `tool_call` chạy, pi đợi các sự kiện Tác nhân được phát ra trước đó hoàn tất việc tiêu hao thông qua `AgentSession`. Điều này có nghĩa là `ctx.sessionManager` được cập nhật thông qua tin nhắn gọi công cụ trợ lý hiện tại.\n\nTrong chế độ thực thi công cụ song song mặc định, các lệnh gọi công cụ anh chị em từ cùng một thông báo trợ lý sẽ được chiếu trước một cách tuần tự, sau đó được thực thi đồng thời. `tool_call` không đảm bảo sẽ thấy kết quả của công cụ tương tự từ cùng một thông báo trợ lý đó trong `ctx.sessionManager`.\n\n`event.input` có thể thay đổi. Thay đổi nó tại chỗ để vá các đối số của công cụ trước khi thực thi.\n\nHành vi đảm bảo:\n- Các đột biến đối với `event.input` ảnh hưởng đến việc thực thi công cụ thực tế\n- Trình xử lý `tool_call` sau này sẽ thấy các đột biến do trình xử lý trước đó thực hiện\n- Không có xác nhận lại được thực hiện sau khi đột biến của bạn\n- Trả về các giá trị từ việc chặn kiểm soát `tool_call` thông qua `{ block: true, reason?: string, terminate?: boolean }`\n- `terminate` chỉ áp dụng cho cuộc gọi bị chặn; tác nhân chỉ dừng sớm khi mọi kết quả cuối cùng trong đợt chấm dứt\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#### Nhập công cụ tùy chỉnh\n\nCác công cụ tùy chỉnh sẽ xuất loại đầu vào của chúng:\n\n```typescript\n// my-extension.ts\nexport type MyToolInput = Static<typeof myToolSchema>;\n```\n\nSử dụng `isToolCallEventType` với các tham số loại rõ ràng:\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#### công cụ_kết quả\n\nĐược kích hoạt sau khi quá trình thực thi công cụ kết thúc và trước `tool_execution_end` cộng với các sự kiện thông báo kết quả công cụ cuối cùng được phát ra. **Có thể sửa đổi kết quả.**\n\nỞ chế độ công cụ song song, `tool_result` và `tool_execution_end` có thể xen kẽ theo thứ tự hoàn thành công cụ, trong khi các sự kiện thông báo `toolResult` cuối cùng vẫn được phát ra sau đó theo thứ tự nguồn trợ lý.\n\n`tool_result` chuỗi xử lý giống như phần mềm trung gian:\n- Trình xử lý chạy theo thứ tự tải mở rộng\n- Mỗi trình xử lý nhìn thấy kết quả mới nhất sau khi thay đổi trình xử lý trước đó\n- Trình xử lý có thể trả về các bản vá một phần (`content`, `details`, `isError` hoặc `usage`); các trường bị bỏ qua giữ nguyên giá trị hiện tại của chúng\n\nSử dụng `ctx.signal` cho công việc không đồng bộ lồng nhau bên trong trình xử lý. Điều này cho phép Esc hủy lệnh gọi mô hình, `fetch()` và các hoạt động nhận biết hủy bỏ khác do tiện ích mở rộng bắt đầu.\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### Sự kiện Bash của người dùng\n\n#### người dùng_bash\n\nĐược kích hoạt khi người dùng thực thi lệnh `!` hoặc `!!`. **Có thể chặ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### Sự kiện đầu vào\n\n#### đầu vào\n\nĐược kích hoạt khi nhận được đầu vào của người dùng, sau khi các lệnh mở rộng được kiểm tra nhưng trước khi mở rộng kỹ năng và mẫu. Sự kiện nhìn thấy văn bản đầu vào thô, vì vậy `/skill:foo` và `/template` chưa được mở rộng.\n\n**Xử lý đơn hàng:**\n1. Các lệnh mở rộng (`/cmd`) được chọn trước - nếu tìm thấy, trình xử lý sẽ chạy và sự kiện đầu vào bị bỏ qua\n2. `input` cháy sự kiện - có thể chặn, biến đổi hoặc xử lý\n3. Nếu không được xử lý: lệnh kỹ năng (`/skill:name`) sẽ mở rộng sang nội dung kỹ năng\n4. Nếu không được xử lý: prompt templates (`/template`) được mở rộng sang nội dung mẫu\n5. Quá trình xử lý tác nhân bắt đầu (`before_agent_start`, v.v.)\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**Kết quả:**\n- `continue` - chuyển qua không thay đổi (mặc định nếu trình xử lý không trả về gì)\n- `transform` - sửa đổi văn bản/hình ảnh, sau đó tiếp tục mở rộng\n- `handled` - bỏ qua hoàn toàn tác nhân (người xử lý đầu tiên trả lại chiến thắng này)\n\nChuyển đổi chuỗi trên các trình xử lý. Xem [input-transform.ts](../examples/extensions/input-transform.ts) và [input-transform-streaming.ts](../examples/extensions/input-transform-streaming.ts) để biết định tuyến nhận biết `streamingBehavior`.\n\n## Bối cảnh mở rộng\n\nTất cả người xử lý đều nhận được `ctx: ExtensionContext`.\n\n### ctx.ui\n\nPhương thức UI để tương tác với người dùng. Xem [Custom UI](#custom-ui) để biết chi tiết đầy đủ.\n\n### ctx.mode\n\nChế độ chạy hiện tại: `\"tui\"`, `\"rpc\"`, `\"json\"` hoặc `\"print\"`. Sử dụng `ctx.mode === \"tui\"` để bảo vệ các tính năng chỉ dành cho thiết bị đầu cuối như `custom()`, nhà máy sản xuất thành phần, đầu vào thiết bị đầu cuối và hiển thị TUI trực tiếp.\n\n### ctx.hasUI\n\n`true` ở chế độ TUI và RPC. `false` ở chế độ in (`-p`) và chế độ JSON. Sử dụng tính năng này để bảo vệ các phương thức hộp thoại (`select`, `confirm`, `input`, `editor`) và các phương thức bắn và quên (`notify`, `setStatus`, `setWidget`, `setTitle`, `setEditorText`) hoạt động ở cả TUI và chế độ RPC. Trong chế độ RPC, một số phương pháp dành riêng cho TUI là không hoạt động hoặc trả về giá trị mặc định (xem [rpc.md](rpc.md#extension-ui-protocol)).\n\n### ctx.cwd\n\nThư mục làm việc hiện tại.\n\nSử dụng `CONFIG_DIR_NAME` thay vì mã hóa cứng `.pi` khi xây dựng đường dẫn cấu hình cục bộ dự án. Các bản phân phối được đổi thương hiệu có thể sử dụng tên thư mục cấu hình khác.\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\nTrả về xem độ tin cậy của dự án cục bộ có hoạt động trong bối cảnh phiên hiện tại hay không. Điều này bao gồm các quyết định tin cậy tạm thời và CLI ghi đè tin cậy, không chỉ các quyết định đã lưu trong kho ủy thác toàn cầu.\n\nSử dụng điều này trước khi đọc cấu hình tiện ích mở rộng cục bộ của dự án chỉ nên được tôn trọng đối với các dự án đáng tin cậy.\n\n### ctx.sessionManager\n\nQuyền truy cập chỉ đọc vào trạng thái phiên. Xem [Session Format](session-format.md) để biết đầy đủ SessionManager API và các loại mục nhập.\n\nĐối với `tool_call`, trạng thái này được đồng bộ hóa thông qua thông báo trợ lý hiện tại trước khi trình xử lý chạy. Trong chế độ thực thi công cụ song song, vẫn không đảm bảo bao gồm các kết quả của công cụ anh chị em từ cùng một thông báo trợ lý.\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\nTruy cập vào các mô hình, nhà cung cấp và xác thực được giải quyết. `ctx.modelRegistry.getProvider(id)` trả về nhà cung cấp pi-ai hiệu quả, trong khi `getProviderAuth(id)` giải quyết API key, tiêu đề, URL cơ sở và môi trường trong phạm vi nhà cung cấp hiện tại mà không yêu cầu mô hình được tải. `ctx.model` là mô hình hoạt động và `ctx.thinkingLevel` là mức độ tư duy hiệu quả hiện tại.\n\n`ctx.scopedModels` là danh sách các mô hình chỉ đọc trong phạm vi phiên hiện tại — giống như tập hợp mà lệnh `/scoped-models` hiển thị. Vấn đề này được giải quyết khi bắt đầu phiên từ cờ `--models` CLI và cài đặt `enabledModels` (khớp với danh mục có sẵn với trận đấu nhỏ trên `provider/modelId` hoặc trần `modelId`). Nó trống khi không định cấu hình phạm vi, nghĩa là mọi mô hình có sẵn đều có thể sử dụng được. Mỗi mục nhập là `{ model, thinkingLevel? }`, trong đó `thinkingLevel` chỉ được đặt khi một mẫu đã ghim nó (ví dụ: `anthropic/*:high`). Sử dụng nó để điền vào bộ chọn mô hình phản ánh bộ chọn tích hợp thay vì liệt kê toàn bộ danh mục thông qua `ctx.modelRegistry.getAvailable()`.\n\n### ctx.signal\n\nTín hiệu hủy tác nhân hiện tại hoặc `undefined` khi không có lượt tác nhân nào được kích hoạt.\n\nSử dụng công cụ này để hủy bỏ công việc lồng nhau được bắt đầu bởi trình xử lý tiện ích mở rộng, ví dụ:\n- `fetch(..., { signal: ctx.signal })`\n- cuộc gọi mô hình chấp nhận `signal`\n- người trợ giúp tệp hoặc quy trình chấp nhận `AbortSignal`\n\n`ctx.signal` thường được xác định trong các sự kiện lượt đang hoạt động như `tool_call`, `tool_result`, `message_update` và `turn_end`.\nNó thường là `undefined` trong các bối cảnh nhàn rỗi hoặc không rẽ, chẳng hạn như sự kiện phiên, lệnh mở rộng và các phím tắt được kích hoạt khi pi không hoạt động.\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\nKiểm soát người trợ giúp luồng. `ctx.isIdle()` là sai trong khi Pi đang xử lý một lần chạy tác nhân, thử lại tự động, thử lại tự động nén hoặc tiếp tục xếp hàng đợi.\n\n### ctx.shutdown()\n\nYêu cầu tắt pi một cách duyên dáng.\n\n- **Chế độ tương tác:** Trì hoãn cho đến khi tổng đài viên không hoạt động (sau khi xử lý tất cả các tin nhắn chỉ đạo và theo dõi được xếp hàng đợi).\n- **RPC chế độ:** Trì hoãn cho đến trạng thái không hoạt động tiếp theo (sau khi hoàn thành phản hồi lệnh hiện tại, khi chờ lệnh tiếp theo).\n- **Chế độ in:** Không hoạt động. Quá trình sẽ tự động thoát khi tất cả lời nhắc được xử lý.\n\nPhát ra sự kiện `session_shutdown` cho tất cả tiện ích mở rộng trước khi thoát. Có sẵn trong mọi ngữ cảnh (trình xử lý sự kiện, công cụ, lệnh, phím tắt).\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\nTrả về mức sử dụng ngữ cảnh hiện tại cho mô hình đang hoạt động. Sử dụng mức sử dụng trợ lý cuối cùng khi có sẵn, sau đó ước tính mã thông báo cho các tin nhắn theo dõi.\n\n```typescript\nconst usage = ctx.getContextUsage();\nif (usage && usage.tokens > 100_000) {\n  // ...\n}\n```\n\n### ctx.compact()\n\nKích hoạt nén mà không cần chờ hoàn thành. Sử dụng `onComplete` và `onError` cho các hành động tiếp theo.\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\nTrả về chuỗi lời nhắc hệ thống hiện tại của Pi.\n\n- Trong `before_agent_start`, điều này phản ánh những thay đổi theo chuỗi của hệ thống được thực hiện cho đến lượt hiện tại.\n- Nó không bao gồm các đột biến tin nhắn `context` sau này.\n- Nó không bao gồm việc ghi lại tải trọng `before_provider_request`.\n- Nếu các tiện ích mở rộng được tải sau này chạy sau tiện ích mở rộng của bạn, chúng vẫn có thể thay đổi nội dung được gửi cuối cùng.\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## Phần mở rộngCommandContext\n\nTrình xử lý lệnh nhận `ExtensionCommandContext`, mở rộng `ExtensionContext` bằng các phương thức kiểm soát phiên. Những điều này chỉ có trong lệnh vì chúng có thể gây bế tắc nếu được gọi từ trình xử lý sự kiện.\n\n### ctx.getSystemPromptOptions()\n\nTrả về đầu vào cơ sở Pi hiện đang sử dụng để xây dựng lời nhắc hệ thống.\n\n```typescript\nconst options = ctx.getSystemPromptOptions();\nconst contextPaths = options.contextFiles?.map((file) => file.path) ?? [];\n```\n\nĐiều này có hình dạng và khả năng thay đổi tương tự như `before_agent_start` `event.systemPromptOptions`: lời nhắc tùy chỉnh, công cụ hoạt động, đoạn công cụ, hướng dẫn lời nhắc, văn bản lời nhắc hệ thống được nối thêm, cwd, đã tải context files và các kỹ năng đã tải. Nó có thể bao gồm nội dung tệp ngữ cảnh đầy đủ, vì vậy hãy coi nó như dữ liệu cục bộ mở rộng nhạy cảm và tránh hiển thị nó thông qua danh sách lệnh, nhật ký hoặc siêu dữ liệu tự động hoàn thành.\n\nĐiều này báo cáo các đầu vào nhắc cơ sở hiện tại. Nó không bao gồm các thay đổi nhắc nhở của hệ thống theo chuỗi `before_agent_start` mỗi lượt, các đột biến thông báo sự kiện `context` sau này hoặc ghi lại tải trọng `before_provider_request`.\n\n### ctx.waitForIdle()\n\nĐợi tác nhân xử lý hoàn toàn, bao gồm thử lại tự động, thử lại tự động nén và tiếp tục xếp hàng:\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(tùy chọn?)\n\nTạo một phiên mới:\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\nTùy chọn:\n- `parentSession`: tệp phiên gốc để ghi vào tiêu đề phiên mới\n- `setup`: thay đổi `SessionManager` của phiên mới trước khi `withSession` chạy\n- `withSession`: chạy công việc sau chuyển đổi dựa trên bối cảnh phiên thay thế mới. Không sử dụng `pi` / lệnh `ctx` cũ đã bắt; xem [Session replacement lifecycle and footguns](#session-replacement-lifecycle-and-footguns).\n\n### ctx.fork(entryId, tùy chọn?)\n\nNgã ba từ một mục cụ thể, tạo một tệp phiên mới:\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\nTùy chọn:\n- `position`: `\"before\"` (mặc định) phân nhánh trước thông báo của người dùng đã chọn, khôi phục lời nhắc đó vào trình chỉnh sửa\n- `position`: `\"at\"` sao chép đường dẫn đang hoạt động thông qua mục đã chọn mà không khôi phục văn bản soạn thảo\n- `withSession`: chạy công việc sau chuyển đổi dựa trên bối cảnh phiên thay thế mới. Không sử dụng `pi` / lệnh `ctx` cũ đã bắt; xem [Session replacement lifecycle and footguns](#session-replacement-lifecycle-and-footguns).\n\n### ctx.navigateTree(targetId, tùy chọn?)\n\nĐiều hướng đến một điểm khác trong 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\nTùy chọn:\n- `summarize`: Có tạo bản tóm tắt về nhánh bị bỏ rơi hay không\n- `customInstructions`: Hướng dẫn tùy chỉnh cho phần tóm tắt\n- `replaceInstructions`: Nếu đúng, `customInstructions` thay thế lời nhắc mặc định thay vì được thêm vào\n- `label`: Nhãn để gắn vào mục tóm tắt nhánh (hoặc mục tiêu nếu không tóm tắt)\n\n### ctx.switchSession(sessionPath, tùy chọn?)\n\nChuyển sang một tệp phiên khác:\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\nTùy chọn:\n- `withSession`: chạy công việc sau chuyển đổi dựa trên bối cảnh phiên thay thế mới. Không sử dụng `pi` / lệnh `ctx` cũ đã bắt; xem [Session replacement lifecycle and footguns](#session-replacement-lifecycle-and-footguns).\n\nĐể khám phá các phiên có sẵn, hãy sử dụng các phương thức tĩnh `SessionManager.list()` hoặc `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### Vòng đời thay thế phiên và súng ngắn\n\n`withSession` nhận được `ReplacedSessionContext` mới, mở rộng `ExtensionCommandContext` với các trình trợ giúp không đồng bộ `sendMessage()` và `sendUserMessage()` được liên kết với phiên thay thế.\n\nVòng đời và súng ngắn:\n- `withSession` chỉ chạy sau khi phiên cũ đã phát ra `session_shutdown`, thời gian chạy cũ đã bị phá bỏ, phiên thay thế đã được khôi phục và phiên bản tiện ích mở rộng mới đã nhận được `session_start`.\n- Cuộc gọi lại vẫn thực thi trong bao đóng ban đầu, không phải bên trong phiên bản tiện ích mở rộng mới. Điều đó có nghĩa là phiên bản tiện ích mở rộng cũ của bạn có thể đã chạy quá trình dọn dẹp tắt máy trước khi `withSession` bắt đầu.\n- Các đối tượng cũ `pi` / lệnh cũ `ctx` đã bị bắt giữ sẽ cũ sau khi thay thế và sẽ bị loại bỏ nếu được sử dụng. Chỉ sử dụng `ctx` được chuyển đến `withSession` cho công việc theo phiên.\n- Các đối tượng thô được trích xuất trước đó vẫn là trách nhiệm của bạn. Ví dụ: nếu bạn chụp `const sm = ctx.sessionManager` trước khi thay thế, `sm` vẫn là đối tượng `SessionManager` cũ. Không sử dụng lại nó sau khi thay thế.\n- Mã trong `withSession` sẽ giả sử bất kỳ trạng thái nào bị trình xử lý `session_shutdown` của bạn vô hiệu hóa đều đã biến mất. Chỉ thu thập dữ liệu đơn giản còn tồn tại sau khi tắt máy một cách sạch sẽ, chẳng hạn như chuỗi, id và cấu hình được tuần tự hóa.\n\nMô hình an toà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\nMẫu không an toà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\nChạy luồng tải lại tương tự như `/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\nHành vi quan trọng:\n- `await ctx.reload()` phát ra `session_shutdown` cho thời gian chạy tiện ích mở rộng hiện tại\n- Sau đó, nó tải lại tài nguyên và phát ra `session_start` với `reason: \"reload\"` và `resources_discover` với lý do `\"reload\"`\n- Trình xử lý lệnh hiện đang chạy vẫn tiếp tục trong khung cuộc gọi cũ\n- Mã sau `await ctx.reload()` vẫn chạy từ phiên bản tải lại trước\n- Mã sau `await ctx.reload()` không được cho rằng trạng thái tiện ích mở rộng trong bộ nhớ cũ vẫn hợp lệ\n- Sau khi trình xử lý quay trở lại, các lệnh/sự kiện/cuộc gọi công cụ trong tương lai sẽ sử dụng phiên bản tiện ích mở rộng mới\n\nĐể có thể dự đoán hành vi, hãy coi tải lại là thiết bị đầu cuối cho trình xử lý đó (`await ctx.reload(); return;`).\n\nCác công cụ chạy bằng `ExtensionContext` nên không thể gọi trực tiếp `ctx.reload()`. Sử dụng lệnh làm điểm nhập tải lại, sau đó hiển thị công cụ xếp hàng lệnh đó dưới dạng thông báo tiếp theo của người dùng.\n\nCông cụ ví dụ mà LLM có thể gọi để kích hoạt tải lại:\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## Phương thức mở rộngAPI\n\n### pi.on(sự kiện, trình xử lý)\n\nĐăng ký các sự kiện. Xem [Events](#events) để biết loại sự kiện và giá trị trả về.\n\n### pi.registerTool(định nghĩa)\n\nĐăng ký một công cụ tùy chỉnh có thể gọi được bởi LLM. Xem [Custom Tools](#custom-tools) để biết chi tiết đầy đủ.\n\n`pi.registerTool()` hoạt động cả trong khi tải tiện ích mở rộng và sau khi khởi động. Bạn có thể gọi nó bên trong `session_start`, trình xử lý lệnh hoặc các trình xử lý sự kiện khác. Các công cụ mới được làm mới ngay lập tức trong cùng một phiên, vì vậy chúng xuất hiện trong `pi.getAllTools()` và có thể được LLM gọi mà không cần `/reload`.\n\nSử dụng `pi.setActiveTools()` để bật hoặc tắt các công cụ (bao gồm cả các công cụ được thêm động) khi chạy.\n\nSử dụng `promptSnippet` để chọn một công cụ tùy chỉnh vào mục nhập một dòng trong `Available tools` và `promptGuidelines` để nối các dấu đầu dòng dành riêng cho công cụ vào phần `Guidelines` mặc định khi công cụ này đang hoạt động.\n\n**Quan trọng:** `promptGuidelines` đầu đạn được gắn phẳng vào phần `Guidelines` không có tiền tố tên công cụ. Mỗi hướng dẫn phải đặt tên cho công cụ mà nó đề cập đến - tránh \"Sử dụng công cụ này khi...\" vì LLM không thể cho biết \"cái này\" nghĩa là công cụ nào. Thay vào đó hãy viết \"Sử dụng my_tool khi...\".\n\nXem [dynamic-tools.ts](../examples/extensions/dynamic-tools.ts) để biết ví dụ đầy đủ.\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(tin nhắn, tùy chọn?)\n\nĐưa một tin nhắn tùy chỉnh vào phiên. Thông báo tùy chỉnh tham gia vào ngữ cảnh LLM. Đối với nội dung lâu dài chỉ dành cho TUI không được gửi tới LLM, hãy sử dụng [`pi.appendEntry()`](#piappendentrycustomtype-data) với [`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**Tùy chọn:**\n- `deliverAs` - Chế độ giao hàng:\n  - `\"steer\"` (mặc định) - Xếp hàng tin nhắn trong khi truyền phát. Được phân phối sau khi lượt trợ lý hiện tại hoàn tất việc thực hiện các lệnh gọi công cụ của nó, trước lệnh gọi LLM tiếp theo.\n  - `\"followUp\"` - Chờ đại lý kết thúc. Chỉ được giao khi đại lý không còn cuộc gọi công cụ nào nữa.\n  - `\"nextTurn\"` - Xếp hàng chờ lời nhắc của người dùng tiếp theo. Không làm gián đoạn hoặc kích hoạt bất cứ điều gì.\n- `triggerTurn: true` - Nếu tác nhân không hoạt động, hãy kích hoạt phản hồi LLM ngay lập tức. Chỉ áp dụng cho chế độ `\"steer\"` và `\"followUp\"` (bỏ qua đối với `\"nextTurn\"`).\n\n### pi.sendUserMessage(nội dung, tùy chọn?)\n\nGửi tin nhắn người dùng đến đại lý. Không giống như `sendMessage()` gửi tin nhắn tùy chỉnh, thao tác này sẽ gửi một tin nhắn thực tế của người dùng xuất hiện như thể được người dùng nhập. Luôn kích hoạt một lượt.\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**Tùy chọn:**\n- `deliverAs` - Bắt buộc khi tác nhân đang phát trực tuyến:\n  - `\"steer\"` - Xếp hàng tin nhắn để gửi sau khi lượt trợ lý hiện tại thực hiện xong các lệnh gọi công cụ của mình\n  - `\"followUp\"` - Chờ đại lý làm xong hết công cụ\n\nKhi không phát trực tuyến, tin nhắn sẽ được gửi ngay lập tức và kích hoạt một lượt mới. Khi phát trực tiếp mà không có `deliverAs`, sẽ xảy ra lỗi.\n\nXem [send-user-message.ts](../examples/extensions/send-user-message.ts) để biết ví dụ đầy đủ.\n\n### pi.appendEntry(customType, dữ liệu?)\n\nKiên trì dữ liệu mở rộng. Các mục tùy chỉnh KHÔNG tham gia vào ngữ cảnh LLM. Ở chế độ tương tác, chúng cũng có thể hiển thị bên trong bản ghi cuộc trò chuyện khi được ghép nối với `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(name)\n\nĐặt tên hiển thị phiên (hiển thị trong bộ chọn phiên thay vì tin nhắn đầu tiên).\n\n```typescript\npi.setSessionName(\"Refactor auth module\");\n```\n\n### pi.getSessionName()\n\nLấy tên phiên hiện tại, nếu được đặt.\n\n```typescript\nconst name = pi.getSessionName();\nif (name) {\n  console.log(`Session: ${name}`);\n}\n```\n\n### pi.setLabel(entryId, nhãn)\n\nĐặt hoặc xóa nhãn trên một mục nhập. Nhãn là các điểm đánh dấu do người dùng xác định để đánh dấu trang và điều hướng (hiển thị trong bộ chọ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\nNhãn vẫn tồn tại trong phiên và tồn tại khi khởi động lại. Sử dụng chúng để đánh dấu các điểm quan trọng (điểm rẽ, điểm kiểm tra) trong cây hội thoại.\n\n### pi.registerCommand(tên, tùy chọn)\n\nĐăng ký một lệnh.\n\nNếu nhiều tiện ích mở rộng đăng ký cùng một tên lệnh, pi sẽ giữ tất cả chúng và gán các hậu tố gọi số theo thứ tự tải, ví dụ: `/review:1` và `/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\nTùy chọn: thêm tính năng tự động hoàn thành đối số cho `/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\nNhận slash commands có sẵn để gọi thông qua `prompt` trong phiên hiện tại. Bao gồm các lệnh mở rộng, prompt templates và các lệnh kỹ năng.\nDanh sách khớp với thứ tự RPC `get_commands`: đầu tiên là tiện ích mở rộng, sau đó là mẫu, sau đó là kỹ năng.\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\nMỗi mục có hình dạng này:\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\nSử dụng `sourceInfo` làm trường xuất xứ chuẩn. Không suy ra quyền sở hữu từ tên lệnh hoặc từ phân tích cú pháp đường dẫn đặc biệt.\n\nCác lệnh tương tác tích hợp (như `/model` và `/settings`) không được đưa vào đây. Chúng chỉ được xử lý trong tương tác\nvà sẽ không thực thi nếu được gửi qua `prompt`.\n\n### pi.registerMessageRenderer(customType, trình kết xuất)\n\nĐăng ký trình kết xuất TUI tùy chỉnh cho các tin nhắn tùy chỉnh với `customType` của bạn. Thông báo tùy chỉnh được tạo bằng `pi.sendMessage()` và tham gia vào ngữ cảnh LLM. Xem [Custom UI](#custom-ui).\n\n### pi.registerMarkdownTransformer(máy biến áp)\n\nĐăng ký một máy biến áp cho Markdown trong văn bản người dùng thông thường, văn bản trợ lý và khối tư duy. Máy biến áp chạy theo thứ tự tải mở rộng và mỗi máy biến áp nhận được Markdown do máy biến áp trước đó trả về. Sau khi chuỗi kết thúc, Pi hiển thị nội dung đã chuyển đổi bằng trình kết xuất tích hợp sẵn.\n\nMáy biến áp nhận chuỗi Markdown và ngữ cảnh có:\n\n- `messageType` — `\"user\"`, `\"assistant\"` hoặc `\"assistant-thinking\"`\n- `isStreaming` — `true` để cập nhật một phần trợ lý; `false` dành cho người dùng, trợ lý đã hoàn thiện và tin nhắn được khôi phục\n- `availableWidth` — các cột đầu cuối chính xác có sẵn cho nội dung Markdown được chuyển đổi\n\nTrả về Markdown đã biến đổi:\n\n```typescript\npi.registerMarkdownTransformer((markdown, { messageType, isStreaming }) => {\n  if (isStreaming || messageType === \"assistant-thinking\") return markdown;\n  return markdown.replaceAll(\"-->\", \"→\");\n});\n```\n\nNếu một máy biến áp bị hỏng, Pi sẽ giữ nguyên Markdown được tạo ra cho đến nay và tiếp tục với máy biến áp tiếp theo. Hook chỉ hiển thị: thông báo ban đầu không thay đổi trong bối cảnh phiên và mô hình. Nó chạy cho các tin nhắn của người dùng mới, các bản cập nhật phát trực tuyến của trợ lý, các tin nhắn phiên được khôi phục và các thay đổi về độ rộng của thiết bị đầu cuối, do đó, máy biến áp sẽ vẫn đồng bộ và không tốn kém.\n\n### pi.registerEntryRenderer(customType, trình kết xuất)\n\nĐăng ký trình kết xuất TUI tùy chỉnh cho các mục tùy chỉnh bằng `customType` của bạn. Các mục nhập tùy chỉnh được tạo bằng `pi.appendEntry()` và không tham gia vào ngữ cảnh 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(phím tắt, tùy chọn)\n\nĐăng ký một phím tắt. Xem [keybindings.md](keybindings.md) để biết định dạng phím tắt và tổ hợp phím tích hợp.\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(tên, tùy chọn)\n\nĐăng ký cờ 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(lệnh, args, tùy chọn?)\n\nThực hiện lệnh shell.\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\nQuản lý các công cụ đang hoạt động. Điều này hoạt động cho cả công cụ tích hợp và công cụ được đăng ký động. `pi.getActiveTools()` trả về tên công cụ đang hoạt động là `string[]`; `pi.getAllTools()` trả về siêu dữ liệu cho tất cả các công cụ được định cấu hình.\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()` trả về `name`, `description`, `parameters`, `promptGuidelines` và `sourceInfo`.\n\nGiá trị `sourceInfo.source` điển hình:\n- `builtin` cho các công cụ tích hợp\n- `sdk` đối với các công cụ được chuyển qua `createAgentSession({ customTools })`\n- siêu dữ liệu nguồn mở rộng cho các công cụ được đăng ký bởi tiện ích mở rộng\n\n### pi.setModel(model)\n\nĐặt mô hình hiện tại. Trả về `false` nếu không có sẵn API key cho mô hình. Xem [models.md](models.md) để biết cách định cấu hình các mô hình tùy chỉnh.\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(level)\n\nNhận hoặc thiết lập mức độ suy nghĩ. Mức độ được giới hạn theo khả năng của mô hình (các mô hình không suy luận luôn sử dụng \"tắt\"). Những thay đổi phát ra `thinking_level_select`.\n\n```typescript\nconst current = pi.getThinkingLevel();  // \"off\" | \"minimal\" | \"low\" | \"medium\" | \"high\" | \"xhigh\" | \"max\"\npi.setThinkingLevel(\"high\");\n```\n\n### pi.events\n\nBus sự kiện được chia sẻ để liên lạc giữa các tiện ích mở rộng:\n\n```typescript\npi.events.on(\"my:event\", (data) => { ... });\npi.events.emit(\"my:event\", { ... });\n```\n\n### pi.registerProvider(tên, cấu hình)\n\nĐăng ký hoặc ghi đè nhà cung cấp mô hình một cách linh hoạt. Hữu ích cho proxy, điểm cuối tùy chỉnh hoặc cấu hình mô hình toàn nhóm.\n\nCác cuộc gọi được thực hiện trong chức năng của nhà máy mở rộng sẽ được xếp hàng đợi và được áp dụng sau khi trình chạy khởi tạo. Các lệnh gọi được thực hiện sau đó — ví dụ: từ trình xử lý lệnh theo luồng thiết lập của người dùng — sẽ có hiệu lực ngay lập tức mà không yêu cầu `/reload`.\n\nNhà cung cấp năng động có thể triển khai `refreshModels`. Pi gọi nó trong quá trình làm mới mô hình, xuất bản danh sách trả về một cách đồng bộ thông qua nhà cung cấp và chuyển ngữ cảnh thông tin xác thực chuẩn/danh mục được lưu trữ/mạng/tín hiệu. Tiện ích mở rộng quyết định xem có duy trì siêu dữ liệu danh mục thông qua `context.publish({ persist: entry })` được kiểm tra thế hệ hay không; các máy chủ trực tiếp như llama.cpp có thể trả về các mô hình mà không cần lưu giữ chúng.\n\n`context.signal` luôn là một tín hiệu cụ thể và lệnh gọi lại của nhà cung cấp phải chuyển tín hiệu đó sang chặn I/O. Các lệnh gọi `ModelRuntime.refresh()` và `ModelRegistry.refresh()` công khai chấp nhận tín hiệu tùy chọn và không bị chặn khi nó bị bỏ qua; tiện ích mở rộng và ứng dụng chọn thời hạn riêng của họ. Việc hủy sẽ khiến người gọi không phải chờ đợi ngay cả khi nhà cung cấp bỏ qua tín hiệu, nhưng vẫn cần phải hợp tác để dừng công việc cơ bản.\n\nExtensions cần hành vi xác thực, lọc, làm mới hoặc truyền phát của nhà cung cấp gốc có thể đăng ký `Provider` hoàn chỉnh từ `@earendil-works/pi-ai`. Nhà cung cấp trở thành cơ sở tổng hợp và phần ghi đè `models.json` vẫn áp dụng ở trên nó.\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\nBiểu mẫu đối tượng chấp nhận một pi-ai `Provider` hoàn chỉnh, bao gồm hành vi gốc `auth`, `getModels`, `refreshModels`, `filterModels`, `stream` và `streamSimple`.\n\n**Tùy chọn cấu hình cũ:**\n- `name` - Tên hiển thị của nhà cung cấp trong giao diện người dùng, chẳng hạn như `/login`.\n- `baseUrl` - API URL điểm cuối. Cần thiết khi xác định mô hình.\n- `apiKey` - API key theo nghĩa đen, nội suy môi trường (`$ENV_VAR` hoặc `${ENV_VAR}`) hoặc dẫn đầu `!command`. Bắt buộc khi xác định mô hình (trừ khi được cung cấp `oauth`). `$` thoát ``apiKey` - API key theo nghĩa đen, nội suy môi trường (`$ENV_VAR` hoặc `${ENV_VAR}`) hoặc dẫn đầu `!command`. Bắt buộc khi xác định mô hình (trừ khi được cung cấp `oauth`). `$` thoát  và `$!` thoát `!` theo nghĩa đen mà không kích hoạt thực thi lệnh.\n- `api` - API gõ: `\"anthropic-messages\"`, `\"openai-completions\"`, `\"openai-responses\"`, v.v.\n- `headers` - Tiêu đề tùy chỉnh để đưa vào yêu cầu.\n- `authHeader` - Nếu đúng, tự động thêm tiêu đề `Authorization: Bearer`.\n- `models` - Mảng định nghĩa mô hình. Nếu được cung cấp, sẽ thay thế tất cả các mô hình hiện có cho nhà cung cấp này. Định nghĩa mô hình có thể đặt `baseUrl` để ghi đè điểm cuối của nhà cung cấp cho mô hình đó.\n- `refreshModels` - Gọi lại khám phá động không đồng bộ. Các mô hình được trả về của nó sẽ thay thế các mô hình do tiện ích mở rộng cung cấp. `context.stored` chứa ảnh chụp nhanh của nhà cung cấp liên tục; chỉ sử dụng `context.publish({ persist: entry })` được kiểm tra thế hệ khi dữ liệu danh mục đã cập nhật vẫn tồn tại. Sử dụng `persist: null` để xóa ảnh chụp nhanh đó.\n- `oauth` - Cấu hình nhà cung cấp OAuth để hỗ trợ `/login`. Khi được cung cấp, nhà cung cấp sẽ xuất hiện trong menu đăng nhập.\n- `streamSimple` - Triển khai phát trực tuyến tùy chỉnh cho APIs không chuẩn.\n\nXem [custom-provider.md](custom-provider.md) để biết các chủ đề nâng cao: phát trực tuyến tùy chỉnh APIs, OAuth chi tiết, tham khảo định nghĩa mô hình.\n\n### pi.unregisterProvider(tên)\n\nXóa nhà cung cấp đã đăng ký trước đó và các mô hình của nhà cung cấp đó. Các mô hình tích hợp đã bị nhà cung cấp ghi đè sẽ được khôi phục. Không có hiệu lực nếu nhà cung cấp chưa được đăng ký.\n\nGiống như `registerProvider`, điều này có hiệu lực ngay lập tức khi được gọi sau giai đoạn tải ban đầu, do đó không cần phải có `/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## Quản lý nhà nước\n\nExtensions có trạng thái nên lưu trữ nó trong kết quả công cụ `details` để được hỗ trợ phân nhánh phù hợp:\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## Công cụ tùy chỉnh\n\nĐăng ký các công cụ mà LLM có thể gọi qua `pi.registerTool()`. Các công cụ xuất hiện trong lời nhắc hệ thống và có thể có kết xuất tùy chỉnh.\n\nSử dụng `promptSnippet` cho mục nhập một dòng ngắn trong phần `Available tools` trong lời nhắc hệ thống mặc định. Nếu bị bỏ qua, các công cụ tùy chỉnh sẽ bị loại khỏi phần đó.\n\nSử dụng `promptGuidelines` để thêm dấu đầu dòng dành riêng cho công cụ vào phần lời nhắc hệ thống mặc định `Guidelines`. Những dấu đầu dòng này chỉ được đưa vào khi công cụ đang hoạt động (ví dụ: sau `pi.setActiveTools([...])`).\n\n**Quan trọng:** `promptGuidelines` đầu đạn được gắn phẳng vào phần `Guidelines` mà không có tiền tố hoặc nhóm tên công cụ. Mỗi hướng dẫn phải đặt tên cho công cụ mà nó đề cập đến - tránh \"Sử dụng công cụ này khi...\" vì LLM không thể cho biết \"cái này\" nghĩa là công cụ nào. Thay vào đó hãy viết \"Sử dụng my_tool khi...\".\n\nLưu ý: Một số mô hình thật ngu ngốc và bao gồm tiền tố @ trong đối số đường chạy dao. Các công cụ tích hợp sẽ loại bỏ dấu @ ở đầu trước khi giải quyết các đường dẫn. Nếu công cụ tùy chỉnh của bạn chấp nhận một đường dẫn, hãy chuẩn hóa cả @ ở đầu.\n\nNếu công cụ tùy chỉnh của bạn thay đổi tệp, hãy sử dụng `withFileMutationQueue()` để nó tham gia vào cùng một hàng đợi cho mỗi tệp như `edit` và `write` tích hợp sẵn. Điều này quan trọng vì các lệnh gọi công cụ chạy song song theo mặc định. Không có hàng đợi, hai công cụ có thể đọc cùng một nội dung tệp cũ, tính toán các bản cập nhật khác nhau và sau đó bất kỳ vùng ghi nào cuối cùng sẽ ghi đè lên vùng kia.\n\nTrường hợp lỗi ví dụ: công cụ tùy chỉnh của bạn chỉnh sửa `foo.ts` trong khi `edit` tích hợp cũng thay đổi `foo.ts` trong cùng một lượt trợ lý. Nếu công cụ của bạn không tham gia vào hàng đợi, cả hai đều có thể đọc `foo.ts` gốc, áp dụng các thay đổi riêng biệt và một trong những thay đổi đó sẽ bị mất.\n\nChuyển đường dẫn tệp đích thực tới `withFileMutationQueue()` chứ không phải đối số người dùng thô. Trước tiên hãy giải quyết nó thành đường dẫn tuyệt đối, liên quan đến `ctx.cwd` hoặc thư mục làm việc của công cụ của bạn. Đối với các tệp hiện có, trình trợ giúp chuẩn hóa thông qua `realpath()`, do đó, các bí danh liên kết tượng trưng cho cùng một tệp sẽ chia sẻ một hàng đợi. Đối với các tệp mới, nó sẽ quay trở lại đường dẫn tuyệt đối đã được giải quyết vì chưa có gì cho `realpath()`.\n\nXếp hàng toàn bộ cửa sổ đột biến trên đường dẫn đích đó. Điều đó bao gồm logic đọc-sửa-ghi, không chỉ là lần ghi cuối cùng.\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### Định nghĩa công cụ\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**Tính toán mức sử dụng:** Nếu một công cụ thực hiện lệnh gọi LLM lồng nhau, hãy trả về `Usage` kết hợp của chúng dưới dạng `usage`. Pi duy trì nó trên kết quả của công cụ và đưa nó vào tổng số phiên ở chân trang, `/session` và RPC. `tool_result` trình xử lý có thể kiểm tra hoặc thay thế giá trị này.\n\n**Lỗi báo hiệu:** Để đánh dấu việc thực thi công cụ là không thành công (đặt `isError: true` trên kết quả và báo cáo cho LLM), hãy gửi lỗi từ `execute`. Trả về một giá trị không bao giờ đặt cờ lỗi bất kể bạn đưa thuộc tính nào vào đối tượng trả về.\n\n**Chấm dứt sớm:** Trả về `terminate: true` từ `execute()` để gợi ý rằng cuộc gọi LLM tiếp theo tự động sẽ bị bỏ qua sau đợt công cụ hiện tại. Điều này chỉ có hiệu lực khi mọi công cụ hoàn thiện trong lô đó đều chấm dứt. Xem [examples/extensions/structured-output.ts](../examples/extensions/structured-output.ts) để biết ví dụ tối thiểu trong đó tác nhân kết thúc bằng lệnh gọi công cụ đầu ra có cấu trúc cuối cùng.\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**Quan trọng:** Sử dụng `StringEnum` từ `@earendil-works/pi-ai` cho enum chuỗi. `Type.Union`/`Type.Literal` không hoạt động với API của Google.\n\n**Chuẩn bị lập luận:** `prepareArguments(args)` là tùy chọn. Nếu được xác định, nó sẽ chạy trước xác thực lược đồ và trước `execute()`. Sử dụng nó để bắt chước hình dạng đầu vào cũ hơn được chấp nhận khi pi tiếp tục phiên cũ hơn có các đối số lệnh gọi công cụ được lưu trữ không còn khớp với lược đồ hiện tại. Trả về đối tượng bạn muốn xác thực dựa trên `parameters`. Giữ lược đồ công khai nghiêm ngặt. Không thêm các trường tương thích không được dùng nữa vào `parameters` chỉ để giữ cho các phiên cũ được tiếp tục hoạt động.\n\nVí dụ: phiên cũ hơn có thể chứa lệnh gọi công cụ `edit` với `oldText` và `newText` cấp cao nhất, trong khi lược đồ hiện tại chỉ chấp nhận `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### Ghi đè các công cụ tích hợp\n\nExtensions có thể ghi đè các công cụ tích hợp (`read`, `bash`, `edit`, `write`, `grep`, `find`, `ls`) bằng cách đăng ký một công cụ có cùng tên. Chế độ tương tác hiển thị cảnh báo khi điều này xảy ra.\n\n```bash\n# Extension's read tool replaces built-in read\npi -e ./tool-override.ts\n```\n\nNgoài ra, hãy sử dụng `--no-builtin-tools` để bắt đầu mà không cần bất kỳ công cụ tích hợp nào trong khi vẫn bật các công cụ mở rộng:\n```bash\n# No built-in tools, only extension tools\npi --no-builtin-tools -e ./my-extension.ts\n```\n\nXem [examples/extensions/tool-override.ts](../examples/extensions/tool-override.ts) để biết ví dụ hoàn chỉnh ghi đè `read` bằng kiểm soát ghi nhật ký và truy cập.\n\n**Kết xuất:** Tính kế thừa của trình kết xuất tích hợp được giải quyết trên mỗi vị trí. Ghi đè thực thi và ghi đè kết xuất là độc lập. Nếu phần ghi đè của bạn bỏ qua `renderCall` thì `renderCall` tích hợp sẽ được sử dụng. Nếu phần ghi đè của bạn bỏ qua `renderResult` thì `renderResult` tích hợp sẽ được sử dụng. Nếu phần ghi đè của bạn bỏ qua cả hai thì trình kết xuất tích hợp sẵn sẽ tự động được sử dụng (đánh dấu cú pháp, khác biệt, v.v.). Điều này cho phép bạn bao bọc các công cụ tích hợp để ghi nhật ký hoặc kiểm soát quyền truy cập mà không cần triển khai lại giao diện người dùng.\n\n**Siêu dữ liệu nhắc nhở:** `promptSnippet` và `promptGuidelines` không được kế thừa từ công cụ tích hợp sẵn. Nếu phần ghi đè của bạn phải giữ lại các hướng dẫn nhắc nhở đó, hãy xác định chúng một cách rõ ràng trên phần ghi đè.\n\n**Việc triển khai của bạn phải khớp với hình dạng kết quả chính xác**, bao gồm loại `details`. Logic phiên và giao diện người dùng phụ thuộc vào các hình dạng này để hiển thị và theo dõi trạng thái.\n\nTriển khai công cụ tích hợp:\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### Thực thi từ xa\n\nCác công cụ tích hợp hỗ trợ các hoạt động có thể cắm để ủy quyền cho các hệ thống từ xa (SSH, vùng chứa, v.v.):\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**Giao diện hoạt động:** `ReadOperations`, `WriteOperations`, `EditOperations`, `BashOperations`, `LsOperations`, `GrepOperations`, `FindOperations`\n\nĐối với `user_bash`, các tiện ích mở rộng có thể sử dụng lại phần phụ trợ shell cục bộ của pi thông qua `createLocalBashOperations()` thay vì triển khai lại việc sinh sản quy trình cục bộ, phân giải shell và chấm dứt cây quy trình.\n\nCông cụ bash cũng hỗ trợ spawn hook để điều chỉnh lệnh, cwd hoặc env trước khi thực thi:\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()` hiển thị phiên hiện tại với các lệnh thông qua `PI_SESSION_ID`, `PI_SESSION_FILE`, `PI_PROVIDER`, `PI_MODEL` và `PI_REASONING_LEVEL`. Việc tiêm xảy ra trước `spawnHook`, vì vậy hook nhận các giá trị này trong `env` và bảo toàn chúng khi chúng lan truyền ra môi trường hiện có như trên. Đặt `exposeSessionEnvironment: false` để tắt chúng:\n\n```typescript\nconst bashTool = createBashTool(cwd, {\n  exposeSessionEnvironment: false,\n});\n```\n\nXem [Bash tool session environment](environment-variables.md#bash-tool-session-environment) để biết ngữ nghĩa thay đổi. Xem [examples/extensions/ssh.ts](../examples/extensions/ssh.ts) để biết ví dụ SSH hoàn chỉnh với cờ `--ssh`.\n\n### Cắt ngắn đầu ra\n\n**Các công cụ PHẢI cắt bớt đầu ra** để tránh làm choáng ngợp ngữ cảnh LLM. Đầu ra lớn có thể gây ra:\n- Lỗi tràn ngữ cảnh (lời nhắc quá dài)\n- Lỗi nén\n- Hiệu suất mô hình bị suy giảm\n\nGiới hạn tích hợp là **50KB** (~10k mã thông báo) và **2000 dòng**, tùy theo điều kiện nào được nhấn trước. Sử dụng các tiện ích cắt ngắn đã xuất:\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**Các điểm chính:**\n- Sử dụng `truncateHead` cho nội dung có phần mở đầu quan trọng (kết quả tìm kiếm, lượt đọc tệp)\n- Sử dụng `truncateTail` cho nội dung mà phần cuối quan trọng (nhật ký, đầu ra lệnh)\n- Luôn thông báo cho LLM khi đầu ra bị cắt bớt và tìm phiên bản đầy đủ ở đâu\n- Ghi lại giới hạn cắt ngắn trong mô tả công cụ của bạn\n\nXem [examples/extensions/truncated-tool.ts](../examples/extensions/truncated-tool.ts) để biết ví dụ hoàn chỉnh về cách gói `rg` (ripgrep) với cách cắt ngắn thích hợp.\n\n### Nhiều công cụ\n\nMột tiện ích mở rộng có thể đăng ký nhiều công cụ với trạng thái chia sẻ:\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### Hiển thị tùy chỉnh\n\nCác công cụ có thể cung cấp `renderCall` và `renderResult` để hiển thị TUI tùy chỉnh. Xem [tui.md](tui.md) để biết thành phần đầy đủ API và [tool-execution.ts](https://github.com/earendil-works/pi-mono/blob/main/packages/coding-agent/src/modes/interactive/components/tool-execution.ts) để biết cách tạo các hàng công cụ.\n\nTheo mặc định, đầu ra của công cụ được bao bọc trong `Box` xử lý phần đệm và nền. Một `renderCall` hoặc `renderResult` được xác định phải trả về `Component`. Nếu trình kết xuất vị trí không được xác định, `tool-execution.ts` sẽ sử dụng kết xuất dự phòng cho vị trí đó.\n\nĐặt `renderShell: \"self\"` khi công cụ sẽ hiển thị shell của chính nó thay vì sử dụng `Box` mặc định. Điều này hữu ích cho các công cụ cần kiểm soát hoàn toàn hành vi đóng khung hoặc nền, ví dụ như các bản xem trước lớn phải ổn định về mặt hình ảnh sau khi công cụ ổn định.\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` và `renderResult` mỗi người nhận được một đối tượng `context` với:\n- `args` - đối số lệnh gọi công cụ hiện tại\n- `state` - trạng thái hàng cục bộ được chia sẻ trên `renderCall` và `renderResult`\n- `lastComponent` - thành phần được trả về trước đó cho vị trí đó, nếu có\n- `invalidate()` - yêu cầu hiển thị lại hàng công cụ này\n- `toolCallId`, `cwd`, `executionStarted`, `argsComplete`, `isPartial`, `expanded`, `showImages`, `isError`\n\nSử dụng `context.state` cho trạng thái chia sẻ giữa các khe. Giữ bộ đệm cục bộ theo vị trí trên phiên bản thành phần được trả về khi bạn muốn sử dụng lại và thay đổi cùng một thành phần trên các kết xuất.\n\n#### kết xuấtCuộc gọi\n\nHiển thị lệnh gọi công cụ hoặc tiêu đề:\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#### kết xuấtKết quả\n\nHiển thị kết quả hoặc đầu ra của công cụ:\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\nNếu một vị trí cố ý không có nội dung hiển thị, hãy trả về một `Component` trống, chẳng hạn như `Container` trống.\n\n#### Gợi ý liên kết phím\n\nSử dụng `keyHint()` để hiển thị các gợi ý liên kết phím tuân theo cấu hình liên kết phím đang hoạt động:\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\nCác chức năng có sẵn:\n- `keyHint(keybinding, description)` - Định dạng id liên kết phím được định cấu hình, chẳng hạn như `\"app.tools.expand\"` hoặc `\"tui.select.confirm\"`\n- `keyText(keybinding)` - Trả về văn bản khóa được định cấu hình thô cho id liên kết phím\n- `rawKeyHint(key, description)` - Định dạng chuỗi khóa thô\n\nSử dụng id liên kết phím được đặt tên:\n- Id tác nhân mã hóa sử dụng không gian tên `app.*`, ví dụ: `app.tools.expand`, `app.editor.external`, `app.session.rename`\n- Các id TUI được chia sẻ sử dụng không gian tên `tui.*`, ví dụ: `tui.select.confirm`, `tui.select.cancel`, `tui.input.tab`\n\nĐể biết danh sách đầy đủ các id và mặc định liên kết phím, hãy xem [keybindings.md](keybindings.md). `keybindings.json` sử dụng các id được đặt tên giống nhau.\n\nTrình chỉnh sửa tùy chỉnh và thành phần `ctx.ui.custom()` nhận `keybindings: KeybindingsManager` dưới dạng đối số được chèn. Họ nên sử dụng trực tiếp trình quản lý được chèn đó thay vì gọi `getKeybindings()` hoặc `setKeybindings()`.\n\n#### Thực tiễn tốt nhất\n\n- Sử dụng `Text` với phần đệm `(0, 0)`. Hộp mặc định xử lý phần đệm.\n- Sử dụng `\\n` cho nội dung nhiều dòng.\n- Xử lý `isPartial` để biết tiến trình phát trực tuyến.\n- Hỗ trợ `expanded` để biết chi tiết theo yêu cầu.\n- Giữ chế độ xem mặc định nhỏ gọn.\n- Đọc `context.args` trong `renderResult` thay vì sao chép đối số vào `context.state`.\n- Chỉ sử dụng `context.state` cho dữ liệu phải được chia sẻ giữa các vùng cuộc gọi và kết quả.\n- Tái sử dụng `context.lastComponent` khi có thể cập nhật cùng một phiên bản thành phần.\n- Chỉ sử dụng `renderShell: \"self\"` khi vỏ đóng hộp mặc định bị cản trở. Trong chế độ self-shell, công cụ này chịu trách nhiệm về khung, phần đệm và nền của chính nó.\n\n#### Dự phòng\n\nNếu trình kết xuất vị trí không được xác định hoặc ném:\n- `renderCall`: Hiển thị tên công cụ\n- `renderResult`: Hiển thị văn bản thô từ `content`\n\n### Đang tải công cụ động\n\nExtensions có thể đăng ký nhiều công cụ trong khi chỉ duy trì hoạt động một bộ công cụ ban đầu nhỏ. Sau đó, một công cụ có thể thêm nhiều công cụ hơn với `pi.setActiveTools()` trong khi thực thi. Pi phát hiện các thay đổi hoàn toàn bổ sung, ghi lại tên công cụ mới có sẵn trên kết quả công cụ đó và áp dụng bộ hoạt động được cập nhật trước yêu cầu mô hình tiếp theo.\n\nĐiều này hoạt động với mọi mô hình. Models với hỗ trợ tải hoãn lại gốc, duy trì tiền tố nhắc ổn định và tải các định nghĩa mới ở vị trí kết quả công cụ. Các mô hình khác sử dụng dự phòng được mô tả bên dưới.\n\nVòng đời là:\n\n1. Đăng ký mọi công cụ với `pi.registerTool()` để nó xuất hiện trong `pi.getAllTools()`.\n2. Giữ các công cụ của trình tải, chẳng hạn như `search_tools`, ở trạng thái hoạt động và để các công cụ có thể tìm kiếm ở trạng thái không hoạt động.\n3. Trong quá trình thực thi trình tải, hãy gọi `pi.setActiveTools([...currentTools,...matchingTools])`. Thay đổi phải mang tính bổ sung: không xóa các công cụ hiện đang hoạt động trong cùng một lệnh gọi.\n4. Pi ghi lại những công cụ nào đã được thêm vào kết quả công cụ của trình tải.\n5. Trước phản hồi mô hình tiếp theo, Pi hiển thị các định nghĩa đã thêm bằng cách sử dụng tải trì hoãn gốc khi được hỗ trợ hoặc danh sách công cụ hoạt động thông thường nếu không.\n\nBạn không cần phải trả lại các tham chiếu công cụ dành riêng cho nhà cung cấp hoặc đánh dấu trình tải là một công cụ tìm kiếm đặc biệt. Sự thay đổi công cụ hoạt động là tín hiệu. Tên được chuyển đến `pi.setActiveTools()` phải được đăng ký; những cái tên chưa biết sẽ bị bỏ qua.\n\n#### Models với tải trả chậm gốc\n\n- **Nhân loại**\n  - **Models:** Sonnet, Opus, Fable phiên bản 4.5 trở lên (không có Haiku)\n  - **Biểu diễn gốc:** Định nghĩa hoãn lại sử dụng `defer_loading`; điểm tải sử dụng nội dung `tool_reference`.\n- **MởAI**\n  - **Models:** `gpt-5.4` và dòng mới hơn\n  - **Đại diện gốc:** Pi thêm các mục khách hàng `tool_search_call` và `tool_search_output` đã hoàn thành tại điểm tải.\n\nĐối với mô hình hoặc proxy tùy chỉnh đã được xác minh, bạn có thể bật tính năng xử lý gốc bằng `compat.supportsToolReferences: true` cho `anthropic-messages` hoặc `compat.supportsToolSearch: true` cho `openai-responses` và `openai-codex-responses`. Hãy tắt những tính năng này trừ khi điểm cuối và mô hình chấp nhận giao thức gốc tương ứng.\n\n#### Hành vi dự phòng\n\nĐối với tất cả các kiểu máy và nhà cung cấp khác, kích hoạt động vẫn hoạt động: Pi gửi danh sách công cụ hiện hoạt hoàn chỉnh một cách bình thường theo yêu cầu tiếp theo. Mô hình có thể gọi các công cụ mới được kích hoạt nhưng việc thêm định nghĩa của chúng có thể làm mất hiệu lực tiền tố lời nhắc được lưu trong bộ nhớ đệm của nhà cung cấp.\n\nPi cũng sử dụng dự phòng an toàn này khi bộ hoạt động không hoàn toàn là phụ gia, chẳng hạn như thay thế một nhóm công cụ này bằng một nhóm công cụ khác. Do đó, việc loại bỏ công cụ có tác dụng nhưng chúng không sử dụng chế độ tải chậm.\n\nĐể có hoạt động bộ nhớ đệm tốt nhất, hãy duy trì hoạt động của công cụ tải trong toàn bộ phiên và thêm các công cụ thay vì thay thế nhóm hoạt động. Cũng lưu ý rằng việc kích hoạt công cụ bằng `promptSnippet` hoặc `promptGuidelines` sẽ xây dựng lại lời nhắc hệ thống; thay đổi theo lời nhắc hệ thống đó có thể làm mất hiệu lực tiền tố ngay cả khi nhà cung cấp hỗ trợ các lược đồ trì hoãn. Các công cụ được tải chậm thường phải dựa vào công cụ `description` của chúng và bỏ qua siêu dữ liệu lời nhắc chỉ hoạt động.\n\n#### Ví dụ về công cụ tìm kiếm\n\nTiện ích mở rộng sau đăng ký hai công cụ có thể tìm kiếm, xóa chúng khỏi nhóm hoạt động ban đầu và chỉ giữ `search_tools` làm trình tải của chúng. Ví dụ này sử dụng kết hợp từ khóa đơn giản nhưng việc triển khai tìm kiếm có thể sử dụng BM25, phần nhúng, danh mục từ xa hoặc định tuyến dành riêng cho dự án.\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\nKhi `search_tools` thêm một kết quả khớp, mô hình sẽ nhận được định nghĩa đó theo yêu cầu ngay sau đó. Trên mô hình có khả năng gốc, định nghĩa được neo sau kết quả tìm kiếm mà không thay đổi tiền tố lược đồ công cụ ban đầu. Trên các kiểu máy khác, nó xuất hiện trong danh sách công cụ thông thường theo cùng yêu cầu sau đây.\n\n## Giao diện người dùng tùy chỉnh\n\nExtensions có thể tương tác với người dùng thông qua các phương thức `ctx.ui` và tùy chỉnh cách hiển thị thông báo/công cụ.\n\n**Đối với các thành phần tùy chỉnh, hãy xem [tui.md](tui.md)** có mẫu sao chép-dán cho:\n- Hộp thoại lựa chọn (SelectList)\n- Hoạt động không đồng bộ với hủy (BorderedLoader)\n- Chuyển đổi cài đặt (Danh sách cài đặt)\n- Chỉ báo trạng thái (setStatus)\n- Thông báo, khả năng hiển thị và chỉ báo hoạt động trong khi phát trực tuyến (`setWorkingMessage`, `setWorkingVisible`, `setWorkingIndicator`)\n- Các widget trên/dưới trình soạn thảo (setWidget)\n- Trình cung cấp tự động hoàn thành được xếp chồng lên trên tính năng hoàn thành dấu gạch chéo/đường dẫn tích hợp sẵn (addAutocompleteProvider)\n- Chân trang tùy chỉnh (setFooter)\n\n### Hộp thoại\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#### Hộp thoại hẹn giờ với đếm ngược\n\nHộp thoại hỗ trợ tùy chọn `timeout` tự động loại bỏ bằng màn hình đếm ngược trực tiếp:\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**Trả về giá trị khi hết thời gian chờ:**\n- `select()` trả về `undefined`\n- `confirm()` trả về `false`\n- `input()` trả về `undefined`\n\n#### Loại bỏ thủ công bằng AbortSignal\n\nĐể kiểm soát nhiều hơn (ví dụ: để phân biệt thời gian chờ với việc hủy của người dùng), hãy sử dụng `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\nXem [examples/extensions/timed-confirm.ts](../examples/extensions/timed-confirm.ts) để biết ví dụ đầy đủ.\n\n### Widget, trạng thái và chân trang\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\nCác khung chỉ báo hoạt động tùy chỉnh được hiển thị nguyên văn. Nếu bạn muốn màu sắc, hãy tự thêm chúng vào chuỗi khung, ví dụ như bằng `ctx.ui.theme.fg(...)`.\n\n### Tự động hoàn thành Providers\n\nSử dụng `ctx.ui.addAutocompleteProvider()` để xếp chồng logic tự động hoàn thành tùy chỉnh lên trên trình cung cấp đường dẫn và lệnh gạch chéo tích hợp sẵn. Đặt `triggerCharacters` cho trình kích hoạt tự nhiên tùy chỉnh, chẳng hạn như `Sử dụng `ctx.ui.addAutocompleteProvider()` để xếp chồng logic tự động hoàn thành tùy chỉnh lên trên trình cung cấp đường dẫn và lệnh gạch chéo tích hợp sẵn. Đặt `triggerCharacters` cho trình kích hoạt tự nhiên tùy chỉnh, chẳng hạn như.\n\nMẫu điển hình:\n\n- kiểm tra văn bản trước con trỏ\n- trả lại đề xuất của riêng bạn khi cú pháp dành riêng cho tiện ích mở rộng của bạn khớp\n- nếu không thì ủy quyền cho `current.getSuggestions(...)`\n- ủy quyền `applyCompletion(...)` trừ khi bạn cần hành vi chèn tùy chỉnh\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\nXem [github-issue-autocomplete.ts](../examples/extensions/github-issue-autocomplete.ts) để biết ví dụ hoàn chỉnh tải trước các vấn đề GitHub mở mới nhất với `gh issue list` và lọc chúng cục bộ để hoàn thành `#...` nhanh chóng. Nó yêu cầu GitHub CLI (`gh`) và kiểm tra kho lưu trữ GitHub.\n\n### Thành phần tùy chỉnh\n\nĐối với giao diện người dùng phức tạp, hãy sử dụng `ctx.ui.custom()`. Điều này tạm thời thay thế trình soạn thảo bằng thành phần của bạn cho đến khi `done()` được gọi:\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\nCuộc gọi lại nhận được:\n- Phiên bản `tui` - TUI (đối với kích thước màn hình, quản lý tiêu điểm)\n- `theme` - Chủ đề tạo kiểu hiện tại\n- `keybindings` - Trình quản lý liên kết phím ứng dụng (để kiểm tra phím tắt)\n- `done(value)` - Lệnh gọi để đóng thành phần và trả về giá trị\n\nXem [tui.md](tui.md) để biết thành phần đầy đủ API.\n\n#### Chế độ lớp phủ (Thử nghiệm)\n\nVượt qua `{ overlay: true }` để hiển thị thành phần dưới dạng phương thức nổi bên trên nội dung hiện có mà không xóa màn hình:\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Để định vị nâng cao (neo, lề, tỷ lệ phần trăm, khả năng hiển thị phản hồi), hãy chuyển `overlayOptions`. Sử dụng `onHandle` để kiểm soát tiêu điểm hoặc khả năng hiển thị theo chương trình:\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\nLớp phủ hiển thị tập trung có thể lấy lại thông tin đầu vào sau khi giao diện người dùng tùy chỉnh không có lớp phủ tạm thời đóng lại. Nếu bạn cố tình muốn một thành phần khác giữ nguyên dữ liệu đầu vào trong khi lớp phủ vẫn hiển thị, hãy gọi `handle.unfocus({ target })`. Việc chuyển `{ target: null }` sẽ giải phóng lớp phủ mà không tập trung vào thành phần khác.\n\nXem [tui.md](tui.md) để biết đầy đủ `OverlayOptions` và `OverlayHandle` API và [overlay-qa-tests.ts](../examples/extensions/overlay-qa-tests.ts) để biết ví dụ.\n\n### Trình chỉnh sửa tùy chỉnh\n\nThay thế trình chỉnh sửa đầu vào chính bằng cách triển khai tùy chỉnh (chế độ vim, chế độ emacs, v.v.):\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**Các điểm chính:**\n- Mở rộng `CustomEditor` (không phải cơ sở `Editor`) để nhận các tổ hợp phím ứng dụng (thoát để hủy bỏ, ctrl+d, chuyển đổi mô hình)\n- Gọi `super.handleInput(data)` để biết các phím bạn không xử lý\n- Nhà máy nhận được `tui`, `theme` và `keybindings` từ ứng dụng\n- Sử dụng `ctx.ui.getEditorComponent()` trước `setEditorComponent()` để gói trình chỉnh sửa tùy chỉnh đã định cấu hình trước đó\n- Vượt qua `undefined` để khôi phục mặc định: `ctx.ui.setEditorComponent(undefined)`\n\nĐể soạn thảo bằng một tiện ích mở rộng khác đã thay thế trình chỉnh sửa, hãy nắm bắt nhà máy trước đó trước khi cài đặt tiện ích mở rộng của bạ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\nXem [tui.md](tui.md) Mẫu 7 để biết ví dụ hoàn chỉnh về chỉ báo chế độ.\n\n### Hiển thị tin nhắn và mục nhập\n\nĐăng ký trình kết xuất tùy chỉnh cho tin nhắn bằng `customType` của bạn. Sử dụng trình kết xuất thông báo cho nội dung cần tham gia vào ngữ cảnh 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\nTin nhắn được gửi qua `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Đối với nội dung chỉ dành cho TUI không được gửi tới LLM, thay vào đó hãy hiển thị các mục tùy chỉnh:\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### Màu chủ đề\n\nTất cả các hàm kết xuất đều nhận được đối tượng `theme`. Xem [themes.md](themes.md) để tạo chủ đề tùy chỉnh và bảng màu đầy đủ.\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Để đánh dấu cú pháp trong trình kết xuất công cụ tùy chỉnh:\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## Xử lý lỗi\n\n- Lỗi tiện ích mở rộng được ghi lại, tác nhân tiếp tục\n- `tool_call` lỗi chặn công cụ (không an toàn)\n- Lỗi công cụ `execute` phải được báo hiệu bằng cách ném; lỗi ném được phát hiện, được báo cáo cho LLM với `isError: true` và quá trình thực thi tiếp tục\n\n## Chế độ hành vi\n\n| Cách thức | `ctx.mode` | `ctx.hasUI` | Ghi chú |\n|------|------------|-------------|-------|\n| Tương tác | `\"tui\"` | `true` | Đầy đủ TUI với kết xuất thiết bị đầu cuối |\n| RPC (`--mode rpc`) | `\"rpc\"` | `true` | Hộp thoại và thông báo qua giao thức JSON; `custom()` trả về `undefined`. Xem [rpc.md](rpc.md) |\n| JSON (`--mode json`) | `\"json\"` | `false` | Luồng sự kiện tới stdout; Các phương thức giao diện người dùng không hoạt động |\n| In (`-p`) | `\"print\"` | `false` | Extensions chạy nhưng không nhắc được |\n\nSử dụng `ctx.mode === \"tui\"` trước các tính năng dành riêng cho TUI (`custom()`, nhà máy sản xuất thành phần, đầu vào thiết bị đầu cuối). Sử dụng `ctx.hasUI` trước các phương thức hộp thoại và thông báo hoạt động ở cả hai chế độ TUI và RPC.\n\n## Ví dụ tham khảo\n\nTất cả các ví dụ trong [examples/extensions/](../examples/extensions/).\n\n| Ví dụ | Sự miêu tả | Chìa khóa APIs |\n|---------|-------------|----------|\n| **Công cụ** |  |  |\n| `hello.ts` | Đăng ký công cụ tối thiểu | `registerTool` |\n| `question.ts` | Công cụ có sự tương tác của người dùng | `registerTool`, `ui.select` |\n| `questionnaire.ts` | Công cụ hướng dẫn nhiều bước | `registerTool`, `ui.custom` |\n| `todo.ts` | Công cụ trạng thái có tính bền bỉ | `registerTool`, `appendEntry`, `renderResult`, sự kiện phiên |\n| `dynamic-tools.ts` | Đăng ký công cụ sau khi khởi động và trong khi ra lệnh | `registerTool`, `session_start`, `registerCommand` |\n| `structured-output.ts` | Công cụ đầu ra có cấu trúc cuối cùng với `terminate: true` | `registerTool`, kết quả công cụ chấm dứt |\n| `truncated-tool.ts` | Ví dụ cắt ngắn đầu ra | `registerTool`, `truncateHead` |\n| `tool-override.ts` | Ghi đè công cụ đọc tích hợp | `registerTool` (cùng tên với phần tích hợp sẵn) |\n| **Lệnh** |  |  |\n| `pirate.ts` | Sửa đổi lời nhắc hệ thống mỗi lượt | `registerCommand`, `before_agent_start` |\n| `summarize.ts` | Lệnh tóm tắt cuộc hội thoại | `registerCommand`, `ui.custom` |\n| `handoff.ts` | Chuyển giao mô hình giữa các nhà cung cấp | `registerCommand`, `ui.editor`, `ui.custom` |\n| `qna.ts` | Hỏi đáp với giao diện người dùng tùy chỉnh | `registerCommand`, `ui.custom`, `setEditorText` |\n| `send-user-message.ts` | Chèn tin nhắn của người dùng | `registerCommand`, `sendUserMessage` |\n| `reload-runtime.ts` | Lệnh tải lại và chuyển giao công cụ LLM | `registerCommand`, `ctx.reload()`, `sendUserMessage` |\n| `shutdown-command.ts` | Lệnh tắt duyên dáng | `registerCommand`, `shutdown()` |\n| **Sự kiện & Cổng** |  |  |\n| `permission-gate.ts` | Chặn các lệnh nguy hiểm | `on(\"tool_call\")`, `ui.confirm` |\n| `project-trust.ts` | Quyết định hoặc trì hoãn sự tin cậy của dự án từ tiện ích mở rộng người dùng/toàn cầu hoặc CLI | `on(\"project_trust\")`, giao diện người dùng tin cậy, kết quả tin cậy bắt buộc |\n| `protected-paths.ts` | Chặn ghi vào các đường dẫn cụ thể | `on(\"tool_call\")` |\n| `confirm-destructive.ts` | Xác nhận thay đổi phiên | `on(\"session_before_switch\")`, `on(\"session_before_fork\")` |\n| `dirty-repo-guard.ts` | Cảnh báo về repo git bẩn | `on(\"session_before_*\")`, `exec` |\n| `input-transform.ts` | Chuyển đổi đầu vào của người dùng | `on(\"input\")` |\n| `input-transform-streaming.ts` | Chuyển đổi đầu vào nhận biết luồng | `on(\"input\")`, `streamingBehavior` |\n| `model-status.ts` | React để thay đổi mô hình | `on(\"model_select\")`, `setStatus` |\n| `provider-payload.ts` | Kiểm tra tải trọng và tiêu đề phản hồi của nhà cung cấp | `on(\"before_provider_request\")`, `on(\"after_provider_response\")` |\n| `system-prompt-header.ts` | Hiển thị thông tin nhắc nhở hệ thống | `on(\"agent_start\")`, `getSystemPrompt` |\n| `claude-rules.ts` | Tải quy tắc từ tập tin | `on(\"session_start\")`, `on(\"before_agent_start\")` |\n| `prompt-customizer.ts` | Thêm hướng dẫn về công cụ nhận biết ngữ cảnh bằng cách sử dụng `systemPromptOptions` | `on(\"before_agent_start\")`, `BuildSystemPromptOptions` |\n| `file-trigger.ts` | Trình xem tập tin kích hoạt tin nhắn | `sendMessage` |\n| **Nén & Phiên** |  |  |\n| `custom-compaction.ts` | Tóm tắt nén tùy chỉnh | `on(\"session_before_compact\")` |\n| `trigger-compact.ts` | Kích hoạt nén thủ công | `compact()` |\n| `git-checkpoint.ts` | Git cất theo lượt | `on(\"turn_start\")`, `on(\"session_before_fork\")`, `exec` |\n| `git-merge-and-resolve.ts` | Tìm nạp, hợp nhất và giải quyết xung đột | `on(\"agent_end\")`, `exec`, `sendUserMessage` |\n| `auto-commit-on-exit.ts` | Cam kết tắt máy | `on(\"session_shutdown\")`, `exec` |\n| **Thành phần giao diện người dùng** |  |  |\n| `status-line.ts` | Chỉ báo trạng thái chân trang | `setStatus`, sự kiện phiên |\n| `working-indicator.ts` | Tùy chỉnh chỉ báo hoạt động phát trực tuyến | `setWorkingIndicator`, `registerCommand` |\n| `github-issue-autocomplete.ts` | Thêm `#1234` số lần hoàn thành vấn đề bên cạnh tính năng tự động hoàn thành được tích hợp sẵn bằng cách tải trước các vấn đề đang mở gần đây từ `gh issue list` | `addAutocompleteProvider`, `on(\"session_start\")`, `exec` |\n| `custom-footer.ts` | Thay thế hoàn toàn chân trang | `registerCommand`, `setFooter` |\n| `custom-header.ts` | Thay thế tiêu đề khởi động | `on(\"session_start\")`, `setHeader` |\n| `modal-editor.ts` | Trình chỉnh sửa phương thức kiểu Vim | `setEditorComponent`, `CustomEditor` |\n| `rainbow-editor.ts` | Kiểu trình chỉnh sửa tùy chỉnh | `setEditorComponent` |\n| `widget-placement.ts` | Widget trên/dưới trình chỉnh sửa | `setWidget` |\n| `overlay-test.ts` | Thành phần lớp phủ | `ui.custom` với các tùy chọn lớp phủ |\n| `overlay-qa-tests.ts` | Kiểm tra lớp phủ toàn diện | `ui.custom`, tất cả các tùy chọn lớp phủ |\n| `notify.ts` | Thông báo đơn giản | `ui.notify` |\n| `timed-confirm.ts` | Hộp thoại có thời gian chờ | `ui.confirm` với thời gian chờ/tín hiệu |\n| `mac-system-theme.ts` | Tự động chuyển đổi chủ đề | `setTheme`, `exec` |\n| **Phức tạp Extensions** |  |  |\n| `plan-mode/` | Thực hiện chế độ kế hoạch đầy đủ | Tất cả các loại sự kiện, `registerCommand`, `registerShortcut`, `registerFlag`, `setStatus`, `setWidget`, `sendMessage`, `setActiveTools` |\n| `preset.ts` | Các cài đặt trước có thể lưu được (mô hình, công cụ, tư duy) | `registerCommand`, `registerShortcut`, `registerFlag`, `setModel`, `setActiveTools`, `setThinkingLevel`, `appendEntry` |\n| `tools.ts` | Bật/tắt công cụ UI | `registerCommand`, `setActiveTools`, `SettingsList`, sự kiện phiên |\n| **Điều khiển từ xa & Hộp cát** |  |  |\n| `ssh.ts` | SSH thực thi từ xa | `registerFlag`, `on(\"user_bash\")`, `on(\"before_agent_start\")`, thao tác với công cụ |\n| `interactive-shell.ts` | Phiên shell liên tục | `on(\"user_bash\")` |\n| `sandbox/` | Thực thi công cụ hộp cát | Hoạt động của công cụ |\n| `gondolin/` | Định tuyến các công cụ tích hợp và lệnh `!` vào micro-VM Gondolin | Thao tác với công cụ, ghi đè công cụ tích hợp, `on(\"user_bash\")` |\n| `subagent/` | Đại lý phụ sinh sản | `registerTool`, `exec` |\n| **Trò chơi** |  |  |\n| `snake.ts` | Trò chơi rắn | `registerCommand`, `ui.custom`, xử lý bàn phím |\n| `space-invaders.ts` | Trò chơi Kẻ xâm lược không gian | `registerCommand`, `ui.custom` |\n| `doom-overlay/` | Sự diệt vong trong lớp phủ | `ui.custom` với lớp phủ |\n| **Providers** |  |  |\n| `custom-provider-anthropic/` | Proxy nhân loại tùy chỉnh | `registerProvider` |\n| `custom-provider-gitlab-duo/` | Git Tích hợp Lab Duo | `registerProvider` với OAuth |\n| **Tin nhắn & Giao tiếp** |  |  |\n| `message-renderer.ts` | Hiển thị tin nhắn tùy chỉnh | `registerMessageRenderer`, `sendMessage` |\n| `entry-renderer.ts` | Kết xuất mục nhập tùy chỉnh chỉ TUI | `registerEntryRenderer`, `appendEntry` |\n| `event-bus.ts` | Sự kiện liên mở rộng | `pi.events` |\n| **Siêu dữ liệu phiên** |  |  |\n| `session-name.ts` | Đặt tên phiên cho bộ chọn | `setSessionName`, `getSessionName` |\n| `bookmark.ts` | Đánh dấu các mục cho /cây | `setLabel` |\n| **Khác** |  |  |\n| `inline-bash.ts` | Nội tuyến bash trong lệnh gọi công cụ | `on(\"tool_call\")` |\n| `bash-spawn-hook.ts` | Điều chỉnh lệnh bash, cwd và env trước khi thực thi | `createBashTool`, `spawnHook` |\n| `with-deps/` | Tiện ích mở rộng với phần phụ thuộc npm | Cấu trúc gói với `package.json` |","sourceFile":"extensions.md"},"index":{"title":"Pi Tài liệu","markdown":"Pi là một khai thác mã hóa thiết bị đầu cuối tối thiểu. Nó được thiết kế để duy trì cốt lõi ở quy mô nhỏ trong khi được mở rộng thông qua các tiện ích mở rộng, kỹ năng, prompt templates, chủ đề và gói pi TypeScript.\n\n## Bắt đầu nhanh\n\nCài đặt Pi với npm:\n\n```bash\nnpm install -g --ignore-scripts @earendil-works/pi-coding-agent\n```\n\n`--ignore-scripts` vô hiệu hóa các tập lệnh vòng đời phụ thuộc trong khi cài đặt. Pi không yêu cầu tập lệnh cài đặt cho các lần cài đặt npm thông thường.\n\nTrên Linux hoặc macOS, bạn cũng có thể sử dụng trình cài đặt:\n\n```bash\ncurl -fsSL https://pi.dev/install.sh | sh\n```\n\nĐể gỡ cài đặt pi, hãy sử dụng npm để cài đặt cuộn tròn và npm:\n\n```bash\nnpm uninstall -g @earendil-works/pi-coding-agent\n```\n\nĐối với các lượt cài đặt pnpm, Yarn hoặc Bun, hãy sử dụng lệnh xóa chung phù hợp: `pnpm remove -g @earendil-works/pi-coding-agent`, `yarn global remove @earendil-works/pi-coding-agent` hoặc `bun uninstall -g @earendil-works/pi-coding-agent`.\n\nSau đó chạy nó trong một thư mục dự án:\n\n```bash\npi\n```\n\nXác thực bằng `/login` cho subscription providers hoặc đặt API key chẳng hạn như `ANTHROPIC_API_KEY` trước khi bắt đầu pi.\n\nĐể biết toàn bộ luồng chạy đầu tiên, hãy xem [Quickstart](quickstart.md).\n\n## Bắt đầu ở đây\n\n- [Quickstart](quickstart.md) - cài đặt, xác thực và chạy phiên đầu tiên.\n- [Using Pi](usage.md) - chế độ tương tác, tham chiếu slash commands, context files và CLI.\n- [Providers](providers.md) - đăng ký và thiết lập khóa API cho các nhà cung cấp tích hợp.\n- [llama.cpp](llama-cpp.md) - chạy bộ định tuyến cục bộ và quản lý các mô hình với `/llama`.\n- [Security](security.md) - sự tin cậy của dự án, ranh giới sandbox và báo cáo lỗ hổng.\n- [Containerization](containerization.md) - sandbox pi với Gondolin, Docker hoặc OpenShell.\n- [Settings](settings.md) - cài đặt toàn cầu và dự án.\n- [Keybindings](keybindings.md) - phím tắt mặc định và tổ hợp phím tùy chỉnh.\n- [Sessions](sessions.md) - quản lý phiên, phân nhánh và điều hướng cây.\n- [Compaction](compaction.md) - context compaction và branch summarization.\n\n## Tùy chỉnh\n\n- [Extensions](extensions.md) - TypeScript mô-đun dành cho công cụ, lệnh, sự kiện và giao diện người dùng tùy chỉnh.\n- [Skills](skills.md) - Đại lý Skills để có khả năng tái sử dụng theo yêu cầu.\n- [Prompt templates](prompt-templates.md) - lời nhắc có thể sử dụng lại mở rộng từ slash commands.\n- [Themes](themes.md) - tích hợp và tùy chỉnh terminal themes.\n- [Pi packages](packages.md) - gói và chia sẻ các tiện ích mở rộng, kỹ năng, lời nhắc và chủ đề.\n- [Custom models](models.md) - thêm các mục nhập mô hình cho nhà cung cấp được hỗ trợ APIs.\n- [Custom providers](custom-provider.md) - triển khai các luồng APIs và OAuth tùy chỉnh.\n\n## Sử dụng có lập trình\n\n- [SDK](sdk.md) - nhúng pi vào các ứng dụng Node.js.\n- [RPC mode](rpc.md) - lấy tích phân trên stdin/stdout JSONL.\n- [JSON event stream mode](json.md) - chế độ in với các sự kiện có cấu trúc.\n- [TUI components](tui.md) - xây dựng giao diện người dùng thiết bị đầu cuối tùy chỉnh cho tiện ích mở rộng.\n\n## Thẩm quyền giải quyết\n\n- [Environment variables](environment-variables.md) - Pi cấu hình quy trình và siêu dữ liệu phiên có sẵn cho các công cụ bash.\n- [Session format](session-format.md) - JSONL định dạng tệp phiên, loại mục nhập và Trình quản lý phiên API.\n\n## Thiết lập nền tảng\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## Phát triển\n\n- [Development](development.md) - thiết lập cục bộ, cấu trúc dự án và gỡ lỗi.","sourceFile":"index.md"},"json":{"title":"JSON Chế độ phát trực tiếp sự kiện","markdown":"```bash\npi --mode json \"Your prompt\"\n```\n\nXuất tất cả các sự kiện phiên dưới dạng dòng JSON thành stdout. Hữu ích cho việc tích hợp pi vào các công cụ khác hoặc giao diện người dùng tùy chỉnh.\n\n## Các loại sự kiện\n\nSự kiện dây sử dụng `JsonAgentSessionEvent`. Nó phù hợp\n[`AgentSessionEvent`](https://github.com/earendil-works/pi-mono/blob/main/packages/coding-agent/src/core/agent-session.ts)\nngoại trừ việc cập nhật tin nhắn trực tuyến bỏ qua ảnh chụp nhanh tích lũy:\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` phát ra toàn bộ hàng đợi chỉ đạo và theo dõi đang chờ xử lý bất cứ khi nào chúng thay đổi. `compaction_start` và `compaction_end` bao gồm cả việc nén thủ công và tự động.\n\nCác sự kiện cơ bản khác đến từ\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## Các loại tin nhắn\n\nTin nhắn cơ bản từ [`packages/ai/src/types.ts`](https://github.com/earendil-works/pi-mono/blob/main/packages/ai/src/types.ts#L134):\n- `UserMessage` (dòng 134)\n- `AssistantMessage` (dòng 140)\n- `ToolResultMessage` (dòng 152)\n\nTin nhắn mở rộng từ [`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` (dòng 29)\n- `CustomMessage` (dòng 46)\n- `BranchSummaryMessage` (dòng 55)\n- `CompactionSummaryMessage` (dòng 62)\n\n## Định dạng đầu ra\n\nMỗi dòng là một đối tượng JSON. Dòng đầu tiên là tiêu đề phiên:\n\n```json\n{\"type\":\"session\",\"version\":3,\"id\":\"uuid\",\"timestamp\":\"...\",\"cwd\":\"/path\"}\n```\n\nTiếp theo là các sự kiện khi chúng xảy ra:\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` bản ghi chỉ ở dạng delta. Họ bỏ qua cả trường `message` tích lũy và\n`assistantMessageEvent.partial` để giữ kích thước luồng tuyến tính. Sử dụng `contentIndex` và `delta`\nđể tập hợp các đối số văn bản trực tiếp, suy nghĩ hoặc lệnh gọi công cụ nếu cần. `message_end` chứa\nthông điệp có thẩm quyền cuối cùng.\n\n## Ví dụ\n\n```bash\npi --mode json \"List files\" 2>/dev/null | jq -c 'select(.type == \"message_end\")'\n```","sourceFile":"json.md"},"keybindings":{"title":"Tổ hợp phím","markdown":"Tất cả các phím tắt có thể được tùy chỉnh thông qua `~/.pi/agent/keybindings.json`. Mỗi hành động có thể được liên kết với một hoặc nhiều phím.\n\nTệp cấu hình sử dụng cùng các id liên kết phím có khoảng cách tên mà pi sử dụng nội bộ và các tác giả tiện ích mở rộng sử dụng trong `keyHint()` và được chèn vào trình quản lý `keybindings`.\n\nCác cấu hình cũ hơn sử dụng id được đặt tên trước như `cursorUp` hoặc `expandTools` được tự động di chuyển sang các id được đặt tên khi khởi động.\n\nSau khi chỉnh sửa `keybindings.json`, hãy chạy `/reload` trong pi để áp dụng các thay đổi mà không cần khởi động lại phiên.\n\n## Định dạng khóa\n\n`modifier+key` trong đó các công cụ sửa đổi là `ctrl`, `shift`, `alt`, `super` (có thể kết hợp) và các khóa là:\n\n- **Chữ cái:** `a-z`\n- **Chữ số:** `0-9`\n- **Phím đặc biệt:** `escape`, `esc`, `enter`, `return`, `tab`, `space`, `backspace`, `delete`, `insert`, `clear`, `home`, `end`, `pageUp`, `pageDown`, `up`, `down`, `left`, `right`\n- **Phím chức năng:** `f1`-`f12`\n- **Ký hiệu:** `` ` ``, `-`, `=`, `[`, `]`, `\\`, `;`, `'`, `,`, `.`, `/`, `!`, `@`, `#`, `$`, `%`, `^`, `&`, `*`, `(`, `)`, `_`, `+`, `|`, `~`, `{`, `}`, `:`, `<`, `>`, `?`\n\nCác tổ hợp bổ nghĩa: `ctrl+shift+x`, `alt+ctrl+x`, `ctrl+shift+alt+x`, `super+k`, `ctrl+super+k`, `ctrl+1`, v.v.\n\nCác ràng buộc `super` yêu cầu một thiết bị đầu cuối báo cáo công cụ sửa đổi riêng biệt, thường thông qua giao thức bàn phím Kitty. Chúng có thể không hoạt động trong thiết bị đầu cuối nếu không có sự hỗ trợ đó.\n\n## Tất cả hành động\n\n### TUI Chuyển động con trỏ của trình chỉnh sửa\n\n| Id liên kết phím | Mặc định | Sự miêu tả |\n|--------|---------|-------------|\n| `tui.editor.cursorUp` | `up` | Di chuyển con trỏ lên, duyệt lịch sử cũ hơn ở trên cùng |\n| `tui.editor.cursorDown` | `down` | Di chuyển con trỏ xuống, duyệt lịch sử mới hơn ở phía dưới |\n| `tui.editor.historyPrevious` | *(không có)* | Chọn mục nhập lịch sử lời nhắc trước đó |\n| `tui.editor.historyNext` | *(không có)* | Chọn mục nhập lịch sử lời nhắc tiếp theo |\n| `tui.editor.cursorLeft` | `left`, `ctrl+b` | Di chuyển con trỏ sang trái |\n| `tui.editor.cursorRight` | `right`, `ctrl+f` | Di chuyển con trỏ sang phải |\n| `tui.editor.cursorWordLeft` | `alt+left`, `ctrl+left`, `alt+b` | Di chuyển con trỏ từ sang trái |\n| `tui.editor.cursorWordRight` | `alt+right`, `ctrl+right`, `alt+f` | Di chuyển con trỏ từ sang phải |\n| `tui.editor.cursorLineStart` | `home`, `ctrl+home`, `ctrl+a` | Di chuyển đến đầu dòng |\n| `tui.editor.cursorLineEnd` | `end`, `ctrl+end`, `ctrl+e` | Di chuyển đến cuối dòng |\n| `tui.editor.jumpForward` | `ctrl+]` | Chuyển tiếp tới nhân vật |\n| `tui.editor.jumpBackward` | `ctrl+alt+]` | Nhảy lùi về nhân vật |\n| `tui.editor.pageUp` | `pageUp`, `ctrl+pageUp` | Cuộn lên theo trang |\n| `tui.editor.pageDown` | `pageDown`, `ctrl+pageDown` | Cuộn xuống theo trang |\n\nCác hành động lịch sử chuyên dụng luôn thay đổi các mục nhập lịch sử, bất kể vị trí con trỏ trong dấu nhắc nhiều dòng. Các liên kết lịch sử rõ ràng được ưu tiên hơn các hành động của ứng dụng trong khi trình chỉnh sửa chính được tập trung, do đó, liên kết `tui.editor.historyPrevious` với `ctrl+p` sẽ ghi đè chu kỳ mô hình trong ngữ cảnh đó mà không thay đổi `Ctrl+P` trong bộ chọn.\n\n### TUI Xóa trình chỉnh sửa\n\n| Id liên kết phím | Mặc định | Sự miêu tả |\n|--------|---------|-------------|\n| `tui.editor.deleteCharBackward` | `backspace` | Xóa ký tự lùi |\n| `tui.editor.deleteCharForward` | `delete`, `ctrl+d` | Xóa ký tự phía trước |\n| `tui.editor.deleteWordBackward` | `ctrl+w`, `alt+backspace` | Xóa từ lùi |\n| `tui.editor.deleteWordForward` | `alt+d`, `alt+delete` | Xóa từ tiếp theo |\n| `tui.editor.deleteToLineStart` | `ctrl+u` | Xóa để bắt đầu dòng |\n| `tui.editor.deleteToLineEnd` | `ctrl+k` | Xóa đến cuối dòng |\n\n### TUI Đầu vào\n\n| Id liên kết phím | Mặc định | Sự miêu tả |\n|--------|---------|-------------|\n| `tui.input.newLine` | `shift+enter`, `ctrl+j` | Chèn dòng mới |\n| `tui.input.submit` | `enter` | Gửi đầu vào |\n| `tui.input.tab` | `tab` | Tab/tự động hoàn thành |\n\n### TUI Vòng tiêu diệt\n\n| Id liên kết phím | Mặc định | Sự miêu tả |\n|--------|---------|-------------|\n| `tui.editor.yank` | `ctrl+y` | Dán văn bản đã xóa gần đây nhất |\n| `tui.editor.yankPop` | `alt+y` | Duyệt qua văn bản đã xóa sau khi kéo |\n| `tui.editor.undo` | `ctrl+-` | Hoàn tác chỉnh sửa cuối cùng |\n\n### TUI Bảng tạm và lựa chọn\n\n| Id liên kết phím | Mặc định | Sự miêu tả |\n|--------|---------|-------------|\n| `tui.input.copy` | `ctrl+c` | Sao chép lựa chọn |\n| `tui.select.up` | `up` | Di chuyển lựa chọn lên |\n| `tui.select.down` | `down` | Di chuyển lựa chọn xuống |\n| `tui.select.pageUp` | `pageUp` | Trang lên trong danh sách |\n| `tui.select.pageDown` | `pageDown` | Trang xuống trong danh sách |\n| `tui.select.confirm` | `enter` | Xác nhận lựa chọn |\n| `tui.select.cancel` | `escape`, `ctrl+c` | Hủy lựa chọn |\n\n### TUI Chế độ xem toàn màn hình\n\nNhững hành động này áp dụng khi chế độ tương tác sử dụng `--tui-mode fullscreen` và nhắm mục tiêu vùng cuộn bản ghi chính. Bàn di chuột bằng hai ngón tay và đầu vào con lăn chuột cuộn vùng bên dưới con trỏ, quay trở lại bản ghi trên dock trình soạn thảo/trạng thái/chân trang cố định. Nhấp vào siêu liên kết OSC 8 sẽ mở siêu liên kết đó trong trình xử lý mặc định. Kéo bằng nút chuột chính sẽ chọn văn bản và sao chép nó vào khay nhớ tạm; giữ ở cạnh trên hoặc dưới của bản ghi sẽ tự động cuộn vào nội dung ngoài màn hình.\n\nCác ràng buộc bản ghi toàn màn hình được ưu tiên hơn các ràng buộc biên tập. Do đó, các phím điều hướng mặc định chưa được sửa đổi sẽ kiểm soát bản ghi ở chế độ toàn màn hình, trong khi các biến thể `ctrl` của chúng tiếp tục kiểm soát trình chỉnh sửa. Bên ngoài chế độ toàn màn hình, cả hai biến thể đều điều khiển trình chỉnh sửa.\n\n| Chìa khóa | Chế độ mặc định | Chế độ toàn màn hình |\n|-----|--------------|-----------------|\n| `home`, `end` | Biên tập viên | bảng điểm |\n| `ctrl+home`, `ctrl+end` | Biên tập viên | Biên tập viên |\n| `pageUp`, `pageDown` | Biên tập viên | bảng điểm |\n| `ctrl+pageUp`, `ctrl+pageDown` | Biên tập viên | Biên tập viên |\n\nĐịnh tuyến này vẫn có thể cấu hình được thông qua các ràng buộc hành động thông thường. Ví dụ: `\"tui.altScreen.pageUp\": \"ctrl+pageUp\"` làm cho `pageUp` kiểm soát trình chỉnh sửa và `ctrl+pageUp` kiểm soát bản ghi ở chế độ toàn màn hình. Ràng buộc `tui.altScreen.halfPageUp` và `tui.altScreen.halfPageDown` cho các bước chép lại nhỏ hơn trong khi vẫn giữ các ràng buộc toàn trang. Cài đặt `\"tui.altScreen.pageUp\": []` sẽ tắt hoàn toàn lối tắt chuyển biên đó. Ràng buộc người dùng thay thế các giá trị mặc định cho hành động đó.\n\n| Id liên kết phím | Mặc định | Sự miêu tả |\n|--------|---------|-------------|\n| `tui.altScreen.pageUp` | `pageUp` | Cuộn bản ghi lên một trang |\n| `tui.altScreen.pageDown` | `pageDown` | Cuộn bản ghi xuống một trang |\n| `tui.altScreen.halfPageUp` | *(không có)* | Cuộn bản ghi lên nửa trang |\n| `tui.altScreen.halfPageDown` | *(không có)* | Cuộn bản ghi xuống nửa trang |\n| `tui.altScreen.previousPrompt` | `ctrl+shift+up` | Chuyển đến tin nhắn được đánh dấu trước đó |\n| `tui.altScreen.nextPrompt` | `ctrl+shift+down` | Chuyển tới tin nhắn được đánh dấu tiếp theo |\n| `tui.altScreen.top` | `home` | Cuộn đến đầu bản ghi |\n| `tui.altScreen.bottom` | `end` | Cuộn đến cuối bản ghi và theo dõi đầu ra mới |\n\n### Ứng dụng\n\n| Id liên kết phím | Mặc định | Sự miêu tả |\n|--------|---------|-------------|\n| `app.interrupt` | `escape` | Hủy/hủy bỏ |\n| `app.clear` | `ctrl+c` | Xóa trình chỉnh sửa (đầu tiên) / thoát (thứ hai) |\n| `app.exit` | `ctrl+d` | Thoát (khi trình soạn thảo trống) |\n| `app.suspend` | `ctrl+z` (không có trên Windows) | Tạm dừng ở chế độ nền |\n| `app.editor.external` | `ctrl+g` | Mở bằng trình chỉnh sửa bên ngoài (`externalEditor`, `$VISUAL`, `$EDITOR`, Notepad trên Windows hoặc `nano` ở nơi khác) |\n| `app.clipboard.pasteImage` | `ctrl+v` (`alt+v` trên Windows) | Dán hình ảnh hoặc văn bản từ clipboard |\n\n### Phiên\n\n| Id liên kết phím | Mặc định | Sự miêu tả |\n|--------|---------|-------------|\n| `app.session.new` | *(không có)* | Bắt đầu phiên mới (`/new`) |\n| `app.session.tree` | *(không có)* | Mở trình điều hướng session tree (`/tree`) |\n| `app.session.fork` | *(không có)* | Phiên hiện tại phân nhánh (`/fork`) |\n| `app.session.resume` | *(không có)* | Bộ chọn sơ yếu lý lịch phiên mở (`/resume`) |\n| `app.session.togglePath` | `ctrl+p` | Chuyển đổi hiển thị đường dẫn |\n| `app.session.toggleSort` | `ctrl+s` | Chuyển đổi chế độ sắp xếp |\n| `app.session.toggleNamedFilter` | `ctrl+n` | Chuyển đổi bộ lọc chỉ có tên |\n| `app.session.rename` | `ctrl+r` | Đổi tên phiên |\n| `app.session.delete` | `ctrl+d` | Xóa phiên |\n| `app.session.deleteNoninvasive` | `ctrl+backspace` | Xóa phiên khi truy vấn trống |\n\n### Models và Suy nghĩ\n\n| Id liên kết phím | Mặc định | Sự miêu tả |\n|--------|---------|-------------|\n| `app.model.select` | `ctrl+l` | Mở bộ chọn mô hình |\n| `app.model.cycleForward` | `ctrl+p` | Chuyển sang mô hình tiếp theo |\n| `app.model.cycleBackward` | `shift+ctrl+p` | Chuyển sang mô hình trước đó |\n| `app.thinking.cycle` | `shift+tab` | Mức độ tư duy chu kỳ |\n| `app.thinking.toggle` | `ctrl+t` | Thu gọn hoặc mở rộng các khối tư duy |\n\n### Hàng đợi hiển thị và tin nhắn\n\n| Id liên kết phím | Mặc định | Sự miêu tả |\n|--------|---------|-------------|\n| `app.tools.expand` | `ctrl+o` | Thu gọn hoặc mở rộng đầu ra công cụ |\n| `app.message.copy` | `ctrl+x` | Sao chép tin nhắn trợ lý cuối cùng hoặc tin nhắn đã chọn trong `/tree` |\n| `app.message.followUp` | `alt+enter` | Tin nhắn theo dõi hàng đợi |\n| `app.message.dequeue` | `alt+up` | Khôi phục tin nhắn được xếp hàng đợi vào trình chỉnh sửa |\n\n### Điều hướng cây\n\n| Id liên kết phím | Mặc định | Sự miêu tả |\n|--------|---------|-------------|\n| `app.tree.foldOrUp` | `ctrl+left`, `alt+left` | Gấp đoạn nhánh hiện tại hoặc nhảy về phần bắt đầu của đoạn trước đó |\n| `app.tree.unfoldOrDown` | `ctrl+right`, `alt+right` | Mở rộng đoạn nhánh hiện tại hoặc chuyển sang đoạn bắt đầu hoặc kết thúc nhánh tiếp theo |\n| `app.tree.editLabel` | `shift+l` | Chỉnh sửa nhãn trên nút cây đã chọn |\n| `app.tree.toggleLabelTimestamp` | `shift+t` | Chuyển đổi dấu thời gian của nhãn trong cây |\n| `app.tree.filter.default` | `ctrl+d` | Đặt bộ lọc cây thành chế độ xem mặc định |\n| `app.tree.filter.noTools` | `ctrl+t` | Chuyển đổi bộ lọc cây ẩn kết quả công cụ |\n| `app.tree.filter.userOnly` | `ctrl+u` | Chuyển đổi bộ lọc cây chỉ hiển thị tin nhắn của người dùng |\n| `app.tree.filter.labeledOnly` | `ctrl+l` | Chuyển đổi bộ lọc cây chỉ hiển thị các mục được gắn nhãn |\n| `app.tree.filter.all` | `ctrl+a` | Chuyển đổi bộ lọc cây hiển thị tất cả các mục |\n| `app.tree.filter.cycleForward` | `ctrl+o` | Lọc cây tuần hoàn về phía trước |\n| `app.tree.filter.cycleBackward` | `shift+ctrl+o` | Lọc cây tuần hoàn ngược |\n\n### Bộ chọn phạm vi Models\n\nĐược sử dụng bên trong bộ chọn mô hình có phạm vi (được mở qua `/scoped-models`).\n\n| Id liên kết phím | Mặc định | Sự miêu tả |\n|--------|---------|-------------|\n| `app.models.save` | `ctrl+s` | Lưu lựa chọn mô hình hiện tại vào cài đặt |\n| `app.models.enableAll` | `ctrl+a` | Kích hoạt tất cả các mô hình (hoặc tất cả phù hợp với tìm kiếm hiện tại) |\n| `app.models.clearAll` | `ctrl+x` | Xóa tất cả các mô hình (hoặc tất cả phù hợp với tìm kiếm hiện tại) |\n| `app.models.toggleProvider` | `ctrl+p` | Chuyển đổi tất cả các mô hình cho nhà cung cấp hiện tại |\n| `app.models.reorderUp` | `alt+up` | Di chuyển mô hình đã chọn lên theo thứ tự chu kỳ |\n| `app.models.reorderDown` | `alt+down` | Di chuyển mô hình đã chọn xuống theo thứ tự chu kỳ |\n\n## Cấu hình tùy chỉnh\n\nTạo `~/.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\nMỗi hành động có thể có một phím duy nhất hoặc một mảng phím. Cấu hình người dùng ghi đè mặc định.\n\nTrên Windows gốc, `app.suspend` không có ràng buộc mặc định vì thiết bị đầu cuối Windows không hỗ trợ kiểm soát công việc Unix. Nếu bạn liên kết nó theo cách thủ công, pi sẽ hiển thị thông báo trạng thái thay vì tạm dừng. Trong WSL, hành vi `ctrl+z`/`fg` bình thường của Linux vẫn được áp dụng.\n\n### Ví dụ về Emac\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### Ví dụ 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 hỗ trợ máy chủ bộ định tuyến [llama.cpp](https://github.com/ggml-org/llama.cpp). Bộ định tuyến phát hiện nhiều mô hình GGUF và tải hoặc dỡ chúng theo yêu cầu.\n\nSử dụng bản dựng llama.cpp hiện tại có hỗ trợ bộ định tuyến. Hãy làm theo [build instructions](https://github.com/ggml-org/llama.cpp/blob/master/docs/build.md) hoặc cài đặt [prebuilt release](https://github.com/ggml-org/llama.cpp/releases) cho nền tảng của bạn.\n\n## Khởi động bộ định tuyến\n\nBắt đầu `llama-server` mà không có `--model` hoặc `-m`. Việc chuyển một mô hình sẽ bắt đầu chế độ một mô hình thay vì chế độ bộ định tuyến.\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\nCác tùy chọn quan trọng:\n\n- `--models-dir ~/models` khám phá các tệp GGUF cục bộ.\n- `--no-models-autoload` tiếp tục tải rõ ràng cho đến `/llama`.\n- `--jinja` cho phép các mẫu trò chuyện và công cụ gọi điện tương thích.\n- `-ngl 999` giảm tải càng nhiều lớp cho GPU càng tốt.\n- `-c 32768` đặt cửa sổ ngữ cảnh cho từng mô hình được tải. Bỏ qua nó để sử dụng bối cảnh gốc của mô hình, có thể cần nhiều bộ nhớ hơn đáng kể.\n\nMột mô hình một tập tin có thể nằm trực tiếp trong thư mục mô hình. Đặt các mô hình đa phương thức và đa phân đoạn trong các thư mục con riêng biệt:\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\nKhởi động lại bộ định tuyến sau khi thêm tệp theo cách thủ công. Đối với kích thước ngữ cảnh của mỗi mô hình và các tùy chọn khác, hãy sử dụng [llama.cpp model presets](https://github.com/ggml-org/llama.cpp/blob/master/tools/server/README.md#model-presets).\n\n## Định cấu hình Pi\n\nBắt đầu Pi và định cấu hình nhà cung cấp:\n\n```text\n/login llama.cpp\n```\n\nNhập URL bộ định tuyến và tùy chọn API key. URL mặc định là `http://127.0.0.1:8080`.\n\nCác biến môi trường có thể định cấu hình các giá trị giống nhau mà không cần `/login`:\n\n```bash\nexport LLAMA_BASE_URL=http://127.0.0.1:8080\nexport LLAMA_API_KEY=optional-secret\npi\n```\n\nNếu máy chủ sử dụng API key, hãy bắt đầu `llama-server` với giá trị `--api-key` phù hợp. Giữ `--host 127.0.0.1` để chỉ truy cập cục bộ.\n\n## Quản lý mô hình\n\nChạy:\n\n```text\n/llama\n```\n\n- Chọn một mô hình chưa tải để tải nó.\n- Chọn một mô hình đã tải để dỡ nó.\n- Chọn **Tải xuống mô hình…**, tìm kiếm Hugging Face, sau đó chọn kho lưu trữ và lượng tử hóa. Các giá trị `owner/repository[:quant]` chính xác cũng hoạt động.\n- Nhấn Escape trong khi tải hoặc tải xuống để xác nhận hủy.\n\nHugging Face tìm kiếm sử dụng `HF_TOKEN` khi được đặt, sau đó kiểm tra `$HF_TOKEN_PATH`, `$HF_HOME/token`, `$XDG_CACHE_HOME/huggingface/token` và `~/.cache/huggingface/token`. Tìm kiếm cũng hoạt động mà không cần xác thực, tùy thuộc vào giới hạn tốc độ thấp hơn. Pi cảnh báo trước khi tải xuống các kho lưu trữ có kiểm soát và các liên kết tới trang truy cập của họ. Máy chủ llama.cpp thực hiện tải xuống nên quá trình của nó cũng phải có `HF_TOKEN` khi kho lưu trữ đã chọn yêu cầu quyền truy cập.\n\nNếu các mẫu khác đã được tải, Pi sẽ hỏi xem nên dỡ chúng trước hay tiếp tục tải. Pi không âm thầm tải mô hình và không bao giờ xóa các tệp mô hình. Bộ định tuyến có thể được chia sẻ với các máy khách khác, vì vậy `/llama` luôn hiển thị trạng thái hiện tại của bộ định tuyến.\n\nChỉ các mô hình đã tải mới xuất hiện trong `/model`. Sau khi tải mô hình, hãy chạy `/model` để chọn mô hình đó cho phiên Pi hiện tại.\n\nNếu bộ định tuyến ngắt kết nối, `/llama` hiển thị **Thử lại** và **Đóng**. Thử lại sẽ kết nối lại và làm mới trạng thái mô hình mà không phát lại thao tác bị gián đoạn.\n\n## Khắc phục sự cố\n\nKiểm tra xem bộ định tuyến có thể truy cập được không:\n\n```bash\ncurl http://127.0.0.1:8080/health\ncurl http://127.0.0.1:8080/models\n```\n\n- **Không có kiểu máy nào trong `/llama`:** Kiểm tra `--models-dir`, bố cục thư mục và khởi động lại bộ định tuyến.\n- **Mô hình bị thiếu trong `/model`:** Tải nó bằng `/llama` trước.\n- **Tải không thành công hoặc sử dụng quá nhiều bộ nhớ:** Hạ `-c` hoặc dỡ mô hình khác.\n- **Máy chủ không ở chế độ bộ định tuyến:** Khởi động mà không có `--model`, `-m` hoặc `-hf`.","sourceFile":"llama-cpp.md"},"models":{"title":"Tùy chỉnh Models","markdown":"Thêm nhà cung cấp và mô hình tùy chỉnh (Ollama, vLLM, LM Studio, proxy) qua `~/.pi/agent/models.json`.\n\n## Mục lục\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## Ví dụ tối thiểu\n\nĐối với các mô hình địa phương (Ollama, LM Studio, vLLM), mỗi mô hình chỉ yêu cầu `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\nGiá trị `apiKey` là phần giữ chỗ vì Ollama bỏ qua nó. pi vẫn coi các mô hình là yêu cầu xác thực trước khi chúng xuất hiện trong `/model`, vì vậy, các máy chủ cục bộ không cần khóa phải giữ một giá trị giả, lưu khóa cho nhà cung cấp đó bằng `/login` hoặc chuyển `--api-key` khi chọn mô hình.\n\nMột số máy chủ tương thích với OpenAI không hiểu vai trò `developer` được sử dụng cho các mô hình có khả năng suy luận. Đối với những nhà cung cấp đó, hãy đặt `compat.supportsDeveloperRole` thành `false` để pi gửi lời nhắc hệ thống dưới dạng tin nhắn `system`. Nếu máy chủ cũng không hỗ trợ `reasoning_effort`, hãy đặt `compat.supportsReasoningEffort` thành `false`.\n\nBạn có thể đặt `compat` ở cấp nhà cung cấp để áp dụng cho tất cả các kiểu máy hoặc ở cấp độ mô hình để ghi đè một mô hình cụ thể. Điều này thường áp dụng cho Ollama, vLLM, SGLang và các máy chủ tương thích với OpenAI tương tự.\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## Ví dụ đầy đủ\n\nGhi đè mặc định khi bạn cần các giá trị cụ thể:\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\nTệp sẽ tải lại mỗi khi bạn mở `/model`. Chỉnh sửa trong phiên; không cần khởi động lại.\n\n## Ví dụ về Google AI Studio\n\nSử dụng `google-generative-ai` với `baseUrl` để thêm mô hình từ Google AI Studio, bao gồm các mục nhập Gemma 4 tùy chỉnh:\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\nBắt buộc phải có `baseUrl` khi thêm mô hình tùy chỉnh vào loại `google-generative-ai` API.\n\n## Được hỗ trợ APIs\n\n| API | Sự miêu tả |\n|-----|-------------|\n| `openai-completions` | Hoàn thành trò chuyện OpenAI (tương thích nhất) |\n| `openai-responses` | Phản hồi OpenAI API |\n| `anthropic-messages` | Thông điệp nhân loại API |\n| `google-generative-ai` | AI sáng tạo của Google |\n\nĐặt `api` ở cấp nhà cung cấp (mặc định cho tất cả kiểu máy) hoặc cấp độ mô hình (ghi đè cho mỗi kiểu máy).\n\n## Cấu hình nhà cung cấp\n\n| Cánh đồng | Sự miêu tả |\n|-------|-------------|\n| `baseUrl` | API URL điểm cuối |\n| `api` | loại API (xem ở trên) |\n| `apiKey` | Cấu hình API key tùy chọn (xem độ phân giải giá trị bên dưới). Bỏ qua nó khi xác thực được cung cấp bởi `/login`/`auth.json` hoặc CLI `--api-key`. |\n| `oauth` | Loại nhà cung cấp OAuth động. Hiện hỗ trợ `\"radius\"`; yêu cầu cổng `baseUrl`. |\n| `headers` | Tiêu đề tùy chỉnh (xem độ phân giải giá trị bên dưới) |\n| `authHeader` | Đặt `true` để tự động thêm `Authorization: Bearer <apiKey>` |\n| `models` | Mảng cấu hình mô hình |\n| `modelOverrides` | Ghi đè theo mô hình cho các mô hình được tích hợp sẵn hoặc đã đăng ký mở rộng trên nhà cung cấp này |\n\nĐối với các nhà cung cấp có `models`, cấu hình nhà cung cấp không được tích hợp sẵn cần `baseUrl` và giá trị `api` ở cấp nhà cung cấp hoặc cấp mô hình. `apiKey` không bắt buộc phải tải tệp: các mô hình sẽ khả dụng khi xác thực được định cấu hình thông qua `/login`/`auth.json`, CLI `--api-key` hoặc nhà cung cấp `apiKey`. Nếu không định cấu hình xác thực, các mô hình sẽ tải nhưng vẫn không khả dụng ở `/model` và `--list-models`.\n\n### Độ phân giải giá trị\n\nCác trường `apiKey` và `headers` hỗ trợ thực thi lệnh, nội suy môi trường và chữ:\n\n- **Lệnh Shell:** `\"!command\"` khi bắt đầu thực thi toàn bộ giá trị dưới dạng lệnh và sử dụng stdout\n  ```json\n  \"apiKey\": \"!security find-generic-password -ws 'anthropic'\"\n  \"apiKey\": \"!op read 'op://vault/item/credential'\"\n  ```\n- **Nội suy môi trường:** `\"$ENV_VAR\"` hoặc `\"${ENV_VAR}\"` sử dụng giá trị của biến được đặt tên. Nội suy hoạt động bên trong các chữ lớn hơn.\n  ```json\n  \"apiKey\": \"$MY_API_KEY\"\n  \"apiKey\": \"${KEY_PREFIX}_${KEY_SUFFIX}\"\n  ```\n  `$FOO_BAR` là biến `FOO_BAR`; sử dụng `${FOO}_BAR` khi `BAR` là văn bản chữ. Thiếu biến môi trường làm cho giá trị không được giải quyết.\n- **Thoát:** `\"$\"` phát ra chữ `\"$\"`; `\"$!\"` phát ra `\"!\"` theo nghĩa đen mà không kích hoạt thực thi lệnh.\n  ```json\n  \"apiKey\": \"$$literal-dollar-prefix\"\n  \"apiKey\": \"$!literal-bang-prefix\"\n  ```\n- **Giá trị bằng chữ:** Được sử dụng trực tiếp. Các chuỗi viết hoa đơn giản như `MY_API_KEY` là chữ; sử dụng `$MY_API_KEY` cho các biến môi trường.\n  ```json\n  \"apiKey\": \"sk-...\"\n  ```\n\nĐối với `models.json`, các lệnh shell được giải quyết tại thời điểm yêu cầu. pi cố tình không áp dụng TTL tích hợp, tái sử dụng cũ hoặc logic khôi phục cho các lệnh tùy ý. Các lệnh khác nhau cần các chiến lược lưu trữ và thất bại khác nhau và pi không thể suy ra chiến lược đúng.\n\nNếu lệnh của bạn chậm, tốn kém, bị giới hạn tốc độ hoặc phải tiếp tục sử dụng giá trị trước đó đối với các lỗi tạm thời, hãy gói lệnh đó trong tập lệnh hoặc lệnh của riêng bạn để thực hiện hành vi lưu vào bộ nhớ đệm hoặc TTL mà bạn muốn.\n\n`/model` kiểm tra tính khả dụng sử dụng sự hiện diện xác thực đã định cấu hình và không thực thi các lệnh shell.\n\n### Tiêu đề tùy chỉnh\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## Cấu hình mô hình\n\n| Cánh đồng | Yêu cầu | Mặc định | Sự miêu tả |\n|-------|----------|---------|-------------|\n| `id` | Đúng | — | Mã nhận dạng mẫu (được chuyển tới API) |\n| `name` | KHÔNG | `id` | Nhãn mô hình con người có thể đọc được. Được sử dụng để khớp (mẫu `--model`) và hiển thị dưới dạng văn bản chi tiết mô hình phụ. |\n| `api` | KHÔNG | của nhà cung cấp `api` | Ghi đè API của nhà cung cấp cho mô hình này |\n| `reasoning` | KHÔNG | `false` | Hỗ trợ tư duy mở rộng |\n| `thinkingLevelMap` | KHÔNG | bỏ qua | Ánh xạ các cấp độ tư duy pi tới các giá trị của nhà cung cấp và đánh dấu các cấp độ không được hỗ trợ (xem bên dưới) |\n| `input` | KHÔNG | `[\"text\"]` | Các loại đầu vào: `[\"text\"]` hoặc `[\"text\", \"image\"]` |\n| `contextWindow` | KHÔNG | `128000` | Kích thước cửa sổ ngữ cảnh tính bằng mã thông báo |\n| `maxTokens` | KHÔNG | `16384` | Mã thông báo đầu ra tối đa |\n| `samplingParams` | KHÔNG | bỏ qua | Các tham số lấy mẫu đã hợp nhất nguyên văn vào mọi nội dung yêu cầu (xem bên dưới) |\n| `cost` | KHÔNG | tất cả số không | Tỷ lệ mỗi triệu mã thông báo với các mức giá đầu vào cho toàn bộ yêu cầu tùy chọn |\n| `compat` | KHÔNG | nhà cung cấp `compat` | Ghi đè khả năng tương thích của nhà cung cấp. Được hợp nhất với cấp nhà cung cấp `compat` khi cả hai đều được đặt. |\n\nMột bậc chi phí cung cấp một bộ giá thay thế hoàn chỉnh và áp dụng cho toàn bộ yêu cầu khi tổng mức sử dụng đầu vào (`input + cacheRead + cacheWrite`) vượt quá `inputTokensAbove`. Khi nhiều bậc khớp nhau, ngưỡng cao nhất sẽ thắng.\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\nHành vi hiện tại:\n- `/model`, `--list-models` và các mục hiển thị chân trang tương tác theo mô hình `id`.\n- `name` được định cấu hình được sử dụng để khớp mô hình và văn bản chi tiết mô hình phụ. Nó không thay thế id mô hình chân trang/thanh trạng thái.\n\n### Thông số lấy mẫu\n\n`samplingParams` là một đối tượng dạng tự do được hợp nhất nguyên văn vào mọi nội dung yêu cầu cho mô hình, sau khi các trường pi tự đặt, do đó, khóa của nó sẽ thắng. Sử dụng nó để gửi các tham số lấy mẫu mà pi không lập mô hình — bao gồm các tham số dành riêng cho máy chủ như llama.cpp's `min_p` hoặc `top_k` của vLLM:\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\nChỉ những API tương thích với OpenAI mới áp dụng nó (`openai-completions`, `openai-responses`, `azure-openai-responses`); các API khác bỏ qua nó. Các khóa ghi đè các trường yêu cầu được đặt tên của pi (ví dụ: khóa `temperature` ở đây đánh bại nhiệt độ ở mức yêu cầu), vì vậy, hãy ưu tiên khóa này làm nguồn lấy mẫu đúng cho một mô hình. Trong `modelOverrides`, `samplingParams` hợp nhất từng khóa với giá trị của mô hình cơ sở.\n\n### Bản đồ cấp độ tư duy\n\nSử dụng `thinkingLevelMap` trên một mô hình để mô tả các biện pháp kiểm soát tư duy dành riêng cho mô hình. Chìa khóa là các cấp độ tư duy pi: `off`, `minimal`, `low`, `medium`, `high`, `xhigh`, `max`. Bản đồ có thể chứa lỗ hổng; ví dụ: một mô hình có thể hiển thị `high` và `max` mà không hiển thị `xhigh`.\n\nCác giá trị là ba trạng thái:\n\n| Giá trị | Nghĩa |\n|-------|---------|\n| bỏ qua | Các cấp độ tiêu chuẩn cho đến `high` sử dụng ánh xạ mặc định của nhà cung cấp; mức `xhigh` và `max` mở rộng không được hỗ trợ |\n| sợi dây | Cấp độ được hỗ trợ và giá trị này được gửi đến nhà cung cấp |\n| `null` | Cấp độ không được hỗ trợ và bị ẩn/bỏ qua/kẹp đi |\n\nVí dụ về mô hình chỉ hỗ trợ lý luận tắt, cao và tối đa:\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\nVí dụ về mô hình không thể vô hiệu hóa suy nghĩ:\n\n```json\n{\n  \"id\": \"always-thinking-model\",\n  \"reasoning\": true,\n  \"thinkingLevelMap\": {\n    \"off\": null\n  }\n}\n```\n\nDi chuyển: các cấu hình cũ hơn đã sử dụng `compat.reasoningEffortMap` sẽ di chuyển ánh xạ đó sang cấp mô hình `thinkingLevelMap`. Sử dụng `null` cho các cấp độ không xuất hiện trong giao diện người dùng.\n\n## Ghi đè tích hợp sẵn Providers\n\nĐịnh tuyến nhà cung cấp tích hợp thông qua proxy mà không cần xác định lại mô hình:\n\n```json\n{\n  \"providers\": {\n    \"anthropic\": {\n      \"baseUrl\": \"https://my-proxy.example.com/v1\"\n    }\n  }\n}\n```\n\nTất cả các mẫu Anthropic tích hợp vẫn có sẵn. Xác thực OAuth hoặc API key hiện tại vẫn tiếp tục hoạt động.\n\nĐể hợp nhất các mô hình tùy chỉnh vào một nhà cung cấp tích hợp, hãy bao gồm mảng `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\nHợp nhất ngữ nghĩa:\n- Các mô hình tích hợp được giữ lại.\n- Các mô hình tùy chỉnh được nâng cấp thêm `id` trong nhà cung cấp.\n- Nếu mô hình tùy chỉnh `id` khớp với mô hình tích hợp sẵn `id` thì mô hình tùy chỉnh sẽ thay thế mô hình tích hợp đó.\n- Nếu mô hình tùy chỉnh `id` là mô hình mới, mô hình đó sẽ được thêm cùng với các mô hình tích hợp sẵn.\n\n## Ghi đè trên mỗi mô hình\n\nSử dụng `modelOverrides` để tùy chỉnh các mô hình tích hợp và khớp với các mô hình đã đăng ký tiện ích mở rộng mà không thay thế danh sách mô hình đầy đủ của nhà cung cấp.\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` hỗ trợ các trường này cho mỗi mô hình: `name`, `reasoning`, `thinkingLevelMap`, `input`, `cost` (một phần), `contextWindow`, `maxTokens`, `samplingParams` (được hợp nhất cho mỗi khóa), `headers`, `compat`.\n\nOpenAI GPT-5.6 Sol, Terra và Luna trực tiếp mặc định có cửa sổ ngữ cảnh `272000` để các yêu cầu vẫn nằm trong bậc định giá ngữ cảnh ngắn của OpenAI. Để chọn tham gia cửa sổ ngữ cảnh 1,05M của OpenAI, hãy tăng cửa sổ ngữ cảnh đó cho từng kiểu máy bạn sử dụng:\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\nViệc ghi đè sẽ giữ nguyên siêu dữ liệu về giá được tích hợp sẵn. Các yêu cầu có tổng số mã thông báo đầu vào hơn 272K sẽ sử dụng tốc độ ngữ cảnh dài của GPT-5.6 cho toàn bộ yêu cầu. Áp dụng ghi đè tương tự cho `gpt-5.6-terra` hoặc `gpt-5.6-luna` khi cần.\n\nGhi chú hành vi:\n- `modelOverrides` được áp dụng cho các mô hình nhà cung cấp tích hợp và phù hợp với các mô hình nhà cung cấp đã đăng ký mở rộng.\n- ID mô hình không xác định sẽ bị bỏ qua.\n- Bạn có thể kết hợp `baseUrl`/`headers` cấp nhà cung cấp với `modelOverrides`.\n- Ghi đè `name` chỉ thay đổi văn bản khớp mô hình và chi tiết phụ; danh sách mô hình chân trang và chính tiếp tục hiển thị mô hình `id`.\n- Nếu `models` cũng được xác định cho nhà cung cấp, các mô hình tùy chỉnh sẽ được hợp nhất sau khi ghi đè tích hợp sẵn. Một mô hình tùy chỉnh có cùng `id` sẽ thay thế mục nhập mô hình tích hợp bị ghi đè.\n\n## Tính tương thích của thông điệp nhân loại\n\nĐối với các nhà cung cấp hoặc proxy sử dụng `api: \"anthropic-messages\"`, hãy sử dụng `compat` để kiểm soát khả năng tương thích của yêu cầu dành riêng cho Anthropic.\n\nTheo mặc định pi gửi mỗi công cụ `eager_input_streaming: true`. Nếu proxy hoặc chương trình phụ trợ tương thích với Anthropic từ chối trường đó, hãy đặt `supportsEagerToolInputStreaming` thành `false`. Pi sẽ bỏ qua `tools[].eager_input_streaming` và thay vào đó gửi tiêu đề beta `fine-grained-tool-streaming-2025-05-14` cũ cho các yêu cầu hỗ trợ công cụ.\n\nMột số mô hình Nhân học yêu cầu tư duy thích ứng (`thinking.type: \"adaptive\"` cộng `output_config.effort`) thay vì khối lượng tư duy dựa trên ngân sách truyền thống. Các mô hình tích hợp tự động thiết lập điều này. Đối với các nhà cung cấp tùy chỉnh hoặc bí danh định tuyến đến các mô hình đó, hãy đặt `forceAdaptiveThinking` thành `true`.\n\nMột số nhà cung cấp tương thích với Anthropic phát ra các khối tư duy có chữ ký trống và vẫn mong đợi chúng được phát lại. Chỉ đặt `allowEmptySignature` thành `true` cho những nhà cung cấp đó; Anthropic thực sự từ chối những chữ ký suy nghĩ trống rỗng.\n\nCác mô hình Anthropic tích hợp sẵn cho phép `supportsStrictTools` trong siêu dữ liệu mô hình của chúng. Các mô hình tương thích với Anthropic tùy chỉnh phải đặt thành `true` khi điểm cuối của chúng chấp nhận các định nghĩa công cụ lược đồ JSON nghiêm ngặt.\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| Cánh đồng | Sự miêu tả |\n|-------|-------------|\n| `supportsEagerToolInputStreaming` | Liệu nhà cung cấp có chấp nhận mỗi công cụ `eager_input_streaming` hay không. Mặc định: `true`. Đặt thành `false` để bỏ qua trường đó và sử dụng tiêu đề beta phát trực tuyến công cụ chi tiết cũ cho các yêu cầu hỗ trợ công cụ. |\n| `supportsLongCacheRetention` | Liệu nhà cung cấp có chấp nhận lưu giữ bộ nhớ đệm dài Anthropic (`cache_control.ttl: \"1h\"`) hay không khi lưu giữ bộ nhớ đệm là `long`. Mặc định: `true`. |\n| `sendSessionAffinityHeaders` | Có gửi `x-session-affinity` từ id phiên khi bật bộ nhớ đệm hay không. Mặc định: tự động phát hiện các nhà cung cấp đã biết. |\n| `supportsCacheControlOnTools` | Liệu nhà cung cấp có chấp nhận điểm đánh dấu `cache_control` kiểu Anthropic trên định nghĩa công cụ hay không. Mặc định: `true`. |\n| `forceAdaptiveThinking` | Có gửi tư duy thích ứng (`thinking.type: \"adaptive\"` cộng `output_config.effort`) cho mô hình này hay không. Các mô hình thích ứng tích hợp tự động thiết lập điều này. Mặc định: `false`. |\n| `allowEmptySignature` | Có phát lại các dấu hiệu suy nghĩ trống rỗng dưới dạng `signature: \"\"` thay vì chuyển đổi suy nghĩ thành văn bản hay không. Mặc định: `false`. |\n| `supportsStrictTools` | Liệu nhà cung cấp có chấp nhận các định nghĩa công cụ lược đồ JSON nghiêm ngặt hay không. Mặc định: `false`; các mô hình Anthropic tích hợp sẵn cho phép nó tạo ra siêu dữ liệu. |\n\n## Khả năng tương thích OpenAI\n\nĐối với các nhà cung cấp có khả năng tương thích một phần với OpenAI, hãy sử dụng trường `compat`.\n\n- Cấp nhà cung cấp `compat` áp dụng mặc định cho tất cả các kiểu máy thuộc nhà cung cấp đó.\n- Cấp mô hình `compat` ghi đè các giá trị cấp nhà cung cấp cho mô hình đó.\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| Cánh đồng | Sự miêu tả |\n|-------|-------------|\n| `supportsStore` | Nhà cung cấp hỗ trợ trường `store` |\n| `supportsDeveloperRole` | Sử dụng vai trò `developer` so với `system` |\n| `supportsReasoningEffort` | Hỗ trợ tham số `reasoning_effort` |\n| `supportsUsageInStreaming` | Hỗ trợ `stream_options: { include_usage: true }` (mặc định: `true`) |\n| `supportsFinishReason` | Câu trả lời được truyền trực tuyến có bao gồm `finish_reason` hay không. Khi `false`, pi suy ra `stop` hoặc `toolUse` khi luồng kết thúc. Mặc định: `true`. |\n| `maxTokensField` | Sử dụng `max_completion_tokens` hoặc `max_tokens` |\n| `requiresToolResultName` | Bao gồm `name` trên thông báo kết quả công cụ |\n| `requiresAssistantAfterToolResult` | Chèn tin nhắn trợ lý trước tin nhắn người dùng sau kết quả của công cụ |\n| `requiresThinkingAsText` | Chuyển đổi các khối suy nghĩ thành văn bản thuần túy |\n| `requiresReasoningContentOnAssistantMessages` | Bao gồm `reasoning_content` trống trên tất cả các tin nhắn trợ lý được phát lại khi tính năng lý luận được bật |\n| `thinkingFormat` | Sử dụng các tham số suy nghĩ `reasoning_effort`, `openrouter`, `deepseek`, `together`, `baseten`, `zai`, `qwen`, `chat-template` hoặc `qwen-chat-template` |\n| `chatTemplateKwargs` | giá trị `chat_template_kwargs` cho `thinkingFormat: \"chat-template\"`; sử dụng `{ \"$var\": \"thinking.enabled\" }` hoặc `{ \"$var\": \"thinking.effort\" }` cho các giá trị tư duy do pi điều khiển |\n| `chatTemplateArgs` | giá trị `chat_template_args` cho `thinkingFormat: \"baseten\"`; sử dụng `{ \"$var\": \"thinking.enabled\" }` hoặc `{ \"$var\": \"thinking.effort\" }` cho các giá trị tư duy do pi điều khiển |\n| `cacheControlFormat` | Sử dụng các điểm đánh dấu `cache_control` kiểu Anthropic trên lời nhắc hệ thống, định nghĩa công cụ cuối cùng và nội dung văn bản kết quả công cụ, trợ lý hoặc người dùng cuối cùng. Hiện tại chỉ hỗ trợ `anthropic`. |\n| `sendSessionAffinityHeaders` | Đối với `openai-completions`, hãy gửi tiêu đề sở thích phiên từ id phiên khi bộ nhớ đệm được bật. Mặc định: `false`. |\n| `sessionAffinityFormat` | Đối với `openai-completions` và `openai-responses`, định dạng tiêu đề sở thích phiên: `openai` gửi `session_id`/`x-client-request-id` (các lần hoàn thành cũng `x-session-affinity`), `openai-nosession` bỏ qua tiêu đề chứa dấu gạch dưới `session_id`, `openrouter` gửi `x-session-id`. Không ảnh hưởng đến thông số cơ thể `prompt_cache_key`. Mặc định: tự động phát hiện. |\n| `supportsStrictMode` | Liệu nhà cung cấp có chấp nhận các định nghĩa nghiêm ngặt của công cụ hàm lược đồ JSON hay không. Giá trị mặc định phụ thuộc vào API; các mô hình OpenAI tích hợp mang siêu dữ liệu khả năng rõ ràng. |\n| `supportsOpenAIGrammarTools` | Liệu các API tương thích với OpenAI có phát ra các công cụ ngữ pháp Lark/regex tùy chỉnh hay không. Khi `false`, các công cụ bị hạn chế về ngữ pháp sẽ quay trở lại các công cụ chức năng thông thường. Mặc định: `false`; danh mục mô hình tích hợp sẵn hỗ trợ các mô hình GPT-5+ trên OpenAI, OpenAI Codex, Azure OpenAI, GitHub Copilot, opencode và Cloudflare AI Gateway. |\n| `deferredToolsMode` | Sử dụng tuần tự hóa công cụ trì hoãn dành riêng cho nhà cung cấp. Hiện tại, chỉ `\"kimi\"` được hỗ trợ cho định dạng Hoàn thành trò chuyện tương thích với OpenAI của Kimi. |\n| `supportsLongCacheRetention` | Liệu nhà cung cấp có chấp nhận lưu giữ bộ nhớ đệm trong thời gian dài khi lưu giữ bộ nhớ đệm là `long`: `prompt_cache_retention: \"24h\"` đối với bộ nhớ đệm nhắc nhở OpenAI hay `cache_control.ttl: \"1h\"` khi `cacheControlFormat` là `anthropic`. Mặc định: `true`. |\n| `openRouterRouting` | Tùy chọn định tuyến của nhà cung cấp OpenRouter. Đối tượng này được gửi nguyên trạng trong trường `provider` của [OpenRouter API request](https://openrouter.ai/docs/guides/routing/provider-selection). |\n| `vercelGatewayRouting` | Cấu hình định tuyến Vercel AI Gateway để lựa chọn nhà cung cấp (`only`, `order`) |\n\n`openrouter` sử dụng `reasoning: { effort }`. `together` sử dụng `reasoning: { enabled }` và cả `reasoning_effort` khi `supportsReasoningEffort` được bật. `qwen` sử dụng cấp cao nhất `enable_thinking`. Sử dụng `qwen-chat-template` cho các máy chủ tương thích với Qwen cục bộ yêu cầu `chat_template_kwargs.enable_thinking` và `preserve_thinking`. Sử dụng `chat-template` cho các mẫu trò chuyện vLLM/Hugging Face cần có thể định cấu hình `chat_template_kwargs`, chẳng hạn như `chatTemplateKwargs: { \"thinking\": { \"$var\": \"thinking.enabled\" } }` cho các mẫu DeepSeek V3.x. Sử dụng `thinkingFormat: \"baseten\"` với `chatTemplateArgs` đối với các nhà cung cấp hiển thị các điều khiển chuyển đổi thông qua `chat_template_args` và tùy chọn hỗ trợ cấp cao nhất `reasoning_effort`.\n\n`cacheControlFormat: \"anthropic\"` dành cho các nhà cung cấp tương thích với OpenAI, hiển thị bộ nhớ đệm nhắc nhở kiểu Anthropic thông qua các điểm đánh dấu `cache_control` trên nội dung văn bản và định nghĩa công cụ.\n\nVí dụ:\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\nVí dụ về Cổng AI của Vercel:\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 có thể giúp bạn tạo các gói pi. Yêu cầu nó đóng gói các tiện ích mở rộng, kỹ năng, prompt templates hoặc chủ đề của bạn.\n\n\nPi gói các tiện ích mở rộng, kỹ năng, prompt templates và chủ đề để bạn có thể chia sẻ chúng thông qua npm hoặc git. Một gói có thể khai báo tài nguyên trong `package.json` dưới khóa `pi` hoặc sử dụng các thư mục thông thường.\n\n## Mục lục\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## Cài đặt và quản lý\n\n> **Bảo mật:** Pi gói chạy với quyền truy cập toàn bộ hệ thống. Extensions thực thi mã tùy ý và các kỹ năng có thể hướng dẫn mô hình thực hiện bất kỳ hành động nào bao gồm cả việc chạy các tệp thực thi. Xem lại mã nguồn trước khi cài đặt các gói của bên thứ ba.\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\nCác lệnh này quản lý các gói pi và `pi update` có thể cập nhật cài đặt pi CLI. Để gỡ cài đặt pi, hãy xem [Quickstart](quickstart.md#uninstall).\n\nTheo mặc định, `install` và `remove` ghi vào cài đặt người dùng (`~/.pi/agent/settings.json`). Thay vào đó, hãy sử dụng `-l` để ghi vào cài đặt dự án (`.pi/settings.json`). Cài đặt dự án có thể được chia sẻ với nhóm của bạn và pi sẽ tự động cài đặt mọi gói bị thiếu khi khởi động sau khi dự án được tin cậy.\n\nĐể thử một gói mà không cần cài đặt, hãy sử dụng `--extension` hoặc `-e`. Điều này chỉ cài đặt vào một thư mục tạm thời cho lần chạy hiện tại:\n\n```bash\npi -e npm:@foo/bar\npi -e git:github.com/user/repo\n```\n\n## Nguồn gói\n\nPi chấp nhận ba loại nguồn trong cài đặt và `pi install`.\n\n### npm\n\n```\nnpm:@scope/pkg@1.2.3\nnpm:pkg\n```\n\n- Thông số kỹ thuật được phiên bản được ghim và bỏ qua bởi các bản cập nhật gói (`pi update --extensions`, `pi update --all`).\n- Số lượt cài đặt của người dùng đạt dưới `~/.pi/agent/npm/`.\n- Số lượt cài đặt dự án đạt dưới `.pi/npm/`.\n- Đặt `npmCommand` trong `settings.json` để ghim các thao tác tra cứu và cài đặt gói npm vào một lệnh trình bao bọc cụ thể, chẳng hạn như `mise` hoặc `asdf`.\n\nVí dụ:\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- Nếu không có tiền tố `git:`, chỉ các URL giao thức mới được chấp nhận (`https://`, `http://`, `ssh://`, `git://`).\n- Với tiền tố `git:`, các định dạng tốc ký được chấp nhận, bao gồm `github.com/user/repo` và `git@github.com:user/repo`.\n- URL HTTPS và SSH đều được hỗ trợ.\n- SSH URL tự động sử dụng các khóa SSH đã định cấu hình của bạn (tôn trọng `~/.ssh/config`).\n- Đối với các lần chạy không tương tác (ví dụ: CI), bạn có thể đặt `GIT_TERMINAL_PROMPT=0` để tắt lời nhắc thông tin xác thực và đặt `GIT_SSH_COMMAND` (ví dụ: `ssh -o BatchMode=yes -o ConnectTimeout=5`) để không thành công nhanh.\n- Tham chiếu là thẻ được ghim hoặc cam kết. `pi update --extensions` và `pi update --all` không chuyển chúng sang các ref mới hơn, nhưng chúng điều chỉnh một bản sao hiện có với ref đã định cấu hình.\n- Sử dụng `pi install git:host/user/repo@new-ref` để cập nhật cài đặt và di chuyển gói hiện có sang một tham chiếu được ghim mới.\n- Được nhân bản thành `~/.pi/agent/git/<host>/<path>` (toàn cầu) hoặc `.pi/git/<host>/<path>` (dự án).\n- Khi đối chiếu thay đổi thanh toán, pi đặt lại và xóa bản sao, sau đó chạy `npm install` nếu `package.json` tồn tại.\n\n**SSH ví dụ:**\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### Đường dẫn cục bộ\n\n```\n/absolute/path/to/package\n./relative/path/to/package\n```\n\nĐường dẫn cục bộ trỏ đến tệp hoặc thư mục trên đĩa và được thêm vào cài đặt mà không cần sao chép. Các đường dẫn tương đối được giải quyết dựa trên tệp cài đặt mà chúng xuất hiện. Nếu đường dẫn là một tệp, nó sẽ tải dưới dạng một phần mở rộng duy nhất. Nếu là một thư mục, pi sẽ tải tài nguyên bằng các quy tắc gói.\n\n## Tạo gói Pi\n\nThêm tệp kê khai `pi` vào `package.json` hoặc sử dụng các thư mục thông thường. Bao gồm từ khóa `pi-package` để có thể khám phá.\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Đường dẫn có liên quan đến gói gốc. Mảng hỗ trợ các mẫu hình cầu và `!exclusions`.\n\n### Siêu dữ liệu thư viện\n\n[package gallery](https://pi.dev/packages) hiển thị các gói được gắn thẻ `pi-package`. Thêm các trường `video` hoặc `image` để hiển thị bản xem trước:\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- **video**: Chỉ MP4. Trên máy tính để bàn, tự động phát khi di chuột. Nhấp vào sẽ mở trình phát toàn màn hình.\n- **hình ảnh**: PNG, JPEG, GIF hoặc WebP. Được hiển thị dưới dạng bản xem trước tĩnh.\n\nNếu cả hai được đặt, video sẽ được ưu tiên.\n\n## Cấu trúc gói\n\n### Thư mục hội nghị\n\nNếu không có tệp kê khai `pi`, pi sẽ tự động khám phá tài nguyên từ các thư mục này:\n\n- `extensions/` tải tệp `.ts` và `.js`\n- `skills/` tìm đệ quy các thư mục `SKILL.md` và tải các tệp `.md` cấp cao nhất dưới dạng kỹ năng\n- `prompts/` tải `.md` tệp\n- `themes/` tải `.json` tệp\n\n## phụ thuộc\n\nCác phần phụ thuộc thời gian chạy của bên thứ ba thuộc về `dependencies` trong `package.json`. Các phần phụ thuộc không đăng ký tiện ích mở rộng, kỹ năng, prompt templates hoặc chủ đề cũng thuộc về `dependencies`. Khi pi cài đặt một gói từ npm hoặc git, nó sẽ chạy `npm install`, vì vậy những phần phụ thuộc đó sẽ được cài đặt tự động.\n\nPi gói các gói cốt lõi dành cho tiện ích mở rộng và kỹ năng. Nếu bạn nhập bất kỳ thứ nào trong số này, hãy liệt kê chúng trong `peerDependencies` với phạm vi `\"*\"` và không gộp chúng: `@earendil-works/pi-ai`, `@earendil-works/pi-agent-core`, `@earendil-works/pi-coding-agent`, `@earendil-works/pi-tui`, `typebox`.\n\nCác gói pi khác phải được gói trong tarball của bạn. Thêm chúng vào `dependencies` và `bundledDependencies`, sau đó tham chiếu tài nguyên của chúng thông qua đường dẫn `node_modules/`. Pi tải các gói có gốc mô-đun riêng biệt, do đó các bản cài đặt riêng biệt không xung đột hoặc chia sẻ các mô-đun.\n\nVí dụ:\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## Lọc gói\n\nLọc nội dung gói sẽ tải bằng cách sử dụng biểu mẫu đối tượng trong cài đặt:\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` và `-path` là các đường dẫn chính xác liên quan đến thư mục gốc của gói.\n\n- Bỏ qua một phím để tải tất cả các loại đó.\n- Sử dụng `[]` để không tải loại nào.\n- `!pattern` không bao gồm kết quả trùng khớp.\n- `+path` lực-bao gồm một đường dẫn chính xác.\n- `-path` lực-loại trừ một đường dẫn chính xác.\n- Lớp bộ lọc ở trên cùng của tệp kê khai. Họ thu hẹp những gì đã được cho phép.\n\n## Kích hoạt và vô hiệu hóa tài nguyên\n\nSử dụng `pi config` để bật hoặc tắt các tiện ích mở rộng, kỹ năng, prompt templates và chủ đề từ các gói đã cài đặt và thư mục cục bộ. `pi config` bắt đầu trong cài đặt chung (`~/.pi/agent/settings.json`); nhấn Tab để chuyển giữa chế độ toàn cầu và dự án cục bộ. Sử dụng `pi config -l` để bắt đầu ghi đè dự án (`.pi/settings.json`) với tài nguyên toàn cầu được kế thừa bị làm mờ.\n\n## Phạm vi và chống trùng lặp\n\nCác gói có thể xuất hiện trong cả cài đặt chung và dự án. Nếu cùng một gói xuất hiện trong cả hai, thì mục nhập dự án sẽ thắng trừ khi mục nhập dự án có `autoload: false`, trong trường hợp đó, mục nhập đó được áp dụng dưới dạng delta trên mục nhập chung. Danh tính được xác định bởi:\n\n- npm: tên gói\n- git: URL kho lưu trữ không có ref\n- local: đường dẫn tuyệt đối đã được giải quyết","sourceFile":"packages.md"},"prompt-templates":{"title":"Mẫu nhắc nhở","markdown":"> pi có thể tạo prompt templates. Yêu cầu nó xây dựng một cái cho quy trình làm việc của bạn.\n\n\nMẫu lời nhắc là Markdown đoạn trích mở rộng thành lời nhắc đầy đủ. Nhập `/name` vào trình chỉnh sửa để gọi mẫu, trong đó `name` là tên tệp không có `.md`.\n\n## Địa điểm\n\nPi tải prompt templates từ:\n\n- Toàn cầu: `~/.pi/agent/prompts/*.md`\n- Dự án: `.pi/prompts/*.md` (chỉ sau khi dự án được tin cậy)\n- Các gói: `prompts/` thư mục hoặc `pi.prompts` mục trong `package.json`\n- Cài đặt: mảng `prompts` với các tệp hoặc thư mục\n- CLI: `--prompt-template <path>` (có thể lặp lại)\n\nTắt tính năng khám phá bằng `--no-prompt-templates`.\n\n## Định dạng\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- Tên tập tin trở thành tên lệnh. `review.md` trở thành `/review`.\n- `description` là tùy chọn. Nếu thiếu, dòng không trống đầu tiên sẽ được sử dụng.\n- `argument-hint` là tùy chọn. Khi được đặt, gợi ý sẽ hiển thị trước phần mô tả trong danh sách thả xuống tự động hoàn thành.\n\n### Gợi ý lập luận\n\nSử dụng `argument-hint` trong phần đầu để hiển thị các đối số dự kiến ​​trong tính năng tự động hoàn thành. Sử dụng `<angle brackets>` cho các đối số bắt buộc và `[square brackets]` cho các đối số tùy chọn:\n\n```markdown\n---\ndescription: Review PRs from URLs with structured issue and code analysis\nargument-hint: \"<PR-URL>\"\n---\n```\n\nĐiều này hiển thị trong danh sách thả xuống tự động hoàn thành dưới dạng:\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## Cách sử dụng\n\nNhập `/` theo sau là tên mẫu trong trình chỉnh sửa. Tự động hoàn thành hiển thị các mẫu có sẵn cùng với mô tả.\n\n```\n/review                           # Expands review.md\n/component Button                 # Expands with argument\n/component Button \"click handler\" # Multiple arguments\n```\n\n## Đối số\n\nCác mẫu hỗ trợ các đối số vị trí, mặc định và cắt lát đơn giản:\n\n- `$1`, `$2`,... đối số vị trí\n- `$@` hoặc `$ARGUMENTS` cho tất cả các đối số đã tham gia\n- `${1:-default}` sử dụng arg 1 khi hiện diện/không trống, nếu không thì `default`\n- `${@:-default}` hoặc `${ARGUMENTS:-default}` sử dụng tất cả đối số khi có/không trống, nếu không thì `default`\n- `${@:N}` cho các đối số từ vị trí thứ N (được lập chỉ mục 1)\n- `${@:N:L}` cho `L` đối số bắt đầu từ N\n\nVí dụ:\n\n```markdown\n---\ndescription: Create a component\n---\nCreate a React component named $1 with features: $@\n```\n\nCác giá trị mặc định rất hữu ích cho các đối số tùy chọn:\n\n```markdown\nSummarize the current state in ${1:-7} bullet points.\n```\n\nCách sử dụng: `/component Button \"onClick handler\" \"disabled support\"`\n\n## Đang tải quy tắc\n\n- Khám phá mẫu trong `prompts/` là không đệ quy.\n- Nếu bạn muốn các mẫu trong thư mục con, hãy thêm chúng một cách rõ ràng thông qua cài đặt `prompts` hoặc bảng kê khai gói.","sourceFile":"prompt-templates.md"},"providers":{"title":"Providers","markdown":"Pi hỗ trợ các nhà cung cấp dựa trên đăng ký thông qua các nhà cung cấp OAuth và API key thông qua các biến môi trường hoặc tệp xác thực. Danh mục tích hợp đi kèm với pi; các nhà cung cấp đã định cấu hình có thể làm mới các danh mục mới hơn và lưu chúng vào bộ nhớ đệm trong `~/.pi/agent/models-store.json` để sử dụng ngoại tuyến.\n\n## Mục lục\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## Đăng ký\n\nSử dụng `/login` ở chế độ tương tác, sau đó chọn nhà cung cấp:\n\n- ChatGPT Plus/Pro (Codex)\n- Claude Pro/Max\n- GitHub Phi công phụ\n- xAI (đăng ký Grok/X)\n- OpenRouter (OAuth đúc API key được tính phí từ tín dụng OpenRouter)\n- Bán kính\n\nSử dụng `/logout` để xóa thông tin xác thực. Mã thông báo được lưu trữ trong `~/.pi/agent/auth.json` và tự động làm mới khi hết hạn. Thay vào đó, OpenRouter tạo ra một API key do người dùng kiểm soát và không tự động hết hạn.\n\n### Codex OpenAI\n\n- Yêu cầu đăng ký ChatGPT Plus hoặc Pro\n- Được xác nhận chính thức bởi OpenAI: [Codex for OSS](https://developers.openai.com/community/codex-for-oss)\n\n### Claude Pro/Max\n\nXác thực đăng ký Anthropic đang hoạt động cho tài khoản Claude Pro/Max. Mức sử dụng khai thác của bên thứ ba rút ra từ [extra usage](https://claude.ai/settings/usage) và được tính phí theo mã thông báo, không vượt quá giới hạn của gói Claude.\n\n### GitHub Phi công phụ\n\n- Nhấn Enter để truy cập github.com hoặc nhập miền GitHub Enterprise Server của bạn\n- Nếu bạn nhận được \"kiểu máy không được hỗ trợ\", hãy bật nó trong Mã VS: Trò chuyện phi công phụ → bộ chọn mô hình → chọn mô hình → \"Bật\"\n\n### xAI (đăng ký Grok/X)\n\n- Chạy `/login xai`, sau đó chọn **Sử dụng đăng ký**\n- `XAI_API_KEY` vẫn có sẵn thông qua **Sử dụng API key**\n\n### Bộ định tuyến mở\n\n- Chạy `/login openrouter`, sau đó chọn **Đăng nhập bằng OpenRouter** để mở luồng ủy quyền OpenRouter PKCE\n- Ủy quyền tạo OpenRouter API key do người dùng kiểm soát được tính phí từ tín dụng OpenRouter của bạn\n- Trên các máy từ xa/không có đầu (ví dụ: trên SSH), trình duyệt không thể truy cập lệnh gọi lại vòng lặp; thay vào đó hãy dán URL chuyển hướng cuối cùng (hoặc mã ủy quyền) vào lời nhắc đăng nhập\n- `OPENROUTER_API_KEY` vẫn có sẵn thông qua **Sử dụng API key**\n\n### Bán kính\n\nBán kính là một cổng `pi-messages` động. `/login radius` lưu trữ OAuth mã thông báo trong `auth.json`; danh mục cổng được làm mới độc lập và được lưu vào bộ nhớ đệm trong `models-store.json`. Cổng Bán kính tùy chỉnh có thể được khai báo trong `models.json` với `\"oauth\": \"radius\"` và cổng `baseUrl`.\n\n## API Phím\n\n### Biến môi trường hoặc tệp xác thực\n\nSử dụng `/login` ở chế độ tương tác và chọn nhà cung cấp để lưu trữ API key trong `auth.json` hoặc đặt thông tin xác thực thông qua biến môi trường:\n\n```bash\nexport ANTHROPIC_API_KEY=sk-ant-...\npi\n```\n\n| nhà cung cấp | Biến môi trường | phím `auth.json` |\n|----------|----------------------|------------------|\n| nhân loại | `ANTHROPIC_API_KEY` | `anthropic` |\n| Kiến Linh | `ANT_LING_API_KEY` | `ant-ling` |\n| Phản hồi của Azure OpenAI | `AZURE_OPENAI_API_KEY` | `azure-openai-responses` |\n| OpenAI | `OPENAI_API_KEY` | `openai` |\n| tìm kiếm sâu | `DEEPSEEK_API_KEY` | `deepseek` |\n| NVIDIA NIM | `NVIDIA_API_KEY` | `nvidia` |\n| Google Song Tử | `GEMINI_API_KEY` | `google` |\n| Đá nền Amazon | `AWS_BEARER_TOKEN_BEDROCK` | `amazon-bedrock` |\n| Mistral | `MISTRAL_API_KEY` | `mistral` |\n| Groq | `GROQ_API_KEY` | `groq` |\n| não | `CEREBRAS_API_KEY` | `cerebras` |\n| Cổng AI của Cloudflare | `CLOUDFLARE_API_KEY` (+ `CLOUDFLARE_ACCOUNT_ID`, `CLOUDFLARE_GATEWAY_ID`) | `cloudflare-ai-gateway` |\n| Công nhân Cloudflare AI | `CLOUDFLARE_API_KEY` (+ `CLOUDFLARE_ACCOUNT_ID`) | `cloudflare-workers-ai` |\n| xAI | `XAI_API_KEY` | `xai` |\n| Bộ định tuyến mở | `OPENROUTER_API_KEY` | `openrouter` |\n| Cổng AI Vercel | `AI_GATEWAY_API_KEY` | `vercel-ai-gateway` |\n| Gói mã hóa ZAI (Toàn cầu) | `ZAI_API_KEY` | `zai` |\n| Kế hoạch mã hóa ZAI (Trung Quốc) | `ZAI_CODING_CN_API_KEY` | `zai-coding-cn` |\n| Mã mở Zen | `OPENCODE_API_KEY` | `opencode` |\n| Mã mở đi | `OPENCODE_API_KEY` | `opencode-go` |\n| Bán kính | `RADIUS_API_KEY` | `radius` |\n| Hugging Face | `HF_TOKEN` | `huggingface` |\n| Pháo hoa | `FIREWORKS_API_KEY` | `fireworks` |\n| AI cùng nhau | `TOGETHER_API_KEY` | `together` |\n| Baseten | `BASETEN_API_KEY` | `baseten` |\n| Kimi để viết mã | `KIMI_API_KEY` | `kimi-coding` |\n| MiniMax | `MINIMAX_API_KEY` | `minimax` |\n| MiniMax (Trung Quốc) | `MINIMAX_CN_API_KEY` | `minimax-cn` |\n| Gói mã thông báo Qwen (danh mục hiện có) | `QWEN_TOKEN_PLAN_API_KEY` | `qwen-token-plan` |\n| Gói mã thông báo Qwen (Cá nhân) | `QWEN_TOKEN_PLAN_API_KEY` | `qwen-token-plan-individual` |\n| Kế hoạch mã thông báo Qwen (Trung Quốc) | `QWEN_TOKEN_PLAN_CN_API_KEY` | `qwen-token-plan-cn` |\n| Xiaomi MiMo | `XIAOMI_API_KEY` | `xiaomi` |\n| Gói mã thông báo Xiaomi MiMo (Trung Quốc) | `XIAOMI_TOKEN_PLAN_CN_API_KEY` | `xiaomi-token-plan-cn` |\n| Gói mã thông báo Xiaomi MiMo (Amsterdam) | `XIAOMI_TOKEN_PLAN_AMS_API_KEY` | `xiaomi-token-plan-ams` |\n| Gói mã thông báo Xiaomi MiMo (Singapore) | `XIAOMI_TOKEN_PLAN_SGP_API_KEY` | `xiaomi-token-plan-sgp` |\n\nTham chiếu cho các biến môi trường và khóa `auth.json`: [`const envMap`](https://github.com/earendil-works/pi-mono/blob/main/packages/ai/src/env-api-keys.ts) trong [`packages/ai/src/env-api-keys.ts`](https://github.com/earendil-works/pi-mono/blob/main/packages/ai/src/env-api-keys.ts).\n\n#### Tệp xác thực\n\nLưu trữ thông tin xác thực trong `~/.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` sử dụng cùng điểm cuối quốc tế và `QWEN_TOKEN_PLAN_API_KEY` như\n`qwen-token-plan`, nhưng giới hạn bộ chọn ở các mô hình được ghi lại cho đăng ký Cá nhân. Hiện có\nnhà cung cấp giữ danh mục rộng hơn để có khả năng tương thích ngược. Khi sử dụng `auth.json`, hãy lưu trữ\nthông tin xác thực theo nhà cung cấp bạn chọn; một biến môi trường được chia sẻ bởi cả hai nhà cung cấp quốc tế.\n\nTệp được tạo với quyền `0600` (chỉ người dùng đọc/ghi). Thông tin xác thực tệp xác thực được ưu tiên hơn các biến môi trường.\n\nAPI key thông tin đăng nhập cũng có thể bao gồm các giá trị môi trường trong phạm vi nhà cung cấp. Các giá trị này được sử dụng trước các biến môi trường quy trình khi phân giải khóa thông tin xác thực, tiêu đề nhà cung cấp/mô hình và cấu hình nhà cung cấp, chẳng hạn như ID tài khoản Cloudflare, cài đặt Azure OpenAI, dự án/vị trí Vertex, cài đặt Bedrock, `PI_CACHE_RETENTION` và `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\nSử dụng điều này khi pi nên sử dụng các cài đặt nhà cung cấp khác với môi trường shell dự án.\n\n### Độ phân giải chính\n\nTrường `key` hỗ trợ thực thi lệnh, nội suy môi trường và chữ:\n\n- **Lệnh Shell:** `\"!command\"` khi bắt đầu thực thi toàn bộ giá trị dưới dạng lệnh và sử dụng stdout (được lưu trong bộ nhớ đệm trong suốt thời gian xử lý)\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- **Nội suy môi trường:** `\"$ENV_VAR\"` hoặc `\"${ENV_VAR}\"` sử dụng giá trị của biến được đặt tên. Nội suy hoạt động bên trong các chữ lớn hơn.\n  ```json\n  { \"type\": \"api_key\", \"key\": \"$MY_ANTHROPIC_KEY\" }\n  { \"type\": \"api_key\", \"key\": \"${KEY_PREFIX}_${KEY_SUFFIX}\" }\n  ```\n  `$FOO_BAR` là biến `FOO_BAR`; sử dụng `${FOO}_BAR` khi `BAR` là văn bản chữ. Thiếu biến môi trường làm cho giá trị không được giải quyết.\n- **Thoát:** `\"$\"` phát ra chữ `\"$\"`; `\"$!\"` phát ra `\"!\"` theo nghĩa đen mà không kích hoạt thực thi lệnh.\n  ```json\n  { \"type\": \"api_key\", \"key\": \"$$literal-dollar-prefix\" }\n  { \"type\": \"api_key\", \"key\": \"$!literal-bang-prefix\" }\n  ```\n- **Giá trị bằng chữ:** Được sử dụng trực tiếp. Các chuỗi viết hoa đơn giản như `MY_API_KEY` là chữ; sử dụng `$MY_API_KEY` cho các biến môi trường.\n  ```json\n  { \"type\": \"api_key\", \"key\": \"sk-ant-...\" }\n  { \"type\": \"api_key\", \"key\": \"public\" }\n  ```\n\nThông tin xác thực OAuth cũng được lưu trữ tại đây sau `/login` và được quản lý tự động.\n\n## Đám mây Providers\n\n### Azure OpenAI\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 Amazon\n\nSử dụng `/login amazon-bedrock` để lưu trữ Bedrock API key hoặc định cấu hình một trong các nguồn thông tin xác thực AWS xung quanh bên dưới:\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Đồng thời hỗ trợ các vai trò nhiệm vụ ECS (`AWS_CONTAINER_CREDENTIALS_*`) và IRSA (`AWS_WEB_IDENTITY_TOKEN_FILE`).\n\n```bash\npi --provider amazon-bedrock --model us.anthropic.claude-sonnet-4-20250514-v1:0\n```\n\nBộ nhớ đệm nhắc nhở được bật tự động cho các mô hình Claude có ID chứa tên mô hình có thể nhận dạng được (mô hình cơ sở và cấu hình suy luận do hệ thống xác định). Đối với cấu hình suy luận ứng dụng (có ARN không chứa tên kiểu máy), hãy đặt `AWS_BEDROCK_FORCE_CACHE=1` để bật điểm bộ đệm:\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\nNếu bạn đang kết nối với proxy Bedrock API, có thể sử dụng các biến môi trường sau:\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### Cổng AI của Cloudflare\n\n`CLOUDFLARE_API_KEY` có thể được đặt qua `/login`. ID tài khoản và sên cổng có thể được đặt làm biến môi trường hoặc trong đối tượng `env` của thông tin xác thực trong `auth.json`.\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\nCác tuyến đến OpenAI, Anthropic và Workers AI thông qua Cloudflare AI Gateway. AI công nhân sử dụng ID mô hình hợp nhất API (`/compat`) và tiền tố (`workers-ai/@cf/...`). OpenAI sử dụng lộ trình chuyển tiếp OpenAI (`/openai`) với ID mô hình OpenAI gốc như `gpt-5.1`. Anthropic sử dụng lộ trình chuyển tiếp Anthropic (`/anthropic`) với ID mô hình Anthropic gốc chẳng hạn như `claude-sonnet-4-5`.\n\nXác thực Cổng AI sử dụng `CLOUDFLARE_API_KEY` làm `cf-aig-authorization`. Xác thực ngược dòng có thể là một trong:\n\n| Cách thức | Yêu cầu xác thực | Xác thực ngược dòng |\n|------|--------------|---------------|\n| AI công nhân | Chỉ mã thông báo Cloudflare | Cloudflare có nguồn gốc |\n| Thanh toán thống nhất | Chỉ mã thông báo Cloudflare | Cloudflare xử lý việc xác thực ngược dòng và khấu trừ tín dụng |\n| Được lưu trữ BYOK | Chỉ mã thông báo Cloudflare | Cloudflare tiêm các khóa của nhà cung cấp được lưu trữ trong bảng điều khiển AI Gateway |\n| BYOK nội tuyến | Mã thông báo Cloudflare cộng với tiêu đề ngược dòng `Authorization` | Yêu cầu cung cấp khóa của nhà cung cấp ngược dòng |\n\nĐể sử dụng pi thông thường, hãy ưu tiên thanh toán thống nhất hoặc BYOK được lưu trữ. BYOK nội tuyến yêu cầu định cấu hình tiêu đề `Authorization` ngược dòng bổ sung cho nhà cung cấp Cloudflare AI Gateway, chẳng hạn như thông qua ghi đè mô hình/nhà cung cấp `models.json`.\n\n### Công nhân Cloudflare AI\n\n`CLOUDFLARE_API_KEY` có thể được đặt qua `/login`. `CLOUDFLARE_ACCOUNT_ID` có ​​thể được đặt làm biến môi trường hoặc trong đối tượng `env` của thông tin xác thực trong `auth.json`.\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 tự động đặt `x-session-affinity` để được giảm giá [prefix caching](https://developers.cloudflare.com/workers-ai/features/prompt-caching/).\n\n### AI của Google Vertex\n\nSử dụng thông tin xác thực mặc định của ứng dụng:\n\n```bash\ngcloud auth application-default login\nexport GOOGLE_CLOUD_PROJECT=your-project\nexport GOOGLE_CLOUD_LOCATION=us-central1\n```\n\nHoặc đặt `GOOGLE_APPLICATION_CREDENTIALS` thành tệp khóa tài khoản dịch vụ.\n\n## llama.cpp\n\nPi hỗ trợ máy chủ bộ định tuyến llama.cpp. Định cấu hình nó bằng `/login llama.cpp`, quản lý các mô hình đã tải bằng `/llama` và chọn một mô hình đã tải bằng `/model`.\n\nXem [llama.cpp](llama-cpp.md) để biết cách thiết lập máy chủ, bố cục thư mục mô hình, biến môi trường và cách sử dụng lệnh.\n\n## Tùy chỉnh Providers\n\n**Qua models.json:** Thêm Ollama, LM Studio, vLLM hoặc bất kỳ nhà cung cấp nào nói API được hỗ trợ (Hoàn thành OpenAI, Phản hồi OpenAI, Tin nhắn nhân loại, AI sáng tạo của Google). Xem [models.md](models.md).\n\n**Thông qua tiện ích mở rộng:** Đối với các nhà cung cấp cần triển khai API tùy chỉnh hoặc luồng OAuth, hãy tạo tiện ích mở rộng. Xem [custom-provider.md](custom-provider.md) và [examples/extensions/custom-provider-gitlab-duo](../examples/extensions/custom-provider-gitlab-duo/).\n\n## Lệnh giải quyết\n\nKhi giải quyết thông tin xác thực cho nhà cung cấp:\n\n1. cờ CLI `--api-key`\n2. `auth.json` mục nhập (API key hoặc OAuth mã thông báo)\n3. Biến môi trường\n4. Khóa nhà cung cấp tùy chỉnh từ `models.json`","sourceFile":"providers.md"},"quickstart":{"title":"Bắt đầu nhanh","markdown":"Trang này đưa bạn từ cài đặt đến phiên pi đầu tiên hữu ích.\n\n## Cài đặt\n\nPi được phân phối dưới dạng gói npm:\n\n```bash\nnpm install -g --ignore-scripts @earendil-works/pi-coding-agent\n```\n\n`--ignore-scripts` vô hiệu hóa các tập lệnh vòng đời phụ thuộc trong khi cài đặt. Pi không yêu cầu tập lệnh cài đặt cho các lần cài đặt npm thông thường.\n\n### Gỡ cài đặt\n\nSử dụng trình quản lý gói đã cài đặt pi. Trình cài đặt Curl sử dụng npm trên toàn cầu, do đó, các lượt cài đặt Curl và npm sẽ bị xóa bằng 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\nGỡ cài đặt pi để lại cài đặt, thông tin xác thực, phiên và gói pi đã cài đặt trong `~/.pi/agent/`.\n\nSau đó khởi động pi trong thư mục dự án mà bạn muốn nó hoạt động:\n\n```bash\ncd /path/to/project\npi\n```\n\n## Xác thực\n\nPi có thể sử dụng subscription providers đến `/login` hoặc nhà cung cấp khóa API thông qua các biến môi trường hoặc tệp xác thực.\n\n### Tùy chọn 1: đăng nhập đăng ký\n\nBắt đầu pi và chạy:\n\n```text\n/login\n```\n\nSau đó chọn nhà cung cấp. Thông tin đăng nhập đăng ký tích hợp bao gồm Claude Pro/Max, ChatGPT Plus/Pro (Codex) và GitHub Copilot.\n\n### Tùy chọn 2: API key\n\nĐặt API key trước khi khởi chạy pi:\n\n```bash\nexport ANTHROPIC_API_KEY=sk-ant-...\npi\n```\n\nBạn cũng có thể chạy `/login` và chọn nhà cung cấp khóa API để lưu trữ khóa trong `~/.pi/agent/auth.json`.\n\nXem [Providers](providers.md) để biết tất cả các nhà cung cấp được hỗ trợ, biến môi trường và thiết lập nhà cung cấp đám mây.\n\n## Buổi đầu tiên\n\nKhi pi khởi động, hãy nhập yêu cầu và nhấn Enter:\n\n```text\nSummarize this repository and tell me how to run its checks.\n```\n\nTheo mặc định, pi cung cấp cho mô hình bốn công cụ:\n\n- `read` - đọc tập tin\n- `write` - tạo hoặc ghi đè lên tập tin\n- `edit` - vá các tập tin\n- `bash` - chạy lệnh shell\n\nCác công cụ chỉ đọc tích hợp bổ sung (`grep`, `find`, `ls`) có sẵn thông qua các tùy chọn công cụ. Pi chạy trong thư mục làm việc hiện tại của bạn và có thể sửa đổi các tệp ở đó. Sử dụng git hoặc quy trình kiểm tra điểm kiểm tra khác nếu bạn muốn khôi phục dễ dàng.\n\n## Đưa ra hướng dẫn dự án pi\n\nPi tải context files khi khởi động. Thêm tệp `AGENTS.md` để cho nó biết cách làm việc trong dự án:\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 tải:\n\n- `~/.pi/agent/AGENTS.md` để được hướng dẫn chung\n- `AGENTS.md` hoặc `CLAUDE.md` từ thư mục mẹ và thư mục hiện tại\n\nNếu thư mục chứa `AGENTS.override.md`, Pi sẽ tải thư mục đó thay vì `AGENTS.md` hoặc `CLAUDE.md` từ thư mục đó.\n\nKhởi động lại pi hoặc chạy `/reload`, sau khi thay đổi context files.\n\n## Những điều phổ biến để thử\n\n### Tệp tham khảo\n\nNhập `@` trong trình chỉnh sửa để tìm kiếm tệp mờ hoặc truyền tệp trên dòng lệnh:\n\n```bash\npi @README.md \"Summarize this\"\npi @src/app.ts @src/app.test.ts \"Review these together\"\n```\n\nCó thể dán hình ảnh hoặc văn bản bằng Ctrl+V (Alt+V trên Windows); hình ảnh cũng có thể được kéo vào các thiết bị đầu cuối được hỗ trợ.\n\n### Chạy lệnh shell\n\nỞ chế độ tương tác:\n\n```text\n!npm run lint\n```\n\nĐầu ra lệnh được gửi đến mô hình. Sử dụng `!!command` để chạy lệnh mà không cần thêm đầu ra của nó vào ngữ cảnh mô hình.\n\n### Chuyển đổi mô hình\n\nSử dụng `/model` hoặc Ctrl+L để chọn mô hình. Sử dụng Shift+Tab để xoay vòng mức độ tư duy. Sử dụng Ctrl+P / Shift+Ctrl+P để duyệt qua các mô hình có phạm vi.\n\n### Tiếp tục sau\n\nPhiên được lưu tự động:\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\nBên trong pi, sử dụng `/resume`, `/new`, `/tree`, `/fork` và `/clone` để quản lý phiên.\n\n### Chế độ không tương tác\n\nĐối với lời nhắc một lầ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\nSử dụng `--mode json` cho đầu ra sự kiện JSON hoặc `--mode rpc` để tích hợp quy trình.\n\n## Các bước tiếp theo\n\n- [Using Pi](usage.md) - chế độ tương tác, slash commands, phiên, context files và CLI tham chiếu.\n- [Providers](providers.md) - xác thực và thiết lập mô hình.\n- [Settings](settings.md) - cấu hình toàn cầu và dự án.\n- [Keybindings](keybindings.md) - phím tắt và tùy chỉnh.\n- [Pi Packages](packages.md) - cài đặt các tiện ích mở rộng, kỹ năng, lời nhắc và chủ đề được chia sẻ.\n\nGhi chú nền tảng: [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 Chế độ","markdown":"Chế độ RPC cho phép tác nhân mã hóa hoạt động không đầu thông qua giao thức JSON trên stdin/stdout. Điều này hữu ích cho việc nhúng tác nhân vào các ứng dụng, IDE hoặc giao diện người dùng tùy chỉnh khác.\n\n**Lưu ý dành cho người dùng Node.js/TypeScript**: Nếu bạn đang xây dựng ứng dụng Node.js, hãy cân nhắc sử dụng `AgentSession` trực tiếp từ `@earendil-works/pi-coding-agent` thay vì tạo ra một quy trình con. Xem [`src/core/agent-session.ts`](../src/core/agent-session.ts) để biết API. Đối với ứng dụng khách TypeScript dựa trên quy trình con, hãy xem [`src/modes/rpc/rpc-client.ts`](../src/modes/rpc/rpc-client.ts).\n\n## Đang bắt đầu chế độ RPC\n\n```bash\npi --mode rpc [options]\n```\n\nCác tùy chọn phổ biến:\n- `--provider <name>`: Đặt nhà cung cấp LLM (anthropic, openai, google, v.v.)\n- `--model <pattern>`: Mẫu hoặc ID mẫu (hỗ trợ `provider/id` và tùy chọn `:<thinking>`)\n- `--name <name>` / `-n <name>`: Đặt tên hiển thị phiên khi khởi động\n- `--no-session`: Tắt tính năng duy trì phiên\n- `--session-dir <path>`: Thư mục lưu trữ phiên tùy chỉnh\n\n## Tổng quan về giao thức\n\n- **Lệnh**: JSON đối tượng được gửi đến stdin, mỗi lệnh một dòng\n- **Phản hồi**: JSON đối tượng có `type: \"response\"` biểu thị lệnh thành công/thất bại\n- **Sự kiện**: Sự kiện của đại lý được truyền trực tiếp tới stdout dưới dạng dòng JSON\n\nTất cả các lệnh đều hỗ trợ trường `id` tùy chọn để tương quan yêu cầu/phản hồi. Nếu được cung cấp, phản hồi tương ứng sẽ bao gồm `id` tương tự. Các sự kiện `bash_execution_update` cũng bao gồm `id` của lệnh `bash` ban đầu của chúng.\n\n### đóng khung\n\nChế độ RPC sử dụng ngữ nghĩa JSONL nghiêm ngặt với LF (`\\n`) là dấu phân cách bản ghi duy nhất.\n\nĐiều này quan trọng đối với khách hàng:\n- Chỉ chia bản ghi trên `\\n`\n- Chấp nhận đầu vào `\\r\\n` tùy chọn bằng cách loại bỏ dấu `\\r`\n- Không sử dụng các trình đọc dòng chung coi dấu phân cách Unicode là dòng mới\n\nĐặc biệt, Nút `readline` không tuân thủ giao thức cho chế độ RPC vì nó cũng phân tách trên `U+2028` và `U+2029`, hợp lệ bên trong các chuỗi JSON.\n\n## Lệnh\n\n### Nhắc nhở\n\n#### nhắc nhở\n\nGửi lời nhắc của người dùng đến đại lý. Phản hồi lệnh được phát ra sau khi lời nhắc được chấp nhận, xếp hàng hoặc xử lý. Các sự kiện tiếp tục phát trực tuyến không đồng bộ sau khi được chấp nhận.\n\n```json\n{\"id\": \"req-1\", \"type\": \"prompt\", \"message\": \"Hello, world!\"}\n```\n\nVới hình ảnh:\n```json\n{\"type\": \"prompt\", \"message\": \"What's in this image?\", \"images\": [{\"type\": \"image\", \"data\": \"base64-encoded-data\", \"mimeType\": \"image/png\"}]}\n```\n\n**Trong khi phát trực tuyến**: Nếu tác nhân đã phát trực tuyến, bạn phải chỉ định `streamingBehavior` để xếp hàng tin nhắn:\n\n```json\n{\"type\": \"prompt\", \"message\": \"New instruction\", \"streamingBehavior\": \"steer\"}\n```\n\n- `\"steer\"`: Xếp hàng tin nhắn trong khi tác nhân đang chạy. Nó được phân phối sau khi lượt trợ lý hiện tại hoàn tất việc thực hiện các lệnh gọi công cụ của nó, trước lệnh gọi LLM tiếp theo.\n- `\"followUp\"`: Đợi cho đến khi nhân viên kết thúc. Tin nhắn chỉ được gửi khi tác nhân dừng lại.\n\nNếu tác nhân đang phát trực tuyến và không chỉ định `streamingBehavior` thì lệnh sẽ trả về lỗi.\n\n**Lệnh mở rộng**: Nếu thông báo là lệnh mở rộng (ví dụ: `/mycommand`), thì nó sẽ thực thi ngay lập tức ngay cả khi đang phát trực tuyến. Các lệnh mở rộng quản lý tương tác LLM của riêng chúng thông qua `pi.sendMessage()`.\n\n**Mở rộng đầu vào**: Lệnh kỹ năng (`/skill:name`) và prompt templates (`/template`) được mở rộng trước khi gửi/xếp hàng.\n\nPhản ứng:\n```json\n{\"id\": \"req-1\", \"type\": \"response\", \"command\": \"prompt\", \"success\": true}\n```\n\n`success: true` có nghĩa là lời nhắc đã được chấp nhận, xếp hàng hoặc xử lý ngay lập tức. `success: false` có nghĩa là lời nhắc đã bị từ chối trước khi được chấp nhận. Lỗi sau khi chấp nhận được báo cáo thông qua luồng tin nhắn và sự kiện thông thường, không phải dưới dạng `response` thứ hai cho cùng một id yêu cầu.\n\nTrường `images` là tùy chọn. Mỗi hình ảnh sử dụng định dạng `ImageContent`: `{\"type\": \"image\", \"data\": \"base64-encoded-data\", \"mimeType\": \"image/png\"}`.\n\n#### chỉ đạo\n\nXếp hàng tin nhắn chỉ đạo trong khi tác nhân đang chạy. Nó được phân phối sau khi lượt trợ lý hiện tại hoàn tất việc thực hiện các lệnh gọi công cụ của nó, trước lệnh gọi LLM tiếp theo. Các lệnh kỹ năng và prompt templates được mở rộng. Các lệnh mở rộng không được phép (thay vào đó hãy sử dụng `prompt`).\n\n```json\n{\"type\": \"steer\", \"message\": \"Stop and do this instead\"}\n```\n\nVới hình ảnh:\n```json\n{\"type\": \"steer\", \"message\": \"Look at this instead\", \"images\": [{\"type\": \"image\", \"data\": \"base64-encoded-data\", \"mimeType\": \"image/png\"}]}\n```\n\nTrường `images` là tùy chọn. Mỗi hình ảnh sử dụng định dạng `ImageContent` (giống như `prompt`).\n\nPhản ứng:\n```json\n{\"type\": \"response\", \"command\": \"steer\", \"success\": true}\n```\n\nXem [set_steering_mode](#set_steering_mode) để biết cách xử lý thông báo điều khiển.\n\n#### theo dõi\n\nXếp hàng tin nhắn tiếp theo để xử lý sau khi tác nhân kết thúc. Chỉ được gửi khi đại lý không còn cuộc gọi công cụ hoặc tin nhắn chỉ đạo nào nữa. Các lệnh kỹ năng và prompt templates được mở rộng. Các lệnh mở rộng không được phép (thay vào đó hãy sử dụng `prompt`).\n\n```json\n{\"type\": \"follow_up\", \"message\": \"After you're done, also do this\"}\n```\n\nVới hình ảnh:\n```json\n{\"type\": \"follow_up\", \"message\": \"Also check this image\", \"images\": [{\"type\": \"image\", \"data\": \"base64-encoded-data\", \"mimeType\": \"image/png\"}]}\n```\n\nTrường `images` là tùy chọn. Mỗi hình ảnh sử dụng định dạng `ImageContent` (giống như `prompt`).\n\nPhản ứng:\n```json\n{\"type\": \"response\", \"command\": \"follow_up\", \"success\": true}\n```\n\nXem [set_follow_up_mode](#set_follow_up_mode) để kiểm soát cách xử lý tin nhắn tiếp theo.\n\n#### hủy bỏ\n\nHủy bỏ hoạt động đại lý hiện tại.\n\n```json\n{\"type\": \"abort\"}\n```\n\nPhản ứng:\n```json\n{\"type\": \"response\", \"command\": \"abort\", \"success\": true}\n```\n\n#### phiên_mới\n\nBắt đầu một phiên mới. Có thể bị hủy bởi trình xử lý sự kiện mở rộng `session_before_switch`.\n\n```json\n{\"type\": \"new_session\"}\n```\n\nVới tính năng theo dõi phiên gốc tùy chọn:\n```json\n{\"type\": \"new_session\", \"parentSession\": \"/path/to/parent-session.jsonl\"}\n```\n\nPhản ứng:\n```json\n{\"type\": \"response\", \"command\": \"new_session\", \"success\": true, \"data\": {\"cancelled\": false}}\n```\n\nNếu tiện ích mở rộng bị hủy:\n```json\n{\"type\": \"response\", \"command\": \"new_session\", \"success\": true, \"data\": {\"cancelled\": true}}\n```\n\n### Tình trạng\n\n#### get_state\n\nNhận trạng thái phiên hiện tại.\n\n```json\n{\"type\": \"get_state\"}\n```\n\nPhản ứng:\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\nTrường `model` là đối tượng [Model](#model) đầy đủ hoặc `null`. Trường `sessionName` là tên hiển thị được đặt qua `set_session_name` hoặc bị bỏ qua nếu không được đặt.\n\n#### get_messages\n\nNhận tất cả tin nhắn trong cuộc trò chuyện.\n\n```json\n{\"type\": \"get_messages\"}\n```\n\nPhản ứng:\n```json\n{\n  \"type\": \"response\",\n  \"command\": \"get_messages\",\n  \"success\": true,\n  \"data\": {\"messages\": [...]}\n}\n```\n\nTin nhắn là đối tượng `AgentMessage` (xem [Message Types](#message-types)).\n\n### Người mẫu\n\n#### set_model\n\nChuyển sang một mô hình cụ thể.\n\n```json\n{\"type\": \"set_model\", \"provider\": \"anthropic\", \"modelId\": \"claude-sonnet-4-20250514\"}\n```\n\nPhản hồi chứa đối tượng [Model](#model) đầy đủ:\n```json\n{\n  \"type\": \"response\",\n  \"command\": \"set_model\",\n  \"success\": true,\n  \"data\": {...}\n}\n```\n\n#### mô hình chu kỳ\n\nChuyển sang mô hình có sẵn tiếp theo. Trả về dữ liệu `null` nếu chỉ có một mô hình.\n\n```json\n{\"type\": \"cycle_model\"}\n```\n\nPhản ứng:\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\nTrường `model` là một đối tượng [Model](#model) đầy đủ.\n\n#### get_available_models\n\nLiệt kê tất cả các mô hình được cấu hình.\n\n```json\n{\"type\": \"get_available_models\"}\n```\n\nPhản hồi chứa một mảng các đối tượng [Model](#model) đầy đủ:\n```json\n{\n  \"type\": \"response\",\n  \"command\": \"get_available_models\",\n  \"success\": true,\n  \"data\": {\n    \"models\": [...]\n  }\n}\n```\n\n### suy nghĩ\n\n#### set_thinking_level\n\nĐặt mức độ lý luận/tư duy cho các mô hình hỗ trợ nó.\n\n```json\n{\"type\": \"set_thinking_level\", \"level\": \"high\"}\n```\n\nCấp độ: `\"off\"`, `\"minimal\"`, `\"low\"`, `\"medium\"`, `\"high\"`, `\"xhigh\"`, `\"max\"`\n\n`\"xhigh\"` và `\"max\"` chỉ hiển thị khi được mô hình đã chọn hỗ trợ. Một số kiểu máy, bao gồm GPT-5.6, có cả hai.\n\nPhản ứng:\n```json\n{\"type\": \"response\", \"command\": \"set_thinking_level\", \"success\": true}\n```\n\n#### chu kỳ_thinking_level\n\nXoay vòng qua các cấp độ tư duy sẵn có. Trả về dữ liệu `null` nếu mô hình không hỗ trợ suy nghĩ.\n\n```json\n{\"type\": \"cycle_thinking_level\"}\n```\n\nPhản ứng:\n```json\n{\n  \"type\": \"response\",\n  \"command\": \"cycle_thinking_level\",\n  \"success\": true,\n  \"data\": {\"level\": \"high\"}\n}\n```\n\n#### get_available_thinking_levels\n\nLiệt kê các cấp độ tư duy được hỗ trợ bởi mô hình hiện tại. Trả về `[\"off\"]` cho một mô hình không có hỗ trợ lý luận.\n\n```json\n{\"type\": \"get_available_thinking_levels\"}\n```\n\nPhản ứng:\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### Chế độ xếp hàng\n\n#### set_steering_mode\n\nKiểm soát cách gửi thông báo chỉ đạo (từ `steer`).\n\n```json\n{\"type\": \"set_steering_mode\", \"mode\": \"one-at-a-time\"}\n```\n\nChế độ:\n- `\"all\"`: Gửi tất cả thông báo chỉ đạo sau khi lượt trợ lý hiện tại thực hiện xong lệnh gọi công cụ của mình\n- `\"one-at-a-time\"`: Gửi một thông báo chỉ đạo cho mỗi lượt trợ lý đã hoàn thành (mặc định)\n\nPhản ứng:\n```json\n{\"type\": \"response\", \"command\": \"set_steering_mode\", \"success\": true}\n```\n\n#### set_follow_up_mode\n\nKiểm soát cách gửi tin nhắn tiếp theo (từ `follow_up`).\n\n```json\n{\"type\": \"set_follow_up_mode\", \"mode\": \"one-at-a-time\"}\n```\n\nChế độ:\n- `\"all\"`: Gửi tất cả các tin nhắn tiếp theo khi nhân viên hỗ trợ kết thúc\n- `\"one-at-a-time\"`: Gửi một tin nhắn tiếp theo cho mỗi lần hoàn thành của tổng đài viên (mặc định)\n\nPhản ứng:\n```json\n{\"type\": \"response\", \"command\": \"set_follow_up_mode\", \"success\": true}\n```\n\n### nén chặt\n\n#### nhỏ gọn\n\nNgữ cảnh hội thoại được thu gọn theo cách thủ công để giảm mức sử dụng mã thông báo.\n\n```json\n{\"type\": \"compact\"}\n```\n\nVới hướng dẫn tùy chỉnh:\n```json\n{\"type\": \"compact\", \"customInstructions\": \"Focus on code changes\"}\n```\n\nPhản ứng:\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` là ước tính theo kinh nghiệm đối với bối cảnh thông báo được xây dựng lại ngay sau khi nén, không phải là số lượng mã thông báo chính xác của nhà cung cấp. `usage` báo cáo cuộc gọi LLM hoặc các cuộc gọi đã tạo ra bản tóm tắt và có thể bị trình xử lý nén tùy chỉnh bỏ qua.\n\n#### set_auto_compaction\n\nBật hoặc tắt tính năng nén tự động khi ngữ cảnh gần đầy.\n\n```json\n{\"type\": \"set_auto_compaction\", \"enabled\": true}\n```\n\nPhản ứng:\n```json\n{\"type\": \"response\", \"command\": \"set_auto_compaction\", \"success\": true}\n```\n\n### Thử lại\n\n#### set_auto_retry\n\nBật hoặc tắt tính năng tự động thử lại đối với các lỗi nhất thời (quá tải, giới hạn tốc độ, 5xx).\n\n```json\n{\"type\": \"set_auto_retry\", \"enabled\": true}\n```\n\nPhản ứng:\n```json\n{\"type\": \"response\", \"command\": \"set_auto_retry\", \"success\": true}\n```\n\n#### hủy bỏ_retry\n\nHủy bỏ quá trình thử lại đang diễn ra (hủy bỏ thời gian trì hoãn và dừng thử lại).\n\n```json\n{\"type\": \"abort_retry\"}\n```\n\nPhản ứng:\n```json\n{\"type\": \"response\", \"command\": \"abort_retry\", \"success\": true}\n```\n\n### đánh\n\n#### bash\n\nThực thi lệnh shell và thêm đầu ra vào ngữ cảnh hội thoại. Luồng đầu ra dưới dạng sự kiện `bash_execution_update` trong khi lệnh chạy; phản hồi chứa kết quả cuối cùng.\n\n```json\n{\"id\": \"req-1\", \"type\": \"bash\", \"command\": \"ls -la\"}\n```\n\nBao gồm `id` để liên kết các sự kiện `bash_execution_update` được truyền trực tuyến với lệnh này.\n\nPhản ứng:\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\nNếu đầu ra bị cắt bớt, bao gồm `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** Làm thế nào bash kết quả đạt được LLM:**\n\nLệnh `bash` thực thi ngay lập tức và trả về `BashResult`. Trong nội bộ, `BashExecutionMessage` được tạo và lưu trữ ở trạng thái tin nhắn của tác nhân.\n\nKhi lệnh `prompt` tiếp theo được gửi, tất cả tin nhắn (bao gồm `BashExecutionMessage`) sẽ được chuyển đổi trước khi gửi đến LLM. `BashExecutionMessage` được chuyển đổi thành `UserMessage` với định dạng sau:\n\n````\nRan `ls -la`\n```\ntổng cộng 48\ndrwxr-xr-x...\n```\n````\n\nĐiều này có nghĩa là:\n1. Đầu ra Bash được đưa vào ngữ cảnh LLM trên **dấu nhắc tiếp theo**, không phải ngay lập tức\n2. Nhiều lệnh bash có thể được thực thi trước lời nhắc; tất cả đầu ra sẽ được bao gồm\n\n#### hủy bỏ_bash\n\nHủy bỏ lệnh bash đang chạy.\n\n```json\n{\"type\": \"abort_bash\"}\n```\n\nPhản ứng:\n```json\n{\"type\": \"response\", \"command\": \"abort_bash\", \"success\": true}\n```\n\n### Phiên họp\n\n#### get_session_stats\n\nNhận mức sử dụng mã thông báo, thống kê chi phí và mức sử dụng cửa sổ ngữ cảnh hiện tại.\n\n```json\n{\"type\": \"get_session_stats\"}\n```\n\nPhản ứng:\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` và `cost` bao gồm các thông báo hỗ trợ, mức sử dụng được các công cụ báo cáo và tạo bản tóm tắt/tóm tắt nhánh trong toàn bộ phiên. `contextUsage` chứa ước tính cửa sổ ngữ cảnh hiện tại thực tế được sử dụng để nén và hiển thị chân trang.\n\n`contextUsage` bị bỏ qua khi không có sẵn mô hình hoặc cửa sổ ngữ cảnh. `contextUsage.tokens` và `contextUsage.percent` là `null` ngay sau khi nén cho đến khi có phản hồi trợ lý mới sau khi nén cung cấp dữ liệu sử dụng hợp lệ.\n\n#### xuất_html\n\nXuất phiên sang tệp HTML.\n\n```json\n{\"type\": \"export_html\"}\n```\n\nVới đường dẫn tùy chỉnh:\n```json\n{\"type\": \"export_html\", \"outputPath\": \"/tmp/session.html\"}\n```\n\nPhản ứng:\n```json\n{\n  \"type\": \"response\",\n  \"command\": \"export_html\",\n  \"success\": true,\n  \"data\": {\"path\": \"/tmp/session.html\"}\n}\n```\n\n#### switch_session\n\nTải một tập tin phiên khác. Có thể bị hủy bởi trình xử lý sự kiện mở rộng `session_before_switch`.\n\n```json\n{\"type\": \"switch_session\", \"sessionPath\": \"/path/to/session.jsonl\"}\n```\n\nPhản ứng:\n```json\n{\"type\": \"response\", \"command\": \"switch_session\", \"success\": true, \"data\": {\"cancelled\": false}}\n```\n\nNếu tiện ích mở rộng đã hủy quá trình chuyển đổi:\n```json\n{\"type\": \"response\", \"command\": \"switch_session\", \"success\": true, \"data\": {\"cancelled\": true}}\n```\n\n#### cái nĩa\n\nTạo một nhánh mới từ tin nhắn của người dùng trước đó trên nhánh đang hoạt động. Có thể bị hủy bởi trình xử lý sự kiện mở rộng `session_before_fork`. Trả về nội dung của tin nhắn được phân nhánh từ đó.\n\n```json\n{\"type\": \"fork\", \"entryId\": \"abc123\"}\n```\n\nPhản ứng:\n```json\n{\n  \"type\": \"response\",\n  \"command\": \"fork\",\n  \"success\": true,\n  \"data\": {\"text\": \"The original prompt text...\", \"cancelled\": false}\n}\n```\n\nNếu tiện ích mở rộng đã hủy phân nhánh:\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#### dòng vô tính\n\nSao chép nhánh hoạt động hiện tại vào một phiên mới ở vị trí hiện tại. Có thể bị hủy bởi trình xử lý sự kiện mở rộng `session_before_fork`.\n\n```json\n{\"type\": \"clone\"}\n```\n\nPhản ứng:\n```json\n{\n  \"type\": \"response\",\n  \"command\": \"clone\",\n  \"success\": true,\n  \"data\": {\"cancelled\": false}\n}\n```\n\nNếu tiện ích mở rộng đã hủy bản sao:\n```json\n{\n  \"type\": \"response\",\n  \"command\": \"clone\",\n  \"success\": true,\n  \"data\": {\"cancelled\": true}\n}\n```\n\n#### get_fork_messages\n\nNhận tin nhắn của người dùng có sẵn để phân nhánh.\n\n```json\n{\"type\": \"get_fork_messages\"}\n```\n\nPhản ứng:\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#### get_entries\n\nNhận tất cả các mục phiên theo thứ tự nối thêm (không bao gồm tiêu đề phiên). Phiên là một cây mục chỉ bổ sung có id ổn định, do đó, id mục nhập hoạt động như một con trỏ bền: chuyển id mục nhập cuối cùng mà bạn đã thấy là `since` để chỉ nhận các mục nhập nghiêm ngặt sau nó, ngay cả khi máy khách khởi động lại. Không giống như `get_messages`, phần này bao gồm lịch sử nén trước và các nhánh bị bỏ rơi.\n\n```json\n{\"type\": \"get_entries\"}\n```\n\nVới một con trỏ:\n```json\n{\"type\": \"get_entries\", \"since\": \"abc123\"}\n```\n\nPhản ứng:\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` là id của mục nhập lá hiện tại (`null` cho phiên trống), do đó, khách hàng có thể biết trong một lần xem liệu nhánh đang hoạt động có di chuyển hay không. Nếu `since` không khớp với bất kỳ id mục nhập nào thì phản hồi là `success: false`.\n\n#### lấy_cây\n\nNhận phiên làm việc dưới dạng cây mục. Mỗi nút là `{entry, children, label?, labelTimestamp?}`. Một phiên được định dạng đúng có một gốc duy nhất; các mục mồ côi (chuỗi gốc bị hỏng) cũng xuất hiện dưới dạng gốc.\n\n```json\n{\"type\": \"get_tree\"}\n```\n\nPhản ứng:\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#### get_last_assistant_text\n\nLấy nội dung văn bản của tin nhắn trợ lý cuối cùng.\n\n```json\n{\"type\": \"get_last_assistant_text\"}\n```\n\nPhản ứng:\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\nTrả về `{\"text\": null}` nếu không có tin nhắn trợ lý nào tồn tại.\n\n#### set_session_name\n\nĐặt tên hiển thị cho phiên hiện tại. Tên xuất hiện trong danh sách phiên và giúp xác định phiên.\n\n```json\n{\"type\": \"set_session_name\", \"name\": \"my-feature-work\"}\n```\n\nPhản ứng:\n```json\n{\n  \"type\": \"response\",\n  \"command\": \"set_session_name\",\n  \"success\": true\n}\n```\n\nTên phiên hiện tại có sẵn thông qua `get_state` trong trường `sessionName`. Để đặt tên ban đầu khi bắt đầu chế độ RPC, hãy chuyển `--name <name>` hoặc `-n <name>` cho quy trình `pi --mode rpc`.\n\n### Lệnh\n\n#### get_commands\n\nNhận các lệnh có sẵn (lệnh mở rộng, prompt templates và kỹ năng). Chúng có thể được gọi thông qua lệnh `prompt` bằng cách thêm tiền tố `/`.\n\n```json\n{\"type\": \"get_commands\"}\n```\n\nPhản ứng:\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\nMỗi lệnh có:\n- `name`: Tên lệnh (gọi bằng `/name`)\n- `description`: Mô tả mà con người có thể đọc được (tùy chọn cho các lệnh mở rộng)\n- `source`: Loại lệnh gì:\n  - `\"extension\"`: Đã đăng ký qua `pi.registerCommand()` trong tiện ích mở rộng\n  - `\"prompt\"`: Được tải từ tệp mẫu lời nhắc `.md`\n  - `\"skill\"`: Được tải từ thư mục kỹ năng (tên có tiền tố là `skill:`)\n- `location`: Nó được tải từ đâu (tùy chọn, không có cho tiện ích mở rộng):\n  - `\"user\"`: Cấp độ người dùng (`~/.pi/agent/`)\n  - `\"project\"`: Cấp dự án (`./.pi/agent/`)\n  - `\"path\"`: Đường dẫn rõ ràng qua CLI hoặc cài đặt\n- `path`: Đường dẫn tệp tuyệt đối đến nguồn lệnh (tùy chọn)\n\n**Lưu ý**: Không bao gồm các lệnh TUI tích hợp (`/settings`, `/hotkeys`, v.v.). Chúng chỉ được xử lý ở chế độ tương tác và sẽ không thực thi nếu được gửi qua `prompt`.\n\n## Sự kiện\n\nCác sự kiện được truyền trực tuyến đến các dòng stdout dưới dạng JSON trong quá trình vận hành tổng đài viên. Các sự kiện thường không bao gồm trường `id`; `bash_execution_update` bao gồm `id` của lệnh `bash` ban đầu khi lệnh này được cung cấp.\n\n### Các loại sự kiện\n\n| Sự kiện | Sự miêu tả |\n|-------|-------------|\n| `agent_start` | Đại lý bắt đầu xử lý |\n| `agent_end` | Một lần chạy tác nhân cấp thấp hoàn tất (vẫn có thể được theo sau bởi việc thử lại, nén hoặc tiếp tục xếp hàng đợi) |\n| `agent_settled` | Hoạt động đại lý đã được giải quyết đầy đủ; không còn tự động thử lại, thử lại nén hoặc tiếp tục xếp hàng |\n| `turn_start` | Lượt mới bắt đầu |\n| `turn_end` | Lần lượt hoàn tất (bao gồm thông báo trợ lý và kết quả công cụ) |\n| `message_start` | Tin nhắn bắt đầu |\n| `message_update` | Cập nhật trực tuyến (văn bản/suy nghĩ/đồng bằng cuộc gọi công cụ) |\n| `message_end` | Tin nhắn hoàn tất |\n| `bash_execution_update` | Đoạn đầu ra lệnh trực tiếp RPC bash |\n| `tool_execution_start` | Công cụ bắt đầu thực hiện |\n| `tool_execution_update` | Tiến trình thực hiện công cụ (đầu ra phát trực tuyến) |\n| `tool_execution_end` | Công cụ hoàn thành |\n| `queue_update` | Đã thay đổi hàng đợi chỉ đạo/theo dõi đang chờ xử lý |\n| `compaction_start` | Quá trình nén bắt đầu |\n| `compaction_end` | Quá trình nén hoàn tất |\n| `auto_retry_start` | Tự động thử lại bắt đầu (sau lỗi thoáng qua) |\n| `auto_retry_end` | Tự động thử lại hoàn tất (thành công hoặc thất bại cuối cùng) |\n| `summarization_retry_scheduled` | Đã lên lịch thử lại cho lỗi nén tạm thời hoặc lỗi tóm tắt tóm tắt nhánh |\n| `summarization_retry_attempt_start` | Đã bắt đầu yêu cầu tóm tắt lại |\n| `summarization_retry_finished` | Vòng lặp thử lại tóm tắt hoàn tất |\n| `extension_error` | Tiện ích mở rộng đã gây ra lỗi |\n\n### đại lý_bắt đầu\n\nĐược phát ra khi tác nhân bắt đầu xử lý lời nhắc.\n\n```json\n{\"type\": \"agent_start\"}\n```\n\n### đại lý_end\n\nĐược phát ra khi một lần chạy tác nhân cấp thấp hoàn tất. Chứa tất cả các tin nhắn được tạo trong quá trình chạy này. Nếu `willRetry` đúng, quá trình thử lại tự động sẽ diễn ra.\n\n```json\n{\n  \"type\": \"agent_end\",\n  \"messages\": [...],\n  \"willRetry\": false\n}\n```\n\n### đại lý_đã giải quyết\n\nĐược phát ra sau khi quá trình chạy ở cấp phiên hoàn tất được giải quyết. Tại thời điểm này, Pi sẽ không tự động tiếp tục thông qua thử lại, thử lại nén hoặc các tin nhắn theo dõi được xếp hàng đợi.\n\n```json\n{\"type\": \"agent_settled\"}\n```\n\n### lượt_bắt đầu / lượt_kết thúc\n\nMột lượt bao gồm một phản hồi trợ lý cộng với bất kỳ cuộc gọi và kết quả công cụ nào được tạo ra.\n\n```json\n{\"type\": \"turn_start\"}\n```\n\n```json\n{\n  \"type\": \"turn_end\",\n  \"message\": {...},\n  \"toolResults\": [...]\n}\n```\n\n### tin nhắn_bắt đầu / tin nhắn_end\n\nĐược phát ra khi một tin nhắn bắt đầu và kết thúc. Trường `message` chứa `AgentMessage`.\n\n```json\n{\"type\": \"message_start\", \"message\": {...}}\n{\"type\": \"message_end\", \"message\": {...}}\n```\n\n### message_update (Truyền phát)\n\nĐược phát ra trong quá trình truyền phát tin nhắn trợ lý. Chứa sự kiện delta không có ảnh chụp nhanh thông báo tích lũy.\n\n```json\n{\n  \"type\": \"message_update\",\n  \"assistantMessageEvent\": {\n    \"type\": \"text_delta\",\n    \"contentIndex\": 0,\n    \"delta\": \"Hello \"\n  }\n}\n```\n\nTrường `assistantMessageEvent` chứa một trong các loại delta sau:\n\n| Kiểu | Sự miêu tả |\n|------|-------------|\n| `text_start` | Khối nội dung văn bản đã bắt đầu |\n| `text_delta` | Đoạn nội dung văn bản |\n| `text_end` | Khối nội dung văn bản đã kết thúc |\n| `thinking_start` | Khối suy nghĩ bắt đầu |\n| `thinking_delta` | Đoạn nội dung suy nghĩ |\n| `thinking_end` | Khối suy nghĩ đã kết thúc |\n| `toolcall_start` | Cuộc gọi công cụ đã bắt đầu |\n| `toolcall_delta` | Đoạn đối số cuộc gọi công cụ |\n| `toolcall_end` | Cuộc gọi công cụ đã kết thúc (bao gồm đối tượng `toolCall` đầy đủ) |\n\nVí dụ truyền phát phản hồi văn bả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` cố tình bỏ qua trường `message` tích lũy trước đây và\n`assistantMessageEvent.partial`. Khách hàng cần tin nhắn trực tiếp một phần phải tập hợp nó\ntừ `message_start` và các sự kiện tiếp theo sử dụng `contentIndex`. Điều trị `message_end.message`\nnhư có thẩm quyền. Đối với các lệnh gọi công cụ, bộ đệm `toolcall_delta.delta`; `toolcall_end.toolCall`\nchứa cuộc gọi đã hoàn thành.\n\n### bash_thực thi_cập nhật\n\nĐược phát ra một lần cho mỗi đoạn đầu ra từ lệnh `bash` trực tiếp. `id` khớp với `id` của lệnh, cho phép khách hàng liên kết đầu ra với lệnh chính xác.\n\nCác sự kiện truyền phát tất cả đầu ra trong khi lệnh chạy, ngay cả khi phản hồi `bash` cuối cùng của `output` bị cắt bớt.\n\n```json\n{\n  \"type\": \"bash_execution_update\",\n  \"id\": \"req-1\",\n  \"delta\": \"total 48\\n\"\n}\n```\n\n### tool_execution_start / tool_execution_update / tool_execution_end\n\nĐược phát ra khi một công cụ bắt đầu, truyền tiến trình và hoàn tất quá trình thực thi.\n\n```json\n{\n  \"type\": \"tool_execution_start\",\n  \"toolCallId\": \"call_abc123\",\n  \"toolName\": \"bash\",\n  \"args\": {\"command\": \"ls -la\"}\n}\n```\n\nTrong quá trình thực thi, các sự kiện `tool_execution_update` truyền một phần kết quả (ví dụ: đầu ra bash khi nó đến):\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\nKhi hoàn thành:\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\nSử dụng `toolCallId` để liên hệ các sự kiện. `partialResult` trong `tool_execution_update` chứa đầu ra tích lũy cho đến nay (không chỉ delta), cho phép khách hàng chỉ cần thay thế màn hình của họ trên mỗi bản cập nhật.\n\n### hàng_cập nhật\n\nĐược phát ra bất cứ khi nào chỉ đạo đang chờ xử lý hoặc hàng đợi theo dõi thay đổi.\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_start / nén_end\n\nPhát ra khi quá trình nén diễn ra, dù là thủ công hay tự động.\n\n```json\n{\"type\": \"compaction_start\", \"reason\": \"threshold\"}\n```\n\nTrường `reason` là `\"manual\"`, `\"threshold\"` hoặc `\"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\nNếu `reason` là `\"overflow\"` và quá trình nén thành công, `willRetry` là `true` và tác nhân sẽ tự động thử lại lời nhắc.\n\nNếu quá trình nén bị hủy bỏ, `result` là `null` và `aborted` là `true`.\n\nNếu quá trình nén không thành công (ví dụ: vượt quá hạn ngạch API), `result` là `null`, `aborted` là `false` và `errorMessage` chứa mô tả lỗi.\n\n### auto_retry_start / auto_retry_end\n\nĐược phát ra khi kích hoạt tự động thử lại sau một lỗi nhất thời (quá tải, giới hạn tốc độ, 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\nTrong lần thất bại cuối cùng (vượt quá số lần thử lại tối đa):\n```json\n{\n  \"type\": \"auto_retry_end\",\n  \"success\": false,\n  \"attempt\": 3,\n  \"finalError\": \"529 overloaded_error: Overloaded\"\n}\n```\n\n### tóm tắt_retry_scheduled / tóm tắt_retry_attempt_start / tóm tắt_retry_kết thúc\n\nĐược phát ra khi thử lại quá trình nén hoặc tóm tắt tóm tắt nhánh sau lỗi của nhà cung cấp tạm thời. Những sự kiện này sử dụng cài đặt thử lại giống như các lần thử lại theo lượt trợ lý tự động.\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Đối với tóm tắt nhánh, `source` là `\"branchSummary\"` và không có `reason`.\n\n```json\n{\n  \"type\": \"summarization_retry_finished\"\n}\n```\n\n### phần mở rộng_error\n\nĐược phát ra khi tiện ích mở rộng gặp lỗi.\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## Giao thức UI mở rộng\n\nExtensions có ​​thể yêu cầu tương tác của người dùng thông qua `ctx.ui.select()`, `ctx.ui.confirm()`, v.v. Ở chế độ RPC, những điều này được dịch thành giao thức phụ yêu cầu/phản hồi ở đầu luồng lệnh/sự kiện cơ sở.\n\nCó hai loại phương thức giao diện người dùng mở rộng:\n\n- **Phương thức hộp thoại** (`select`, `confirm`, `input`, `editor`): phát ra `extension_ui_request` trên stdout và chặn cho đến khi khách hàng gửi lại `extension_ui_response` trên stdin với `id` phù hợp.\n- **Phương pháp bắn và quên** (`notify`, `setStatus`, `setWidget`, `setTitle`, `set_editor_text`): phát ra `extension_ui_request` trên stdout nhưng không mong đợi phản hồi. Khách hàng có thể hiển thị thông tin hoặc bỏ qua nó.\n\nNếu phương thức hộp thoại bao gồm trường `timeout` thì phía tác nhân sẽ tự động giải quyết bằng giá trị mặc định khi hết thời gian chờ. Khách hàng không cần phải theo dõi thời gian chờ.\n\nMột số phương thức `ExtensionUIContext` không được hỗ trợ hoặc xuống cấp ở chế độ RPC vì chúng yêu cầu quyền truy cập trực tiếp TUI:\n- `custom()` trả về `undefined`\n- `setWorkingMessage()`, `setWorkingIndicator()`, `setFooter()`, `setHeader()`, `setEditorComponent()`, `setToolsExpanded()` là không hoạt động\n- `getEditorText()` trả về `\"\"`\n- `getToolsExpanded()` trả về `false`\n- `pasteToEditor()` ủy quyền cho `setEditorText()` (không xử lý dán/thu gọn)\n- `getAllThemes()` trả về `[]`\n- `getTheme()` trả về `undefined`\n- `setTheme()` trả về `{ success: false, error: \"...\" }`\n\nLưu ý: `ctx.mode` là `\"rpc\"` và `ctx.hasUI` là `true` ở chế độ RPC vì hộp thoại và các phương thức bắn và quên hoạt động thông qua giao thức phụ UI mở rộng. Sử dụng `ctx.mode === \"tui\"` để bảo vệ các tính năng dành riêng cho TUI như `custom()` yêu cầu thiết bị đầu cuối thực sự.\n\n### Yêu cầu giao diện người dùng mở rộng (stdout)\n\nTất cả các yêu cầu đều có `type: \"extension_ui_request\"`, một trường `id` duy nhất và một trường `method`.\n\n#### lựa chọn\n\nNhắc người dùng chọn từ danh sách. Các phương thức hộp thoại có trường `timeout` bao gồm thời gian chờ tính bằng mili giây; đại lý tự động giải quyết bằng `undefined` nếu khách hàng không phản hồi kịp thời.\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\nPhản hồi dự kiến: `extension_ui_response` với `value` (chuỗi tùy chọn đã chọn) hoặc `cancelled: true`.\n\n#### xác nhận\n\nNhắc người dùng xác nhận có/không.\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\nPhản hồi dự kiến: `extension_ui_response` với `confirmed: true/false` hoặc `cancelled: true`.\n\n#### đầu vào\n\nNhắc người dùng về văn bản dạng tự do.\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\nPhản hồi dự kiến: `extension_ui_response` với `value` (văn bản đã nhập) hoặc `cancelled: true`.\n\n#### biên tập viên\n\nMở trình soạn thảo văn bản nhiều dòng với nội dung được điền sẵn tùy chọ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\nPhản hồi dự kiến: `extension_ui_response` với `value` (văn bản đã chỉnh sửa) hoặc `cancelled: true`.\n\n#### thông báo\n\nHiển thị một thông báo. Bắn và quên, không có phản hồi mong đợi.\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\nTrường `notifyType` là `\"info\"`, `\"warning\"` hoặc `\"error\"`. Mặc định là `\"info\"` nếu bị bỏ qua.\n\n#### setStatus\n\nĐặt hoặc xóa mục nhập trạng thái trong thanh chân trang/trạng thái. Bắn và quên đi.\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\nGửi `statusText: undefined` (hoặc bỏ qua) để xóa mục nhập trạng thái cho khóa đó.\n\n#### setWidget\n\nĐặt hoặc xóa một tiện ích (khối dòng văn bản) hiển thị phía trên hoặc bên dưới trình chỉnh sửa. Bắn và quên đi.\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\nGửi `widgetLines: undefined` (hoặc bỏ qua) để xóa tiện ích. Trường `widgetPlacement` là `\"aboveEditor\"` (mặc định) hoặc `\"belowEditor\"`. Chỉ mảng chuỗi được hỗ trợ ở chế độ RPC; các nhà máy sản xuất linh kiện được bỏ qua.\n\n#### setTitle\n\nĐặt tiêu đề cửa sổ/tab đầu cuối. Bắn và quên đi.\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#### set_editor_text\n\nĐặt văn bản trong trình soạn thảo đầu vào. Bắn và quên đi.\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### Phản hồi giao diện người dùng tiện ích mở rộng (stdin)\n\nPhản hồi chỉ được gửi cho các phương thức hộp thoại (`select`, `confirm`, `input`, `editor`). `id` phải phù hợp với yêu cầu.\n\n#### Phản hồi giá trị (chọn, nhập, chỉnh sửa)\n\n```json\n{\"type\": \"extension_ui_response\", \"id\": \"uuid-1\", \"value\": \"Allow\"}\n```\n\n#### Phản hồi xác nhận (xác nhận)\n\n```json\n{\"type\": \"extension_ui_response\", \"id\": \"uuid-2\", \"confirmed\": true}\n```\n\n#### Phản hồi hủy (bất kỳ hộp thoại nào)\n\nLoại bỏ bất kỳ phương pháp hộp thoại nào. Tiện ích mở rộng nhận được `undefined` (để chọn/nhập/chỉnh sửa) hoặc `false` (để xác nhận).\n\n```json\n{\"type\": \"extension_ui_response\", \"id\": \"uuid-3\", \"cancelled\": true}\n```\n\n## Xử lý lỗi\n\nCác lệnh không thành công sẽ trả về phản hồi với `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\nLỗi phân tích cú pháp:\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## Các loại\n\nCác tập tin nguồ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 loại lệnh/phản hồi, loại yêu cầu/phản hồi giao diện người dùng mở rộng\n\n### Người mẫu\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### Tin nhắn người dùng\n\n```json\n{\n  \"role\": \"user\",\n  \"content\": \"Hello!\",\n  \"timestamp\": 1733234567890,\n  \"attachments\": []\n}\n```\n\nTrường `content` có thể là một chuỗi hoặc một mảng gồm các khối `TextContent`/`ImageContent`.\n\n### Trợ lýTin nhắ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\nLý do dừng: `\"stop\"`, `\"length\"`, `\"toolUse\"`, `\"error\"`, `\"aborted\"`\n\n### Công cụKết quảThông báo\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` là tùy chọn và báo cáo công việc LLM lồng nhau được thực hiện bởi công cụ. Khi xuất hiện, nó đóng góp vào mã thông báo phiên và tổng chi phí.\n\n### BashThực thiTin nhắn\n\nĐược tạo bởi lệnh `bash` RPC (không phải bằng lệnh gọi công cụ 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### Tệp đính kèm\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## Ví dụ: Máy khách cơ bả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## Ví dụ: Ứng dụng khách tương tác (Node.js)\n\nXem [`test/rpc-example.ts`](../test/rpc-example.ts) để biết ví dụ tương tác hoàn chỉnh hoặc [`src/modes/rpc/rpc-client.ts`](../src/modes/rpc/rpc-client.ts) để biết cách triển khai ứng dụng khách đã nhập.\n\nĐể biết ví dụ đầy đủ về cách xử lý giao thức UI tiện ích mở rộng, hãy xem [`examples/rpc-extension-ui.ts`](../examples/rpc-extension-ui.ts) kết hợp với tiện ích mở rộng [`examples/extensions/rpc-demo.ts`](../examples/extensions/rpc-demo.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 có thể giúp bạn sử dụng SDK. Yêu cầu nó xây dựng một tích hợp cho trường hợp sử dụng của bạn.\n\n\nSDK cung cấp quyền truy cập theo chương trình vào các khả năng của tác nhân pi. Sử dụng nó để nhúng pi vào các ứng dụng khác, xây dựng giao diện tùy chỉnh hoặc tích hợp với quy trình làm việc tự động.\n\n**Các trường hợp sử dụng ví dụ:**\n- Xây dựng giao diện người dùng tùy chỉnh (web, máy tính để bàn, thiết bị di động)\n- Tích hợp khả năng của đại lý vào các ứng dụng hiện có\n- Tạo quy trình tự động với lý luận của tác nhân\n- Xây dựng các công cụ tùy chỉnh sinh ra các đại lý phụ\n- Kiểm tra hành vi của tác nhân theo chương trình\n\nXem [examples/sdk/](../examples/sdk/) để biết các ví dụ hoạt động từ kiểm soát tối thiểu đến kiểm soát hoàn toàn.\n\n## Bắt đầu nhanh\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## Cài đặt\n\n```bash\nnpm install @earendil-works/pi-coding-agent\n```\n\nSDK được bao gồm trong gói chính. Không cần cài đặt riêng biệt.\n\n## Khái niệm cốt lõi\n\n### createAgentSession()\n\nChức năng chính của nhà máy cho một `AgentSession`.\n\n`createAgentSession()` sử dụng `ResourceLoader` để cung cấp các tiện ích mở rộng, kỹ năng, prompt templates, chủ đề và context files. Nếu bạn không cung cấp, nó sẽ sử dụng `DefaultResourceLoader` với tính năng khám phá tiêu chuẩn.\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### Phiên đại lý\n\nPhiên này quản lý vòng đời của tác nhân, lịch sử tin nhắn, trạng thái mô hình, quá trình nén và truyền phát sự kiệ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\nThay thế phiên API chẳng hạn như phiên mới, tiếp tục, phân nhánh và nhập trực tiếp trên `AgentSessionRuntime` chứ không phải trên `AgentSession`.\n\n### createAgentSessionRuntime() và AgentSessionRuntime\n\nSử dụng thời gian chạy API khi bạn cần thay thế phiên hoạt động và xây dựng lại trạng thái thời gian chạy giới hạn cwd.\nĐây là cùng một lớp được sử dụng bởi các chế độ tương tác, in và RPC tích hợp sẵn.\n\n`createAgentSessionRuntime()` lấy một nhà máy thời gian chạy cộng với mục tiêu cwd/phiên ban đầu. Nhà máy đóng các đầu vào cố định toàn quy trình, tạo lại các dịch vụ liên kết với cwd cho cwd hiệu quả, giải quyết các tùy chọn phiên đối với các dịch vụ đó và trả về kết quả thời gian chạy đầy đủ.\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` sở hữu sự thay thế thời gian chạy hoạt động trên:\n\n- `newSession()`\n- `switchSession()`\n- `fork()`\n- bản sao chảy qua `fork(entryId, { position: \"at\" })`\n- `importFromJsonl()`\n\nHành vi quan trọng:\n\n- `runtime.session` thay đổi sau những thao tác đó\n- đăng ký sự kiện được đính kèm với một `AgentSession` cụ thể, vì vậy hãy đăng ký lại sau khi thay thế\n- nếu bạn sử dụng tiện ích mở rộng, hãy gọi lại `runtime.session.bindExtensions(...)` cho phiên mới\n- quá trình tạo trả về chẩn đoán trên `runtime.diagnostics`\n- nếu việc tạo hoặc thay thế thời gian chạy không thành công, phương thức sẽ được đưa ra và người gọi sẽ quyết định cách xử lý 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### Nhắc nhở và xếp hàng tin nhắn\n\n`PromptOptions` kiểm soát việc mở rộng lời nhắc, hành vi xếp hàng trong khi phát trực tuyến và nhắc nhở thông báo trước ánh sáng:\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`preflightResult` được gọi một lần cho mỗi lần gọi `prompt()`:\n\n- `true` khi lời nhắc được chấp nhận, xếp hàng hoặc xử lý ngay lập tức\n- `false` khi lời nhắc preflight bị từ chối trước khi được chấp nhận\n\nNó kích hoạt trước khi `prompt()` được giải quyết. `prompt()` vẫn chỉ giải quyết sau khi quá trình chạy được chấp nhận hoàn toàn kết thúc, bao gồm cả lần thử lại. Các lỗi sau khi chấp nhận được báo cáo thông qua luồng sự kiện và tin nhắn thông thường, không phải thông qua `preflightResult(false)`.\n\nPhương thức `prompt()` xử lý prompt templates, các lệnh mở rộng và gửi tin nhắn:\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**Hành vi:**\n- **Lệnh mở rộng** (ví dụ: `/mycommand`): Thực thi ngay lập tức, ngay cả trong khi phát trực tuyến. Họ quản lý tương tác LLM của riêng mình thông qua `pi.sendMessage()`.\n- **Dựa trên tệp prompt templates** (từ tệp `.md`): Đã mở rộng sang nội dung của chúng trước khi gửi hoặc xếp hàng.\n- **Trong khi phát trực tuyến mà không có `streamingBehavior`**: Xảy ra lỗi. Sử dụng trực tiếp `steer()` hoặc `followUp()` hoặc chỉ định tùy chọn.\n- **`preflightResult(true)`**: Có nghĩa là lời nhắc đã được chấp nhận, xếp hàng hoặc xử lý ngay lập tức.\n- **`preflightResult(false)`**: Có nghĩa là chuyến bay trước bị từ chối trước khi được chấp nhận.\n\nĐể xếp hàng rõ ràng trong khi phát trực tuyế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\nCả `steer()` và `followUp()` đều mở rộng prompt templates dựa trên tệp nhưng có lỗi trên các lệnh mở rộng (không thể xếp hàng các lệnh mở rộng).\n\n### Đại lý và Trạng thái đại lý\n\nLớp `Agent` (từ `@earendil-works/pi-agent-core`) xử lý tương tác LLM cốt lõi. Truy cập nó qua `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### Sự kiện\n\nĐăng ký các sự kiện để nhận thông báo đầu ra và vòng đời phát trực tuyế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## Tùy chọn tham khảo\n\n### Thư mục\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` được sử dụng bởi `DefaultResourceLoader` cho:\n- Mở rộng dự án (`.pi/extensions/`)\n- Kỹ năng dự án:\n  - `.pi/skills/`\n  - `.agents/skills/` trong `cwd` và các thư mục tổ tiên (tối đa git repo root hoặc root hệ thống tập tin khi không có trong repo)\n- Lời nhắc dự án (`.pi/prompts/`)\n- Tệp ngữ cảnh (`AGENTS.md` đi lên từ cwd)\n- Đặt tên thư mục phiên\n\n`agentDir` được sử dụng bởi `DefaultResourceLoader` cho:\n- Tiện ích mở rộng toàn cầu (`extensions/`)\n- Kỹ năng toàn cầu:\n  - `skills/` dưới `agentDir` (ví dụ `~/.pi/agent/skills/`)\n  - `~/.agents/skills/`\n- Lời nhắc chung (`prompts/`)\n- Tệp ngữ cảnh chung (`AGENTS.md`)\n- Cài đặt (`settings.json`)\n- Mô hình tùy chỉnh (`models.json`)\n- Thông tin xác thực (`auth.json`)\n- Phiên (`sessions/`)\n\nKhi bạn chuyển một tùy chỉnh `ResourceLoader`, `cwd` và `agentDir` không còn kiểm soát việc khám phá tài nguyên nữa. Chúng vẫn ảnh hưởng đến việc đặt tên phiên và độ phân giải đường chạy dao.\n\n### Người mẫu\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\nNếu không có mô hình nào được cung cấp:\n1. Cố gắng khôi phục từ phiên (nếu tiếp tục)\n2. Sử dụng mặc định từ cài đặt\n3. Quay trở lại mô hình có sẵn đầu tiên\n\nĐể khớp với phân tích cú pháp mô hình CLI, hãy sử dụng trình trợ giúp trình phân giải đã xuất:\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()` sử dụng tất cả các mô hình đã đăng ký nên thiết lập lần đầu tiên theo kiểu `--api-key` có thể giải quyết một mô hình trước khi tồn tại xác thực được lưu trữ. `resolveModelScopeWithDiagnostics()` khớp với ngữ nghĩa của `--models` và `enabledModels` trong khi trả lại cảnh báo thay vì in chúng.\n\n> Xem [examples/sdk/02-custom-model.ts](../examples/sdk/02-custom-model.ts)\n\n### API Chìa khóa và OAuth\n\nMức độ ưu tiên của độ phân giải xác thực (được xử lý bởi `ModelRuntime`):\n1. Ghi đè thời gian chạy (thông qua `setRuntimeApiKey`, không được duy trì)\n2. Thông tin xác thực được lưu trữ trong `auth.json` (API keys hoặc OAuth mã thông báo)\n3. Biến môi trường (`ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, v.v.)\n4. Trình phân giải dự phòng (đối với khóa nhà cung cấp tùy chỉnh từ `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()` và `removeRuntimeApiKey()` giải quyết sau khi danh mục, thành phần và ảnh chụp nhanh tình trạng sẵn có được lưu vào bộ nhớ đệm/tích hợp của nhà cung cấp bị ảnh hưởng nhất quán cục bộ. Họ không chờ đợi sự cập nhật danh mục từ xa. Nếu thông tin xác thực đã được cam kết nhưng đồng bộ hóa cục bộ không thành công thì chúng sẽ từ chối bằng `CredentialSynchronizationError` đã xuất; kiểm tra các trường `providerId`, `operation`, `credential` và `cause` của nó thay vì thử lại đột biến thông tin xác thực một cách mù quáng.\n\nHoạt động mô hình/xác thực công khai và `ModelRuntime.create({ signal })` chấp nhận tín hiệu hủy bỏ tùy chọn và không bị giới hạn khi bị bỏ qua. SDK chính sách về thời hạn của ứng dụng đối với việc làm mới danh mục từ xa:\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\nViệc làm mới mạng không thành công hoặc hết thời gian chờ sẽ không hoàn tác thao tác xác thực thành công. `refresh()` bắt đầu một thế hệ nhà cung cấp mới, do đó, nó không chờ đợi sau một lần làm mới cũ bị đình trệ và các thế hệ cũ không thể xuất bản sau đó.\n\n> Xem [examples/sdk/09-api-keys-and-oauth.ts](../examples/sdk/09-api-keys-and-oauth.ts)\n\n### Lời nhắc hệ thống\n\nSử dụng `ResourceLoader` để ghi đè lời nhắc hệ thống:\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> Xem [examples/sdk/03-custom-prompt.ts](../examples/sdk/03-custom-prompt.ts)\n\n### Công cụ\n\nChỉ định những công cụ tích hợp nào sẽ bật:\n\n- Tên công cụ tích hợp: `read`, `bash`, `edit`, `write`, `grep`, `find`, `ls`\n- Các phần dựng sẵn mặc định: `read`, `bash`, `edit`, `write`\n- `noTools: \"all\"` vô hiệu hóa tất cả các công cụ\n- `noTools: \"builtin\"` vô hiệu hóa các phần tích hợp mặc định trong khi vẫn bật tiện ích mở rộng và công cụ tùy chỉnh\n- `excludeTools` vô hiệu hóa các tên công cụ tùy chỉnh, tiện ích mở rộng hoặc tích hợp cụ thể sau khi áp dụng bất kỳ danh sách cho phép `tools` nào\n\nCông cụ `edit` trả về `details.diff` cho màn hình TUI của Pi và `details.patch` dưới dạng bản vá thống nhất tiêu chuẩn cho người tiêu dùng SDK.\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#### Công cụ với cwd tùy chỉnh\n\nKhi bạn chuyển một `cwd` tùy chỉnh, `createAgentSession()` sẽ xây dựng các công cụ tích hợp đã chọn cho 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> Xem [examples/sdk/05-tools.ts](../examples/sdk/05-tools.ts)\n\n### Công cụ tùy chỉnh\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\nSử dụng `defineTool()` cho các định nghĩa và mảng độc lập như `customTools: [myTool]`. Nội tuyến `pi.registerTool({... })` đã suy ra chính xác các loại tham số.\n\nCác công cụ tùy chỉnh được chuyển qua `customTools` được kết hợp với các công cụ đã đăng ký tiện ích mở rộng. Extensions được tải bởi ResourceLoader cũng có thể đăng ký các công cụ thông qua `pi.registerTool()`.\n\nNếu bạn vượt qua `tools`, hãy bao gồm từng tên công cụ tùy chỉnh hoặc tiện ích mở rộng mà bạn muốn bật, ví dụ: `tools: [\"read\", \"bash\", \"my_tool\"]`.\n\n> Xem [examples/sdk/05-tools.ts](../examples/sdk/05-tools.ts)\n\n### Extensions\n\nExtensions được tải bởi `ResourceLoader`. `DefaultResourceLoader` khám phá các tiện ích mở rộng từ các nguồn tiện ích mở rộng `~/.pi/agent/extensions/`, `.pi/extensions/` và 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 có thể đăng ký công cụ, đăng ký sự kiện, thêm lệnh, v.v. Xem [extensions.md](extensions.md) để biết đầy đủ API.\n\n**Tiện ích mở rộng nội tuyến được đặt tên:** Theo mặc định, các nhà máy nội tuyến hiển thị dưới dạng `<inline:1>`, `<inline:2>`, v.v. trong danh sách khởi động Extensions. Thay vào đó, để hiển thị tên mô tả, hãy bọc nhà máy:\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Điều này hiển thị dưới dạng `<inline:my-provider>` thay vì `<inline:1>`. Các chức năng của nhà máy trần vẫn được chấp nhận để tương thích ngược.\n\n**Xe buýt sự kiện:** Extensions có thể giao tiếp qua `pi.events`. Chuyển `eventBus` được chia sẻ đến `DefaultResourceLoader` nếu bạn cần phát hoặc nghe từ bên ngoài:\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> Xem [examples/sdk/06-extensions.ts](../examples/sdk/06-extensions.ts) và [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> Xem [examples/sdk/04-skills.ts](../examples/sdk/04-skills.ts)\n\n### Tệp ngữ cảnh\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> Xem [examples/sdk/07-context-files.ts](../examples/sdk/07-context-files.ts)\n\n### Lệnh gạch chéo\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> Xem [examples/sdk/08-prompt-templates.ts](../examples/sdk/08-prompt-templates.ts)\n\n### Quản lý phiên\n\nPhiên sử dụng cấu trúc cây với liên kết `id`/`parentId`, cho phép phân nhánh tại chỗ.\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**Cây quản lý phiên 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> Xem [examples/sdk/11-sessions.ts](../examples/sdk/11-sessions.ts) và [Session Format](session-format.md)\n\n### Quản lý cài đặt\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**Nhà máy tĩnh:**\n- `SettingsManager.create(cwd?, agentDir?)` - Tải từ tập tin\n- `SettingsManager.inMemory(settings?)` - Không có tệp vào/ra\n\n**Cài đặt dành riêng cho dự án:**\n\nTải cài đặt từ hai vị trí và hợp nhất:\n1. Toàn cầu: `~/.pi/agent/settings.json`\n2. Dự án: `<cwd>/.pi/settings.json`\n\nDự án ghi đè toàn cầu. Các đối tượng lồng nhau hợp nhất các khóa. Setters sửa đổi cài đặt chung theo mặc định.\n\n**Ngữ nghĩa xử lý kiên trì và lỗi:**\n\n- Getters/setters cài đặt đồng bộ cho trạng thái trong bộ nhớ.\n- Tính kiên trì của Setters enqueue ghi không đồng bộ.\n- Gọi `await settingsManager.flush()` khi bạn cần ranh giới độ bền (ví dụ: trước khi thoát quá trình hoặc trước khi xác nhận nội dung tệp trong các bài kiểm tra).\n- `SettingsManager` không in các lỗi I/O cài đặt. Sử dụng `settingsManager.drainErrors()` và báo cáo chúng trong lớp ứng dụng của bạn.\n\n> Xem [examples/sdk/10-settings.ts](../examples/sdk/10-settings.ts)\n\n## Trình tải tài nguyên\n\nSử dụng `DefaultResourceLoader` để khám phá các tiện ích mở rộng, kỹ năng, lời nhắc, chủ đề và 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## Giá trị trả về\n\n`createAgentSession()` trả về:\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## Ví dụ hoàn chỉnh\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## Chế độ chạy\n\nSDK xuất các tiện ích chế độ chạy để xây dựng giao diện tùy chỉnh trên `createAgentSession()`:\n\n### Chế độ tương tác\n\nChế độ tương tác đầy đủ TUI với trình chỉnh sửa, lịch sử trò chuyện và tất cả các lệnh tích hợp:\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### runPrintMode\n\nChế độ chụp một lần: gửi lời nhắc, kết quả đầu ra, thoát:\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### runRpcMode\n\nChế độ JSON-RPC để tích hợp quy trình con:\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\nXem [RPC documentation](rpc.md) để biết giao thức JSON.\n\n## RPC Chế độ thay thế\n\nĐể tích hợp dựa trên quy trình con mà không cần xây dựng bằng SDK, hãy sử dụng trực tiếp CLI:\n\n```bash\npi --mode rpc --no-session\n```\n\nXem [RPC documentation](rpc.md) để biết giao thức JSON.\n\nSDK được ưu tiên khi:\n- Bạn muốn loại an toàn\n- Bạn đang trong quá trình Node.js tương tự\n- Bạn cần truy cập trực tiếp vào trạng thái đại lý\n- Bạn muốn tùy chỉnh các công cụ/tiện ích mở rộng theo chương trình\n\nChế độ RPC được ưu tiên khi:\n- Bạn đang tích hợp từ một ngôn ngữ khác\n- Bạn muốn cách ly quá trình\n- Bạn đang xây dựng một ứng dụng khách không phân biệt ngôn ngữ\n\n## Xuất khẩu\n\nCác điểm xuất khẩu chính:\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Đối với các loại tiện ích mở rộng, hãy xem [extensions.md](extensions.md) để biết đầy đủ API.","sourceFile":"sdk.md"},"security":{"title":"Bảo vệ","markdown":"Pi là một tác nhân mã hóa cục bộ. Nó chạy với quyền của tài khoản người dùng đã khởi động nó và nó xử lý các tệp mà người dùng đó có thể ghi như bên trong cùng một ranh giới tin cậy cục bộ.\n\n## Dự án ủy thác\n\nSự tin cậy của dự án kiểm soát xem pi có tải các cài đặt, tài nguyên, gói và tiện ích mở rộng cục bộ của dự án hay không. Nó không phải là sandbox và nó không hạn chế những gì mô hình có thể yêu cầu các công cụ thực hiện sau khi bạn bắt đầu làm việc trong một thư mục.\n\nPi coi một dự án có các tài nguyên cần có sự tin cậy khi nó tìm thấy bất kỳ tài nguyên nào trong số này từ thư mục làm việc hiện tại:\n\n- `.pi/settings.json`\n- `.pi/extensions`, `.pi/skills`, `.pi/prompts` hoặc `.pi/themes`\n- `.pi/SYSTEM.md` hoặc `.pi/APPEND_SYSTEM.md`\n- dự án `.agents/skills` trong thư mục hiện tại hoặc thư mục tổ tiên\n\nThư mục `.pi` trống không được tính là tài nguyên dự án cần có sự tin cậy.\n\nKhi một phiên tương tác bắt đầu trong một dự án có các tài nguyên yêu cầu sự tin cậy và không có quyết định nào được lưu cho thư mục hiện tại hoặc thư mục mẹ, pi sẽ tuân theo `defaultProjectTrust` từ cài đặt chung. Giá trị mặc định là `\"ask\"`, giá trị này hỏi xem có nên tin cậy dự án khi có giao diện người dùng hay không. Các quyết định đã lưu được lưu trữ theo thư mục chuẩn trong `~/.pi/agent/trust.json` và quyết định đã lưu gần nhất trên đường dẫn hiện tại hoặc đường dẫn gốc sẽ được áp dụng trước mặc định chung.\n\nTin cậy một dự án cho phép pi tải các tài nguyên dự án yêu cầu sự tin cậy, bao gồm:\n\n- `.pi/settings.json`\n- `.pi` các tài nguyên như tiện ích mở rộng, kỹ năng, prompt templates, chủ đề và tệp nhắc hệ thống\n- gói dự án bị thiếu được định cấu hình thông qua cài đặt dự án\n- tiện ích mở rộng dự án cục bộ và tiện ích mở rộng được quản lý gói dự án\n\nSự suy giảm niềm tin sẽ bỏ qua các tài nguyên được bảo vệ. Các tệp ngữ cảnh như `AGENTS.override.md`, `AGENTS.md` và `CLAUDE.md` được tải bất kể độ tin cậy của dự án trừ khi tính năng tải ngữ cảnh bị tắt. Trước khi giải quyết được sự tin cậy, pi chỉ tải context files, tiện ích mở rộng người dùng/toàn cầu và tiện ích mở rộng CLI `-e`. Tiện ích mở rộng người dùng/toàn cầu và CLI có thể xử lý sự kiện `project_trust`; phần mở rộng đầu tiên trả về quyết định có/không sẽ sở hữu quyết định đó.\n\nCác chế độ không tương tác (`-p`, `--mode json` và `--mode rpc`) không hiển thị lời nhắc tin cậy. Nếu không có quyết định tin cậy đã lưu hiện hành, `defaultProjectTrust: \"ask\"` và `\"never\"` bỏ qua các tài nguyên đó, trong khi `\"always\"` tin tưởng chúng. Sử dụng `--approve`/`-a` hoặc `--no-approve`/`-na` để ghi đè độ tin cậy của dự án trong một lần chạy.\n\n## Không có hộp cát tích hợp\n\nPi không bao gồm sandbox tích hợp. Các công cụ tích hợp có thể đọc tệp, ghi tệp, chỉnh sửa tệp và chạy lệnh shell với quyền của quy trình pi. Extensions là các mô-đun TypeScript chạy với cùng quyền. Cài đặt gói, lệnh shell, máy chủ ngôn ngữ, lệnh kiểm tra và các công cụ dành cho nhà phát triển khác hoạt động như các quy trình cục bộ thông thường.\n\nĐây là cố ý. Pi được thiết kế để hoạt động trên cây nguồn cục bộ, gọi chuỗi công cụ dự án và tích hợp với môi trường phát triển hiện có của người dùng. Một phần sandbox trong quá trình sẽ dễ bị hiểu nhầm là ranh giới bảo mật trong khi vẫn phụ thuộc vào vỏ máy chủ, hệ thống tệp, trình quản lý gói, thông tin xác thực và mã mở rộng. Sự cô lập thực sự cần đến từ hệ điều hành hoặc ranh giới ảo hóa/container.\n\nSự tin cậy của dự án chỉ là một người bảo vệ tải đầu vào. Nó ngăn kho lưu trữ âm thầm thay đổi cài đặt hoặc tiện ích mở rộng của pi trước khi bạn phê duyệt. Nó không làm cho mã không đáng tin cậy, lời nhắc không đáng tin cậy hoặc đầu ra mô hình không đáng tin cậy trở nên an toàn. Việc tiêm nhắc nhở từ các tệp kho lưu trữ, nhận xét, tài liệu, context files hoặc đầu ra của bản dựng có thể là rủi ro tác nhân cục bộ và không thể được ngăn chặn một cách đáng tin cậy bằng pi.\n\n## Chạy công việc không đáng tin cậy hoặc không được giám sát\n\nĐối với các kho lưu trữ không đáng tin cậy, mã được tạo mà bạn không có ý định giám sát chặt chẽ hoặc tự động hóa không được giám sát, hãy chạy pi trong một môi trường được chứa. Sử dụng vùng chứa, VM, micro-VM, sandbox từ xa hoặc sandbox được kiểm soát chính sách chỉ với các tệp và thông tin xác thực cần thiết cho tác vụ.\n\nCác mẫu phổ biến được ghi lại trong [Containerization](containerization.md):\n\n- chạy toàn bộ quy trình `pi` bên trong vùng chứa/sandbox\n- chạy máy chủ pi trong khi định tuyến việc thực thi công cụ tích hợp vào Gondolin micro-VM\n- chỉ gắn kết các đường dẫn không gian làm việc mà tác nhân nên truy cập\n- tránh gắn máy chủ `~/.pi/agent` trừ khi vùng chứa truy cập vào các phiên, cài đặt và thông tin đăng nhập của máy chủ\n- vượt qua API keys được yêu cầu tối thiểu hoặc sử dụng thông tin xác thực có thời hạn sử dụng ngắn\n- hạn chế truy cập mạng khi tác vụ không cần thiết\n- xem xét các khác biệt và kết quả đầu ra trước khi sao chép kết quả trở lại hệ thống đáng tin cậy\n\nNếu bạn gắn liên kết đọc/ghi không gian làm việc trên máy chủ, ghi từ bên trong vùng chứa hoặc VM vẫn có thể sửa đổi tệp máy chủ. Sử dụng các giá trị gắn kết chỉ đọc hoặc sao chép tệp vào và ra khỏi sandbox khi bạn cần được bảo vệ mạnh mẽ hơn khỏi việc ghi ngoài ý muốn.\n\n## Báo cáo vấn đề bảo mật\n\nĐể báo cáo vấn đề bảo mật, hãy theo dõi kho lưu trữ [Security Policy](https://github.com/earendil-works/pi-mono/blob/main/SECURITY.md). Không mở một vấn đề công khai cho các báo cáo nhạy cảm về bảo mật.\n\nHành vi dự kiến ​​của tác nhân cục bộ, thiếu sandbox tích hợp, đưa nội dung không đáng tin cậy vào và hành vi của các tiện ích hoặc kỹ năng do người dùng cài đặt thường nằm ngoài ranh giới bảo mật trừ khi báo cáo thể hiện việc vượt qua ranh giới đặc quyền thực sự hoặc cho biết cách pi cấp quyền truy cập mà người dùng cục bộ chưa có.","sourceFile":"security.md"},"session-format":{"title":"Định dạng tệp phiên","markdown":"Phiên được lưu trữ dưới dạng tệp JSONL (JSON Lines). Mỗi dòng là một đối tượng JSON có trường `type`. Các mục nhập phiên tạo thành cấu trúc cây thông qua các trường `id`/`parentId`, cho phép phân nhánh tại chỗ mà không cần tạo tệp mới.\n\n## Vị trí tệp\n\n```\n~/.pi/agent/sessions/--<path>--/<timestamp>_<uuid>.jsonl\n```\n\nTrong đó `<path>` là thư mục làm việc với `/` được thay thế bằng `-`.\n\n## Xóa phiên\n\nCó thể xóa phiên bằng cách xóa tệp `.jsonl` của chúng trong `~/.pi/agent/sessions/`.\n\nPi cũng hỗ trợ xóa các phiên tương tác từ `/resume` (chọn một phiên và nhấn `Ctrl+D`, sau đó xác nhận). Khi có sẵn, pi sử dụng `trash` CLI để tránh bị xóa vĩnh viễn.\n\n## Phiên bản phiên\n\nPhiên có trường phiên bản trong tiêu đề:\n\n- **Phiên bản 1**: Trình tự nhập tuyến tính (cũ, tự động di chuyển khi tải)\n- **Phiên bản 2**: Cấu trúc cây với liên kết `id`/`parentId`\n- **Phiên bản 3**: Đã đổi tên vai trò `hookMessage` thành `custom` (thống nhất các tiện ích mở rộng)\n\nCác phiên hiện tại sẽ tự động được di chuyển sang phiên bản hiện tại (v3) khi được tải.\n\n## Tệp nguồn\n\nNguồn trên GitHub ([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) - Các loại mục nhập phiên và Trình quản lý phiê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) - Các loại thông báo mở rộng (BashExecutionMessage, CustomMessage, v.v.)\n- [`packages/ai/src/types.ts`](https://github.com/earendil-works/pi-mono/blob/main/packages/ai/src/types.ts) - Các loại thông báo cơ bản (UserMessage, AssistantMessage, ToolResultMessage)\n- [`packages/agent/src/types.ts`](https://github.com/earendil-works/pi-mono/blob/main/packages/agent/src/types.ts) - Loại kết hợp AgentMessage\n\nĐối với các định nghĩa TypeScript trong dự án của bạn, hãy kiểm tra `node_modules/@earendil-works/pi-coding-agent/dist/` và `node_modules/@earendil-works/pi-ai/dist/`.\n\n## Các loại tin nhắn\n\nCác mục trong phiên chứa `AgentMessage` đối tượng. Hiểu các loại này là điều cần thiết cho các phiên phân tích cú pháp và viết phần mở rộng.\n\n### Khối nội dung\n\nTin nhắn chứa mảng các khối nội dung được gõ:\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### Các loại thông báo cơ sở (từ 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\nLoại pi-ai `StopReason` đã xuất cũng bao gồm `\"pending\"`, nhưng giá trị đó được dành riêng cho một phần tin nhắn trong các sự kiện phát trực tuyến. Thông báo Terminal `done`/`error` thay thế nó bằng lý do hoàn thành trước khi pi tiếp tục thông báo trợ lý, vì vậy `\"pending\"` sẽ không bao giờ xuất hiện trong phiên JSONL.\n\n### Các loại tin nhắn mở rộng (từ 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### Đại lýTin nhắn Liên minh\n\n```typescript\ntype AgentMessage =\n  | UserMessage\n  | AssistantMessage\n  | ToolResultMessage\n  | BashExecutionMessage\n  | CustomMessage\n  | BranchSummaryMessage\n  | CompactionSummaryMessage;\n```\n\n## Căn cứ vào\n\nTất cả các mục (ngoại trừ `SessionHeader`) mở rộng `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## Các loại mục nhập\n\n### Tiêu đề phiên\n\nDòng đầu tiên của tập tin. Chỉ siêu dữ liệu, không phải là một phần của cây (không có `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Đối với các phiên có phụ huynh (được tạo qua `/fork`, `/clone` hoặc `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### PhiênTin nhắnMục nhập\n\nMột tin nhắn trong cuộc trò chuyện. Trường `message` chứa `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### ModelChangeEntry\n\nĐược phát ra khi người dùng chuyển đổi mô hình giữa phiê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### Cấp độ tư duyThay đổiMục nhập\n\nPhát ra khi người dùng thay đổi mức độ suy nghĩ/lý luậ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Đầu vào\n\nĐược tạo khi bối cảnh được nén. Lưu trữ một bản tóm tắt các tin nhắn trước đó.\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\nCác phép nén mới hơn do khai thác tạo nhúng trực tiếp bối cảnh sau nén được giữ lại vào mục nhập, thay vì `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\nCác trường tùy chọn:\n- `usage`: Việc sử dụng LLM từ việc tạo bản tóm tắt; được bao gồm trong mã thông báo phiên và tổng chi phí\n- `retainedTail`: Vật liệu hóa `AgentMessage[]` được giữ lại sau khi nén chặt. Đây chỉ là tùy chọn để tương thích ngược với các phiên cũ hơn. Các phép nén mới hơn do khai thác tạo ra bao gồm nó để chúng tôi có thể xây dựng lại bối cảnh từ điểm kiểm tra này mà không cần xem các mục cũ hơn trước mục nhập nén.\n- `details`: Dữ liệu dành riêng cho việc triển khai (ví dụ: `{ readFiles: string[], modifiedFiles: string[] }` cho dữ liệu mặc định hoặc dữ liệu tùy chỉnh cho tiện ích mở rộng)\n- `fromHook`: `true` nếu được tạo bởi tiện ích mở rộng, `false`/`undefined` nếu được tạo bằng pi (tên trường cũ)\n- `firstKeptEntryId`: để tương thích với định dạng mục nhập cũ.\n\n### Chi nhánhTóm tắtMục nhập\n\nĐược tạo khi chuyển nhánh thông qua `/tree` với bản tóm tắt được tạo LLM của nhánh bên trái lên đến tổ tiên chung. Ghi lại bối cảnh từ con đường bị bỏ hoang.\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\nCác trường tùy chọn:\n- `usage`: Việc sử dụng LLM từ việc tạo bản tóm tắt; được bao gồm trong mã thông báo phiên và tổng chi phí\n- `details`: Dữ liệu theo dõi tệp (`{ readFiles: string[], modifiedFiles: string[] }`) cho mặc định hoặc dữ liệu tùy chỉnh cho tiện ích mở rộng\n- `fromHook`: `true` nếu được tạo bởi tiện ích mở rộng, `false`/`undefined` nếu được tạo bằng pi (tên trường cũ)\n\n### Mục nhập tùy chỉnh\n\nSự kiên trì của trạng thái mở rộng. KHÔNG tham gia vào bối cảnh LLM.\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\nSử dụng `customType` để xác định các mục nhập của tiện ích mở rộng của bạn khi tải lại. Chế độ tương tác có thể hiển thị các mục tùy chỉnh thông qua `pi.registerEntryRenderer(customType, renderer)`, nhưng chúng vẫn không tham gia vào ngữ cảnh LLM.\n\n### Tin nhắn tùy chỉnhMục nhập\n\nCác tin nhắn được đưa vào tiện ích mở rộng CÓ tham gia vào ngữ cảnh 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\nLĩnh vực:\n- `content`: Chuỗi hoặc `(TextContent | ImageContent)[]` (giống như UserMessage)\n- `display`: `true` = hiển thị trong TUI với kiểu dáng riêng biệt, `false` = ẩn\n- `details`: Siêu dữ liệu dành riêng cho tiện ích mở rộng tùy chọn (không được gửi tới LLM)\n\n### NhãnNhập\n\nDấu trang/điểm đánh dấu do người dùng xác định trên một mục nhập.\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Đặt `label` thành `undefined` để xóa nhãn.\n\n### Thông tin phiênEntry\n\nSiêu dữ liệu phiên (ví dụ: tên hiển thị do người dùng xác định). Đặt qua `/name`, `--name` / `-n` hoặc `pi.setSessionName()` trong tiện ích mở rộng.\n\n```json\n{\"type\":\"session_info\",\"id\":\"k1l2m3n4\",\"parentId\":\"j0k1l2m3\",\"timestamp\":\"2024-12-03T14:35:00.000Z\",\"name\":\"Refactor auth module\"}\n```\n\nTên phiên được hiển thị trong bộ chọn phiên (`/resume`) thay vì thông báo đầu tiên khi được đặt.\n\n## Cấu trúc cây\n\nCác mục tạo thành một cây:\n- Mục đầu tiên có `parentId: null`\n- Mỗi mục nhập tiếp theo trỏ tới mục gốc của nó thông qua `parentId`\n- Phân nhánh tạo ra các mục con mới từ mục nhập trước đó\n- \"Chiếc lá\" là vị trí hiện tại trong cây\n\n```\n[user msg] ─── [assistant] ─── [user msg] ─── [assistant] ─┬─ [user msg] ← current leaf\n                                                            │\n                                                            └─ [branch_summary] ─── [user msg] ← alternate branch\n```\n\n## Xây dựng bối cảnh\n\n`buildContextEntries()` đi từ lá hiện tại đến thư mục gốc, tạo ra danh sách mục đang hoạt động đồng thời tôn trọng việc nén:\n\n1. Thu thập tất cả các mục trên đường dẫn\n2. Nếu có `CompactionEntry` trên đường dẫn:\n   - Bao gồm mục nén đầu tiên\n   - Nếu có `retainedTail`, nó hoạt động như một điểm kiểm tra độc lập và các mục sau khi nén sẽ được đưa vào\n   - Nếu không thì các mục từ `firstKeptEntryId` đến phần nén sẽ được bao gồm\n   - Sau đó, các mục sau khi nén được bao gồm\n3. Giữ nguyên các mục không có tin nhắn trong phạm vi đã chọn để chế độ tương tác có thể hiển thị chúng\n\n`buildSessionContext()` dựa trên danh sách mục nhập đó để tạo danh sách thông báo cho LLM:\n\n1. Trích xuất các cài đặt mô hình và cấp độ tư duy hiện tại từ đường dẫn đầy đủ\n2. Chuyển đổi các mục đã chọn thành tin nhắn:\n   - `message` -> được lưu trữ `AgentMessage`\n   - `compaction` -> `compactionSummary` cộng `retainedTail` khi có mặt\n   - `branch_summary` -> `branchSummary`\n   - `custom_message` -> `CustomMessage`\n   - `custom` -> không có thông báo ngữ cảnh\n\nĐiều này làm cho các lần nén mới hơn hoạt động giống như các điểm kiểm tra khép kín. `retainedTail` chỉ là tùy chọn để các phiên cũ hơn chỉ lưu trữ `firstKeptEntryId` tiếp tục tải chính xác.\n\n## Ví dụ phân tích cú pháp\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## Trình quản lý phiên API\n\nCác phương pháp chính để làm việc với phiên theo chương trình.\n\n### Phương pháp tạo tĩnh\n- `SessionManager.create(cwd, sessionDir?)` - Phiên mới\n- `SessionManager.open(path, sessionDir?)` - Mở tệp phiên hiện có\n- `SessionManager.continueRecent(cwd, sessionDir?)` - Tiếp tục gần đây nhất hoặc tạo mới\n- `SessionManager.inMemory(cwd?)` - Không tồn tại tệp\n- `SessionManager.forkFrom(sourcePath, targetCwd, sessionDir?)` - Phiên rẽ nhánh từ một dự án khác\n\n### Phương pháp liệt kê tĩnh\n- `SessionManager.list(cwd, sessionDir?, onProgress?)` - Liệt kê các phiên cho một thư mục\n- `SessionManager.listAll(onProgress?)` - Liệt kê tất cả các phiên trong tất cả các dự án\n\n### Phương thức phiên bản - Quản lý phiên\n- `newSession(options?)` - Bắt đầu phiên mới (tùy chọn: `{ parentSession?: string }`)\n- `setSessionFile(path)` - Chuyển sang tệp phiên khác\n- `createBranchedSession(leafId)` - Trích xuất nhánh sang tệp phiên mới\n\n### Phương thức phiên bản - Đang thêm (tất cả ID mục trả về)\n- `appendMessage(message)` - Thêm tin nhắn\n- `appendThinkingLevelChange(level)` - Ghi lại sự thay đổi suy nghĩ\n- `appendModelChange(provider, modelId)` - Ghi lại sự thay đổi mô hình\n- `appendCompaction(summary, firstKeptEntryId, tokensBefore, details?, fromHook?)` - Thêm độ nén\n- `appendCustomEntry(customType, data?)` - Trạng thái mở rộng (không có trong ngữ cảnh)\n- `appendSessionInfo(name)` - Đặt tên hiển thị phiên\n- `appendCustomMessageEntry(customType, content, display, details?)` - Thông báo mở rộng (trong ngữ cảnh)\n- `appendLabelChange(targetId, label)` - Đặt/xóa nhãn\n\n### Phương thức phiên bản - Điều hướng dạng cây\n- `getLeafId()` - Vị trí hiện tại\n- `getLeafEntry()` - Nhận mục nhập lá hiện tại\n- `getEntry(id)` - Nhận mục nhập bằng ID\n- `getBranch(fromId?)` - Đi từ mục này đến mục gốc\n- `getTree()` - Nhận cấu trúc cây đầy đủ\n- `getChildren(parentId)` - Nhận con trực tiếp\n- `getLabel(id)` - Nhận nhãn để vào\n- `branch(entryId)` - Di chuyển lá tới mục nhập trước đó\n- `resetLeaf()` - Đặt lại lá thành null (trước bất kỳ mục nào)\n- `branchWithSummary(entryId, summary, details?, fromHook?)` - Chi nhánh có tóm tắt ngữ cảnh\n\n### Phương thức phiên bản - Bối cảnh & thông tin\n- `buildContextEntries()` - Nhận các mục nhập nhánh đang hoạt động có áp dụng tính năng nén\n- `buildSessionContext()` - Nhận tin nhắn, cấp độ tư duy và mô hình cho LLM\n- `getEntries()` - Tất cả các mục (không bao gồm tiêu đề)\n- `getHeader()` - Siêu dữ liệu tiêu đề phiên\n- `getSessionName()` - Nhận tên hiển thị từ mục nhập session_info mới nhất\n- `getCwd()` - Thư mục làm việc\n- `getSessionDir()` - Thư mục lưu trữ phiên\n- `getSessionId()` - Phiên UUID\n- `getSessionFile()` - Đường dẫn tệp phiên (không xác định cho trong bộ nhớ)\n- `isPersisted()` - Phiên có được lưu vào đĩa hay không","sourceFile":"session-format.md"},"sessions":{"title":"Phiên","markdown":"Pi lưu các cuộc hội thoại dưới dạng phiên để bạn có thể tiếp tục làm việc, phân nhánh từ các lượt trước đó và truy cập lại các đường dẫn trước đó.\n\n## Lưu trữ phiên\n\nCác phiên tự động lưu vào `~/.pi/agent/sessions/`, được sắp xếp theo thư mục làm việc. Mỗi phiên là một tệp JSONL có cấu trúc dạng cây.\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\nSử dụng `/session` ở chế độ tương tác để xem tệp phiên hiện tại, ID phiên, số lượng tin nhắn, mã thông báo và chi phí.\n\nĐể biết định dạng tệp JSONL và Trình quản lý phiên API, hãy xem [Session Format](session-format.md).\n\n## Lệnh phiên\n\n| Yêu cầu | Sự miêu tả |\n|---------|-------------|\n| `/resume` | Duyệt và chọn các phiên trước |\n| `/new` | Bắt đầu một phiên mới |\n| `/name <name>` | Đặt tên hiển thị phiên hiện tại |\n| `/session` | Hiển thị thông tin phiên |\n| `/tree` | Điều hướng hiện tại session tree |\n| `/fork` | Tạo phiên mới từ tin nhắn của người dùng trước đó |\n| `/clone` | Sao chép nhánh hoạt động hiện tại vào một phiên mới |\n| `/compact [prompt]` | Tóm tắt bối cảnh cũ hơn; xem [Compaction](compaction.md) |\n| `/export [file]` | Xuất phiên sang HTML |\n| `/share` | Tải lên dưới dạng riêng tư GitHub ý chính với liên kết HTML có thể chia sẻ |\n\n## Tiếp tục và xóa phiên\n\n`/resume` mở bộ chọn phiên tương tác cho dự án hiện tại. `pi -r` mở cùng một bộ chọn khi khởi động.\n\nTrong bộ chọn bạn có thể:\n\n- tìm kiếm bằng cách gõ\n- chuyển đổi hiển thị đường dẫn bằng Ctrl+P\n- chuyển đổi chế độ sắp xếp bằng Ctrl+S\n- lọc các phiên được đặt tên bằng Ctrl+N\n- đổi tên bằng Ctrl+R\n- xóa bằng Ctrl+D, sau đó xác nhận\n\nKhi có sẵn, pi sử dụng `trash` CLI để xóa thay vì xóa vĩnh viễn các tệp.\n\n## Phiên đặt tên\n\nSử dụng `/name <name>` để đặt tên phiên mà con người có thể đọc được:\n\n```text\n/name Refactor auth module\n```\n\nĐặt tên khi khởi động bằng `--name` hoặc `-n`:\n\n```bash\npi --name \"Refactor auth module\"\npi --name \"CI audit\" -p \"Review this build failure\"\n```\n\nCác phiên được đặt tên dễ tìm thấy hơn trong `/resume` và `pi -r`.\n\n## Phân nhánh với `/tree`\n\nPhiên được lưu trữ dưới dạng cây. Mọi mục nhập đều có `id` và `parentId` và vị trí hiện tại là lá đang hoạt động. `/tree` cho phép bạn chuyển đến bất kỳ điểm nào trước đó và tiếp tục từ đó mà không cần tạo tệp mới.\n\n<p align=\"center\"><img src=\"images/tree-view.png\" alt=\"Tree View\" width=\"600\"></p>\n\nHình dạng ví dụ:\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### Kiểm soát cây\n\n| Chìa khóa | Hoạt động |\n|-----|--------|\n| ↑/↓ | Điều hướng các mục hiển thị |\n| ←/→ | Trang lên/xuống |\n| Ctrl+←/Ctrl+→ hoặc Alt+←/Alt+→ | Gấp/mở hoặc nhảy giữa các đoạn nhánh |\n| Shift+L | Đặt hoặc xóa nhãn trên mục đã chọn |\n| Shift+T | Chuyển đổi dấu thời gian của nhãn |\n| Đi vào | Chọn mục nhập |\n| Thoát/Ctrl+C | Hủy bỏ |\n| Ctrl+O | Chế độ lọc chu kỳ |\n\nCác chế độ lọc là: mặc định, không có công cụ, chỉ dành cho người dùng, chỉ được gắn nhãn và tất cả. Định cấu hình mặc định với `treeFilterMode` trong [Settings](settings.md).\n\n### Hành vi lựa chọn\n\nChọn người dùng hoặc tin nhắn tùy chỉnh:\n\n1. Di chuyển lá tới thư gốc của thư đã chọn.\n2. Đặt văn bản tin nhắn đã chọn vào trình chỉnh sửa.\n3. Cho phép bạn chỉnh sửa và gửi lại, tạo một nhánh mới.\n\nChọn một trợ lý, công cụ, nén hoặc mục nhập không phải của người dùng khác:\n\n1. Di chuyển lá đến mục đó.\n2. Để trống trình soạn thảo.\n3. Cho phép bạn tiếp tục từ thời điểm đó.\n\nViệc chọn thông báo của người dùng gốc sẽ đặt lại lá về cuộc trò chuyện trống và đặt lời nhắc ban đầu vào trình chỉnh sửa.\n\n## `/tree`, `/fork` và `/clone`\n\n| Tính năng | `/tree` | `/fork` | `/clone` |\n|---------|---------|---------|----------|\n| đầu ra | Cùng một tập tin phiên | Tệp phiên mới | Tệp phiên mới |\n| Xem | Toàn cây | Bộ chọn tin nhắn của người dùng | Chi nhánh hoạt động hiện tại |\n| sử dụng điển hình | Khám phá các lựa chọn thay thế tại chỗ | Bắt đầu phiên mới từ lời nhắc trước đó | Sao chép công việc hiện tại trước khi tiếp tục |\n| Bản tóm tắt | Tóm tắt chi nhánh tùy chọn | Không có | Không có |\n\nSử dụng `/tree` khi bạn muốn giữ các lựa chọn thay thế cùng nhau. Sử dụng `/fork` hoặc `/clone` khi bạn muốn có một tệp phiên riêng biệt.\n\n## Tóm tắt chi nhánh\n\nKhi `/tree` chuyển từ nhánh này sang nhánh khác, pi có thể tóm tắt nhánh bị bỏ rơi và đính kèm bản tóm tắt đó vào vị trí mới. Điều này bảo tồn bối cảnh quan trọng từ đường dẫn bạn đã rời đi mà không phát lại toàn bộ nhánh.\n\nKhi được nhắc, hãy chọn một trong:\n\n1. không có tóm tắt\n2. tóm tắt với lời nhắc mặc định\n3. tóm tắt với hướng dẫn tập trung tùy chỉnh\n\nXem [Compaction](compaction.md) để biết branch summarization móc bên trong và móc mở rộng.\n\n## Định dạng phiên\n\nCác tệp phiên là JSONL và chứa các mục nhập thông báo, thay đổi mô hình, thay đổi ở cấp độ tư duy, nhãn, rút ​​gọn, tóm tắt nhánh và mục mở rộng.\n\nĐể biết trình phân tích cú pháp, tiện ích mở rộng, cách sử dụng SDK và Trình quản lý phiên đầy đủ API, hãy xem [Session Format](session-format.md).","sourceFile":"sessions.md"},"settings":{"title":"Cài đặt","markdown":"Pi sử dụng tệp cài đặt JSON với cài đặt dự án ghi đè cài đặt chung.\n\n| Vị trí | Phạm vi |\n|----------|-------|\n| `~/.pi/agent/settings.json` | Toàn cầu (tất cả các dự án) |\n| `.pi/settings.json` | Dự án (thư mục hiện tại) |\n\nChỉnh sửa trực tiếp hoặc sử dụng `/settings` cho các tùy chọn phổ biến.\n\n## Dự án ủy thác\n\nKhi khởi động tương tác, pi hỏi trước khi tin cậy một thư mục dự án có chứa các cài đặt, tài nguyên hoặc dự án cục bộ của dự án `.agents/skills` và không có quyết định nào được lưu cho thư mục hoặc thư mục mẹ trong `~/.pi/agent/trust.json`. Tin tưởng vào một dự án cho phép pi tải tài nguyên `.pi/settings.json` và `.pi`, cài đặt các gói dự án bị thiếu và thực hiện các phần mở rộng dự án.\n\nCác chế độ không tương tác (`-p`, `--mode json` và `--mode rpc`) không hiển thị lời nhắc tin cậy. Nếu không có quyết định tin cậy đã lưu hiện hành, họ sẽ sử dụng `defaultProjectTrust` từ cài đặt chung: `ask` (mặc định) và `never` bỏ qua các tài nguyên dự án đó, trong khi `always` tin cậy chúng. Vượt qua `--approve`/`-a` hoặc `--no-approve`/`-na` để ghi đè độ tin cậy của dự án trong một lần chạy.\n\nNếu không áp dụng tiện ích mở rộng hoặc quyết định đã lưu, `defaultProjectTrust` sẽ kiểm soát hành vi dự phòng. Đặt thành `\"ask\"`, `\"always\"` hoặc `\"never\"` trong `~/.pi/agent/settings.json` hoặc thay đổi bằng `/settings`.\n\n`pi config` và các lệnh gói sử dụng cùng một luồng tin cậy của dự án, ngoại trừ `pi update` không bao giờ nhắc. Chuyển `--approve` để tin cậy cài đặt dự án cục bộ cho một lệnh hoặc `--no-approve` để bỏ qua chúng.\n\nSử dụng `/trust` trong chế độ tương tác để lưu quyết định tin cậy dự án cho các phiên trong tương lai, bao gồm cả tin cậy cho thư mục mẹ trực tiếp. Nó chỉ viết `~/.pi/agent/trust.json`; phiên hiện tại không được tải lại, vì vậy hãy khởi động lại pi để các thay đổi có hiệu lực.\n\n## Tất cả cài đặt\n\n### Mô hình & Tư duy\n\n| Cài đặt | Kiểu | Mặc định | Sự miêu tả |\n|---------|------|---------|-------------|\n| `defaultProvider` | sợi dây | - | Nhà cung cấp mặc định (ví dụ: `\"anthropic\"`, `\"openai\"`) |\n| `defaultModel` | sợi dây | - | ID mẫu mặc định |\n| `defaultThinkingLevel` | sợi dây | - | `\"off\"`, `\"minimal\"`, `\"low\"`, `\"medium\"`, `\"high\"`, `\"xhigh\"`, `\"max\"` |\n| `hideThinkingBlock` | boolean | `false` | Ẩn các khối suy nghĩ trong đầu ra |\n| `showCacheMissNotices` | boolean | `false` | Hiển thị thông báo bản ghi khi có lỗi nhắc nhở đáng kể trong bộ nhớ đệm |\n| `thinkingBudgets` | sự vật | - | Ngân sách mã thông báo tùy chỉnh cho mỗi cấp độ tư duy |\n\n#### suy nghĩNgân sách\n\n```json\n{\n  \"thinkingBudgets\": {\n    \"minimal\": 1024,\n    \"low\": 4096,\n    \"medium\": 10240,\n    \"high\": 32768\n  }\n}\n```\n\n### Giao diện người dùng & Hiển thị\n\n| Cài đặt | Kiểu | Mặc định | Sự miêu tả |\n|---------|------|---------|-------------|\n| `theme` | sợi dây | `\"dark\"` | Tên chủ đề (`\"dark\"`, `\"light\"` hoặc tùy chỉnh) |\n| `externalEditor` | sợi dây | `$VISUAL`, rồi `$EDITOR`, rồi Notepad trên Windows hoặc `nano` ở nơi khác | Lệnh cho trình soạn thảo bên ngoài Ctrl+G; được ưu tiên hơn các biến môi trường |\n| `quietStartup` | boolean | `false` | Ẩn tiêu đề khởi động |\n| `defaultProjectTrust` | sợi dây | `\"ask\"` | Hành vi tin cậy của dự án dự phòng: `\"ask\"`, `\"always\"` hoặc `\"never\"`. Chỉ cài đặt chung |\n| `collapseChangelog` | boolean | `false` | Hiển thị nhật ký thay đổi cô đọng sau khi cập nhật |\n| `enableInstallTelemetry` | boolean | `true` | Gửi ping phiên bản cài đặt/cập nhật ẩn danh sau lần cài đặt đầu tiên hoặc các bản cập nhật được phát hiện trong nhật ký thay đổi. Điều này không kiểm soát việc kiểm tra cập nhật |\n| `enableAnalytics` | boolean | `false` | Chọn tham gia chia sẻ dữ liệu phân tích. Hiện chỉ được yêu cầu trong lần thiết lập thử nghiệm lần đầu tiên (`PI_EXPERIMENTAL=1`) |\n| `trackingId` | sợi dây | - | Mã nhận dạng theo dõi phân tích, được tạo khi `enableAnalytics` được bật |\n| `doubleEscapeAction` | sợi dây | `\"tree\"` | Hành động để thoát kép: `\"tree\"`, `\"fork\"` hoặc `\"none\"` |\n| `treeFilterMode` | sợi dây | `\"default\"` | Bộ lọc mặc định cho `/tree`: `\"default\"`, `\"no-tools\"`, `\"user-only\"`, `\"labeled-only\"`, `\"all\"` |\n| `editorPaddingX` | con số | `0` | Đệm ngang cho trình soạn thảo đầu vào (0-3) |\n| `outputPad` | con số | `1` | Khoảng đệm ngang cho tin nhắn của người dùng, tin nhắn trợ lý và suy nghĩ (0 hoặc 1) |\n| `autocompleteMaxVisible` | con số | `5` | Các mục hiển thị tối đa trong danh sách thả xuống tự động hoàn thành (3-20) |\n| `showHardwareCursor` | boolean | `false` | Hiển thị con trỏ đầu cuối trong khi TUI định vị nó để hỗ trợ IME |\n| `tuiMode` | sợi dây | `\"regular\"` | Chế độ tương tác TUI: `\"regular\"` hoặc thử nghiệm `\"fullscreen\"`. Những thay đổi từ `/settings` áp dụng ngay lập tức; `--tui-mode` ghi đè cài đặt này khi khởi động |\n| `fullscreenExitOutput` | sợi dây | `\"transcript\"` | Đầu ra thoát toàn màn hình: `\"transcript\"` in bản ghi cuối cùng và gợi ý tiếp tục, trong khi `\"resume-hint\"` khôi phục màn hình trước đó và chỉ in gợi ý sơ yếu lý lịch. Không có tác dụng ở chế độ TUI thông thường |\n| `fullscreenScrollbar` | sợi dây | `\"auto\"` | Thanh cuộn bản ghi toàn màn hình: `\"auto\"` hiển thị tạm thời trong khi cuộn, `\"always\"` đặt cột ngoài cùng bên phải và giữ cho cột đó hiển thị, đồng thời `\"hidden\"` ẩn cột đó. Không có tác dụng ở chế độ TUI thông thường |\n\nĐối với Mã VS, hãy bao gồm `--wait` để pi tiếp tục hoạt động sau khi trình chỉnh sửa thoát:\n\n```json\n{\n  \"externalEditor\": \"code --wait\"\n}\n```\n\n### Kiểm tra từ xa và cập nhật\n\n`enableInstallTelemetry` chỉ kiểm soát ping cài đặt/cập nhật ẩn danh thành `https://pi.dev/api/report-install`. Việc chọn không tham gia đo từ xa sẽ không vô hiệu hóa việc kiểm tra cập nhật; Pi vẫn có thể tìm nạp `https://pi.dev/api/latest-version` để tìm phiên bản mới nhất.\n\nĐặt `PI_SKIP_VERSION_CHECK=1` để tắt kiểm tra cập nhật phiên bản Pi. Sử dụng `--offline` hoặc `PI_OFFLINE=1` để tắt tất cả các hoạt động mạng khởi động được mô tả ở đây, bao gồm kiểm tra cập nhật, kiểm tra cập nhật gói và đo từ xa cài đặt/cập nhật.\n\n### Mạng\n\n| Cài đặt | Kiểu | Mặc định | Sự miêu tả |\n|---------|------|---------|-------------|\n| `httpProxy` | sợi dây | - | URL proxy HTTP được áp dụng dưới dạng `HTTP_PROXY` và `HTTPS_PROXY`. Chỉ cài đặt chung. |\n\n```json\n{\n  \"httpProxy\": \"http://127.0.0.1:7890\"\n}\n```\n\n### Cảnh báo\n\n| Cài đặt | Kiểu | Mặc định | Sự miêu tả |\n|---------|------|---------|-------------|\n| `warnings.anthropicExtraUsage` | boolean | `true` | Hiển thị cảnh báo khi xác thực đăng ký Anthropic có thể sử dụng mức sử dụng bổ sung phải trả phí |\n\n```json\n{\n  \"warnings\": {\n    \"anthropicExtraUsage\": false\n  }\n}\n```\n\n### nén chặt\n\n| Cài đặt | Kiểu | Mặc định | Sự miêu tả |\n|---------|------|---------|-------------|\n| `compaction.enabled` | boolean | `true` | Bật tính năng tự động nén |\n| `compaction.reserveTokens` | con số | `16384` | Mã thông báo dành riêng cho phản hồi LLM |\n| `compaction.keepRecentTokens` | con số | `20000` | Mã thông báo gần đây cần giữ (không được tóm tắt) |\n\n```json\n{\n  \"compaction\": {\n    \"enabled\": true,\n    \"reserveTokens\": 16384,\n    \"keepRecentTokens\": 20000\n  }\n}\n```\n\n### Tóm tắt chi nhánh\n\n| Cài đặt | Kiểu | Mặc định | Sự miêu tả |\n|---------|------|---------|-------------|\n| `branchSummary.reserveTokens` | con số | `16384` | Mã thông báo dành riêng cho branch summarization |\n| `branchSummary.skipPrompt` | boolean | `false` | Bỏ qua \"Tóm tắt chi nhánh?\" lời nhắc trên điều hướng `/tree` (mặc định là không có tóm tắt) |\n\n### Thử lại\n\n| Cài đặt | Kiểu | Mặc định | Sự miêu tả |\n|---------|------|---------|-------------|\n| `retry.enabled` | boolean | `true` | Bật thử lại cấp tác nhân tự động đối với các lỗi tạm thời |\n| `retry.maxRetries` | con số | `3` | Số lần thử lại cấp đại lý tối đa |\n| `retry.baseDelayMs` | con số | `2000` | Độ trễ cơ sở cho thời gian chờ theo cấp số nhân cấp đại lý (2 giây, 4 giây, 8 giây) |\n| `retry.provider.timeoutMs` | con số | SDK mặc định | Nhà cung cấp/SDK hết thời gian chờ yêu cầu tính bằng mili giây |\n| `retry.provider.maxRetries` | con số | `0` | Nhà cung cấp/SDK thử lại |\n| `retry.provider.maxRetryDelayMs` | con số | `60000` | Độ trễ tối đa do máy chủ yêu cầu trước khi thất bại (60 giây) |\n\nKhi nhà cung cấp yêu cầu độ trễ thử lại lâu hơn `retry.provider.maxRetryDelayMs`, yêu cầu sẽ không thành công ngay lập tức kèm theo lỗi thông tin thay vì im lặng chờ đợi. Đặt thành `0` để tắt giới hạn.\n\nGiữ `retry.provider.maxRetries` ở `0` trừ khi rõ ràng cần phải thử lại ở cấp độ nhà cung cấp. Việc đặt nó ở trên `0` có ​​thể khiến SDK/nhà cung cấp thử lại xử lý các lỗi hết giới hạn sử dụng trước khi Pi nhìn thấy chúng, điều này có thể chặn tác nhân cho đến khi hạn mức của nhà cung cấp được đặt lại trong một số trường hợp.\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### Gửi tin nhắn\n\n| Cài đặt | Kiểu | Mặc định | Sự miêu tả |\n|---------|------|---------|-------------|\n| `steeringMode` | sợi dây | `\"one-at-a-time\"` | Cách gửi thông báo chỉ đạo: `\"all\"` hoặc `\"one-at-a-time\"` |\n| `followUpMode` | sợi dây | `\"one-at-a-time\"` | Cách gửi tin nhắn tiếp theo: `\"all\"` hoặc `\"one-at-a-time\"` |\n| `transport` | sợi dây | `\"auto\"` | Phương tiện vận chuyển ưu tiên dành cho nhà cung cấp hỗ trợ nhiều phương tiện vận chuyển: `\"sse\"`, `\"websocket\"`, `\"websocket-cached\"` hoặc `\"auto\"` |\n| `httpIdleTimeoutMs` | con số | `300000` | Thời gian chờ không hoạt động của tiêu đề/nội dung HTTP tính bằng mili giây, cũng được sử dụng bởi các nhà cung cấp có thời gian chờ không hoạt động của luồng rõ ràng. Đặt thành `0` để tắt. |\n| `websocketConnectTimeoutMs` | con số | `15000` | Thời gian chờ bắt tay mở/kết nối WebSocket tính bằng mili giây đối với các nhà cung cấp hỗ trợ truyền tải WebSocket. Đặt thành `0` để tắt. |\n\n### Thiết bị đầu cuối & Hình ảnh\n\n| Cài đặt | Kiểu | Mặc định | Sự miêu tả |\n|---------|------|---------|-------------|\n| `terminal.showImages` | boolean | `true` | Hiển thị hình ảnh trong thiết bị đầu cuối (nếu được hỗ trợ) |\n| `terminal.imageWidthCells` | con số | `60` | Chiều rộng hình ảnh nội tuyến ưa thích trong các ô cuối |\n| `terminal.clearOnShrink` | boolean | `false` | Xóa các hàng trống khi nội dung bị thu nhỏ (có thể gây nhấp nháy) |\n| `images.autoResize` | boolean | `true` | Thay đổi kích thước hình ảnh thành tối đa 2000x2000. Áp dụng cho `@file` tệp đính kèm, `read` và hình ảnh được các công cụ trả về |\n| `images.blockImages` | boolean | `false` | Chặn tất cả hình ảnh được gửi đến LLM |\n\n### Vỏ bọc\n\n| Cài đặt | Kiểu | Mặc định | Sự miêu tả |\n|---------|------|---------|-------------|\n| `shellPath` | sợi dây | - | Đường dẫn shell tùy chỉnh (ví dụ: đối với Cygwin trên Windows); hỗ trợ `~` hàng đầu cho thư mục chính |\n| `shellCommandPrefix` | sợi dây | - | Tiền tố cho mỗi lệnh bash (ví dụ: `\"shopt -s expand_aliases\"`) |\n| `npmCommand` | sợi dây[] | - | Lệnh argv được sử dụng cho các thao tác tra cứu/cài đặt gói npm (ví dụ: `[\"mise\", \"exec\", \"node@20\", \"--\", \"npm\"]`) |\n\n```json\n{\n  \"npmCommand\": [\"mise\", \"exec\", \"node@20\", \"--\", \"npm\"]\n}\n```\n\n`npmCommand` được sử dụng cho tất cả các hoạt động của trình quản lý gói npm, bao gồm cài đặt, gỡ cài đặt và cài đặt phụ thuộc bên trong các gói git. Các gói npm trong phạm vi người dùng được cài đặt trong `~/.pi/agent/npm/`; cài đặt các gói npm trong phạm vi dự án trong `.pi/npm/`. Sử dụng các mục nhập kiểu argv chính xác như quy trình sẽ được khởi chạy. Khi `npmCommand` được định cấu hình, các bản cài đặt phụ thuộc gói git sử dụng `install` đơn giản để tránh các cờ dành riêng cho npm trong trình bao bọc hoặc trình quản lý gói thay thế.\n\n### Phiên\n\n| Cài đặt | Kiểu | Mặc định | Sự miêu tả |\n|---------|------|---------|-------------|\n| `sessionDir` | sợi dây | - | Thư mục lưu trữ các tập tin phiên. Chấp nhận đường dẫn tuyệt đối hoặc tương đối, cộng với `~`. |\n\n```json\n{ \"sessionDir\": \".pi/sessions\" }\n```\n\nKhi nhiều nguồn chỉ định một thư mục phiên, mức độ ưu tiên là `--session-dir`, `PI_CODING_AGENT_SESSION_DIR`, sau đó là `sessionDir` trong settings.json.\n\n### Người mẫu đi xe đạp\n\n| Cài đặt | Kiểu | Mặc định | Sự miêu tả |\n|---------|------|---------|-------------|\n| `enabledModels` | sợi dây[] | - | Các mẫu mô hình cho vòng đạp Ctrl+P (cùng định dạng với cờ `--models` CLI) |\n\n```json\n{\n  \"enabledModels\": [\"claude-*\", \"gpt-4o\", \"gemini-2*\"]\n}\n```\n\n### Markdown\n\n| Cài đặt | Kiểu | Mặc định | Sự miêu tả |\n|---------|------|---------|-------------|\n| `markdown.codeBlockIndent` | sợi dây | `\"  \"` | Thụt lề cho khối mã |\n| `markdown.mermaid` | sợi dây | `\"streaming\"` | Chế độ kết xuất nàng tiên cá: `\"off\"`, `\"final\"` hoặc `\"streaming\"` |\n\n### Tài nguyên\n\nCác cài đặt này xác định nơi tải tiện ích mở rộng, kỹ năng, lời nhắc và chủ đề.\n\nCác đường dẫn trong `~/.pi/agent/settings.json` phân giải tương ứng với `~/.pi/agent`. Các đường dẫn trong `.pi/settings.json` phân giải tương ứng với `.pi`. Đường dẫn tuyệt đối và `~` được hỗ trợ.\n\n| Cài đặt | Kiểu | Mặc định | Sự miêu tả |\n|---------|------|---------|-------------|\n| `packages` | mảng | `[]` | Các gói npm/git để tải tài nguyên từ |\n| `extensions` | sợi dây[] | `[]` | Đường dẫn hoặc thư mục tệp mở rộng cục bộ |\n| `skills` | sợi dây[] | `[]` | Đường dẫn hoặc thư mục tệp kỹ năng cục bộ |\n| `prompts` | sợi dây[] | `[]` | Đường dẫn hoặc thư mục mẫu nhắc nhở cục bộ |\n| `themes` | sợi dây[] | `[]` | Đường dẫn hoặc thư mục tệp chủ đề cục bộ |\n| `enableSkillCommands` | boolean | `true` | Đăng ký kỹ năng dưới dạng lệnh `/skill:name` |\n\nMảng hỗ trợ các mô hình và loại trừ toàn cầu. Sử dụng `!pattern` để loại trừ. Sử dụng `+path` để buộc bao gồm một đường dẫn chính xác và `-path` để buộc loại trừ một đường dẫn chính xác.\n\n#### gói\n\nDạng chuỗi tải tất cả tài nguyên từ một gói:\n\n```json\n{\n  \"packages\": [\"pi-skills\", \"@org/my-extension\"]\n}\n```\n\nBiểu mẫu đối tượng lọc những tài nguyên nào sẽ tải:\n\n```json\n{\n  \"packages\": [\n    {\n      \"source\": \"pi-skills\",\n      \"skills\": [\"brave-search\", \"transcribe\"],\n      \"extensions\": []\n    }\n  ]\n}\n```\n\nXem [packages.md](packages.md) để biết chi tiết quản lý gói.\n\n## Ví dụ\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## Ghi đè dự án\n\nCài đặt dự án (`.pi/settings.json`) ghi đè cài đặt chung. Các đối tượng lồng nhau được hợp nhất:\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":"Bí danh vỏ","markdown":"Pi chạy bash ở chế độ không tương tác (`bash -c`), chế độ này không mở rộng bí danh theo mặc định.\n\nĐể bật bí danh shell của bạn, hãy thêm vào `~/.pi/agent/settings.json`:\n\n```json\n{\n  \"shellCommandPrefix\": \"shopt -s expand_aliases\\neval \\\"$(grep '^alias ' ~/.zshrc)\\\"\"\n}\n```\n\nĐiều chỉnh đường dẫn (`~/.zshrc`, `~/.bashrc`, v.v.) để phù hợp với cấu hình shell của bạn.","sourceFile":"shell-aliases.md"},"skills":{"title":"Skills","markdown":"> pi có thể tạo ra các kỹ năng. Yêu cầu nó xây dựng một cái cho trường hợp sử dụng của bạn.\n\n\nSkills là các gói khả năng độc lập mà tác nhân tải theo yêu cầu. Kỹ năng cung cấp các quy trình làm việc chuyên biệt, hướng dẫn thiết lập, tập lệnh trợ giúp và tài liệu tham khảo cho các tác vụ cụ thể.\n\nPi thực hiện [Agent Skills standard](https://agentskills.io/specification), cảnh báo về hầu hết các hành vi vi phạm nhưng vẫn được khoan dung. Pi cho phép tên kỹ năng khác với thư mục gốc của chúng mặc dù tiêu chuẩn không cho phép điều đó; quy tắc đó là chưa tối ưu cho các thư mục kỹ năng dùng chung được sử dụng trên nhiều cơ chế khai thác tác nhân.\n\n## Mục lục\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## Địa điểm\n\n> **Bảo mật:** Skills có thể hướng dẫn mô hình thực hiện bất kỳ hành động nào và có thể bao gồm mã thực thi mà mô hình gọi ra. Xem lại nội dung kỹ năng trước khi sử dụng.\n\nPi tải kỹ năng từ:\n\n- Toàn cầu:\n  - `~/.pi/agent/skills/`\n  - `~/.agents/skills/`\n- Dự án (chỉ sau khi dự án được tin cậy):\n  - `.pi/skills/`\n  - `.agents/skills/` trong `cwd` và các thư mục tổ tiên (tối đa git repo root hoặc root hệ thống tập tin khi không có trong repo)\n- Các gói: `skills/` thư mục hoặc `pi.skills` mục trong `package.json`\n- Cài đặt: mảng `skills` với các tệp hoặc thư mục\n- CLI: `--skill <path>` (có thể lặp lại, cộng ngay cả với `--no-skills`)\n\nQuy luật khám phá:\n- Trong `~/.pi/agent/skills/` và `.pi/skills/`, các tệp gốc trực tiếp `.md` được phát hiện dưới dạng các kỹ năng riêng lẻ\n- Ở tất cả các vị trí kỹ năng, các thư mục chứa `SKILL.md` được phát hiện theo cách đệ quy\n- Trong `~/.agents/skills/` và dự án `.agents/skills/`, các tệp gốc `.md` bị bỏ qua\n\nTắt tính năng phát hiện bằng `--no-skills` (đường dẫn `--skill` rõ ràng vẫn tải).\n\n### Sử dụng Skills từ các dây nịt khác\n\nĐể sử dụng các kỹ năng từ Claude Code hoặc OpenAI Codex, hãy thêm thư mục của chúng vào cài đặt:\n\n```json\n{\n  \"skills\": [\n    \"~/.claude/skills\",\n    \"~/.codex/skills\"\n  ]\n}\n```\n\nĐối với các kỹ năng Mã Claude cấp dự án, hãy thêm vào `.pi/settings.json`:\n\n```json\n{\n  \"skills\": [\"../.claude/skills\"]\n}\n```\n\n## Skills Làm việc như thế nào\n\n1. Khi khởi động, pi quét các vị trí kỹ năng và trích xuất tên cũng như mô tả\n2. Lời nhắc hệ thống bao gồm các kỹ năng có sẵn ở định dạng XML theo [specification](https://agentskills.io/integrate-skills)\n3. Khi một nhiệm vụ phù hợp, tác nhân sử dụng `read` để tải toàn bộ SKILL.md (các mô hình không phải lúc nào cũng thực hiện việc này; hãy sử dụng lời nhắc hoặc `/skill:name` để ép buộc)\n4. Tác nhân làm theo hướng dẫn, sử dụng đường dẫn tương đối để tham chiếu tập lệnh và nội dung\n\nĐây là sự tiết lộ tiến bộ: chỉ các mô tả luôn nằm trong ngữ cảnh, các hướng dẫn đầy đủ được tải theo yêu cầu.\n\n## Lệnh kỹ năng\n\nSkills đăng ký dưới dạng lệnh `/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\nCác đối số sau lệnh được thêm vào nội dung kỹ năng dưới dạng `User: <args>`.\n\nChuyển đổi các lệnh kỹ năng qua `/settings` ở chế độ tương tác hoặc trong `settings.json`:\n\n```json\n{\n  \"enableSkillCommands\": true\n}\n```\n\n## Cấu trúc kỹ năng\n\nKỹ năng là một thư mục có tệp `SKILL.md`. Mọi thứ khác đều ở dạng tự do.\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### Định dạng KỸ NĂNG.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 /path/to/skill && npm cài đặt\n```\n\n## Usage\n\n```bash\n./scripts/process.sh <input>\n```\n````\n\nSử dụng đường dẫn tương đối từ thư mục kỹ năng:\n\n```markdown\nSee [the reference guide](references/REFERENCE.md) for details.\n```\n\n## tiền tuyến\n\nTheo [Agent Skills specification](https://agentskills.io/specification#frontmatter-required):\n\n| Cánh đồng | Yêu cầu | Sự miêu tả |\n|-------|----------|-------------|\n| `name` | Đúng | Tối đa 64 ký tự. Chữ thường a-z, 0-9, dấu gạch nối. Không giống như tiêu chuẩn, Pi không yêu cầu điều này phải khớp với thư mục mẹ vì yêu cầu tiêu chuẩn đó là chưa tối ưu cho các thư mục kỹ năng dùng chung. |\n| `description` | Đúng | Tối đa 1024 ký tự. Kỹ năng này dùng để làm gì và khi nào nên sử dụng nó. |\n| `license` | KHÔNG | Tên giấy phép hoặc tham chiếu đến tệp được đóng gói. |\n| `compatibility` | KHÔNG | Tối đa 500 ký tự. Yêu cầu về môi trường. |\n| `metadata` | KHÔNG | Ánh xạ khóa-giá trị tùy ý. |\n| `allowed-tools` | KHÔNG | Danh sách các công cụ được phê duyệt trước được phân cách bằng dấu cách (thử nghiệm). |\n| `disable-model-invocation` | KHÔNG | Khi `true`, kỹ năng bị ẩn khỏi lời nhắc hệ thống. Người dùng phải sử dụng `/skill:name`. |\n\n### Quy tắc đặt tên\n\n- 1-64 ký tự\n- Chỉ chữ cái viết thường, số, dấu gạch nối\n- Không có dấu gạch nối đầu/cuối\n- Không có dấu gạch nối liên tiếp\nPi không yêu cầu tên phải khớp với thư mục mẹ. Tiêu chuẩn Tác nhân Skills có, nhưng yêu cầu đó chưa tối ưu cho các thư mục kỹ năng dùng chung được nhiều công cụ sử dụng.\n\nHợp lệ: `pdf-processing`, `data-analysis`, `code-review`\nKhông hợp lệ: `PDF-Processing`, `-pdf`, `pdf--processing`\n\n### Mô tả Các phương pháp hay nhất\n\nMô tả xác định thời điểm tác nhân tải kỹ năng. Hãy cụ thể.\n\nTốt:\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\nNghèo:\n```yaml\ndescription: Helps with PDFs.\n```\n\n## Xác thực\n\nPi xác nhận các kỹ năng theo tiêu chuẩn của Đại lý Skills. Hầu hết các vấn đề đều đưa ra cảnh báo nhưng vẫn tải kỹ năng:\n\n- Tên vượt quá 64 ký tự hoặc chứa ký tự không hợp lệ\n- Tên bắt đầu/kết thúc bằng dấu gạch nối hoặc có dấu gạch nối liên tiếp\n- Mô tả vượt quá 1024 ký tự\n\nCác trường vật chất phía trước không xác định sẽ bị bỏ qua.\n\n**Ngoại lệ:** Skills thiếu mô tả sẽ không được tải.\n\nVa chạm tên (cùng tên từ các vị trí khác nhau) cảnh báo và giữ kỹ năng đầu tiên được tìm thấy.\n\n## Ví dụ\n\n```\nbrave-search/\n├── SKILL.md\n├── search.js\n└── content.js\n```\n\n**KỸ NĂNG.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 cài đặt\n```\n\n## Search\n\n```bash\n./search.js \"truy vấn\" # Tìm kiếm cơ bản\n./search.js \"query\" --content # Bao gồm nội dung trang\n```\n\n## Extract Page Content\n\n```bash\n./content.js https://example.com\n```\n````\n\n## Kho kỹ năng\n\n- [Anthropic Skills](https://github.com/anthropics/skills) - Xử lý tài liệu (docx, pdf, pptx, xlsx), phát triển web\n- [Pi Skills](https://github.com/badlogic/pi-skills) - Tìm kiếm trên web, tự động hóa trình duyệt, Google APIs, phiên âm","sourceFile":"skills.md"},"terminal-setup":{"title":"Thiết lập thiết bị đầu cuối","markdown":"Pi sử dụng [Kitty keyboard protocol](https://sw.kovidgoyal.net/kitty/keyboard-protocol/) để phát hiện khóa bổ trợ đáng tin cậy. Hầu hết các thiết bị đầu cuối hiện đại đều hỗ trợ giao thức này, nhưng một số yêu cầu cấu hình.\n\n## Kitty, iTerm2\n\nLàm việc ra khỏi hộp.\n\n## Nhà ga táo\n\nPi cho phép báo cáo khóa nâng cao khi có sẵn. Nếu Terminal.app vẫn gửi Return đơn giản cho `Shift+Enter`, pi sẽ sử dụng dự phòng công cụ sửa đổi macOS cục bộ để coi Return đó là `Shift+Enter`.\n\nDự phòng này chỉ hoạt động khi pi chạy trên cùng máy Mac với Terminal.app. Nó không thể phát hiện bàn phím cục bộ qua điều khiển từ xa SSH.\n\n## ma quái\n\nThêm vào cấu hình Ghostty của bạn (`~/Library/Application Support/com.mitchellh.ghostty/config` trên macOS, `~/.config/ghostty/config` trên Linux):\n\n```\nkeybind = alt+backspace=text:\\x1b\\x7f\n```\n\nCác phiên bản Mã Claude cũ hơn có thể đã thêm ánh xạ Ghostty này:\n\n```\nkeybind = shift+enter=text:\\n\n```\n\nÁnh xạ đó sẽ gửi một byte nguồn cấp dữ liệu thô. Bên trong pi, không thể phân biệt được với `Ctrl+J`, vì vậy tmux và pi không còn nhìn thấy sự kiện quan trọng `shift+enter` thực sự nữa.\n\nNếu Mã Claude 2.x hoặc mới hơn là lý do duy nhất khiến bạn thêm ánh xạ đó thì bạn có thể xóa nó, trừ khi bạn muốn sử dụng Mã Claude trong tmux, nơi nó vẫn yêu cầu ánh xạ Ghostty đó.\n\nPi liên kết `Ctrl+J` làm bí danh dòng mới mặc định, vì vậy `Shift+Enter` tiếp tục hoạt động trong tmux thông qua bản sửa lại đó mà không cần cấu hình pi bổ sung.\n\n## WezTerm\n\nWezTerm thường hoạt động tốt với `Shift+Enter` thông qua xterm modOtherKeys. Để sử dụng giao thức bàn phím Kitty một cách rõ ràng, hãy tạo `~/.wezterm.lua`:\n\n```lua\nlocal wezterm = require 'wezterm'\nlocal config = wezterm.config_builder()\nconfig.enable_kitty_keyboard = true\nreturn config\n```\n\nTrên macOS, WezTerm liên kết `Option+Enter` với toàn màn hình theo mặc định. Để sử dụng `Option+Enter` cho hàng đợi theo dõi pi, hãy thêm ghi đè khóa này:\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\nNếu bạn đã có bảng `config.keys`, hãy thêm mục nhập vào bảng đó.\n\nTrên WSL, WezTerm có thể yêu cầu con trỏ phần cứng hiển thị để định vị cửa sổ ứng cử viên IME. Nếu ứng viên CJK IME không đi theo con trỏ văn bản, hãy đặt `PI_HARDWARE_CURSOR=1` trước khi chạy pi hoặc đặt `showHardwareCursor` thành `true` trong cài đặt.\n\n## nhanh nhẹn\n\nAlacritty thường hoạt động hiệu quả với giá `Shift+Enter`. Trên macOS, `Option+Enter` có thể xuất hiện dưới dạng `Enter` đơn giản. Để sử dụng `Option+Enter` cho hàng đợi theo dõi pi, hãy thêm vào `~/.config/alacritty/alacritty.toml`:\n\n```toml\n[[keyboard.bindings]]\nkey = \"Enter\"\nmods = \"Alt\"\nchars = \"\\u001b[13;3u\"\n```\n\nKhởi động lại Alacritty sau khi thay đổi cấu hình.\n\n## Mã VS (Thiết bị đầu cuối tích hợp)\n\nTheo mặc định, Mã VS 1.109.5 trở lên kích hoạt giao thức bàn phím Kitty trong thiết bị đầu cuối tích hợp, vì vậy `Shift+Enter` sẽ hoạt động tốt.\n\nCác phiên bản Mã VS cũ hơn 1.109.5 cần có liên kết phím đầu cuối rõ ràng cho `Shift+Enter`.\n\n`keybindings.json` địa điểm:\n- macOS: `~/Library/Application Support/Code/User/keybindings.json`\n- Linux: `~/.config/Code/User/keybindings.json`\n- Cửa sổ: `%APPDATA%\\\\Code\\\\User\\\\keybindings.json`\n\nThêm vào `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## Thiết bị đầu cuối Windows\n\nThêm vào `settings.json` (Ctrl+Shift+ hoặc Cài đặt → Mở tệp JSON) để chuyển tiếp các phím Enter đã sửa đổi mà pi sử dụng:\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` chèn một dòng mới.\n- Theo mặc định, Windows Terminal liên kết `Alt+Enter` với toàn màn hình. Điều đó ngăn pi nhận `Alt+Enter` để xếp hàng tiếp theo.\n- Thay vào đó, việc ánh xạ lại `Alt+Enter` thành `sendInput` sẽ chuyển tiếp hợp âm phím thực tới pi.\n\nNếu bạn đã có mảng `actions`, hãy thêm các đối tượng vào đó. Nếu hiện tượng toàn màn hình cũ vẫn tồn tại, hãy đóng hoàn toàn và mở lại Windows Terminal.\n\n## xfce4-terminal, thiết bị đầu cuối\n\nCác thiết bị đầu cuối này có hỗ trợ trình tự thoát hạn chế. Không thể phân biệt được các phím Enter đã sửa đổi như `Ctrl+Enter` và `Shift+Enter` với `Enter` thông thường, ngăn không cho các phím bấm tùy chỉnh như `submit: [\"ctrl+enter\"]` hoạt động.\n\nĐể có trải nghiệm tốt nhất, hãy sử dụng thiết bị đầu cuối hỗ trợ giao thức bàn phím 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) (yêu cầu biên dịch với hỗ trợ giao thức Kitty)\n\n## IntelliJ IDEA (Thiết bị đầu cuối tích hợp)\n\nThiết bị đầu cuối tích hợp có hỗ trợ trình tự thoát hạn chế. Shift+Enter không thể được phân biệt với Enter trong thiết bị đầu cuối của IntelliJ.\n\nNếu bạn muốn hiển thị con trỏ phần cứng, hãy đặt `PI_HARDWARE_CURSOR=1` trước khi chạy pi (mặc định bị tắt để tương thích).\n\nHãy cân nhắc sử dụng trình mô phỏng thiết bị đầu cuối chuyên dụng để có trải nghiệm tốt nhất.","sourceFile":"terminal-setup.md"},"termux":{"title":"Thiết lập Termux (Android)","markdown":"Pi chạy trên Android thông qua [Termux](https://termux.dev/), trình mô phỏng thiết bị đầu cuối và môi trường Linux dành cho Android.\n\n## Điều kiện tiên quyết\n\n1. Cài đặt [Termux](https://github.com/termux/termux-app#installation) từ GitHub hoặc F-Droid (không phải Google Play, phiên bản đó không được dùng nữa)\n2. Cài đặt [Termux:API](https://github.com/termux/termux-api#installation) từ GitHub hoặc F-Droid cho clipboard và tích hợp thiết bị khác\n\n## Cài đặt\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## Hỗ trợ bảng nhớ tạm\n\nCác thao tác trong bảng nhớ tạm sử dụng `termux-clipboard-set` và `termux-clipboard-get` khi chạy trong Termux. Ứng dụng Termux:API phải được cài đặt để ứng dụng này hoạt động.\n\nBảng nhớ tạm hình ảnh không được hỗ trợ trên Termux (tính năng dán hình ảnh `ctrl+v` sẽ không hoạt động).\n\n## Ví dụ AGENTS.md cho Termux\n\nTạo `~/.pi/agent/AGENTS.md` để giúp tổng đài viên hiểu được môi trường 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 # Mở bằng ứng dụng mặc định\ntermux-open --chooser image.jpg # Chọn ứng dụng\n```\n\n## Clipboard\n```bash\ntermux-clipboard-set \"text\" # Sao chép\ntermux-clipboard-get # Dán\n```\n\n## Notifications\n```bash\ntermux-thông báo -t \"Tiêu đề\" -c \"Nội dung\"\n```\n\n## Device Info\n```bash\ntermux-pin-status # Thông tin về pin\ntermux-wifi-connectioninfo # Thông tin WiFi\ntermux-telephony-deviceinfo # Thông tin thiết bị\n```\n\n## Sharing\n```bash\ntermux-share -a gửi file.txt # Chia sẻ tập tin\n```\n\n## Other Useful Commands\n```bash\n\"tin nhắn\" termux-toast # Cửa sổ bật lên bánh mì nướng nhanh\ntermux-vibrate # Rung thiết bị\ntermux-tts-speak \"hello\" # Chuyển văn bản thành giọng nói\ntermux-máy ảnh-photo out.jpg # Chụp ảnh\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## Hạn chế\n\n- **Không có khay nhớ tạm hình ảnh**: Termux khay nhớ tạm API chỉ hỗ trợ văn bản\n- **Không có tệp nhị phân gốc**: Một số phần phụ thuộc gốc tùy chọn (như mô-đun clipboard) không có sẵn trên Android ARM64 và bị bỏ qua trong quá trình cài đặt\n- **Quyền truy cập bộ nhớ**: Để truy cập các tệp trong `/storage/emulated/0` (Tải xuống, v.v.), hãy chạy `termux-setup-storage` một lần để cấp quyền\n\n## Khắc phục sự cố\n\n### Bảng nhớ tạm không hoạt động\n\nĐảm bảo cả hai ứng dụng đều được cài đặt:\n1. Termux (từ GitHub hoặc F-Droid)\n2. Termux:API (từ GitHub hoặc F-Droid)\n\nSau đó cài đặt công cụ CLI:\n```bash\npkg install termux-api\n```\n\n### Quyền bị từ chối đối với bộ nhớ dùng chung\n\nChạy một lần để cấp quyền lưu trữ:\n```bash\ntermux-setup-storage\n```\n\n### Node.js vấn đề cài đặt\n\nNếu npm không thành công, hãy thử xóa bộ nhớ đệm:\n```bash\nnpm cache clean --force\n```","sourceFile":"termux.md"},"themes":{"title":"Chủ đề","markdown":"> pi có thể tạo chủ đề. Yêu cầu nó xây dựng một cái cho thiết lập của bạn.\n\n\nChủ đề là các tệp JSON xác định màu cho TUI.\n\n## Mục lục\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## Địa điểm\n\nPi tải chủ đề từ:\n\n- Tích hợp sẵn: `dark`, `light`\n- Toàn cầu: `~/.pi/agent/themes/*.json`\n- Dự án: `.pi/themes/*.json` (chỉ sau khi dự án được tin cậy)\n- Các gói: `themes/` thư mục hoặc `pi.themes` mục trong `package.json`\n- Cài đặt: mảng `themes` với các tệp hoặc thư mục\n- CLI: `--theme <path>` (có thể lặp lại)\n\nTắt tính năng khám phá bằng `--no-themes`.\n\n## Chọn một chủ đề\n\nChọn chủ đề qua `/settings` hoặc trong `settings.json`:\n\n```json\n{\n  \"theme\": \"my-theme\"\n}\n```\n\nTrong lần chạy đầu tiên, pi phát hiện nền thiết bị đầu cuối của bạn và đặt mặc định là `dark` hoặc `light`.\n\n## Tạo một chủ đề tùy chỉnh\n\n1. Tạo một tập tin chủ đề:\n\n```bash\nmkdir -p ~/.pi/agent/themes\nvim ~/.pi/agent/themes/my-theme.json\n```\n\n2. Xác định chủ đề với tất cả các màu được yêu cầu (xem [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. Chọn chủ đề qua `/settings`.\n\n**Tải lại nóng:** Khi bạn chỉnh sửa tệp chủ đề tùy chỉnh hiện đang hoạt động, pi sẽ tự động tải lại tệp đó để có phản hồi trực quan ngay lập tức.\n\n## Định dạng chủ đề\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` là bắt buộc, phải là duy nhất và không được chứa `/`.\n- `vars` là tùy chọn. Xác định các màu có thể sử dụng lại ở đây, sau đó tham chiếu chúng trong `colors`.\n- `colors` phải xác định tất cả 51 mã thông báo bắt buộc. `thinkingMax` là tùy chọn và quay trở lại `thinkingXhigh`; `scrollbarThumb` là tùy chọn và quay trở lại `selectedBg`.\n\nTrường `$schema` cho phép tự động hoàn thành và xác thực trình chỉnh sửa.\n\n## Mã thông báo màu\n\nMỗi chủ đề phải xác định tất cả 51 mã thông báo màu bắt buộc. `thinkingMax` và `scrollbarThumb` là tùy chọn để tương thích với các chủ đề hiện có; khi bị bỏ qua, họ sử dụng `thinkingXhigh` và `selectedBg` tương ứng.\n\n### Giao diện người dùng cốt lõi (11 màu)\n\n| Mã thông báo | Mục đích |\n|-------|---------|\n| `accent` | Giọng chính (logo, mục đã chọn, con trỏ) |\n| `border` | Đường viền bình thường |\n| `borderAccent` | Đường viền nổi bật |\n| `borderMuted` | Đường viền tinh tế (biên tập) |\n| `success` | Trạng thái thành công |\n| `error` | Trạng thái lỗi |\n| `warning` | Trạng thái cảnh báo |\n| `muted` | Văn bản phụ |\n| `dim` | văn bản cấp ba |\n| `text` | Văn bản mặc định (thường là `\"\"`) |\n| `thinkingText` | Văn bản khối suy nghĩ |\n\n### Hình nền & Nội dung (11 bắt buộc, 1 tùy chọn)\n\n| Mã thông báo | Mục đích |\n|-------|---------|\n| `selectedBg` | Nền dòng được chọn |\n| `scrollbarThumb` | Nền ngón tay cái của thanh cuộn toàn màn hình; tùy chọn, quay trở lại `selectedBg` |\n| `userMessageBg` | Nền tin nhắn của người dùng |\n| `userMessageText` | Văn bản tin nhắn của người dùng |\n| `customMessageBg` | Nền tin nhắn mở rộng |\n| `customMessageText` | Văn bản tin nhắn mở rộng |\n| `customMessageLabel` | Nhãn tin nhắn mở rộng |\n| `toolPendingBg` | Hộp công cụ (đang chờ xử lý) |\n| `toolSuccessBg` | Hộp công cụ (thành công) |\n| `toolErrorBg` | Hộp công cụ (lỗi) |\n| `toolTitle` | Tiêu đề công cụ |\n| `toolOutput` | Văn bản đầu ra của công cụ |\n\n### Markdown (10 màu)\n\n| Mã thông báo | Mục đích |\n|-------|---------|\n| `mdHeading` | Tiêu đề |\n| `mdLink` | Liên kết văn bản |\n| `mdLinkUrl` | URL liên kết |\n| `mdCode` | Mã nội tuyến |\n| `mdCodeBlock` | Nội dung khối mã |\n| `mdCodeBlockBorder` | Hàng rào khối mã |\n| `mdQuote` | văn bản trích dẫn |\n| `mdQuoteBorder` | Đường viền trích dẫn |\n| `mdHr` | Quy tắc ngang |\n| `mdListBullet` | Danh sách đạn |\n\n### Công cụ khác biệt (3 màu)\n\n| Mã thông báo | Mục đích |\n|-------|---------|\n| `toolDiffAdded` | Đã thêm dòng |\n| `toolDiffRemoved` | Dòng đã xóa |\n| `toolDiffContext` | Dòng ngữ cảnh |\n\n### Đánh dấu cú pháp (9 màu)\n\n| Mã thông báo | Mục đích |\n|-------|---------|\n| `syntaxComment` | Bình luận |\n| `syntaxKeyword` | Từ khóa |\n| `syntaxFunction` | Tên hàm |\n| `syntaxVariable` | Biến |\n| `syntaxString` | Dây |\n| `syntaxNumber` | số |\n| `syntaxType` | Các loại |\n| `syntaxOperator` | Toán tử |\n| `syntaxPunctuation` | Dấu câu |\n\n### Biên giới cấp độ tư duy (6 bắt buộc, 1 tùy chọn)\n\nMàu sắc đường viền của trình soạn thảo biểu thị mức độ tư duy (phân cấp trực quan từ tinh tế đến nổi bật):\n\n| Mã thông báo | Mục đích |\n|-------|---------|\n| `thinkingOff` | Đang suy nghĩ |\n| `thinkingMinimal` | Suy nghĩ tối thiểu |\n| `thinkingLow` | Suy nghĩ thấp |\n| `thinkingMedium` | Tư duy trung bình |\n| `thinkingHigh` | Tư duy cao |\n| `thinkingXhigh` | Tư duy siêu cao |\n| `thinkingMax` | Tư duy tối đa; tùy chọn, quay trở lại `thinkingXhigh` |\n\n### Chế độ Bash (1 màu)\n\n| Mã thông báo | Mục đích |\n|-------|---------|\n| `bashMode` | Đường viền biên tập ở chế độ bash (tiền tố `!`) |\n\n### Xuất HTML (tùy chọn)\n\nPhần `export` kiểm soát màu sắc cho đầu ra HTML `/export`. Nếu bị bỏ qua, màu sắc sẽ bắt nguồn từ `userMessageBg`.\n\n```json\n{\n  \"export\": {\n    \"pageBg\": \"#18181e\",\n    \"cardBg\": \"#1e1e24\",\n    \"infoBg\": \"#3c3728\"\n  }\n}\n```\n\n## Giá trị màu\n\nBốn định dạng được hỗ trợ:\n\n| Định dạng | Ví dụ | Sự miêu tả |\n|--------|---------|-------------|\n| lục giác | `\"#ff0000\"` | RGB 6 chữ số |\n| 256 màu | `39` | chỉ số bảng màu xterm 256 màu (0-255) |\n| Biến | `\"primary\"` | Tham chiếu đến mục `vars` |\n| Mặc định | `\"\"` | Màu mặc định của Terminal |\n\n### Bảng màu 256 màu\n\n- `0-15`: Màu ANSI cơ bản (phụ thuộc vào thiết bị đầu cuối)\n- `16-231`: Khối lập phương RGB 6×6×6 (`16 + 36×R + 6×G + B` trong đó R,G,B là 0-5)\n- `232-255`: Đường dốc thang độ xám\n\n### Khả năng tương thích thiết bị đầu cuối\n\nPi sử dụng màu RGB 24 bit. Hầu hết các thiết bị đầu cuối hiện đại đều hỗ trợ điều này (iTerm2, Kitty, WezTerm, Windows Terminal, VS Code). Đối với các thiết bị đầu cuối cũ hơn chỉ hỗ trợ 256 màu, pi sẽ quay trở lại giá trị gần đúng nhất.\n\nKiểm tra hỗ trợ truecolor:\n\n```bash\necho $COLORTERM  # Should output \"truecolor\" or \"24bit\"\n```\n\n## Mẹo\n\n**Cực tối:** Sử dụng màu sáng, bão hòa với độ tương phản cao hơn.\n\n**Cực sáng:** Sử dụng màu tối hơn, tắt tiếng với độ tương phản thấp hơn.\n\n**Sự hài hòa về màu sắc:** Bắt đầu với bảng màu cơ bản (Nord, Gruvbox, Tokyo Night), xác định bảng màu đó trong `vars` và tham chiếu một cách nhất quán.\n\n**Thử nghiệm:** Kiểm tra chủ đề của bạn bằng các loại thông báo, trạng thái công cụ, nội dung đánh dấu và văn bản được bao bọc dài khác nhau.\n\n**Mã VS:** Đặt `terminal.integrated.minimumContrastRatio` thành `1` để có màu chính xác.\n\n## Ví dụ\n\nXem các chủ đề tích hợp:\n- [dark.json](../src/modes/interactive/theme/dark.json)\n- [light.json](../src/modes/interactive/theme/light.json)","sourceFile":"themes.md"},"tmux":{"title":"tmux Thiết lập","markdown":"Pi hoạt động bên trong tmux, nhưng tmux loại bỏ thông tin sửa đổi khỏi một số phím nhất định theo mặc định. Nếu không có cấu hình, `Shift+Enter` và `Ctrl+Enter` thường không thể phân biệt được với `Enter` thông thường.\n\n## Cấu hình đề xuất\n\nThêm vào `~/.tmux.conf`:\n\n```tmux\nset -g extended-keys on\nset -g extended-keys-format csi-u\n```\n\nSau đó khởi động lại đầy đủ tmux:\n\n```bash\ntmux kill-server\ntmux\n```\n\nPi tự động yêu cầu báo cáo khóa mở rộng khi không có giao thức bàn phím Kitty. Với `extended-keys-format csi-u`, tmux chuyển tiếp các khóa được sửa đổi ở định dạng CSI-u, đây là cấu hình đáng tin cậy nhất. Tùy chọn `extended-keys-format` yêu cầu tmux 3.5 trở lên.\n\n## Tại sao `csi-u` được khuyến nghị\n\nChỉ với:\n\n```tmux\nset -g extended-keys on\n```\n\ntmux mặc định là `extended-keys-format xterm`. Khi ứng dụng yêu cầu báo cáo khóa mở rộng, các khóa đã sửa đổi sẽ được chuyển tiếp ở định dạng xterm `modifyOtherKeys`, chẳng hạn như:\n\n- `Ctrl+C` → `\\x1b[27;5;99~`\n- `Ctrl+D` → `\\x1b[27;5;100~`\n- `Ctrl+Enter` → `\\x1b[27;5;13~`\n\nVới `extended-keys-format csi-u`, các phím tương tự được chuyển tiếp dưới dạng:\n\n- `Ctrl+C` → `\\x1b[99;5u`\n- `Ctrl+D` → `\\x1b[100;5u`\n- `Ctrl+Enter` → `\\x1b[13;5u`\n\nPi hỗ trợ cả hai định dạng, nhưng `csi-u` là thiết lập tmux được đề xuất.\n\n## Cái này sửa cái gì\n\nNếu không có tmux khóa mở rộng, các phím Enter đã sửa đổi sẽ thu gọn thành các chuỗi cũ:\n\n| Chìa khóa | Không có phím mở rộng | Với `csi-u` |\n|-----|-----------------|--------------|\n| Đi vào | `\\r` | `\\r` |\n| Shift+Enter | `\\r` | `\\x1b[13;2u` |\n| Ctrl+Enter | `\\r` | `\\x1b[13;5u` |\n| Alt/Tùy chọn+Enter | `\\x1b\\r` | `\\x1b[13;3u` |\n\nĐiều này ảnh hưởng đến các tổ hợp phím mặc định (`Enter` để gửi, `Shift+Enter` cho dòng mới) và mọi tổ hợp phím tùy chỉnh bằng cách sử dụng Enter đã sửa đổi.\n\n## Yêu cầu\n\n- tmux 3,5 trở lên cho `extended-keys-format csi-u` (chạy `tmux -V` để kiểm tra)\n- Trình mô phỏng thiết bị đầu cuối hỗ trợ các khóa mở rộng (Ghostty, Kitty, iTerm2, WezTerm, Windows Terminal)\n\nVới tmux 3.2 đến 3.4, bỏ qua `extended-keys-format csi-u`; Pi vẫn hỗ trợ định dạng xterm `modifyOtherKeys` mặc định của tmux.","sourceFile":"tmux.md"},"tui":{"title":"TUI Thành phần","markdown":"> pi có thể tạo các thành phần TUI. Yêu cầu nó xây dựng một cái cho trường hợp sử dụng của bạn.\n\n\nExtensions và các công cụ tùy chỉnh có thể hiển thị các thành phần TUI tùy chỉnh cho giao diện người dùng tương tác. Trang này bao gồm hệ thống thành phần và các khối xây dựng có sẵn.\n\n**Nguồn:** [`@earendil-works/pi-tui`](https://github.com/earendil-works/pi-mono/tree/main/packages/tui)\n\n## Giao diện thành phần\n\nTất cả các thành phần thực hiệ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| Phương pháp | Sự miêu tả |\n|--------|-------------|\n| `render(width)` | Trả về mảng các chuỗi (mỗi chuỗi một dòng). Mỗi dòng **không được vượt quá `width`**. |\n| `handleInput?(data)` | Nhận thông tin đầu vào từ bàn phím khi thành phần được lấy tiêu điểm. |\n| `wantsKeyRelease?` | Nếu đúng, thành phần sẽ nhận được các sự kiện phát hành khóa (giao thức Kitty). Mặc định: sai. |\n| `invalidate()` | Xóa trạng thái hiển thị được lưu trong bộ nhớ đệm. Kêu gọi thay đổi chủ đề. |\n\nTUI gắn thêm thiết lập lại SGR đầy đủ và thiết lập lại OSC 8 ở cuối mỗi dòng được hiển thị. Phong cách không vượt qua các dòng. Nếu bạn phát ra văn bản nhiều dòng có kiểu dáng, hãy áp dụng lại kiểu trên mỗi dòng hoặc sử dụng `wrapTextWithAnsi()` để kiểu được giữ nguyên cho mỗi dòng được ngắt dòng.\n\n## Giao diện có thể lấy nét (Hỗ trợ IME)\n\nCác thành phần hiển thị con trỏ văn bản và cần hỗ trợ IME (Trình chỉnh sửa phương thức nhập) phải triển khai giao diện `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\nKhi thành phần `Focusable` có tiêu điểm, TUI:\n1. Đặt `focused = true` trên thành phần\n2. Quét kết quả đầu ra được hiển thị để tìm `CURSOR_MARKER` (chuỗi thoát APC có độ rộng bằng 0)\n3. Định vị con trỏ đầu cuối phần cứng tại vị trí đó\n4. Chỉ hiển thị con trỏ phần cứng khi `showHardwareCursor` được bật\n\nCon trỏ vẫn bị ẩn theo mặc định. Điều này giữ cho kết xuất con trỏ giả trong khi vẫn định vị con trỏ phần cứng cho các thiết bị đầu cuối theo dõi các cửa sổ ứng cử viên IME có con trỏ ẩn. Một số thiết bị đầu cuối yêu cầu con trỏ phần cứng hiển thị để định vị IME; kích hoạt nó bằng `showHardwareCursor`, `setShowHardwareCursor(true)` hoặc `PI_HARDWARE_CURSOR=1`. Các thành phần tích hợp `Editor` và `Input` đã triển khai giao diện này.\n\n### Thành phần vùng chứa có đầu vào được nhúng\n\nKhi một thành phần vùng chứa (hộp thoại, bộ chọn, v.v.) chứa phần tử con `Input` hoặc `Editor`, thì vùng chứa đó phải triển khai `Focusable` và truyền trạng thái tiêu điểm cho phần tử con đó. Nếu không, con trỏ phần cứng sẽ không được định vị chính xác cho đầu vào 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\nNếu không có sự lan truyền này, việc nhập bằng IME (tiếng Trung, tiếng Nhật, tiếng Hàn, v.v.) sẽ hiển thị cửa sổ ứng viên ở vị trí sai trên màn hình.\n\n## Sử dụng thành phần\n\n**Trong tiện ích mở rộng** qua `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**Trong các công cụ tùy chỉnh** qua `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## Lớp phủ\n\nLớp phủ hiển thị các thành phần trên nội dung hiện có mà không xóa màn hình. Chuyển `{ overlay: true }` đến `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Để định vị và định cỡ, hãy sử dụng `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### Tiêu điểm lớp phủ\n\nLớp phủ hiển thị tập trung giữ quyền sở hữu đầu vào trên giao diện người dùng không có lớp phủ tạm thời. Nếu lớp phủ mở một thành phần `ctx.ui.custom()` khác không có `{ overlay: true }`, thì giao diện người dùng thay thế đó sẽ nhận được đầu vào khi nó đang hoạt động; khi nó đóng lại, lớp phủ tập trung có thể lấy lại dữ liệu đầu vào.\n\nSử dụng `handle.unfocus()` khi lớp phủ hiển thị sẽ ngừng sở hữu đầu vào và để TUI quay trở lại lớp phủ chụp hiển thị khác hoặc mục tiêu tiêu điểm trước đó. Sử dụng `handle.unfocus({ target })` khi một thành phần cụ thể sẽ nhận được đầu vào trong khi lớp phủ vẫn hiển thị. Việc chuyển `{ target: null }` có ​​chủ ý không để lại thành phần nào được tập trung cho đến khi tiêu điểm được đặt lại.\n\n### Vòng đời lớp phủ\n\nCác thành phần lớp phủ được xử lý khi đóng. Không sử dụng lại tài liệu tham khảo - tạo phiên bản mới:\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\nXem [overlay-qa-tests.ts](../examples/extensions/overlay-qa-tests.ts) để biết các ví dụ toàn diện bao gồm neo, lề, xếp chồng, khả năng hiển thị phản hồi và hoạt ảnh.\n\n## Các thành phần tích hợp\n\nNhập từ `@earendil-works/pi-tui`:\n\n```typescript\nimport { Text, Box, Container, Spacer, Markdown } from \"@earendil-works/pi-tui\";\n```\n\n### Chữ\n\nVăn bản nhiều dòng có gói từ.\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### Hộp\n\nVùng chứa có phần đệm và màu 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### thùng chứa\n\nNhóm các thành phần con theo chiều dọc.\n\n```typescript\nconst container = new Container();\ncontainer.addChild(component1);\ncontainer.addChild(component2);\ncontainer.removeChild(component1);\n```\n\n### Miếng đệm\n\nKhông gian dọc trống rỗng.\n\n```typescript\nconst spacer = new Spacer(2);  // 2 empty lines\n```\n\n### Markdown\n\nHiển thị đánh dấu bằng cách tô sáng cú pháp.\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### Hình ảnh\n\nHiển thị hình ảnh trong các thiết bị đầu cuối được hỗ trợ (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## Đầu vào bàn phím\n\nSử dụng `matchesKey()` để phát hiện khóa:\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**Mã định danh khóa** (sử dụng `Key.*` để tự động hoàn thành hoặc chuỗi ký tự):\n- Các phím cơ bản: `Key.enter`, `Key.escape`, `Key.tab`, `Key.space`, `Key.backspace`, `Key.delete`, `Key.home`, `Key.end`\n- Phím mũi tên: `Key.up`, `Key.down`, `Key.left`, `Key.right`\n- Với các sửa đổi: `Key.ctrl(\"c\")`, `Key.shift(\"tab\")`, `Key.alt(\"left\")`, `Key.ctrlShift(\"p\")`\n- Định dạng chuỗi cũng hoạt động: `\"enter\"`, `\"ctrl+c\"`, `\"shift+tab\"`, `\"ctrl+shift+p\"`\n\n## Chiều rộng dòng\n\n**Quan trọng:** Mỗi dòng từ `render()` không được vượt quá tham số `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\nTiện ích:\n- `visibleWidth(str)` - Nhận chiều rộng hiển thị (bỏ qua mã ANSI)\n- `truncateToWidth(str, width, ellipsis?)` - Cắt ngắn bằng dấu chấm lửng tùy chọn\n- `wrapTextWithAnsi(str, width)` - Gói từ bảo toàn mã ANSI\n\n## Tạo thành phần tùy chỉnh\n\nVí dụ: Bộ chọn tương tác\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\nCách sử dụng trong tiện ích mở rộng:\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## Chủ đề\n\nCác thành phần chấp nhận các đối tượng chủ đề để tạo kiểu.\n\n**Trong `renderCall`/`renderResult`**, sử dụng tham số `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**Màu nền trước** (`theme.fg(color, text)`):\n\n| Loại | Màu sắc |\n|----------|--------|\n| Tổng quan | `text`, `accent`, `muted`, `dim` |\n| Trạng thái | `success`, `error`, `warning` |\n| Biên giới | `border`, `borderAccent`, `borderMuted` |\n| Tin nhắn | `userMessageText`, `customMessageText`, `customMessageLabel` |\n| Công cụ | `toolTitle`, `toolOutput` |\n| Khác biệt | `toolDiffAdded`, `toolDiffRemoved`, `toolDiffContext` |\n| Markdown | `mdHeading`, `mdLink`, `mdLinkUrl`, `mdCode`, `mdCodeBlock`, `mdCodeBlockBorder`, `mdQuote`, `mdQuoteBorder`, `mdHr`, `mdListBullet` |\n| Cú pháp | `syntaxComment`, `syntaxKeyword`, `syntaxFunction`, `syntaxVariable`, `syntaxString`, `syntaxNumber`, `syntaxType`, `syntaxOperator`, `syntaxPunctuation` |\n| suy nghĩ | `thinkingOff`, `thinkingMinimal`, `thinkingLow`, `thinkingMedium`, `thinkingHigh`, `thinkingXhigh`, `thinkingMax` |\n| Chế độ | `bashMode` |\n\n**Màu nền** (`theme.bg(color, text)`):\n\n`selectedBg`, `userMessageBg`, `customMessageBg`, `toolPendingBg`, `toolSuccessBg`, `toolErrorBg`\n\n**Đối với Markdown**, sử dụng `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**Đối với các thành phần tùy chỉnh**, hãy xác định giao diện chủ đề của riêng bạn:\n\n```typescript\ninterface MyTheme {\n  selected: (s: string) => string;\n  normal: (s: string) => string;\n}\n```\n\n## Ghi nhật ký gỡ lỗi\n\nĐặt `PI_TUI_WRITE_LOG` để ghi lại luồng ANSI thô được ghi vào stdout.\n\n```bash\nPI_TUI_WRITE_LOG=/tmp/tui-ansi.log npx tsx packages/tui/test/chat-simple.ts\n```\n\n## Hiệu suất\n\nĐầu ra được hiển thị bằng bộ nhớ đệm khi có thể:\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\nGọi `invalidate()` khi trạng thái thay đổi, sau đó sử dụng `tui.requestRender()` được chèn để kích hoạt kết xuất lại.\n\n## Vô hiệu hóa và thay đổi chủ đề\n\nKhi chủ đề thay đổi, TUI gọi `invalidate()` trên tất cả các thành phần để xóa bộ nhớ đệm của chúng. Các thành phần phải triển khai đúng `invalidate()` để đảm bảo các thay đổi về chủ đề có hiệu lực.\n\n### Vấn đề\n\nNếu một thành phần nướng trước màu chủ đề thành chuỗi (thông qua `theme.fg()`, `theme.bg()`, v.v.) và lưu chúng vào bộ nhớ đệm thì các chuỗi được lưu trong bộ nhớ đệm sẽ chứa mã thoát ANSI từ chủ đề cũ. Chỉ xóa bộ đệm kết xuất là không đủ nếu thành phần lưu trữ nội dung theo chủ đề riêng biệt.\n\n**Cách tiếp cận sai** (màu chủ đề sẽ không cập nhật):\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### Giải pháp\n\nCác thành phần xây dựng nội dung có màu chủ đề phải xây dựng lại nội dung đó khi `invalidate()` được gọi:\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### Mẫu: Xây dựng lại khi không hợp lệ\n\nĐối với các thành phần có nội dung phức tạp:\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### Khi điều này quan trọng\n\nMẫu này là cần thiết khi:\n\n1. **Màu chủ đề chuẩn bị trước** - Sử dụng `theme.fg()` hoặc `theme.bg()` để tạo các chuỗi theo kiểu được lưu trữ trong các thành phần con\n2. **Đánh dấu cú pháp** - Sử dụng `highlightCode()` áp dụng màu cú pháp dựa trên chủ đề\n3. **Bố cục phức tạp** - Xây dựng cây thành phần con nhúng màu chủ đề\n\nMẫu này KHÔNG cần thiết khi:\n\n1. **Sử dụng lệnh gọi lại chủ đề** - Truyền các hàm như `(text) => theme.fg(\"accent\", text)` được gọi trong khi kết xuất\n2. **Vùng chứa đơn giản** - Chỉ nhóm các thành phần khác mà không thêm nội dung theo chủ đề\n3. **Kết xuất không trạng thái** - Kết quả tính toán theo chủ đề mới trong mỗi cuộc gọi `render()` (không có bộ nhớ đệm)\n\n## Các mẫu chung\n\nCác mẫu này đáp ứng các nhu cầu giao diện người dùng phổ biến nhất trong tiện ích mở rộng. **Sao chép các mẫu này thay vì xây dựng từ đầu.**\n\n### Mẫu 1: Hộp thoại lựa chọn (SelectList)\n\nĐể cho phép người dùng chọn từ danh sách các tùy chọn. Sử dụng `SelectList` từ `@earendil-works/pi-tui` với `DynamicBorder` để đóng khung.\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**Ví dụ:** [preset.ts](../examples/extensions/preset.ts), [tools.ts](../examples/extensions/tools.ts)\n\n### Mẫu 2: Thao tác không đồng bộ với tính năng Hủy (BorderedLoader)\n\nĐối với các hoạt động mất thời gian và có thể hủy được. `BorderedLoader` hiển thị một vòng quay và xử lý lối thoát để hủy.\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**Ví dụ:** [qna.ts](../examples/extensions/qna.ts), [handoff.ts](../examples/extensions/handoff.ts)\n\n### Mẫu 3: Cài đặt/Bật tắt (Danh sách cài đặt)\n\nĐể chuyển đổi nhiều cài đặt. Sử dụng `SettingsList` từ `@earendil-works/pi-tui` với `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**Ví dụ:** [tools.ts](../examples/extensions/tools.ts)\n\n### Mẫu 4: Chỉ báo trạng thái liên tục\n\nHiển thị trạng thái ở chân trang vẫn tồn tại trong các lần hiển thị. Tốt cho các chỉ số chế độ.\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**Ví dụ:** [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### Mẫu 4b: Tùy chỉnh chỉ báo hoạt động\n\nTùy chỉnh chỉ báo hoạt động nội tuyến được hiển thị trong khi pi đang truyền phát phản hồi.\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Điều này chỉ ảnh hưởng đến chỉ báo hoạt động phát trực tuyến bình thường. Bộ tải nén và thử lại vẫn giữ nguyên kiểu dáng tích hợp của chúng. Các khung tùy chỉnh được hiển thị nguyên văn nên các tiện ích mở rộng phải thêm màu riêng khi cần.\n\n**Ví dụ:** [working-indicator.ts](../examples/extensions/working-indicator.ts)\n\n### Mẫu 5: Widget Trên/Dưới Trình chỉnh sửa\n\nHiển thị nội dung liên tục ở trên hoặc bên dưới trình chỉnh sửa đầu vào. Tốt cho danh sách việc cần làm, tiến bộ.\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**Ví dụ:** [plan-mode/index.ts](../examples/extensions/plan-mode/index.ts)\n\n### Mẫu 6: Chân trang tùy chỉnh\n\nThay thế chân trang. `footerData` hiển thị dữ liệu mà các tiện ích mở rộng không thể truy cập được.\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\nSố liệu thống kê về mã thông báo có sẵn qua `ctx.sessionManager.getBranch()` và `ctx.model`.\n\n**Ví dụ:** [custom-footer.ts](../examples/extensions/custom-footer.ts)\n\n### Mẫu 7: Trình chỉnh sửa tùy chỉnh (chế độ vim, v.v.)\n\nThay thế trình chỉnh sửa đầu vào chính bằng cách triển khai tùy chỉnh. Hữu ích cho việc chỉnh sửa phương thức (vim), các tổ hợp phím khác nhau (emacs) hoặc xử lý đầu vào chuyên dụng.\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**Các điểm chính:**\n\n- **Mở rộng `CustomEditor`** (không phải cơ sở `Editor`) để nhận các tổ hợp phím ứng dụng (thoát để hủy, ctrl+d để thoát, chuyển đổi mô hình, v.v.)\n- **Gọi `super.handleInput(data)`** để biết các phím bạn không xử lý\n- **Mẫu nhà máy**: `setEditorComponent` nhận được hàm nhà máy có `tui`, `theme` và `keybindings`\n- **Vượt qua `undefined`** để khôi phục trình chỉnh sửa mặc định: `ctx.ui.setEditorComponent(undefined)`\n\n**Ví dụ:** [modal-editor.ts](../examples/extensions/modal-editor.ts)\n\n## Quy tắc chính\n\n1. **Luôn sử dụng chủ đề từ lệnh gọi lại** - Không nhập trực tiếp chủ đề. Sử dụng `theme` từ lệnh gọi lại `ctx.ui.custom((tui, theme, keybindings, done) =>...)`.\n\n2. **Luôn nhập thông số màu DynamicBorder** - Viết `(s: string) => theme.fg(\"accent\", s)`, không phải `(s) => theme.fg(\"accent\", s)`.\n\n3. **Gọi tôi.requestRender() sau khi thay đổi trạng thái** - Trong `handleInput`, gọi `tui.requestRender()` sau khi cập nhật trạng thái.\n\n4. **Trả về đối tượng ba phương thức** - Các thành phần tùy chỉnh cần `{ render, invalidate, handleInput }`.\n\n5. **Sử dụng các thành phần hiện có** - `SelectList`, `SettingsList`, `BorderedLoader` bao gồm 90% trường hợp. Đừng xây dựng lại chúng.\n\n## Ví dụ\n\n- **Giao diện người dùng lựa chọn**: [examples/extensions/preset.ts](../examples/extensions/preset.ts) - SelectList với khung DynamicBorder\n- **Không đồng bộ với hủy**: [examples/extensions/qna.ts](../examples/extensions/qna.ts) - BorderedLoader cho cuộc gọi LLM\n- **Bật/tắt cài đặt**: [examples/extensions/tools.ts](../examples/extensions/tools.ts) - Danh sách cài đặt để bật/tắt công cụ\n- **Chỉ báo trạng thái**: [examples/extensions/plan-mode/index.ts](../examples/extensions/plan-mode/index.ts) - setStatus và setWidget\n- **Chỉ báo hoạt động**: [examples/extensions/working-indicator.ts](../examples/extensions/working-indicator.ts) - setWorkingIndicator\n- **Chân trang tùy chỉnh**: [examples/extensions/custom-footer.ts](../examples/extensions/custom-footer.ts) - setFooter có số liệu thống kê\n- **Trình chỉnh sửa tùy chỉnh**: [examples/extensions/modal-editor.ts](../examples/extensions/modal-editor.ts) - Chỉnh sửa phương thức giống Vim\n- **Trò chơi rắn**: [examples/extensions/snake.ts](../examples/extensions/snake.ts) - Trò chơi đầy đủ với đầu vào bàn phím, vòng lặp trò chơi\n- **Hiển thị công cụ tùy chỉnh**: [examples/extensions/todo.ts](../examples/extensions/todo.ts) - renderCall và renderResult","sourceFile":"tui.md"},"usage":{"title":"Sử dụng Pi","markdown":"Trang này thu thập các chi tiết sử dụng hàng ngày không phù hợp trên trang bắt đầu nhanh.\n\n## Chế độ tương tác\n\n<p align=\"center\"><img src=\"images/interactive-mode.png\" alt=\"Interactive Mode\" width=\"600\"></p>\n\nGiao diện có 4 khu vực chính:\n\n- **Tiêu đề khởi động** - phím tắt, đã tải context files, prompt templates, kỹ năng và tiện ích mở rộng\n- **Tin nhắn** - tin nhắn của người dùng, phản hồi của trợ lý, cuộc gọi công cụ, kết quả công cụ, thông báo, lỗi và giao diện người dùng tiện ích mở rộng\n- **Trình chỉnh sửa** - nơi bạn nhập; màu đường viền cho biết mức độ suy nghĩ hiện tại\n- **Chân trang** - thư mục làm việc, tên phiên, mức sử dụng mã thông báo/bộ đệm, chi phí, mức sử dụng ngữ cảnh và mô hình hiện tại. Tổng số bao gồm các phản hồi của trợ lý, mức sử dụng được báo cáo bởi các công cụ và tạo bản tóm tắt.\n\nTrình chỉnh sửa có thể được thay thế tạm thời bằng giao diện người dùng tích hợp như `/settings` hoặc bằng giao diện người dùng tiện ích mở rộng tùy chỉnh.\n\n### Tính năng biên tập\n\n| Tính năng | Làm sao |\n|---------|-----|\n| Tham chiếu tập tin | Nhập `@` để tìm kiếm mờ các tệp dự án |\n| Hoàn thành đường dẫn | Nhấn Tab để hoàn thành đường dẫn |\n| Đầu vào nhiều dòng | Shift+Enter hoặc Ctrl+Enter trên Windows Terminal |\n| Sao chép phản hồi | Ctrl+X sao chép tin nhắn trợ lý cuối cùng; trong `/tree`, nó sao chép tin nhắn đã chọn |\n| Hình ảnh | Dán bằng Ctrl+V, Alt+V trên Windows hoặc kéo vào terminal |\n| Lệnh vỏ | `!command` chạy và gửi đầu ra cho mô hình |\n| Lệnh shell ẩn | `!!command` chạy mà không gửi đầu ra tới mô hình |\n| Trình chỉnh sửa bên ngoài | Ctrl+G mở `externalEditor`, `$VISUAL`, `$EDITOR`, Notepad trên Windows hoặc `nano` ở nơi khác |\n\nXem [Keybindings](keybindings.md) để biết tất cả các phím tắt và tùy chỉnh.\n\n## Lệnh gạch chéo\n\nNhập `/` vào trình chỉnh sửa để mở lệnh hoàn thành. Extensions có thể đăng ký các lệnh tùy chỉnh, các kỹ năng có sẵn dưới dạng `/skill:name` và prompt templates mở rộng thông qua `/templatename`.\n\n| Yêu cầu | Sự miêu tả |\n|---------|-------------|\n| `/login`, `/logout` | Quản lý thông tin xác thực khóa OAuth hoặc API |\n| [`/llama`](llama-cpp.md) | Tải xuống, tải và dỡ bỏ các mô hình bộ định tuyến llama.cpp |\n| `/model` | Chuyển đổi mô hình |\n| `/scoped-models` | Bật/tắt các mô hình cho chu trình Ctrl+P |\n| `/settings` | Mức độ tư duy, chủ đề, truyền tải thông điệp, vận chuyển |\n| `/resume` | Pick từ các phiên trước |\n| `/new` | Bắt đầu một phiên mới |\n| `/name <name>` | Đặt tên hiển thị phiên |\n| `/session` | Hiển thị tệp phiên, ID, tin nhắn, mã thông báo và chi phí |\n| `/tree` | Chuyển đến bất kỳ điểm nào trong phiên và tiếp tục từ đó |\n| `/trust` | Lưu quyết định tin cậy của dự án cho các phiên trong tương lai |\n| `/fork` | Tạo phiên mới từ tin nhắn của người dùng trước đó |\n| `/clone` | Sao chép nhánh hoạt động hiện tại vào một phiên mới |\n| `/compact [prompt]` | Ngữ cảnh thu gọn thủ công, tùy chọn với hướng dẫn tùy chỉnh |\n| `/copy` | Sao chép tin nhắn trợ lý cuối cùng vào clipboard |\n| `/export [file]` | Xuất phiên sang HTML hoặc JSONL |\n| `/import <file>` | Nhập và tiếp tục phiên từ tệp JSONL |\n| `/share` | Tải lên dưới dạng riêng tư GitHub ý chính với liên kết HTML có thể chia sẻ |\n| `/reload` | Tải lại các tổ hợp phím, tiện ích mở rộng, kỹ năng, lời nhắc, chủ đề và context files |\n| `/hotkeys` | Hiển thị tất cả các phím tắt |\n| `/changelog` | Hiển thị lịch sử phiên bản |\n| `/quit` | Thoát khỏi pi |\n\n## Hàng đợi tin nhắn\n\nBạn có thể gửi tin nhắn trong khi đại lý vẫn đang làm việc:\n\n- **Enter** xếp hàng đợi một thông báo chỉ đạo, được gửi sau khi lượt trợ lý hiện tại hoàn tất việc thực hiện các lệnh gọi công cụ của nó.\n- **Alt+Enter** xếp hàng tin nhắn tiếp theo, được gửi sau khi tổng đài viên hoàn thành mọi công việc.\n- **Escape** hủy bỏ và khôi phục các tin nhắn đã xếp hàng đợi cho trình chỉnh sửa.\n- **Alt+Up** truy xuất các thư đã xếp hàng đợi trở lại trình chỉnh sửa.\n\nTrên Windows Terminal, Alt+Enter ở chế độ toàn màn hình theo mặc định. Ánh xạ lại nó như được mô tả trong [Terminal setup](terminal-setup.md) nếu bạn muốn pi nhận phím tắt.\n\nĐịnh cấu hình phân phối trong [Settings](settings.md) với `steeringMode` và `followUpMode`.\n\n## Phiên\n\nCác phiên được lưu tự động vào `~/.pi/agent/sessions/`, được sắp xếp theo thư mục làm việc.\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\nCác lệnh phiên hữu ích:\n\n- `/session` hiển thị tệp và ID phiên hiện tại.\n- `/tree` điều hướng session tree trong tệp và có thể tóm tắt các nhánh bị bỏ rơi.\n- `/fork` tạo phiên mới từ tin nhắn của người dùng trước đó.\n- `/clone` sao chép nhánh đang hoạt động hiện tại vào một tệp phiên mới.\n- `/compact` tóm tắt các tin nhắn cũ hơn theo ngữ cảnh miễn phí.\n\nXem [Sessions](sessions.md) và [Compaction](compaction.md) để biết chi tiết.\n\n## Tệp ngữ cảnh\n\nPi tải `AGENTS.md` hoặc `CLAUDE.md` khi khởi động từ:\n\n- `~/.pi/agent/AGENTS.md` để được hướng dẫn chung\n- thư mục mẹ, đi lên từ thư mục làm việc hiện tại\n- thư mục hiện tại\n\nNếu thư mục chứa `AGENTS.override.md`, Pi sẽ tải thư mục đó thay vì `AGENTS.md` hoặc `CLAUDE.md` từ thư mục đó. Các tập tin ngữ cảnh từ các thư mục khác vẫn xếp lớp bình thường.\n\nSử dụng context files cho các quy ước, lệnh, quy tắc an toàn và tùy chọn của dự án. Vô hiệu hóa tải bằng `--no-context-files` hoặc `-nc`.\n\n### Tệp nhắc nhở hệ thống\n\nThay thế lời nhắc hệ thống mặc định bằng:\n\n- `.pi/SYSTEM.md` cho một dự án\n- `~/.pi/agent/SYSTEM.md` trên toàn cầu\n\nThêm vào lời nhắc mặc định mà không thay thế nó bằng `APPEND_SYSTEM.md` ở một trong hai vị trí.\n\n### Dự án ủy thác\n\nKhi khởi động tương tác, pi hỏi trước khi tin cậy một thư mục dự án có chứa các cài đặt, tài nguyên hoặc dự án cục bộ của dự án `.agents/skills` và không có quyết định nào được lưu cho thư mục hoặc thư mục mẹ trong `~/.pi/agent/trust.json`. Tin tưởng vào một dự án cho phép pi tải tài nguyên `.pi/settings.json` và `.pi`, cài đặt các gói dự án bị thiếu và thực hiện các phần mở rộng dự án.\n\nTrước khi có quyết định tin cậy, pi chỉ tải context files, tiện ích mở rộng người dùng/toàn cầu và tiện ích mở rộng CLI `-e` để chúng có thể xử lý sự kiện `project_trust`. Tiện ích mở rộng dự án cục bộ, tiện ích mở rộng được quản lý gói dự án và cài đặt dự án chỉ được tải sau khi dự án được tin cậy. Sự phân chia này cũng áp dụng khi chuyển sang một phiên từ một cwd khác mà sự tin cậy chưa được giải quyết trong quy trình hiện tại.\n\nCác chế độ không tương tác (`-p`, `--mode json` và `--mode rpc`) không hiển thị lời nhắc tin cậy. Nếu không có quyết định tin cậy đã lưu hiện hành, họ sẽ sử dụng `defaultProjectTrust` từ cài đặt chung: `ask` (mặc định) và `never` bỏ qua các tài nguyên dự án đó, trong khi `always` tin cậy chúng. Vượt qua `--approve`/`-a` hoặc `--no-approve`/`-na` để ghi đè độ tin cậy của dự án trong một lần chạy.\n\nNếu không áp dụng tiện ích mở rộng hoặc quyết định đã lưu, `defaultProjectTrust` sẽ kiểm soát hành vi dự phòng. Đặt thành `\"ask\"`, `\"always\"` hoặc `\"never\"` trong `~/.pi/agent/settings.json` hoặc thay đổi bằng `/settings`.\n\n`pi config` và các lệnh gói sử dụng cùng một luồng tin cậy của dự án, ngoại trừ `pi update` không bao giờ nhắc. Chuyển `--approve` để tin cậy cài đặt dự án cục bộ cho một lệnh hoặc `--no-approve` để bỏ qua chúng.\n\nSử dụng `/trust` trong chế độ tương tác để lưu quyết định tin cậy dự án cho các phiên trong tương lai, bao gồm cả tin cậy cho thư mục mẹ trực tiếp. Nó chỉ viết `~/.pi/agent/trust.json`; phiên hiện tại không được tải lại, vì vậy hãy khởi động lại pi để các thay đổi có hiệu lực.\n\n\n## Xuất và chia sẻ phiên\n\nSử dụng `/export [file]` để viết phiên sang HTML.\n\nSử dụng `/share` để tải lên ý chính GitHub riêng tư kèm theo liên kết HTML có thể chia sẻ.\n\nNếu bạn sử dụng pi cho công việc nguồn mở và muốn xuất bản các phiên dành cho nghiên cứu mô hình, lời nhắc, công cụ và đánh giá, hãy xem [`badlogic/pi-share-hf`](https://github.com/badlogic/pi-share-hf). Nó xuất bản các phiên tới tập dữ liệu Hugging Face.\n\n## CLI Tham khảo\n\n```bash\npi [options] [@files...] [messages...]\n```\n\n### Lệnh gói\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\nCác lệnh này quản lý các gói pi và `pi update` có thể cập nhật cài đặt pi CLI. Để gỡ cài đặt pi, hãy xem [Quickstart](quickstart.md#uninstall). `pi config` và các lệnh gói dự án chấp nhận `--approve`/`--no-approve` để tin cậy hoặc bỏ qua cài đặt dự án cục bộ cho một lệnh. `pi update` không bao giờ nhắc nhở về sự tin tưởng vào dự án.\n\nXem [Pi Packages](packages.md) để biết nguồn gói và ghi chú bảo mật.\n\n### Chế độ\n\n| Lá cờ | Sự miêu tả |\n|------|-------------|\n| mặc định | Chế độ tương tác |\n| `-p`, `--print` | In phản hồi và thoát |\n| `--mode json` | Xuất tất cả các sự kiện dưới dạng dòng JSON; xem [JSON mode](json.md) |\n| `--mode rpc` | chế độ RPC trên stdin/stdout; xem [RPC mode](rpc.md) |\n| `--export <in> [out]` | Xuất phiên sang HTML |\n\nỞ chế độ in, pi cũng đọc piped stdin và hợp nhất nó vào dấu nhắc ban đầu:\n\n```bash\ncat README.md | pi -p \"Summarize this text\"\n```\n\n### Tùy chọn mẫu\n\n| Lựa chọn | Sự miêu tả |\n|--------|-------------|\n| `--provider <name>` | Nhà cung cấp, chẳng hạn như `anthropic`, `openai` hoặc `google` |\n| `--model <pattern>` | Mẫu hoặc ID mẫu; hỗ trợ `provider/id` và tùy chọn `:<thinking>` |\n| `--api-key <key>` | API key, ghi đè các biến môi trường |\n| `--thinking <level>` | `off`, `minimal`, `low`, `medium`, `high`, `xhigh`, `max` |\n| `--models <patterns>` | Các mẫu được phân tách bằng dấu phẩy để đạp xe Ctrl+P |\n| `--list-models [search]` | Liệt kê các mẫu có sẵn |\n\n### Tùy chọn phiên\n\n| Lựa chọn | Sự miêu tả |\n|--------|-------------|\n| `-c`, `--continue` | Tiếp tục phiên gần đây nhất |\n| `-r`, `--resume` | Duyệt và chọn một phiên |\n| `--phiên <đường dẫn\\ | id>` | Sử dụng tệp phiên cụ thể hoặc một phần UUID |\n| `--fork <đường dẫn\\ | id>` | Phân nhánh tệp phiên hoặc một phần UUID vào phiên mới |\n| `--session-dir <dir>` | Thư mục lưu trữ phiên tùy chỉnh |\n| `--no-session` | Chế độ phù du; không lưu |\n| `--name <name>`, `-n <name>` | Đặt tên hiển thị phiên khi khởi động |\n\n### Tùy chọn công cụ\n\n| Lựa chọn | Sự miêu tả |\n|--------|-------------|\n| `--tools <list>`, `-t <list>` | Các công cụ tùy chỉnh, tiện ích mở rộng và tích hợp cụ thể trong danh sách cho phép |\n| `--exclude-tools <list>`, `-xt <list>` | Vô hiệu hóa các công cụ tích hợp, tiện ích mở rộng và tùy chỉnh cụ thể |\n| `--no-builtin-tools`, `-nbt` | Tắt các công cụ tích hợp nhưng vẫn bật công cụ mở rộng/tùy chỉnh |\n| `--no-tools`, `-nt` | Vô hiệu hóa tất cả các công cụ |\n\nCác công cụ tích hợp: `read`, `bash`, `edit`, `write`, `grep`, `find`, `ls`.\n\n### Tùy chọn tài nguyên\n\n| Lựa chọn | Sự miêu tả |\n|--------|-------------|\n| `-e`, `--extension <source>` | Tải tiện ích mở rộng từ đường dẫn, npm hoặc git; có thể lặp lại |\n| `--no-extensions` | Tắt tính năng khám phá tiện ích mở rộng |\n| `--skill <path>` | Tải một kỹ năng; có thể lặp lại |\n| `--no-skills` | Vô hiệu hóa khám phá kỹ năng |\n| `--prompt-template <path>` | Tải mẫu lời nhắc; có thể lặp lại |\n| `--no-prompt-templates` | Tắt tính năng khám phá mẫu lời nhắc |\n| `--theme <path>` | Tải một chủ đề; có thể lặp lại |\n| `--no-themes` | Tắt tính năng khám phá chủ đề |\n| `--no-context-files`, `-nc` | Tắt tính năng khám phá `AGENTS.md` và `CLAUDE.md` |\n\nKết hợp `--no-*` với các cờ rõ ràng để tải chính xác những gì bạn cần, bỏ qua cài đặt. Ví dụ:\n\n```bash\npi --no-extensions -e ./my-extension.ts\n```\n\n### Tùy chọn khác\n\n| Lựa chọn | Sự miêu tả |\n|--------|-------------|\n| `--system-prompt <text>` | Thay thế lời nhắc mặc định; context files và các kỹ năng vẫn được bổ sung |\n| `--append-system-prompt <text>` | Nối vào lời nhắc hệ thống |\n| `--tui-mode <mode>` | Chế độ TUI: `regular` (mặc định) hoặc thử nghiệm `fullscreen` |\n| `--verbose` | Buộc khởi động dài dòng |\n| `-a`, `--approve` | Tin cậy các tệp dự án cục bộ cho lần chạy này |\n| `-na`, `--no-approve` | Bỏ qua các tệp dự án cục bộ cho lần chạy này |\n| `-h`, `--help` | Hiển thị trợ giúp |\n| `-v`, `--version` | Hiển thị phiên bản |\n\nỞ chế độ `fullscreen`, bản ghi cuộn bên trong khung nhìn thiết bị đầu cuối trong khi các tin nhắn được xếp hàng đợi, trạng thái làm việc, tiện ích mở rộng, trình chỉnh sửa và chân trang vẫn cố định ở dưới cùng. Đầu vào chuột/bàn di chuột sẽ cuộn vùng bên dưới con trỏ; các thao tác trên khung nhìn bàn phím luôn có sẵn. Hình ảnh nội tuyến hoạt động trong các thiết bị đầu cuối hỗ trợ giao thức đồ họa Kitty, bao gồm Kitty và Ghostty. Trong iTerm2, chúng hiển thị dưới dạng phần giữ chỗ văn bản vì giao thức hình ảnh nội tuyến của nó không thể xóa hoặc cắt bớt các vị trí trong quá trình cuộn do ứng dụng sở hữu. Ở chế độ `regular`, pi sử dụng màn hình chính và cuộn ngược do thiết bị đầu cuối sở hữu, đồng thời hình ảnh nội tuyến iTerm2 tiếp tục hiển thị bình thường.\n\nĐặt **TUI chế độ** trong `/settings` để chuyển đổi ngay lập tức giữa `regular` và `fullscreen` và chọn chế độ mặc định cho các phiên trong tương lai. **Đầu ra thoát toàn màn hình** kiểm soát việc thoát toàn màn hình sẽ in bản ghi cuối cùng hay khôi phục màn hình trước đó và chỉ in gợi ý tiếp tục phiên.\n\n### Đối số tệp\n\nCác tệp tiền tố có `@` để đưa chúng vào tin nhắ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### Ví dụ\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## Nguyên tắc thiết kế\n\nPi giữ cốt lõi ở mức nhỏ và đẩy hành vi dành riêng cho quy trình công việc thành các phần mở rộng, kỹ năng, prompt templates và gói.\n\nNó cố ý không bao gồm MCP tích hợp, tác nhân phụ, cửa sổ bật lên cấp phép, chế độ kế hoạch, việc cần làm hoặc nền bash. Bạn có thể xây dựng hoặc cài đặt các quy trình công việc đó dưới dạng tiện ích mở rộng hoặc gói hoặc sử dụng các công cụ bên ngoài như vùng chứa và tmux.\n\nĐể biết lý do đầy đủ, hãy đọc [blog post](https://mariozechner.at/posts/2025-11-30-pi-coding-agent/).","sourceFile":"usage.md"},"windows":{"title":"Thiết lập Windows","markdown":"Pi yêu cầu shell bash trên Windows. Các vị trí đã kiểm tra (theo thứ tự):\n\n1. Đường dẫn tùy chỉnh từ `~/.pi/agent/settings.json`\n2. Git Đánh (`C:\\Program Files\\Git\\bin\\bash.exe`)\n3. `bash.exe` trên ĐƯỜNG (Cygwin, MSYS2, WSL)\n\nĐối với hầu hết người dùng, [Git for Windows](https://git-scm.com/download/win) là đủ.\n\n## Đường dẫn Shell tùy chỉnh\n\n```json\n{\n  \"shellPath\": \"C:\\\\cygwin64\\\\bin\\\\bash.exe\"\n}\n```","sourceFile":"windows.md"}}},"navigation":{"vi":[{"title":"Bắt đầu tại đây","items":[{"title":"Pi Tài liệu","path":"/docs/latest","slug":"index"},{"title":"Bắt đầu nhanh","path":"/docs/latest/quickstart","slug":"quickstart"},{"title":"Sử dụng Pi","path":"/docs/latest/usage","slug":"usage"},{"title":"Providers","path":"/docs/latest/providers","slug":"providers"},{"title":"Bảo vệ","path":"/docs/latest/security","slug":"security"},{"title":"Container hóa","path":"/docs/latest/containerization","slug":"containerization"},{"title":"Cài đặt","path":"/docs/latest/settings","slug":"settings"},{"title":"Tổ hợp phím","path":"/docs/latest/keybindings","slug":"keybindings"},{"title":"Phiên","path":"/docs/latest/sessions","slug":"sessions"},{"title":"Tóm tắt nén và phân nhánh","path":"/docs/latest/compaction","slug":"compaction"}]},{"title":"Tùy chỉnh","items":[{"title":"Extensions","path":"/docs/latest/extensions","slug":"extensions"},{"title":"Skills","path":"/docs/latest/skills","slug":"skills"},{"title":"Mẫu nhắc nhở","path":"/docs/latest/prompt-templates","slug":"prompt-templates"},{"title":"Chủ đề","path":"/docs/latest/themes","slug":"themes"},{"title":"Pi Packages","path":"/docs/latest/packages","slug":"packages"},{"title":"Tùy chỉnh Models","path":"/docs/latest/models","slug":"models"},{"title":"Tùy chỉnh Providers","path":"/docs/latest/custom-provider","slug":"custom-provider"}]},{"title":"Tham chiếu","items":[{"title":"Định dạng tệp phiên","path":"/docs/latest/session-format","slug":"session-format"}]},{"title":"Sử dụng lập trình","items":[{"title":"SDK","path":"/docs/latest/sdk","slug":"sdk"},{"title":"RPC Chế độ","path":"/docs/latest/rpc","slug":"rpc"},{"title":"JSON Chế độ phát trực tiếp sự kiện","path":"/docs/latest/json","slug":"json"},{"title":"TUI Thành phần","path":"/docs/latest/tui","slug":"tui"}]},{"title":"Thiết lập nền tảng","items":[{"title":"Thiết lập Windows","path":"/docs/latest/windows","slug":"windows"},{"title":"Thiết lập Termux (Android)","path":"/docs/latest/termux","slug":"termux"},{"title":"tmux Thiết lập","path":"/docs/latest/tmux","slug":"tmux"},{"title":"Thiết lập thiết bị đầu cuối","path":"/docs/latest/terminal-setup","slug":"terminal-setup"},{"title":"Bí danh vỏ","path":"/docs/latest/shell-aliases","slug":"shell-aliases"}]},{"title":"Phát triển","items":[{"title":"Phát triển","path":"/docs/latest/development","slug":"development"}]}]}}
