{"locale":"id","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":{"id":{"compaction":{"title":"Pemadatan & Peringkasan Cabang","markdown":"LLM memiliki jendela konteks terbatas. Ketika percakapan menjadi terlalu panjang, Pi menggunakan pemadatan untuk meringkas konten lama sambil mempertahankan karya terbaru. Halaman ini mencakup pemadatan otomatis dan branch summarization.\n\n**File sumber** ([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) - Logika pemadatan otomatis\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) - Ringkasan cabang\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) - Utilitas bersama (pelacakan file, serialisasi)\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) - Jenis entri (`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) - Jenis acara ekstensi\n\nUntuk definisi TypeScript dalam proyek Anda, periksa `node_modules/@earendil-works/pi-coding-agent/dist/`.\n\n## Ringkasan\n\nPi memiliki dua mekanisme peringkasan:\n\n| Mekanisme | Pemicu | Tujuan |\n|-----------|---------|---------|\n| Pemadatan | Konteks melebihi ambang batas, atau `/compact` | Ringkaslah pesan-pesan lama untuk membebaskan konteks |\n| Ringkasan cabang | `/tree` navigasi | Pertahankan konteks saat berpindah cabang |\n\nKeduanya menggunakan format ringkasan terstruktur yang sama dan melacak operasi file secara kumulatif. Permintaan pemadatan dan ringkasan cabang menggunakan ID sesi perutean baru dan, jika didukung oleh penyedia, menonaktifkan penulisan prompt-cache karena permintaan satu kali ini kemungkinan tidak akan digunakan kembali.\n\n## Pemadatan\n\n### Ketika Itu Terpicu\n\nPemadatan otomatis terpicu ketika:\n\n```\ncontextTokens > contextWindow - reserveTokens\n```\n\nSecara default, `reserveTokens` adalah 16384 token (dapat dikonfigurasi dalam `~/.pi/agent/settings.json` atau `<project-dir>/.pi/settings.json`). Hal ini memberikan ruang bagi tanggapan LLM.\n\nAnda juga dapat memicu secara manual dengan `/compact [instructions]`, dengan instruksi opsional yang memfokuskan ringkasan.\n\n### Cara Kerjanya\n\n1. **Temukan titik potong**: Berjalan mundur dari pesan terbaru, mengumpulkan perkiraan token hingga `keepRecentTokens` (default 20k, dapat dikonfigurasi dalam `~/.pi/agent/settings.json` atau `<project-dir>/.pi/settings.json`) tercapai\n2. **Ekstrak pesan**: Kumpulkan pesan dari batas yang disimpan sebelumnya (atau permulaan sesi) hingga titik potong\n3. **Buat ringkasan**: Panggil LLM untuk meringkas dengan format terstruktur, meneruskan ringkasan sebelumnya sebagai konteks berulang jika ada\n4. **Tambahkan entri**: Simpan `CompactionEntry` dengan ringkasan dan `firstKeptEntryId`\n5. **Muat ulang**: Sesi dimuat ulang, menggunakan ringkasan + pesan dari `firstKeptEntryId` dan seterusnya\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\nPada pemadatan berulang, rentang yang diringkas dimulai dari batas pemadatan sebelumnya (`firstKeptEntryId`), bukan pada entri pemadatan itu sendiri, kembali ke entri setelah pemadatan sebelumnya jika entri yang disimpan tersebut tidak dapat ditemukan di jalurnya. Hal ini mempertahankan pesan-pesan yang bertahan dari pemadatan sebelumnya dengan memasukkannya ke dalam proses rangkuman berikutnya juga. Pi juga menghitung ulang `tokensBefore` dari konteks sesi yang dibangun kembali sebelum menulis `CompactionEntry` baru, sehingga jumlah token mencerminkan konteks pra-pemadatan sebenarnya yang diganti.\n\n### Putaran Terpisah\n\nSebuah \"giliran\" dimulai dengan pesan pengguna dan mencakup semua respons asisten dan panggilan alat hingga pesan pengguna berikutnya. Biasanya, pemadatan dilakukan pada batas belokan.\n\nKetika satu putaran melebihi `keepRecentTokens`, titik potong mendarat di tengah putaran pada pesan asisten. Ini adalah \"giliran terpisah\":\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\nUntuk putaran terpisah, Pi menghasilkan dua ringkasan dan menggabungkannya:\n1. **Ringkasan riwayat**: Konteks sebelumnya (jika ada)\n2. **Ringkasan awalan giliran**: Bagian awal giliran split\n\n### Aturan Titik Potong\n\nTitik potong yang valid adalah:\n- Pesan pengguna\n- Pesan asisten\n- Pesan BashExecution\n- Pesan khusus (pesan_khusus, ringkasan_cabang)\n\nJangan pernah memotong hasil alat (hasil alat tersebut harus tetap sesuai dengan panggilan alatnya).\n\n### Struktur Entri Pemadatan\n\nDidefinisikan dalam [`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 dapat menyimpan data apa pun yang dapat diserialkan JSON di `details`. Pemadatan default melacak operasi file, namun penerapan ekstensi khusus dapat menggunakan strukturnya sendiri. Ringkasan yang dihasilkan dan disediakan ekstensi menyimpan LLM `usage` bila tersedia sehingga total sesi mencakup pekerjaan ringkasan.\n\nLihat [`prepareCompaction()`](https://github.com/earendil-works/pi-mono/blob/main/packages/coding-agent/src/core/compaction/compaction.ts) dan [`compact()`](https://github.com/earendil-works/pi-mono/blob/main/packages/coding-agent/src/core/compaction/compaction.ts) untuk implementasinya. Untuk peringkasan terprogram langsung, `generateSummary()` mengembalikan teks ringkasan dan `generateSummaryWithUsage()` mengembalikan `{ text, usage }`.\n\n## Ringkasan Cabang\n\n### Ketika Itu Terpicu\n\nSaat Anda menggunakan `/tree` untuk menavigasi ke cabang lain, Pi menawarkan untuk meringkas pekerjaan yang Anda tinggalkan. Ini memasukkan konteks dari cabang kiri ke cabang baru.\n\n### Cara Kerjanya\n\n1. **Temukan nenek moyang yang sama**: Node terdalam yang dimiliki bersama oleh posisi lama dan baru\n2. **Kumpulkan entri**: Berjalan dari daun tua kembali ke nenek moyang yang sama\n3. **Persiapkan dengan anggaran**: Sertakan pesan hingga anggaran token (yang terbaru terlebih dahulu)\n4. **Buat ringkasan**: Hubungi LLM dengan format terstruktur\n5. **Tambahkan entri**: Simpan `BranchSummaryEntry` di titik navigasi\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### Pelacakan File Kumulatif\n\nBaik pemadatan maupun branch summarization file track secara kumulatif. Saat membuat ringkasan, pi mengekstrak operasi file dari:\n- Alat memanggil pesan yang diringkas\n- Ringkasan pemadatan atau cabang sebelumnya `details` (jika ada)\n\nIni berarti pelacakan file terakumulasi di beberapa pemadatan atau ringkasan cabang yang disarangkan, menjaga riwayat lengkap file yang dibaca dan dimodifikasi.\n\n### Struktur Entri Ringkasan Cabang\n\nDidefinisikan dalam [`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\nSama seperti pemadatan, ekstensi dapat menyimpan data khusus dalam `details`.\n\nLihat [`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), dan [`generateBranchSummary()`](https://github.com/earendil-works/pi-mono/blob/main/packages/coding-agent/src/core/compaction/branch-summarization.ts) untuk penerapannya.\n\n## Format Ringkasan\n\nPemadatan dan branch summarization menggunakan format terstruktur yang sama:\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### Serialisasi Pesan\n\nSebelum diringkas, pesan diserialkan menjadi teks melalui [`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\nHal ini mencegah model memperlakukannya sebagai percakapan yang berlanjut.\n\nHasil alat dipotong menjadi 2000 karakter selama serialisasi. Konten yang melebihi batas tersebut diganti dengan penanda yang menunjukkan berapa banyak karakter yang terpotong. Hal ini menjaga permintaan peringkasan tetap dalam anggaran token yang masuk akal, karena hasil alat (terutama dari `read` dan `bash`) biasanya merupakan kontributor terbesar terhadap ukuran konteks.\n\n## Peringkasan Khusus melalui Extensions\n\nExtensions dapat mencegat dan menyesuaikan pemadatan dan branch summarization. Lihat [`extensions/types.ts`](https://github.com/earendil-works/pi-mono/blob/main/packages/coding-agent/src/core/extensions/types.ts) untuk definisi jenis peristiwa.\n\n### sesi_sebelum_kompak\n\nDitembakkan sebelum pemadatan otomatis atau `/compact`. Dapat membatalkan atau memberikan ringkasan khusus. Lihat `SessionBeforeCompactEvent` dan `CompactionPreparation` di file jenis.\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#### Mengubah Pesan menjadi Teks\n\nUntuk membuat ringkasan dengan model Anda sendiri, ubah pesan menjadi teks menggunakan `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\nLihat [custom-compaction.ts](../examples/extensions/custom-compaction.ts) untuk contoh lengkap menggunakan model yang berbeda.\n\n### sesi_sebelum_pohon\n\nDitembakkan sebelum navigasi `/tree`. Selalu aktif terlepas dari apakah pengguna memilih untuk meringkas. Dapat membatalkan navigasi atau memberikan ringkasan khusus.\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\nLihat `SessionBeforeTreeEvent` dan `TreePreparation` di file jenis.\n\n## Pengaturan\n\nKonfigurasikan pemadatan di `~/.pi/agent/settings.json` atau `<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| Pengaturan | Bawaan | Keterangan |\n|---------|---------|-------------|\n| `enabled` | `true` | Aktifkan pemadatan otomatis |\n| `reserveTokens` | `16384` | Token untuk dicadangkan untuk respons LLM |\n| `keepRecentTokens` | `20000` | Token terkini yang harus disimpan (tidak diringkas) |\n\nNonaktifkan pemadatan otomatis dengan `\"enabled\": false`. Anda masih dapat memadatkannya secara manual dengan `/compact`.","sourceFile":"compaction.md"},"containerization":{"title":"Kontainerisasi","markdown":"Pi berjalan dengan semua izin secara default, namun dalam beberapa kasus, Anda ingin memiliki kontrol lebih besar terhadap direktori mana Pi dapat menulis dan akses apa yang dimilikinya.\n\nAda dua opsi umum. Anda juga bisa\n1. jalankan seluruh proses `pi` di dalam lingkungan yang terisolasi, atau\n2. jalankan `pi` pada host dan rutekan eksekusi alat ke lingkungan yang terisolasi.\n\n## Pilih sebuah pola\n\n| Pola | Apa yang terisolasi | Terbaik untuk | Catatan |\n| --- | --- | --- | --- |\n| Gondolin ekstensi | Alat bawaan dan perintah `!` | Isolasi mikro-VM lokal sambil tetap menjaga autentikasi pada host | Lihat [`examples/extensions/gondolin/`](../examples/extensions/gondolin/). |\n| Biasa Docker | Seluruh proses `pi` dalam wadah lokal | Isolasi lokal sederhana | Penyedia API keys memasuki wadah. |\n| OpenShell | Seluruh proses `pi` dalam sandbox yang dikontrol kebijakan | Dikelola secara lokal atau jarak jauh sandbox | Membutuhkan gerbang OpenShell |\n\nExtensions dijalankan dimanapun proses `pi` berjalan. Jika Anda menjalankan host `pi` dengan ekstensi perutean alat, alat ekstensi khusus lainnya akan tetap berjalan di host kecuali alat tersebut juga mendelegasikan operasinya.\n\n## Gondolin\n\n[Gondolin](https://github.com/earendil-works/gondolin) adalah mikro-VM Linux lokal.\nGunakan [example extension](../examples/extensions/gondolin) bila Anda ingin `pi` di host tetapi semua alat bawaan dialihkan ke VM.\n\nPengaturan:\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\nJalankan dari proyek yang ingin Anda pasang:\n\n```bash\ncd /path/to/project\npi -e ~/.pi/agent/extensions/gondolin\n```\n\nEkstensi memasang cwd host di `/workspace` di VM dan menimpa `read`, `write`, `edit`, `bash`, `grep`, `find`, dan `ls`.\nPerintah pengguna `!` juga dirutekan ke VM.\nPerubahan file di bawah `/workspace` tulis ke host.\n\nPersyaratan: Node.js >= 23.6.0 untuk `@earendil-works/gondolin`, ditambah QEMU (memerlukan instalasi melalui manajer paket Anda).\n\n## Biasa Docker\n\nJalankan seluruh proses `pi` di Docker bila Anda menginginkan batas kontainer lokal yang paling sederhana.\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\nBangun dan jalankan:\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\"` memasang direktori Anda saat ini ke dalam wadah di /workspace sehingga membaca dan menulis di `/workspace` di dalam Docker secara langsung memengaruhi file host Anda, seperti pada contoh Gondolin.\n\nGunakan volume bernama untuk `/root/.pi/agent` jika Anda menginginkan pengaturan dan sesi lokal-kontainer. Memasang host Anda `~/.pi/agent` akan mengekspos file autentikasi host dan sesi ke dalam container.\n\n## OpenShell\n\nGunakan [NVIDIA OpenShell](https://docs.nvidia.com/openshell/about/overview) bila Anda menginginkan sandbox yang dikontrol kebijakan dengan sistem file, proses, jaringan, kredensial, dan kontrol inferensi.\nOpenShell dapat menjalankan sandboxes melalui gateway lokal yang didukung oleh Docker, Podman, atau runtime VM, atau melalui gateway Kubernetes jarak jauh.\n\nSetiap sandbox memerlukan gateway aktif.\nDaftarkan dan pilih salah satu sebelum membuat sandbox:\n\n```bash\nopenshell gateway add <gateway-url> --name <name>\nopenshell gateway select <name>\n```\n\nLuncurkan `pi` di dalam OpenShell sandbox:\n\n```bash\nopenshell sandbox create --name pi-sandbox --from pi -- pi\n```\n\nDalam pola ini, seluruh proses `pi` berjalan di dalam sandbox.\nAlat bawaan, perintah `!`, dan alat ekstensi dijalankan di dalam batas OpenShell.\n\nJika gatewaynya jarak jauh, file proyek tidak akan di-bind-mount dari host, artinya penulisan di sandbox tidak akan tercermin pada mesin Anda.\nKloning repositori di dalam sandbox atau gunakan perintah transfer file OpenShell:\n\n```bash\nopenshell sandbox upload pi-sandbox ./repo /workspace\nopenshell sandbox download pi-sandbox /workspace/repo ./repo-out\n```\n\nOpenShell penyedia dapat menyimpan model mentah API key di luar sandbox.\nSaat perutean inferensi dikonfigurasi, kode di dalam sandbox dapat memanggil `https://inference.local`, dan gateway memasukkan kredensial penyedia yang dikonfigurasi di bagian hulu.\nKonfigurasikan Pi untuk menggunakan titik akhir yang kompatibel dengan OpenAI atau kompatibel dengan Antropik jika Anda ingin lalu lintas model menggunakan rute ini.","sourceFile":"containerization.md"},"custom-provider":{"title":"Kustom Providers","markdown":"Extensions dapat mendaftarkan penyedia model khusus melalui `pi.registerProvider()`. Hal ini memungkinkan:\n\n- **Proxy** - Merutekan permintaan melalui proxy perusahaan atau gateway API\n- **Titik akhir khusus** - Gunakan penerapan model yang dihosting sendiri atau pribadi\n- **OAuth/SSO** - Tambahkan alur autentikasi untuk penyedia perusahaan\n- **Kustom APIs** - Terapkan streaming untuk LLM non-standar APIs\n\n## Contoh Extensions\n\nLihat contoh penyedia lengkap ini:\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## Daftar isi\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## Referensi Cepat\n\nExtensions dapat mendaftarkan pi-ai lengkap `Provider` atau menggunakan formulir konfigurasi penyedia lama. Pilih penyedia yang lengkap ketika autentikasi khusus, pemfilteran, penyegaran, atau perilaku streaming diperlukan. Pi menyusun `models.json` menimpa penyedia asli terdaftar di atas.\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\nPabrik ekstensi juga bisa `async`. Untuk penemuan model dinamis, ambil dan daftarkan model di pabrik, bukan di `session_start`. pi menunggu pabrik sebelum pengaktifan dilanjutkan, sehingga penyedia tersedia selama pengaktifan interaktif dan ke `pi --list-models`.\n\n## Ganti Penyedia yang Ada\n\nKasus penggunaan paling sederhana: mengarahkan ulang penyedia yang ada melalui 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\nJika hanya `baseUrl` dan/atau `headers` yang disediakan (tidak ada `models`), semua model yang ada untuk penyedia tersebut akan dipertahankan dengan titik akhir baru.\n\n## Daftarkan Penyedia Baru\n\nUntuk menambahkan penyedia yang benar-benar baru, tentukan `models` beserta konfigurasi yang diperlukan.\n\nJika daftar model berasal dari titik akhir jarak jauh, gunakan pabrik ekstensi async:\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\nIni mendaftarkan model yang diambil sebelum startup selesai.\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\nJika `models` disediakan, ini **menggantikan** semua model yang ada untuk penyedia tersebut.\n\n`apiKey` dan nilai header khusus menggunakan sintaks nilai konfigurasi yang sama seperti `models.json`: `!command` di awal menjalankan perintah untuk seluruh nilai, `$ENV_VAR` dan `${ENV_VAR}` menginterpolasi variabel lingkungan, `$` memancarkan ``apiKey` dan nilai header khusus menggunakan sintaks nilai konfigurasi yang sama seperti `models.json`: `!command` di awal menjalankan perintah untuk seluruh nilai, `$ENV_VAR` dan `${ENV_VAR}` menginterpolasi variabel lingkungan, `$` memancarkan  literal, dan `$!` memancarkan `!` literal.\n\n## Batalkan Pendaftaran Penyedia\n\nGunakan `pi.unregisterProvider(name)` untuk menghapus penyedia yang sebelumnya terdaftar melalui `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\nMembatalkan pendaftaran akan menghapus model dinamis penyedia tersebut, API key fallback, OAuth pendaftaran penyedia, dan pendaftaran pengendali aliran khusus. Model bawaan atau perilaku penyedia apa pun yang diganti akan dipulihkan.\n\nPanggilan yang dilakukan setelah fase beban ekstensi awal diterapkan segera, jadi tidak diperlukan `/reload`.\n\n### API Jenis\n\nBidang `api` menentukan implementasi streaming mana yang digunakan:\n\n| API | Gunakan untuk |\n|-----|---------|\n| `anthropic-messages` | Claude Antropik API dan yang kompatibel |\n| `openai-completions` | Penyelesaian Obrolan OpenAI API dan yang kompatibel |\n| `openai-responses` | Respons OpenAI API |\n| `azure-openai-responses` | Respons Azure OpenAI API |\n| `openai-codex-responses` | Respons Kodeks OpenAI API |\n| `mistral-conversations` | Streaming Penyelesaian Obrolan Mistral Asli |\n| `google-generative-ai` | AI Generatif Google API |\n| `google-vertex` | Google Vertex AI API |\n| `bedrock-converse-stream` | Batu Dasar Amazon Converse API |\n\nSebagian besar penyedia yang kompatibel dengan OpenAI bekerja dengan `openai-completions`. Gunakan tingkat model `thinkingLevelMap` untuk tingkat pemikiran khusus model, dan `compat` untuk kebiasaan penyedia layanan. Level `xhigh` dan `max` bersifat opt-in, memerlukan entri peta bukan nol, dan dapat dipisahkan oleh lubang yang tidak didukung:\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\nGunakan `openrouter` untuk kontrol `reasoning: { effort }` gaya OpenRouter. Gunakan `together` untuk kontrol gaya Together `reasoning: { enabled }`; dengan `supportsReasoningEffort`, ia juga mengirimkan `reasoning_effort`. Gunakan `qwen-chat-template` untuk server lokal yang kompatibel dengan Qwen yang membaca `chat_template_kwargs.enable_thinking` dan memerlukan `preserve_thinking`.\nGunakan `cacheControlFormat: \"anthropic\"` untuk penyedia yang kompatibel dengan OpenAI yang mengekspos cache cepat gaya Antropik melalui `cache_control` pada perintah sistem, definisi alat terakhir, dan konten teks pengguna, asisten, atau hasil alat terakhir.\n\nUntuk penyedia yang kompatibel dengan Antropik yang menggunakan `api: \"anthropic-messages\"`, tetapkan `compat.forceAdaptiveThinking: true` pada model atau penyedia yang model hulunya memerlukan pemikiran adaptif (`thinking.type: \"adaptive\"` plus `output_config.effort`). Model Claude adaptif bawaan mengatur ini secara otomatis. Tetapkan `compat.allowEmptySignature: true` hanya untuk penyedia yang mengeluarkan tanda tangan berpikir kosong dan mengharapkan `signature: \"\"` diputar ulang.\n\n> Catatan migrasi: Mistral berpindah dari `openai-completions` ke `mistral-conversations`.\n> Gunakan `mistral-conversations` untuk model Mistral asli.\n> Jika Anda sengaja merutekan titik akhir yang kompatibel/khusus Mistral melalui `openai-completions`, setel tanda `compat` secara eksplisit sesuai kebutuhan.\n\n### Tajuk Otentikasi\n\nJika penyedia Anda mengharapkan `Authorization: Bearer <key>` tetapi tidak menggunakan standar API, tetapkan `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\nKuncinya diselesaikan untuk setiap permintaan. Permintaan eksplisit `Authorization` header lebih diutamakan daripada nilai yang dihasilkan.\n\n## OAuth Dukungan\n\nTambahkan autentikasi OAuth/SSO yang terintegrasi dengan `/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\nSetelah registrasi, pengguna dapat mengautentikasi melalui `/login corporate-ai`.\n\n### OAuthLoginCallback\n\nObjek `callbacks` menyediakan interaksi UI-netral untuk aliran milik penyedia:\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### OAuthKredensial\n\nKredensial dipertahankan di `~/.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## Streaming Khusus API\n\nUntuk penyedia dengan API non-standar, terapkan `streamSimple`. Pelajari implementasi penyedia yang ada sebelum menulis implementasi Anda sendiri:\n\n**Implementasi referensi:**\n- [anthropic.ts](https://github.com/earendil-works/pi-mono/blob/main/packages/ai/src/providers/anthropic.ts) - Pesan Antropik API\n- [mistral.ts](https://github.com/earendil-works/pi-mono/blob/main/packages/ai/src/providers/mistral.ts) - Percakapan Mistral API\n- [openai-completions.ts](https://github.com/earendil-works/pi-mono/blob/main/packages/ai/src/providers/openai-completions.ts) - Penyelesaian Obrolan OpenAI\n- [openai-responses.ts](https://github.com/earendil-works/pi-mono/blob/main/packages/ai/src/providers/openai-responses.ts) - Respons OpenAI API\n- [google.ts](https://github.com/earendil-works/pi-mono/blob/main/packages/ai/src/providers/google.ts) - AI Generatif Google\n- [amazon-bedrock.ts](https://github.com/earendil-works/pi-mono/blob/main/packages/ai/src/providers/amazon-bedrock.ts) - Batuan Dasar AWS\n\n### Pola Aliran\n\nSemua penyedia mengikuti pola yang sama:\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### Jenis Acara\n\nDorong acara melalui `stream.push()` dalam urutan ini:\n\n1. `{ type: \"start\", partial: output }` - Streaming dimulai\n\n2. Peristiwa konten (dapat diulang, lacak `contentIndex` untuk setiap blok):\n   - `{ type: \"text_start\", contentIndex, partial }` - Blok teks dimulai\n   - `{ type: \"text_delta\", contentIndex, delta, partial }` - Potongan teks\n   - `{ type: \"text_end\", contentIndex, content, partial }` - Blok teks berakhir\n   - `{ type: \"thinking_start\", contentIndex, partial }` - Pemikiran dimulai\n   - `{ type: \"thinking_delta\", contentIndex, delta, partial }` - Potongan pemikiran\n   - `{ type: \"thinking_end\", contentIndex, content, partial }` - Pemikiran berakhir\n   - `{ type: \"toolcall_start\", contentIndex, partial }` - Panggilan alat dimulai\n   - `{ type: \"toolcall_delta\", contentIndex, delta, partial }` - Panggilan alat JSON potongan\n   - `{ type: \"toolcall_end\", contentIndex, toolCall, partial }` - Panggilan alat berakhir\n\n3. `{ type: \"done\", reason, message }` atau `{ type: \"error\", reason, error }` - Streaming berakhir\n\nBidang `partial` di setiap peristiwa berisi status `AssistantMessage` saat ini. Perbarui `output.content` saat Anda menerima data, lalu sertakan `output` sebagai `partial`.\n\n### Blok Konten\n\nTambahkan blok konten ke `output.content` saat blok tersebut tiba:\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### Panggilan Alat\n\nPanggilan alat memerlukan akumulasi JSON dan penguraian:\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### Penggunaan dan Biaya\n\nPerbarui penggunaan dari API respons dan hitung biaya:\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### Kesalahan Meluap Konteks\n\nKetika permintaan melebihi jendela konteks model, pi dapat pulih secara otomatis dengan memadatkan percakapan dan mencoba lagi. Pemulihan ini hanya dimulai jika pi mengenali kegagalan sebagai luapan.\n\nDeteksi berjalan pada pesan asisten yang diselesaikan:\n\n- `stopReason === \"error\"`\n- `errorMessage` cocok dengan salah satu pola luapan pi yang diketahui (lihat [`packages/ai/src/utils/overflow.ts`](https://github.com/earendil-works/pi-mono/blob/main/packages/ai/src/utils/overflow.ts))\n\nJika penyedia Anda mengembalikan kesalahan overflow dengan pesan yang tidak dikenali pi, normalkan kesalahan dari ekstensi yang sama yang mendaftarkan penyedia tersebut. Gunakan pengendali `message_end` untuk menulis ulang pesan asisten sehingga `errorMessage` dimulai dengan frasa yang dikenali pi. Penggantian umum `context_length_exceeded` adalah pilihan paling aman.\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` berjalan sebelum pi melacak pesan asisten untuk pemadatan otomatis, jadi `errorMessage` yang ditulis ulang itulah yang diperiksa pi. Dengan ini, pi akan:\n\n1. Deteksi luapan dari `errorMessage`.\n2. Hapus pesan asisten yang gagal dari konteks langsung.\n3. Jalankan pemadatan.\n4. Coba lagi permintaan tersebut satu kali.\n\nJaga penulisan ulang dengan hati-hati:\n\n- Cakupannya ke penyedia Anda (`message.provider` dan `ctx.model?.provider`) sehingga kesalahan yang tidak terkait dari penyedia lain tidak tersentuh.\n- Cocokkan pola khusus penyedia, bukan pola luapan umum pi. Kesalahan penulisan ulang batas kecepatan atau pelambatan (`rate limit`, `too many requests`) akan memicu pemadatan secara salah, bukan jalur percobaan ulang dengan kemunduran normal pi.\n- Lewati ketika `errorMessage` sudah menyertakan `context_length_exceeded` sehingga handlernya idempoten.\n\n### Pendaftaran\n\nDaftarkan fungsi streaming Anda:\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## Menguji Implementasi Anda\n\nUji penyedia Anda terhadap rangkaian pengujian yang sama dengan yang digunakan oleh penyedia bawaan. Salin dan sesuaikan file pengujian ini dari [packages/ai/test/](https://github.com/earendil-works/pi-mono/tree/main/packages/ai/test):\n\n| Tes | Tujuan |\n|------|---------|\n| `stream.test.ts` | Streaming dasar, keluaran teks |\n| `tokens.test.ts` | Penghitungan dan penggunaan token |\n| `abort.test.ts` | Batalkan penanganan sinyal |\n| `empty.test.ts` | Respons kosong/minimal |\n| `context-overflow.test.ts` | Batas jendela konteks |\n| `image-limits.test.ts` | Penanganan masukan gambar |\n| `unicode-surrogate.test.ts` | Kasus tepi Unicode |\n| `tool-call-without-result.test.ts` | Kasus tepi panggilan alat |\n| `image-tool-result.test.ts` | Gambar dalam hasil alat |\n| `total-tokens.test.ts` | Perhitungan total token |\n| `cross-provider-handoff.test.ts` | Penyerahan konteks antar penyedia |\n\nJalankan pengujian dengan pasangan penyedia/model Anda untuk memverifikasi kompatibilitas.\n\n## Referensi Konfigurasi\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## Referensi Definisi Model\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` mengirim `reasoning: { effort }`. `deepseek` mengirim `thinking: { type: \"enabled\" | \"disabled\" }` dan `reasoning_effort` saat diaktifkan. `together` mengirim `reasoning: { enabled }` dan juga `reasoning_effort` ketika `supportsReasoningEffort` diaktifkan. `qwen` adalah untuk tingkat atas gaya DashScope `enable_thinking`. Gunakan `qwen-chat-template` untuk server lokal yang kompatibel dengan Qwen yang membaca `chat_template_kwargs.enable_thinking` dan memerlukan `preserve_thinking`. Gunakan `chat-template` untuk `chat_template_kwargs` yang dapat dikonfigurasi, misalnya DeepSeek V3.x di belakang vLLM dengan `chatTemplateKwargs: { \"thinking\": { \"$var\": \"thinking.enabled\" } }`. Gunakan `thinkingFormat: \"baseten\"` dengan `chatTemplateArgs` ketika penyedia mengharapkan nilai peralihan di bawah `chat_template_args` dan secara opsional mendukung `reasoning_effort` tingkat atas.\n`cacheControlFormat: \"anthropic\"` menerapkan penanda gaya Antropis `cache_control` ke perintah sistem, definisi alat terakhir, dan konten teks pengguna, asisten, atau hasil alat terakhir.","sourceFile":"custom-provider.md"},"development":{"title":"Perkembangan","markdown":"Lihat [AGENTS.md](https://github.com/earendil-works/pi-mono/blob/main/AGENTS.md) untuk pedoman tambahan.\n\n## Pengaturan\n\n```bash\ngit clone https://github.com/earendil-works/pi-mono\ncd pi-mono\nnpm install\nnpm run build\n```\n\nJalankan dari sumber:\n\n```bash\n/path/to/pi-mono/pi-test.sh\n```\n\nSkrip dapat dijalankan dari direktori mana pun. Pi menyimpan direktori kerja penelepon saat ini.\n\n## Fork / Rebranding\n\nKonfigurasikan melalui `package.json`:\n\n```json\n{\n  \"piConfig\": {\n    \"name\": \"pi\",\n    \"configDir\": \".pi\"\n  }\n}\n```\n\nUbah kolom `name`, `configDir`, dan `bin` untuk fork Anda. Mempengaruhi CLI spanduk, jalur konfigurasi, dan nama variabel lingkungan.\n\n## Resolusi Jalur\n\nTiga mode eksekusi: npm instal, biner mandiri, tsx dari sumber.\n\n**Selalu gunakan `src/config.ts`** untuk aset paket:\n\n```typescript\nimport { getPackageDir, getThemeDir } from \"./config.js\";\n```\n\nJangan pernah menggunakan `__dirname` secara langsung untuk aset paket.\n\n## Perintah Debug\n\n`/debug` (tersembunyi) menulis ke `~/.pi/agent/pi-debug.log`:\n- Dirender TUI baris dengan kode ANSI\n- Pesan terakhir dikirim ke LLM\n\n## Pengujian\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## Struktur Proyek\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":"Variabel Lingkungan","markdown":"Pi menggunakan variabel lingkungan dalam tiga cara:\n\n- Variabel seperti `PI_OFFLINE` mengonfigurasi proses Pi.\n- Pi menyetel `PI_CODING_AGENT` sehingga proses anak dapat mendeteksi bahwa proses tersebut berjalan di dalam Pi.\n- Perintah yang dijalankan oleh alat bash yang dapat dipanggil LLM menerima variabel `PI_*` yang menjelaskan sesi saat ini.\n\nVariabel kunci API penyedia didokumentasikan secara terpisah di [Providers](providers.md#environment-variables-or-auth-file).\n\n## Penanda Proses\n\nTitik masuk CLI dan RPC ditetapkan `PI_CODING_AGENT=true`. Proses anak mewarisinya dan dapat menggunakannya untuk mendeteksi bahwa proses tersebut berjalan di dalam Pi. Ini tidak spesifik untuk sesi dan tidak diatur secara otomatis ketika Pi tertanam melalui SDK.\n\n## Lingkungan Sesi Alat Bash\n\nPerintah yang dijalankan oleh alat bash menerima status sesi Pi saat ini:\n\n| Variabel | Keterangan |\n|----------|-------------|\n| `PI_SESSION_ID` | ID sesi saat ini |\n| `PI_SESSION_FILE` | Jalur absolut ke file JSONL sesi saat ini; tidak disetel untuk sesi singkat |\n| `PI_PROVIDER` | Penyedia model yang saat ini dipilih |\n| `PI_MODEL` | ID model yang dipilih saat ini |\n| `PI_REASONING_LEVEL` | Tingkat penalaran efektif saat ini: `off`, `minimal`, `low`, `medium`, `high`, `xhigh`, atau `max` |\n\nNilai-nilai tersebut diselesaikan ketika setiap perintah dimulai. Oleh karena itu, mengganti model atau mengubah tingkat penalaran akan memengaruhi perintah bash berikutnya tanpa memulai ulang Pi. `PI_PROVIDER` dan `PI_MODEL` mengidentifikasi model Pi yang dipilih, bukan model upstream berbeda yang dapat dipilih secara internal oleh router.\n\nSaat ditanya model atau penyedia mana yang berjalan, periksa variabel berikut alih-alih menyimpulkan jawaban dari perintah sistem:\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\nFile sesi dapat diperiksa secara langsung ketika sesi tersebut persisten:\n\n```bash\nif [ -n \"$PI_SESSION_FILE\" ]; then\n  tail -n 1 \"$PI_SESSION_FILE\"\nfi\n```\n\nVariabel-variabel ini dimasukkan ke dalam alat bash yang dapat dipanggil LLM. Mereka tidak dimasukkan ke dalam perintah `!` atau `!!` yang dimasukkan pengguna.\n\n### Alat Bash Khusus\n\nAlat Bash yang dibuat dengan `createBashTool()` mengekspos lingkungan sesi secara default ketika didaftarkan dengan Pi. Injeksi terjadi sebelum `spawnHook`, jadi hook menerima variabel di `ctx.env`:\n\n```typescript\nconst bashTool = createBashTool(cwd, {\n  spawnHook: (ctx) => ({\n    ...ctx,\n    env: { ...ctx.env, CI: \"1\" },\n  }),\n});\n```\n\nNonaktifkan metadata sesi secara independen dari spawn hook:\n\n```typescript\nconst bashTool = createBashTool(cwd, {\n  exposeSessionEnvironment: false,\n  spawnHook: (ctx) => ctx,\n});\n```\n\nJika dinonaktifkan, Pi menghapus nilai yang diwariskan untuk variabel-variabel ini sehingga proses Pi yang disarangkan tidak mengekspos metadata sesi induk yang sudah usang.\n\n## Pi Konfigurasi Proses\n\nVariabel-variabel ini dibaca oleh Pi itu sendiri:\n\n| Variabel | Keterangan |\n|----------|-------------|\n| `PI_CODING_AGENT_DIR` | Ganti direktori konfigurasi; standarnya adalah `~/.pi/agent` |\n| `PI_CODING_AGENT_SESSION_DIR` | Ganti penyimpanan sesi; digantikan oleh `--session-dir` |\n| `PI_PACKAGE_DIR` | Ganti direktori paket, berguna untuk jalur penyimpanan Nix/Guix |\n| `PI_OFFLINE` | Nonaktifkan operasi jaringan startup, termasuk pemeriksaan pembaruan, pembaruan paket, dan telemetri pemasangan/perbarui |\n| `PI_SKIP_VERSION_CHECK` | Nonaktifkan permintaan versi terbaru `pi.dev` |\n| `PI_TELEMETRY` | Ganti header atribusi pemasangan/perbarui telemetri dan penyedia: `1`/`true`/`yes` atau `0`/`false`/`no` |\n| `PI_CACHE_RETENTION` | Setel ke `long` untuk caching cepat penyedia yang diperluas jika didukung |\n| `PI_SHARE_VIEWER_URL` | Ganti URL dasar yang digunakan oleh `/share` |\n| `PI_HARDWARE_CURSOR` | Setel ke `1` untuk menampilkan kursor perangkat keras; lihat [Terminal setup](terminal-setup.md) |\n| `VISUAL`, `EDITOR` | Penggantian editor eksternal jika `externalEditor` tidak disetel |\n| `HTTP_PROXY`, `HTTPS_PROXY` | Permintaan HTTP keluar proksi |\n\nKredensial penyedia seperti `ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, dan konfigurasi penyedia cloud tercantum di [Providers](providers.md#environment-variables-or-auth-file).","sourceFile":"environment-variables.md"},"extensions":{"title":"Extensions","markdown":"> pi dapat membuat ekstensi. Mintalah untuk membuat satu untuk kasus penggunaan Anda.\n\n\nExtensions adalah modul TypeScript yang memperluas perilaku pi. Mereka dapat berlangganan peristiwa siklus hidup, mendaftarkan alat khusus yang dapat dipanggil oleh LLM, menambahkan perintah, dan banyak lagi.\n\n> **Penempatan untuk /muat ulang:** Masukkan ekstensi di `~/.pi/agent/extensions/` (global) atau `.pi/extensions/` (proyek-lokal) untuk penemuan otomatis. Gunakan `pi -e./path.ts` hanya untuk tes cepat. Extensions di lokasi yang ditemukan secara otomatis dapat diisi ulang dengan `/reload`.\n\n**Kemampuan utama:**\n- **Alat khusus** - Daftarkan alat yang dapat dihubungi LLM melalui `pi.registerTool()`\n- **Intersepsi peristiwa** - Memblokir atau mengubah panggilan alat, memasukkan konteks, menyesuaikan pemadatan\n- **Interaksi pengguna** - Meminta pengguna melalui `ctx.ui` (pilih, konfirmasi, masukkan, beri tahu)\n- **Komponen UI khusus** - Komponen TUI lengkap dengan input keyboard melalui `ctx.ui.custom()` untuk interaksi kompleks\n- **Perintah khusus** - Daftarkan perintah seperti `/mycommand` melalui `pi.registerCommand()`\n- **Kegigihan sesi** - Status penyimpanan yang bertahan saat dimulai ulang melalui `pi.appendEntry()`\n- **Render khusus** - Kontrol bagaimana panggilan alat/hasil dan pesan muncul di TUI\n\n**Contoh kasus penggunaan:**\n- Gerbang izin (konfirmasi sebelum `rm -rf`, `sudo`, dll.)\n- Git pos pemeriksaan (simpanan di setiap belokan, pulihkan di cabang)\n- Perlindungan jalur (blok tulis ke `.env`, `node_modules/`)\n- Pemadatan khusus (ringkas percakapan sesuai keinginan Anda)\n- Ringkasan percakapan (lihat contoh `summarize.ts`)\n- Alat interaktif (pertanyaan, penyihir, dialog khusus)\n- Alat stateful (daftar tugas, kumpulan koneksi)\n- Integrasi eksternal (pengamat file, webhook, pemicu CI)\n- Permainan sambil menunggu (lihat contoh `snake.ts`)\n\nLihat [examples/extensions/](../examples/extensions/) untuk implementasi kerja.\n\n## Daftar isi\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## Mulai Cepat\n\nBuat `~/.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\nUji dengan tanda `--extension` (atau `-e`):\n\n```bash\npi -e ./my-extension.ts\n```\n\n## Lokasi Perluasan\n\n> **Keamanan:** Extensions dijalankan dengan izin sistem penuh Anda dan dapat mengeksekusi kode arbitrer. Instal hanya dari sumber yang Anda percayai.\n\nExtensions ditemukan secara otomatis dari lokasi tepercaya. Entri proyek-lokal `.pi/extensions` dimuat hanya setelah proyek dipercaya.\n\n| Lokasi | Cakupan |\n|----------|-------|\n| `~/.pi/agent/extensions/*.ts` | Global (semua proyek) |\n| `~/.pi/agent/extensions/*/index.ts` | Global (subdirektori) |\n| `.pi/extensions/*.ts` | Proyek-lokal |\n| `.pi/extensions/*/index.ts` | Proyek-lokal (subdirektori) |\n\nJalur tambahan melalui `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\nUntuk berbagi ekstensi melalui npm atau paket git sebagai pi, lihat [packages.md](packages.md).\n\n## Impor yang Tersedia\n\n| Kemasan | Tujuan |\n|---------|---------|\n| `@earendil-works/pi-coding-agent` | Jenis ekstensi (`ExtensionAPI`, `ExtensionContext`, acara) |\n| `typebox` | Definisi skema untuk parameter alat |\n| `@earendil-works/pi-ai` | Utilitas AI (`StringEnum` untuk enum yang kompatibel dengan Google) |\n| `@earendil-works/pi-tui` | TUI komponen untuk rendering khusus |\n\nnpm dependensi juga berfungsi. Tambahkan `package.json` di sebelah ekstensi Anda (atau di direktori induk), jalankan `npm install`, dan impor dari `node_modules/` diselesaikan secara otomatis.\n\nUntuk paket pi terdistribusi yang diinstal dengan `pi install` (npm atau git), deps runtime harus dalam `dependencies`. Instalasi paket menggunakan instalasi produksi (`npm install --omit=dev`) secara default, jadi `devDependencies` tidak tersedia saat runtime; ketika `npmCommand` dikonfigurasi, paket git menggunakan `install` biasa untuk kompatibilitas dengan pembungkus.\n\nNode.js bawaan (`node:fs`, `node:path`, dll.) juga tersedia.\n\n## Menulis Ekstensi\n\nEkstensi mengekspor fungsi default pabrik yang menerima `ExtensionAPI`. Pabrik bisa sinkron atau asinkron:\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 dimuat melalui [jiti](https://github.com/unjs/jiti), jadi TypeScript berfungsi tanpa kompilasi.\n\nJika pabrik mengembalikan `Promise`, pi menunggunya sebelum melanjutkan startup. Itu berarti inisialisasi asinkron selesai sebelum `session_start`, sebelum `resources_discover`, dan sebelum pendaftaran penyedia yang diantri melalui `pi.registerProvider()` dihapus.\n\n### Fungsi pabrik asinkron\n\nGunakan pabrik async untuk pekerjaan startup satu kali seperti mengambil konfigurasi jarak jauh atau menemukan model yang tersedia secara dinamis.\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\nPola ini membuat model yang diambil tersedia selama pengaktifan normal dan hingga `pi --list-models`.\n\n### Sumber daya berumur panjang dan penutupan\n\nPabrik ekstensi dapat berjalan dalam pemanggilan yang tidak pernah memulai sesi. Jangan memulai sumber daya latar belakang seperti proses, soket, pengamat file, atau pengatur waktu dari pabrik.\n\nTunda pengaktifan sumber daya latar belakang hingga `session_start` atau perintah/alat/peristiwa yang memerlukan sumber daya. Daftarkan penangan `session_shutdown` idempoten untuk menutup sumber daya cakupan sesi apa pun yang Anda mulai.\n\n### Gaya Ekstensi\n\n**File tunggal** - paling sederhana, untuk ekstensi kecil:\n\n```\n~/.pi/agent/extensions/\n└── my-extension.ts\n```\n\n**Direktori dengan index.ts** - untuk ekstensi multi-file:\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**Paket dengan dependensi** - untuk ekstensi yang memerlukan npm paket:\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\nJalankan `npm install` di direktori ekstensi, lalu impor dari `node_modules/` berfungsi secara otomatis.\n\n## Acara\n\n### Ikhtisar Siklus Hidup\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### Acara Permulaan\n\n#### proyek_kepercayaan\n\nDiaktifkan sebelum pi memutuskan apakah akan mempercayai proyek dengan konfigurasi dinamis (`.pi` atau `.agents/skills`). Ini berjalan saat startup dan ketika penggantian sesi (misalnya `/resume`) memasuki cwd yang kepercayaannya belum terselesaikan dalam proses saat ini. Hanya ekstensi pengguna/global dan ekstensi CLI `-e` yang berpartisipasi; ekstensi proyek-lokal tidak dimuat sampai kepercayaan teratasi.\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\nPenangan `project_trust` harus mengembalikan `{ trusted: \"yes\" | \"no\" | \"undecided\" }`. Ekstensi pengguna/global atau CLI yang mengembalikan `\"yes\"` atau `\"no\"` memiliki keputusan; keputusan ya/tidak yang pertama menang dan menekan perintah kepercayaan yang ada di dalamnya. Gunakan `remember: true` untuk mempertahankan keputusan ya/tidak; jika tidak, ini hanya berlaku untuk proses saat ini. Kembalikan `\"undecided\"` agar penangan selanjutnya atau aliran kepercayaan bawaan dapat memutuskan. Periksa `ctx.hasUI` sebelum meminta. Jika tidak ada pengendali yang mengembalikan ya/tidak, resolusi kepercayaan normal berlanjut: keputusan `trust.json` yang disimpan diterapkan terlebih dahulu, lalu `defaultProjectTrust` mengontrol apakah pi bertanya, memercayai, atau menolak secara default.\n\n### Peristiwa Sumber Daya\n\n#### sumber daya_temukan\n\nDiaktifkan setelah `session_start` sehingga ekstensi dapat menyumbangkan keahlian tambahan, prompt, dan jalur tema.\nJalur startup menggunakan `reason: \"startup\"`. Muat ulang menggunakan `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### Acara Sesi\n\nLihat [Session Format](session-format.md) untuk penyimpanan sesi internal dan SessionManager API.\n\n#### sesi_mulai\n\nDiaktifkan saat sesi dimulai, dimuat, atau dimuat ulang.\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#### sesi_info_berubah\n\nDiaktifkan ketika nama tampilan sesi saat ini diatur melalui `/name`, RPC, atau `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#### sesi_sebelum_beralih\n\nDiaktifkan sebelum memulai sesi baru (`/new`) atau berpindah sesi (`/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\nSetelah tindakan peralihan atau sesi baru berhasil, pi mengeluarkan `session_shutdown` untuk instance ekstensi lama, memuat ulang dan mengikat ulang ekstensi untuk sesi baru, lalu memancarkan `session_start` dengan `reason: \"new\" | \"resume\"` dan `previousSessionFile`.\nLakukan pekerjaan pembersihan di `session_shutdown`, lalu bangun kembali status dalam memori di `session_start`.\n\n#### sesi_sebelum_fork\n\nDipecat saat melakukan forking melalui `/fork` atau mengkloning melalui `/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\nSetelah fork atau kloning berhasil, pi mengeluarkan `session_shutdown` untuk instance ekstensi lama, memuat ulang dan mengikat ulang ekstensi untuk sesi baru, lalu memancarkan `session_start` dengan `reason: \"fork\"` dan `previousSessionFile`.\nLakukan pekerjaan pembersihan di `session_shutdown`, lalu bangun kembali status dalam memori di `session_start`.\n\n#### session_before_compact / session_compact\n\nDitembak saat pemadatan. Lihat [compaction.md](compaction.md) untuk detailnya.\n\n```typescript\npi.on(\"session_before_compact\", async (event, ctx) => {\n  const { preparation, branchEntries, customInstructions, reason, willRetry, signal } = event;\n\n  // reason - \"manual\" (/compact), \"threshold\", or \"overflow\"\n  // willRetry - whether the aborted turn is retried after compaction (overflow recovery)\n\n  // Cancel:\n  return { cancel: true };\n\n  // Custom summary:\n  return {\n    compaction: {\n      summary: \"...\",\n      firstKeptEntryId: preparation.firstKeptEntryId,\n      tokensBefore: preparation.tokensBefore,\n      // usage: summaryResponse.usage, // Optional; included in session totals\n    }\n  };\n});\n\npi.on(\"session_compact\", async (event, ctx) => {\n  // event.compactionEntry - the saved compaction\n  // event.fromExtension - whether extension provided it\n  // event.reason - \"manual\" (/compact), \"threshold\", or \"overflow\"\n  // event.willRetry - whether the aborted turn is retried after compaction (overflow recovery)\n});\n```\n\n#### session_before_tree / session_tree\n\nDitembak pada navigasi `/tree`. Lihat [Sessions](sessions.md) untuk konsep navigasi pohon.\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#### sesi_shutdown\n\nDiaktifkan sebelum runtime sesi yang dimulai dirobohkan. Gunakan ini untuk membersihkan sumber daya yang dibuka dari `session_start` atau kait cakupan sesi lainnya.\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### Acara Agen\n\n#### sebelum_agen_mulai\n\nDipecat setelah pengguna mengirimkan prompt, sebelum loop agen. Dapat memasukkan pesan dan/atau memodifikasi prompt sistem.\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\nBidang `systemPromptOptions` memberikan ekstensi akses ke data terstruktur yang sama yang digunakan Pi untuk membuat perintah sistem. Hal ini memungkinkan Anda memeriksa apa yang Pi telah dimuat — perintah khusus, pedoman, cuplikan alat, context files, keterampilan — tanpa menemukan kembali sumber daya atau menguraikan ulang tanda. Gunakan saat ekstensi Anda perlu membuat perubahan mendalam dan terinformasi pada perintah sistem dengan tetap menghormati konfigurasi yang disediakan pengguna.\n\nDi dalam `before_agent_start`, `event.systemPrompt` dan `ctx.getSystemPrompt()` keduanya mencerminkan perintah sistem berantai pada pengendali saat ini. Nanti `before_agent_start` penangan masih bisa memodifikasinya lagi.\n\n#### agen_mulai / agen_end / agen_settled\n\n`agent_start` terpicu saat proses agen tingkat rendah dimulai. `agent_end` terpicu saat proses tersebut berakhir, namun Pi masih dapat mencoba ulang secara otomatis, memadatkan otomatis, dan mencoba lagi, atau melanjutkan dengan pesan tindak lanjut yang antri. Gunakan `agent_settled` untuk integrasi status yang perlu diketahui Pi tidak akan terus berjalan secara otomatis.\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#### turn_start / turn_end\n\nDipecat untuk setiap giliran (satu respons LLM + panggilan alat).\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#### pesan_mulai / pembaruan_pesan / pesan_akhir\n\nDiaktifkan karena pembaruan siklus hidup pesan.\n\n- `message_start` dan `message_end` diaktifkan untuk pesan pengguna, asisten, dan toolResult.\n- `message_update` diaktifkan untuk pembaruan streaming asisten.\n- `message_end` penangan dapat mengembalikan `{ message }` untuk menggantikan pesan yang telah diselesaikan. Penggantinya harus tetap sama `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\nDiaktifkan karena pembaruan siklus hidup eksekusi alat.\n\nDalam mode alat paralel:\n- `tool_execution_start` dipancarkan dalam urutan sumber asisten selama fase pra-penerbangan\n- `tool_execution_update` peristiwa mungkin disisipkan di seluruh alat\n- `tool_execution_end` dikeluarkan dalam urutan penyelesaian alat setelah setiap alat diselesaikan\n- peristiwa pesan `toolResult` terakhir masih dipancarkan kemudian dalam urutan sumber asisten\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#### konteks\n\nDipecat sebelum setiap panggilan LLM. Ubah pesan secara non-destruktif. Lihat [Session Format](session-format.md) untuk jenis pesan.\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#### sebelum_penyedia_header\n\nDiaktifkan setelah header HTTP keluar dipasang. Gunakan untuk menambah, mengganti, atau menghapus header permintaan.\n\nPenangan bermutasi `event.headers` di tempatnya. Tetapkan kunci pada string untuk menambah atau menggantinya, atau ke `null` untuk menghapusnya.\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\nBerjalan sekali per permintaan penyedia; percobaan ulang menggunakan kembali header yang sama daripada menembakkan kembali hook.\n\n#### sebelum_penyedia_permintaan\n\nDiaktifkan setelah payload khusus penyedia dibuat, tepat sebelum permintaan dikirim. Penangan dijalankan dalam urutan pemuatan ekstensi. Mengembalikan `undefined` membuat payload tidak berubah. Mengembalikan nilai lain akan menggantikan payload untuk penangan selanjutnya dan untuk permintaan sebenarnya.\n\nKait ini dapat menulis ulang instruksi sistem tingkat penyedia atau menghapusnya seluruhnya. Perubahan tingkat muatan tersebut tidak tercermin oleh `ctx.getSystemPrompt()`, yang melaporkan string perintah sistem Pi dan bukan muatan penyedia serial akhir.\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\nIni terutama berguna untuk men-debug serialisasi penyedia dan perilaku cache.\n\n#### after_provider_response\n\nDiaktifkan setelah respons HTTP diterima dan sebelum isi alirannya digunakan. Penangan dijalankan dalam urutan pemuatan ekstensi.\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\nKetersediaan header tergantung pada penyedia dan transportasi. Providers bahwa respons HTTP abstrak tidak boleh mengekspos header.\n\n### Acara Model\n\n#### model_pilih\n\nDiaktifkan ketika model berubah melalui perintah `/model`, perputaran model (`Ctrl+P`), atau pemulihan sesi.\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\nGunakan ini untuk memperbarui elemen UI (bilah status, footer) atau melakukan inisialisasi khusus model saat model aktif berubah.\n\n#### berpikir_tingkat_pilih\n\nDipecat ketika tingkat berpikir berubah. Ini hanya untuk pemberitahuan; nilai pengembalian handler diabaikan.\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\nGunakan ini untuk memperbarui UI ekstensi ketika `pi.setThinkingLevel()`, perubahan model, atau kontrol tingkat berpikir bawaan mengubah tingkat berpikir aktif.\n\n### Acara Alat\n\n#### alat_panggilan\n\nDiaktifkan setelah `tool_execution_start`, sebelum alat dijalankan. **Dapat memblokir.** Gunakan `isToolCallEventType` untuk mempersempit dan mendapatkan masukan yang diketik.\n\nSebelum `tool_call` berjalan, pi menunggu peristiwa Agen yang dipancarkan sebelumnya selesai dikuras melalui `AgentSession`. Ini berarti `ctx.sessionManager` diperbarui melalui pesan pemanggil alat asisten saat ini.\n\nDalam mode eksekusi alat paralel default, panggilan alat saudara dari pesan asisten yang sama dipra-penerbangan secara berurutan, lalu dieksekusi secara bersamaan. `tool_call` tidak dijamin melihat hasil alat saudara dari pesan asisten yang sama di `ctx.sessionManager`.\n\n`event.input` bisa berubah. Mutasi di tempatnya untuk menambal argumen alat sebelum dieksekusi.\n\nJaminan perilaku:\n- Mutasi ke `event.input` mempengaruhi eksekusi alat sebenarnya\n- Penangan `tool_call` kemudian melihat mutasi yang dilakukan oleh penangan sebelumnya\n- Tidak ada validasi ulang yang dilakukan setelah mutasi Anda\n- Kembalikan nilai dari `tool_call` pemblokiran kontrol melalui `{ block: true, reason?: string, terminate?: boolean }`\n- `terminate` hanya berlaku untuk panggilan yang diblokir; agen berhenti lebih awal hanya ketika setiap hasil akhir dalam batch dihentikan\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#### Mengetik masukan alat khusus\n\nAlat khusus harus mengekspor jenis masukannya:\n\n```typescript\n// my-extension.ts\nexport type MyToolInput = Static<typeof myToolSchema>;\n```\n\nGunakan `isToolCallEventType` dengan parameter tipe eksplisit:\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#### alat_hasil\n\nDiaktifkan setelah eksekusi alat selesai dan sebelum `tool_execution_end` ditambah peristiwa pesan hasil alat akhir dikeluarkan. **Dapat mengubah hasil.**\n\nDalam mode pahat paralel, `tool_result` dan `tool_execution_end` dapat disisipkan dalam urutan penyelesaian pahat, sedangkan kejadian pesan akhir `toolResult` masih dikirimkan kemudian dalam urutan sumber asisten.\n\n`tool_result` rantai penangan seperti middleware:\n- Penangan dijalankan dalam urutan pemuatan ekstensi\n- Setiap penangan melihat hasil terbaru setelah penangan sebelumnya berubah\n- Penangan dapat mengembalikan sebagian patch (`content`, `details`, `isError`, atau `usage`); bidang yang dihilangkan mempertahankan nilainya saat ini\n\nGunakan `ctx.signal` untuk pekerjaan asinkron bersarang di dalam pengendali. Hal ini memungkinkan Esc membatalkan panggilan model, `fetch()`, dan operasi sadar pembatalan lainnya yang dimulai oleh ekstensi.\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### Acara Pesta Pengguna\n\n#### pengguna_bash\n\nDiaktifkan ketika pengguna menjalankan perintah `!` atau `!!`. **Dapat mencegat.**\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### Masukan Acara\n\n#### masukan\n\nDipicu ketika masukan pengguna diterima, setelah perintah ekstensi diperiksa tetapi sebelum perluasan keterampilan dan templat. Acara ini melihat teks masukan mentah, jadi `/skill:foo` dan `/template` belum diperluas.\n\n**Pemrosesan pesanan:**\n1. Perintah ekstensi (`/cmd`) diperiksa terlebih dahulu - jika ditemukan, handler dijalankan dan event input dilewati\n2. `input` peristiwa kebakaran - dapat mencegat, mengubah, atau menangani\n3. Jika tidak ditangani: perintah keterampilan (`/skill:name`) diperluas ke konten keterampilan\n4. Jika tidak ditangani: prompt templates (`/template`) diperluas ke konten templat\n5. Pemrosesan agen dimulai (`before_agent_start`, dll.)\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**Hasil:**\n- `continue` - melewati tanpa perubahan (default jika handler tidak mengembalikan apa pun)\n- `transform` - ubah teks/gambar, lalu lanjutkan perluasan\n- `handled` - lewati agen sepenuhnya (penangan pertama yang mengembalikan ini menang)\n\nMengubah rantai di seluruh penangan. Lihat [input-transform.ts](../examples/extensions/input-transform.ts) dan [input-transform-streaming.ts](../examples/extensions/input-transform-streaming.ts) untuk perutean `streamingBehavior`.\n\n## Konteks Ekstensi\n\nSemua penangan menerima `ctx: ExtensionContext`.\n\n### ctx.ui\n\nMetode UI untuk interaksi pengguna. Lihat [Custom UI](#custom-ui) untuk detail selengkapnya.\n\n### ctx.mode\n\nMode lari saat ini: `\"tui\"`, `\"rpc\"`, `\"json\"`, atau `\"print\"`. Gunakan `ctx.mode === \"tui\"` untuk menjaga fitur khusus terminal seperti `custom()`, pabrik komponen, input terminal, dan rendering TUI langsung.\n\n### ctx.hasUI\n\n`true` dalam mode TUI dan RPC. `false` dalam mode cetak (`-p`) dan mode JSON. Gunakan ini untuk menjaga metode dialog (`select`, `confirm`, `input`, `editor`) dan metode api-dan-lupakan (`notify`, `setStatus`, `setWidget`, `setTitle`, `setEditorText`) yang berfungsi baik di TUI maupun RPC mode. Dalam mode RPC, beberapa metode khusus TUI tidak dapat dioperasikan atau dikembalikan secara default (lihat [rpc.md](rpc.md#extension-ui-protocol)).\n\n### ctx.cwd\n\nDirektori kerja saat ini.\n\nGunakan `CONFIG_DIR_NAME` alih-alih melakukan hardcoding `.pi` saat membuat jalur konfigurasi proyek-lokal. Distribusi yang diganti mereknya dapat menggunakan nama direktori konfigurasi yang berbeda.\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\nMengembalikan apakah kepercayaan proyek-lokal aktif untuk konteks sesi saat ini. Hal ini mencakup keputusan perwalian sementara dan CLI pengesampingan perwalian, bukan hanya keputusan yang disimpan dalam penyimpanan perwalian global.\n\nGunakan ini sebelum membaca konfigurasi ekstensi proyek-lokal yang hanya berlaku untuk proyek tepercaya.\n\n### ctx.sessionManager\n\nAkses hanya baca ke status sesi. Lihat [Session Format](session-format.md) untuk SessionManager API lengkap dan tipe entri.\n\nUntuk `tool_call`, status ini disinkronkan melalui pesan asisten saat ini sebelum penangan dijalankan. Dalam mode eksekusi alat paralel, masih belum ada jaminan untuk menyertakan hasil alat saudara dari pesan asisten yang sama.\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\nAkses ke model, penyedia, dan autentikasi terselesaikan. `ctx.modelRegistry.getProvider(id)` mengembalikan penyedia pi-ai yang efektif, sementara `getProviderAuth(id)` menyelesaikan API key saat ini, header, URL dasar, dan lingkungan cakupan penyedia tanpa memerlukan model yang dimuat. `ctx.model` adalah model aktif, dan `ctx.thinkingLevel` adalah tingkat berpikir efektif saat ini.\n\n`ctx.scopedModels` adalah daftar model baca-saja yang tercakup dalam sesi saat ini — kumpulan yang sama yang ditampilkan oleh perintah `/scoped-models`. Ini diselesaikan pada awal sesi dari bendera `--models` CLI dan pengaturan `enabledModels` (dicocokkan dengan katalog yang tersedia dengan minimatch di `provider/modelId` atau `modelId` kosong). Ini kosong jika tidak ada pelingkupan yang dikonfigurasi, artinya setiap model yang tersedia dapat digunakan. Setiap entri adalah `{ model, thinkingLevel? }`, dengan `thinkingLevel` diatur hanya ketika pola menyematkannya (misalnya `anthropic/*:high`). Gunakan ini untuk mengisi pemilih model yang mencerminkan pemilih model bawaan, alih-alih menghitung seluruh katalog melalui `ctx.modelRegistry.getAvailable()`.\n\n### ctx.signal\n\nSinyal pembatalan agen saat ini, atau `undefined` ketika tidak ada giliran agen yang aktif.\n\nGunakan ini untuk pekerjaan bersarang yang sadar akan pembatalan yang dimulai oleh penangan ekstensi, misalnya:\n- `fetch(..., { signal: ctx.signal })`\n- panggilan model yang menerima `signal`\n- file atau pembantu proses yang menerima `AbortSignal`\n\n`ctx.signal` biasanya ditentukan selama event giliran aktif seperti `tool_call`, `tool_result`, `message_update`, dan `turn_end`.\nBiasanya `undefined` dalam konteks idle atau non-turn seperti acara sesi, perintah ekstensi, dan pintasan diaktifkan saat pi idle.\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\nKontrol aliran pembantu. `ctx.isIdle()` salah saat Pi sedang memproses proses agen, percobaan ulang otomatis, percobaan pemadatan otomatis, atau kelanjutan antrean.\n\n### ctx.shutdown()\n\nMinta penutupan pi dengan baik.\n\n- **Mode interaktif:** Ditunda hingga agen menganggur (setelah memproses semua pesan kemudi dan tindak lanjut yang diantri).\n- **RPC mode:** Ditunda hingga status siaga berikutnya (setelah menyelesaikan respons perintah saat ini, saat menunggu perintah berikutnya).\n- **Mode cetak:** Tanpa pengoperasian. Proses keluar secara otomatis ketika semua perintah diproses.\n\nMemancarkan acara `session_shutdown` ke semua ekstensi sebelum keluar. Tersedia dalam semua konteks (event handler, alat, perintah, pintasan).\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\nMengembalikan penggunaan konteks saat ini untuk model aktif. Menggunakan penggunaan asisten terakhir bila tersedia, lalu memperkirakan token untuk pesan tambahan.\n\n```typescript\nconst usage = ctx.getContextUsage();\nif (usage && usage.tokens > 100_000) {\n  // ...\n}\n```\n\n### ctx.kompak()\n\nMemicu pemadatan tanpa menunggu selesai. Gunakan `onComplete` dan `onError` untuk tindakan tindak lanjut.\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\nMengembalikan string perintah sistem Pi saat ini.\n\n- Selama `before_agent_start`, hal ini mencerminkan perubahan cepat sistem berantai yang dilakukan sejauh ini untuk belokan saat ini.\n- Ini tidak termasuk mutasi pesan `context` selanjutnya.\n- Ini tidak termasuk `before_provider_request` penulisan ulang payload.\n- Jika ekstensi yang dimuat kemudian dijalankan setelah ekstensi Anda, ekstensi tersebut masih dapat mengubah ekstensi yang dikirimkan.\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## EkstensiPerintahKonteks\n\nPenangan perintah menerima `ExtensionCommandContext`, yang diperluas `ExtensionContext` dengan metode kontrol sesi. Ini hanya tersedia dalam perintah karena dapat menemui jalan buntu jika dipanggil dari event handler.\n\n### ctx.getSystemPromptOptions()\n\nMengembalikan input dasar Pi yang saat ini digunakan untuk membangun prompt sistem.\n\n```typescript\nconst options = ctx.getSystemPromptOptions();\nconst contextPaths = options.contextFiles?.map((file) => file.path) ?? [];\n```\n\nIni memiliki bentuk dan kemampuan berubah yang sama dengan `before_agent_start` `event.systemPromptOptions`: perintah khusus, alat aktif, cuplikan alat, pedoman perintah, teks perintah sistem yang ditambahkan, cwd, context files yang dimuat, dan keterampilan yang dimuat. Ini mungkin berisi konten file konteks penuh, jadi perlakukan itu sebagai data lokal ekstensi yang sensitif dan hindari memaparkannya melalui daftar perintah, log, atau metadata pelengkapan otomatis.\n\nIni melaporkan input prompt dasar saat ini. Ini tidak termasuk perubahan cepat sistem berantai `before_agent_start` per putaran, mutasi pesan peristiwa `context` di kemudian hari, atau penulisan ulang muatan `before_provider_request`.\n\n### ctx.waitForIdle()\n\nTunggu hingga agen menyelesaikan sepenuhnya, termasuk percobaan ulang otomatis, percobaan pemadatan otomatis, dan kelanjutan antrean:\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.sesi baru(pilihan?)\n\nBuat sesi baru:\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\nPilihan:\n- `parentSession`: file sesi induk untuk direkam di header sesi baru\n- `setup`: mutasikan `SessionManager` sesi baru sebelum `withSession` berjalan\n- `withSession`: menjalankan pekerjaan pasca peralihan dengan konteks sesi penggantian yang baru. Jangan gunakan `pi` / perintah `ctx` lama yang diambil; lihat [Session replacement lifecycle and footguns](#session-replacement-lifecycle-and-footguns).\n\n### ctx.fork(entryId, opsi?)\n\nCabang dari entri tertentu, membuat file sesi baru:\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\nPilihan:\n- `position`: `\"before\"` (default) bercabang sebelum pesan pengguna yang dipilih, mengembalikan prompt itu ke editor\n- `position`: `\"at\"` menduplikasi jalur aktif melalui entri yang dipilih tanpa memulihkan teks editor\n- `withSession`: menjalankan pekerjaan pasca peralihan dengan konteks sesi penggantian yang baru. Jangan gunakan `pi` / perintah `ctx` lama yang diambil; lihat [Session replacement lifecycle and footguns](#session-replacement-lifecycle-and-footguns).\n\n### ctx.navigateTree(targetId, opsi?)\n\nArahkan ke titik lain di 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\nPilihan:\n- `summarize`: Apakah akan membuat ringkasan cabang yang ditinggalkan\n- `customInstructions`: Instruksi khusus untuk peringkas\n- `replaceInstructions`: Jika benar, `customInstructions` menggantikan prompt default dan bukannya ditambahkan\n- `label`: Label untuk dilampirkan pada entri ringkasan cabang (atau entri target jika tidak diringkas)\n\n### ctx.switchSession(sessionPath, opsi?)\n\nBeralih ke file sesi lain:\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\nPilihan:\n- `withSession`: menjalankan pekerjaan pasca peralihan dengan konteks sesi penggantian yang baru. Jangan gunakan `pi` / perintah `ctx` lama yang diambil; lihat [Session replacement lifecycle and footguns](#session-replacement-lifecycle-and-footguns).\n\nUntuk menemukan sesi yang tersedia, gunakan metode statis `SessionManager.list()` atau `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### Siklus hidup dan footgun penggantian sesi\n\n`withSession` menerima `ReplacedSessionContext` baru, yang memperluas `ExtensionCommandContext` dengan pembantu asinkron `sendMessage()` dan `sendUserMessage()` yang terikat pada sesi penggantian.\n\nSiklus hidup dan footgun:\n- `withSession` berjalan hanya setelah sesi lama memancarkan `session_shutdown`, runtime lama telah dihapus, sesi pengganti telah di-rebound, dan instance ekstensi baru telah menerima `session_start`.\n- Callback masih dijalankan di penutupan asli, bukan di dalam instance ekstensi baru. Itu berarti instance ekstensi lama Anda mungkin sudah menjalankan pembersihan penutupannya sebelum `withSession` dimulai.\n- Objek terikat sesi `pi` / perintah lama `ctx` lama yang diambil akan menjadi basi setelah diganti dan akan dibuang jika digunakan. Gunakan hanya `ctx` yang diteruskan ke `withSession` untuk pekerjaan terikat sesi.\n- Benda mentah yang diekstraksi sebelumnya tetap menjadi tanggung jawab Anda. Misalnya, jika Anda menangkap `const sm = ctx.sessionManager` sebelum penggantian, `sm` tetap menjadi objek `SessionManager` yang lama. Jangan menggunakannya kembali setelah penggantian.\n- Kode di `withSession` harus mengasumsikan status apa pun yang dibatalkan oleh pengendali `session_shutdown` Anda sudah hilang. Hanya ambil data biasa yang bertahan saat dimatikan dengan bersih, seperti string, id, dan konfigurasi serial.\n\nPola aman:\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\nPola tidak aman:\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\nJalankan alur isi ulang yang sama seperti `/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\nPerilaku penting:\n- `await ctx.reload()` memancarkan `session_shutdown` untuk waktu proses ekstensi saat ini\n- Kemudian memuat ulang sumber daya dan mengeluarkan `session_start` dengan `reason: \"reload\"` dan `resources_discover` dengan alasan `\"reload\"`\n- Pengendali perintah yang sedang berjalan masih berlanjut di bingkai panggilan lama\n- Kode setelah `await ctx.reload()` masih berjalan dari versi pra-muat ulang\n- Kode setelah `await ctx.reload()` tidak boleh menganggap status ekstensi dalam memori yang lama masih valid\n- Setelah handler kembali, perintah/peristiwa/panggilan alat di masa mendatang menggunakan versi ekstensi baru\n\nUntuk perilaku yang dapat diprediksi, perlakukan reload sebagai terminal untuk pengendali tersebut (`await ctx.reload(); return;`).\n\nAlat dijalankan dengan `ExtensionContext`, sehingga tidak dapat memanggil `ctx.reload()` secara langsung. Gunakan perintah sebagai titik masuk muat ulang, lalu tampilkan alat yang mengantri perintah tersebut sebagai pesan pengguna tindak lanjut.\n\nContoh alat yang dapat dipanggil LLM untuk memicu pemuatan ulang:\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## EkstensiAPI Metode\n\n### pi.on(acara, pengendali)\n\nBerlangganan acara. Lihat [Events](#events) untuk jenis peristiwa dan nilai kembalian.\n\n### pi.registerTool(definisi)\n\nDaftarkan alat khusus yang dapat dipanggil oleh LLM. Lihat [Custom Tools](#custom-tools) untuk detail selengkapnya.\n\n`pi.registerTool()` berfungsi selama pemuatan ekstensi dan setelah pengaktifan. Anda dapat memanggilnya di dalam `session_start`, pengendali perintah, atau pengendali kejadian lainnya. Alat baru segera disegarkan di sesi yang sama, sehingga muncul di `pi.getAllTools()` dan dapat dipanggil oleh LLM tanpa `/reload`.\n\nGunakan `pi.setActiveTools()` untuk mengaktifkan atau menonaktifkan alat (termasuk alat yang ditambahkan secara dinamis) saat runtime.\n\nGunakan `promptSnippet` untuk memasukkan alat khusus ke dalam entri satu baris di `Available tools`, dan `promptGuidelines` untuk menambahkan poin khusus alat ke bagian `Guidelines` default saat alat aktif.\n\n**Penting:** `promptGuidelines` poin ditambahkan rata ke bagian `Guidelines` tanpa awalan nama alat. Setiap pedoman harus menyebutkan alat yang dirujuknya — hindari \"Gunakan alat ini ketika...\" karena LLM tidak dapat membedakan alat mana yang dimaksud dengan \"ini\". Tulis \"Gunakan my_tool ketika...\" sebagai gantinya.\n\nLihat [dynamic-tools.ts](../examples/extensions/dynamic-tools.ts) untuk contoh selengkapnya.\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(pesan, opsi?)\n\nMasukkan pesan khusus ke dalam sesi. Pesan khusus berpartisipasi dalam konteks LLM. Untuk konten tahan lama TUI saja yang tidak boleh dikirim ke LLM, gunakan [`pi.appendEntry()`](#piappendentrycustomtype-data) dengan [`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**Pilihan:**\n- `deliverAs` - Modus pengiriman:\n  - `\"steer\"` (default) - Mengantrekan pesan saat streaming. Dikirim setelah giliran asisten saat ini selesai menjalankan panggilan alatnya, sebelum panggilan LLM berikutnya.\n  - `\"followUp\"` - Menunggu agen selesai. Dikirim hanya ketika agen tidak lagi memiliki panggilan alat.\n  - `\"nextTurn\"` - Mengantri untuk permintaan pengguna berikutnya. Tidak mengganggu atau memicu apa pun.\n- `triggerTurn: true` - Jika agen menganggur, segera picu respons LLM. Hanya berlaku untuk mode `\"steer\"` dan `\"followUp\"` (diabaikan untuk `\"nextTurn\"`).\n\n### pi.sendUserMessage(konten, opsi?)\n\nKirim pesan pengguna ke agen. Berbeda dengan `sendMessage()` yang mengirimkan pesan khusus, ini mengirimkan pesan pengguna sebenarnya yang tampak seolah-olah diketik oleh pengguna. Selalu memicu belokan.\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**Pilihan:**\n- `deliverAs` - Diperlukan saat agen sedang streaming:\n  - `\"steer\"` - Mengantri pesan untuk dikirimkan setelah giliran asisten saat ini selesai menjalankan panggilan alatnya\n  - `\"followUp\"` - Menunggu agen menyelesaikan semua alat\n\nSaat tidak streaming, pesan langsung terkirim dan memicu giliran baru. Saat streaming tanpa `deliverAs`, terjadi kesalahan.\n\nLihat [send-user-message.ts](../examples/extensions/send-user-message.ts) untuk contoh lengkap.\n\n### pi.appendEntry(Tipe khusus, data?)\n\nPertahankan data ekstensi. Entri khusus TIDAK berpartisipasi dalam konteks LLM. Dalam mode interaktif, mereka juga dapat merender di dalam transkrip obrolan saat dipasangkan dengan `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(nama)\n\nTetapkan nama tampilan sesi (ditampilkan di pemilih sesi, bukan di pesan pertama).\n\n```typescript\npi.setSessionName(\"Refactor auth module\");\n```\n\n### pi.getSessionName()\n\nDapatkan nama sesi saat ini, jika disetel.\n\n```typescript\nconst name = pi.getSessionName();\nif (name) {\n  console.log(`Session: ${name}`);\n}\n```\n\n### pi.setLabel(entryId, label)\n\nMenetapkan atau menghapus label pada entri. Label adalah penanda yang ditentukan pengguna untuk bookmark dan navigasi (ditampilkan di pemilih `/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\nLabel tetap ada dalam sesi dan bertahan saat dimulai ulang. Gunakan mereka untuk menandai titik-titik penting (belokan, pos pemeriksaan) di pohon percakapan.\n\n### pi.registerCommand(nama, opsi)\n\nDaftarkan perintah.\n\nJika beberapa ekstensi mendaftarkan nama perintah yang sama, pi menyimpan semuanya dan menetapkan sufiks pemanggilan numerik dalam urutan pemuatan, misalnya `/review:1` dan `/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\nOpsional: tambahkan argumen pelengkapan otomatis untuk `/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\nDapatkan slash commands tersedia untuk pemanggilan melalui `prompt` di sesi saat ini. Termasuk perintah ekstensi, prompt templates, dan perintah keterampilan.\nDaftarnya cocok dengan urutan RPC `get_commands`: ekstensi terlebih dahulu, lalu templat, lalu keterampilan.\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\nSetiap entri memiliki bentuk ini:\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\nGunakan `sourceInfo` sebagai bidang asal kanonik. Jangan menyimpulkan kepemilikan dari nama perintah atau dari penguraian jalur ad hoc.\n\nPerintah interaktif bawaan (seperti `/model` dan `/settings`) tidak disertakan di sini. Mereka ditangani hanya secara interaktif\nmode dan tidak akan dijalankan jika dikirim melalui `prompt`.\n\n### pi.registerMessageRenderer(tipe khusus, penyaji)\n\nDaftarkan penyaji TUI khusus untuk pesan khusus dengan `customType` Anda. Pesan khusus dibuat dengan `pi.sendMessage()` dan berpartisipasi dalam konteks LLM. Lihat [Custom UI](#custom-ui).\n\n### pi.registerMarkdownTransformator(transformator)\n\nDaftarkan transformator untuk Markdown dalam teks pengguna normal, teks asisten, dan blok pemikiran. Trafo dijalankan dalam urutan beban ekstensi, dan setiap trafo menerima Markdown yang dikembalikan oleh trafo sebelumnya. Setelah rantai selesai, Pi merender konten yang diubah dengan penyaji bawaannya.\n\nTransformator menerima string Markdown dan konteks dengan:\n\n- `messageType` — `\"user\"`, `\"assistant\"`, atau `\"assistant-thinking\"`\n- `isStreaming` — `true` untuk pembaruan sebagian asisten; `false` untuk pengguna, asisten yang diselesaikan, dan pesan yang dipulihkan\n- `availableWidth` — kolom terminal persis tersedia untuk konten Markdown yang diubah\n\nKembalikan Markdown yang telah diubah:\n\n```typescript\npi.registerMarkdownTransformer((markdown, { messageType, isStreaming }) => {\n  if (isStreaming || messageType === \"assistant-thinking\") return markdown;\n  return markdown.replaceAll(\"-->\", \"→\");\n});\n```\n\nJika trafo mati, Pi mempertahankan Markdown yang dihasilkan sejauh ini dan dilanjutkan dengan trafo berikutnya. Pengaitnya hanya untuk tampilan: pesan asli tetap tidak berubah dalam konteks sesi dan model. Ini berjalan untuk pesan pengguna baru, pembaruan streaming asisten, pesan sesi yang dipulihkan, dan perubahan lebar terminal, sehingga transformator harus tetap sinkron dan murah.\n\n### pi.registerEntryRenderer(tipe khusus, penyaji)\n\nDaftarkan penyaji TUI khusus untuk entri khusus dengan `customType` Anda. Entri khusus dibuat dengan `pi.appendEntry()` dan tidak berpartisipasi dalam konteks 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(pintasan, opsi)\n\nDaftarkan pintasan keyboard. Lihat [keybindings.md](keybindings.md) untuk format pintasan dan pengikatan tombol bawaan.\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(nama, opsi)\n\nDaftarkan bendera 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(perintah, argumen, opsi?)\n\nJalankan perintah 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(nama)\n\nKelola alat aktif. Ini berfungsi untuk alat bawaan dan alat yang terdaftar secara dinamis. `pi.getActiveTools()` mengembalikan nama alat aktif sebagai `string[]`; `pi.getAllTools()` mengembalikan metadata untuk semua alat yang dikonfigurasi.\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()` mengembalikan `name`, `description`, `parameters`, `promptGuidelines`, dan `sourceInfo`.\n\nNilai `sourceInfo.source` yang umum:\n- `builtin` untuk alat bawaan\n- `sdk` untuk alat yang diteruskan melalui `createAgentSession({ customTools })`\n- metadata sumber ekstensi untuk alat yang didaftarkan oleh ekstensi\n\n### pi.setModel(model)\n\nTetapkan model saat ini. Mengembalikan `false` jika tidak ada API key yang tersedia untuk model. Lihat [models.md](models.md) untuk mengonfigurasi model khusus.\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(tingkat)\n\nDapatkan atau atur tingkat berpikir. Level disesuaikan dengan kemampuan model (model non-penalaran selalu menggunakan \"mati\"). Perubahan memancarkan `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 acara bersama untuk komunikasi antar ekstensi:\n\n```typescript\npi.events.on(\"my:event\", (data) => { ... });\npi.events.emit(\"my:event\", { ... });\n```\n\n### pi.registerProvider(nama, konfigurasi)\n\nDaftarkan atau ganti penyedia model secara dinamis. Berguna untuk proxy, titik akhir khusus, atau konfigurasi model seluruh tim.\n\nPanggilan yang dilakukan selama fungsi pabrik ekstensi dimasukkan ke dalam antrean dan diterapkan setelah pelari melakukan inisialisasi. Panggilan yang dilakukan setelah itu — misalnya dari pengendali perintah yang mengikuti alur pengaturan pengguna — langsung berlaku tanpa memerlukan `/reload`.\n\nPenyedia dinamis dapat menerapkan `refreshModels`. Pi memanggilnya selama penyegaran model, menerbitkan daftar yang dikembalikan secara sinkron melalui penyedia, dan meneruskan konteks kredensial/katalog tersimpan/jaringan/sinyal kanonik. Ekstensi memutuskan apakah akan mempertahankan metadata katalog melalui pemeriksaan generasi `context.publish({ persist: entry })`; server langsung seperti llama.cpp dapat mengembalikan model tanpa menyimpannya.\n\n`context.signal` selalu merupakan sinyal konkret dan callback penyedia harus meneruskannya ke pemblokiran I/O. Panggilan publik `ModelRuntime.refresh()` dan `ModelRegistry.refresh()` menerima sinyal opsional dan tidak dibatasi jika dihilangkan; ekstensi dan aplikasi memilih tenggat waktu mereka sendiri. Pembatalan menghentikan penelpon menunggu meskipun penyedia mengabaikan sinyalnya, namun kerja sama tetap diperlukan untuk menghentikan pekerjaan yang mendasarinya.\n\nExtensions yang memerlukan autentikasi, pemfilteran, penyegaran, atau perilaku streaming penyedia asli dapat mendaftarkan `Provider` lengkap dari `@earendil-works/pi-ai`. Penyedia menjadi basis komposisi dan penggantian `models.json` masih berlaku di atasnya.\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\nBentuk objek menerima pi-ai lengkap `Provider`, termasuk perilaku asli `auth`, `getModels`, `refreshModels`, `filterModels`, `stream`, dan `streamSimple`.\n\n**Opsi konfigurasi lama:**\n- `name` - Nama tampilan untuk penyedia di UI seperti `/login`.\n- `baseUrl` - API URL titik akhir. Diperlukan saat mendefinisikan model.\n- `apiKey` - API key literal, interpolasi lingkungan (`$ENV_VAR` atau `${ENV_VAR}`), atau awalan `!command`. Diperlukan saat menentukan model (kecuali `oauth` disediakan). `$` lolos dari ``apiKey` - API key literal, interpolasi lingkungan (`$ENV_VAR` atau `${ENV_VAR}`), atau awalan `!command`. Diperlukan saat menentukan model (kecuali `oauth` disediakan). `$` lolos dari, dan `$!` lolos dari `!` literal tanpa memicu eksekusi perintah.\n- `api` - API ketik: `\"anthropic-messages\"`, `\"openai-completions\"`, `\"openai-responses\"`, dll.\n- `headers` - Header khusus untuk disertakan dalam permintaan.\n- `authHeader` - Jika benar, tambahkan header `Authorization: Bearer` secara otomatis.\n- `models` - Kumpulan definisi model. Jika tersedia, gantikan semua model yang ada untuk penyedia ini. Definisi model dapat mengatur `baseUrl` untuk mengganti titik akhir penyedia untuk model tersebut.\n- `refreshModels` - Panggilan balik penemuan dinamis asinkron. Model yang dikembalikan menggantikan model yang disediakan ekstensi. `context.stored` berisi snapshot penyedia yang ada; gunakan generasi-diperiksa `context.publish({ persist: entry })` hanya ketika data katalog yang diperbarui harus tetap ada. Gunakan `persist: null` untuk menghapus snapshot itu.\n- `oauth` - OAuth konfigurasi penyedia untuk dukungan `/login`. Jika disediakan, penyedia muncul di menu login.\n- `streamSimple` - Implementasi streaming khusus untuk API non-standar.\n\nLihat [custom-provider.md](custom-provider.md) untuk topik lanjutan: streaming khusus APIs, detail OAuth, referensi definisi model.\n\n### pi.unregisterProvider(nama)\n\nHapus penyedia yang terdaftar sebelumnya dan modelnya. Model bawaan yang diganti oleh penyedia akan dipulihkan. Tidak berpengaruh jika penyedia tidak terdaftar.\n\nSeperti `registerProvider`, ini berlaku segera ketika dipanggil setelah fase beban awal, jadi `/reload` tidak diperlukan.\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## Manajemen Negara\n\nExtensions dengan negara harus menyimpannya dalam hasil alat `details` untuk dukungan percabangan yang tepat:\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## Alat Kustom\n\nDaftarkan alat yang dapat dihubungi LLM melalui `pi.registerTool()`. Alat muncul di prompt sistem dan dapat memiliki rendering khusus.\n\nGunakan `promptSnippet` untuk entri satu baris pendek di bagian `Available tools` pada prompt sistem default. Jika dihilangkan, alat khusus tidak dimasukkan dalam bagian itu.\n\nGunakan `promptGuidelines` untuk menambahkan poin khusus alat ke bagian prompt sistem default `Guidelines`. Poin-poin ini hanya disertakan saat alat aktif (misalnya, setelah `pi.setActiveTools([...])`).\n\n**Penting:** `promptGuidelines` poin ditambahkan rata ke bagian `Guidelines` tanpa awalan atau pengelompokan nama alat. Setiap pedoman harus menyebutkan alat yang dirujuknya — hindari \"Gunakan alat ini ketika...\" karena LLM tidak dapat membedakan alat mana yang dimaksud dengan \"ini\". Tulis \"Gunakan my_tool ketika...\" sebagai gantinya.\n\nCatatan: Beberapa model bodoh dan menyertakan awalan @ dalam argumen jalur alat. Alat bawaan menghapus @ terdepan sebelum menyelesaikan jalur. Jika alat khusus Anda menerima jalur, normalkan juga @ di depannya.\n\nJika alat khusus Anda memutasi file, gunakan `withFileMutationQueue()` sehingga alat tersebut berpartisipasi dalam antrean per file yang sama dengan `edit` dan `write` bawaan. Hal ini penting karena pemanggilan alat dijalankan secara paralel secara default. Tanpa antrian, dua alat dapat membaca konten file lama yang sama, menghitung pembaruan yang berbeda, dan kemudian penulisan mana pun yang terakhir akan menimpa yang lain.\n\nContoh kasus kegagalan: alat khusus Anda mengedit `foo.ts` sementara `edit` bawaan juga mengubah `foo.ts` pada giliran asisten yang sama. Jika alat Anda tidak berpartisipasi dalam antrean, keduanya dapat membaca `foo.ts` asli, menerapkan perubahan terpisah, dan salah satu perubahan tersebut akan hilang.\n\nTeruskan jalur file target sebenarnya ke `withFileMutationQueue()`, bukan argumen pengguna mentah. Selesaikan terlebih dahulu ke jalur absolut, relatif terhadap `ctx.cwd` atau direktori kerja alat Anda. Untuk file yang sudah ada, helper melakukan kanonikalisasi melalui `realpath()`, jadi alias symlink untuk file yang sama berbagi satu antrian. Untuk file baru, file tersebut kembali ke jalur absolut yang diselesaikan karena belum ada apa pun untuk `realpath()`.\n\nAntrian seluruh jendela mutasi pada jalur target itu. Itu termasuk logika baca-modifikasi-tulis, bukan hanya penulisan akhir.\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### Definisi Alat\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**Penghitungan penggunaan:** Jika alat melakukan panggilan LLM bertingkat, kembalikan gabungan `Usage` sebagai `usage`. Pi menyimpannya pada hasil alat dan memasukkannya ke dalam footer, `/session`, dan RPC total sesi. `tool_result` penangan dapat memeriksa atau mengganti nilai ini.\n\n**Kesalahan sinyal:** Untuk menandai eksekusi alat sebagai gagal (menetapkan `isError: true` pada hasil dan melaporkannya ke LLM), memunculkan kesalahan dari `execute`. Mengembalikan nilai tidak pernah menyetel tanda kesalahan apa pun properti yang Anda sertakan dalam objek pengembalian.\n\n**Penghentian awal:** Kembalikan `terminate: true` dari `execute()` untuk memberi petunjuk bahwa panggilan LLM tindak lanjut otomatis harus dilewati setelah kumpulan alat saat ini. Ini hanya berlaku ketika setiap hasil alat yang diselesaikan dalam batch tersebut dihentikan. Lihat [examples/extensions/structured-output.ts](../examples/extensions/structured-output.ts) untuk contoh minimal saat agen mengakhiri panggilan alat keluaran terstruktur akhir.\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**Penting:** Gunakan `StringEnum` dari `@earendil-works/pi-ai` untuk enum string. `Type.Union`/`Type.Literal` tidak berfungsi dengan API Google.\n\n**Persiapan argumen:** `prepareArguments(args)` bersifat opsional. Jika ditentukan, ini berjalan sebelum validasi skema dan sebelum `execute()`. Gunakan ini untuk meniru bentuk input lama yang diterima ketika pi melanjutkan sesi lama yang argumen pemanggilan alatnya tidak lagi cocok dengan skema saat ini. Kembalikan objek yang ingin Anda validasi terhadap `parameters`. Jaga agar skema publik tetap ketat. Jangan menambahkan bidang kompatibilitas yang tidak digunakan lagi ke `parameters` hanya agar sesi lama yang dilanjutkan tetap berfungsi.\n\nContoh: sesi lama mungkin berisi panggilan alat `edit` dengan `oldText` dan `newText` tingkat atas, sedangkan skema saat ini hanya menerima `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### Mengganti Alat Bawaan\n\nExtensions dapat mengganti alat bawaan (`read`, `bash`, `edit`, `write`, `grep`, `find`, `ls`) dengan mendaftarkan alat dengan nama yang sama. Mode interaktif menampilkan peringatan ketika hal ini terjadi.\n\n```bash\n# Extension's read tool replaces built-in read\npi -e ./tool-override.ts\n```\n\nAlternatifnya, gunakan `--no-builtin-tools` untuk memulai tanpa alat bawaan apa pun sambil tetap mengaktifkan alat ekstensi:\n```bash\n# No built-in tools, only extension tools\npi --no-builtin-tools -e ./my-extension.ts\n```\n\nLihat [examples/extensions/tool-override.ts](../examples/extensions/tool-override.ts) untuk contoh lengkap yang menggantikan `read` dengan logging dan kontrol akses.\n\n**Rendering:** Warisan perender bawaan diselesaikan per slot. Penimpaan eksekusi dan pengesampingan rendering bersifat independen. Jika penggantian Anda menghilangkan `renderCall`, `renderCall` bawaan akan digunakan. Jika penggantian Anda menghilangkan `renderResult`, `renderResult` bawaan akan digunakan. Jika penggantian Anda menghilangkan keduanya, penyaji bawaan akan digunakan secara otomatis (penyorotan sintaksis, perbedaan, dll.). Hal ini memungkinkan Anda menggabungkan alat bawaan untuk logging atau kontrol akses tanpa mengimplementasikan ulang UI.\n\n**Metadata cepat:** `promptSnippet` dan `promptGuidelines` tidak diwarisi dari alat bawaan. Jika penggantian Anda harus menyimpan instruksi cepat tersebut, tentukan instruksi tersebut pada penggantian secara eksplisit.\n\n**Penerapan Anda harus sesuai dengan bentuk hasil yang tepat**, termasuk jenis `details`. Logika UI dan sesi bergantung pada bentuk ini untuk rendering dan pelacakan status.\n\nImplementasi alat bawaan:\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### Eksekusi Jarak Jauh\n\nAlat bawaan mendukung operasi yang dapat dicolokkan untuk mendelegasikan ke sistem jarak jauh (SSH, container, dll.):\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**Antarmuka operasi:** `ReadOperations`, `WriteOperations`, `EditOperations`, `BashOperations`, `LsOperations`, `GrepOperations`, `FindOperations`\n\nUntuk `user_bash`, ekstensi dapat menggunakan kembali backend shell lokal pi melalui `createLocalBashOperations()` alih-alih mengimplementasikan ulang pemijahan proses lokal, resolusi shell, dan penghentian pohon proses.\n\nAlat bash juga mendukung spawn hook untuk menyesuaikan perintah, cwd, atau env sebelum eksekusi:\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()` memaparkan sesi saat ini ke perintah melalui `PI_SESSION_ID`, `PI_SESSION_FILE`, `PI_PROVIDER`, `PI_MODEL`, dan `PI_REASONING_LEVEL`. Injeksi terjadi sebelum `spawnHook`, jadi hook menerima nilai ini di `env` dan mempertahankannya saat menyebarkan lingkungan yang ada seperti di atas. Atur `exposeSessionEnvironment: false` untuk menonaktifkannya:\n\n```typescript\nconst bashTool = createBashTool(cwd, {\n  exposeSessionEnvironment: false,\n});\n```\n\nLihat [Bash tool session environment](environment-variables.md#bash-tool-session-environment) untuk semantik variabel. Lihat [examples/extensions/ssh.ts](../examples/extensions/ssh.ts) untuk contoh SSH lengkap dengan tanda `--ssh`.\n\n### Pemotongan Keluaran\n\n**Alat HARUS memotong keluarannya** untuk menghindari konteks LLM yang berlebihan. Output yang besar dapat menyebabkan:\n- Kesalahan luapan konteks (prompt terlalu panjang)\n- Kegagalan pemadatan\n- Performa model menurun\n\nBatas bawaannya adalah **50KB** (~10 ribu token) dan **2000 baris**, mana saja yang tercapai terlebih dahulu. Gunakan utilitas pemotongan yang diekspor:\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**Poin-poin penting:**\n- Gunakan `truncateHead` untuk konten yang bagian awalnya penting (hasil pencarian, pembacaan file)\n- Gunakan `truncateTail` untuk konten yang ujungnya penting (log, keluaran perintah)\n- Selalu beri tahu LLM ketika keluaran terpotong dan di mana menemukan versi lengkapnya\n- Dokumentasikan batas pemotongan dalam deskripsi alat Anda\n\nLihat [examples/extensions/truncated-tool.ts](../examples/extensions/truncated-tool.ts) untuk contoh lengkap membungkus `rg` (ripgrep) dengan pemotongan yang tepat.\n\n### Berbagai Alat\n\nSatu ekstensi dapat mendaftarkan beberapa alat dengan status bersama:\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### Rendering Kustom\n\nAlat dapat menyediakan `renderCall` dan `renderResult` untuk tampilan TUI khusus. Lihat [tui.md](tui.md) untuk komponen lengkap API dan [tool-execution.ts](https://github.com/earendil-works/pi-mono/blob/main/packages/coding-agent/src/modes/interactive/components/tool-execution.ts) untuk mengetahui bagaimana baris alat disusun.\n\nSecara default, keluaran alat dibungkus dengan `Box` yang menangani padding dan latar belakang. `renderCall` atau `renderResult` yang ditentukan harus menghasilkan `Component`. Jika penyaji slot tidak ditentukan, `tool-execution.ts` menggunakan rendering cadangan untuk slot tersebut.\n\nSetel `renderShell: \"self\"` kapan alat harus merender shellnya sendiri alih-alih menggunakan `Box` default. Hal ini berguna untuk alat yang memerlukan kontrol penuh atas perilaku pembingkaian atau latar belakang, misalnya pratinjau besar yang harus tetap stabil secara visual setelah alat tersebut dipasang.\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` dan `renderResult` masing-masing menerima objek `context` dengan:\n- `args` - argumen pemanggilan alat saat ini\n- `state` - status baris-lokal bersama di `renderCall` dan `renderResult`\n- `lastComponent` - komponen yang dikembalikan sebelumnya untuk slot tersebut, jika ada\n- `invalidate()` - meminta rendering baris alat ini\n- `toolCallId`, `cwd`, `executionStarted`, `argsComplete`, `isPartial`, `expanded`, `showImages`, `isError`\n\nGunakan `context.state` untuk status bersama lintas slot. Simpan cache slot-lokal pada instance komponen yang dikembalikan ketika Anda ingin menggunakan kembali dan mengubah komponen yang sama di seluruh render.\n\n#### panggilan render\n\nMerender panggilan alat atau header:\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#### hasil render\n\nMerender hasil atau keluaran alat:\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\nJika slot sengaja tidak memiliki konten yang terlihat, kembalikan `Component` kosong seperti `Container` kosong.\n\n#### Petunjuk Pengikatan Kunci\n\nGunakan `keyHint()` untuk menampilkan petunjuk pengikatan kunci yang mengikuti konfigurasi pengikatan kunci aktif:\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\nFungsi yang tersedia:\n- `keyHint(keybinding, description)` - Memformat id pengikat kunci yang dikonfigurasi seperti `\"app.tools.expand\"` atau `\"tui.select.confirm\"`\n- `keyText(keybinding)` - Mengembalikan teks kunci mentah yang dikonfigurasi untuk id pengikat kunci\n- `rawKeyHint(key, description)` - Memformat string kunci mentah\n\nGunakan id pengikat kunci dengan spasi nama:\n- Id agen pengkodean menggunakan namespace `app.*`, misalnya `app.tools.expand`, `app.editor.external`, `app.session.rename`\n- ID TUI yang dibagikan menggunakan namespace `tui.*`, misalnya `tui.select.confirm`, `tui.select.cancel`, `tui.input.tab`\n\nUntuk daftar lengkap id dan default pengikat kunci, lihat [keybindings.md](keybindings.md). `keybindings.json` menggunakan id dengan namespace yang sama.\n\nEditor khusus dan komponen `ctx.ui.custom()` menerima `keybindings: KeybindingsManager` sebagai argumen yang dimasukkan. Mereka harus menggunakan manajer yang disuntikkan itu secara langsung daripada menelepon `getKeybindings()` atau `setKeybindings()`.\n\n#### Praktik Terbaik\n\n- Gunakan `Text` dengan bantalan `(0, 0)`. Kotak default menangani padding.\n- Gunakan `\\n` untuk konten multi-baris.\n- Tangani `isPartial` untuk kemajuan streaming.\n- Dukungan `expanded` untuk detail sesuai permintaan.\n- Pertahankan tampilan default tetap ringkas.\n- Baca `context.args` di `renderResult` alih-alih menyalin argumen ke `context.state`.\n- Gunakan `context.state` hanya untuk data yang harus dibagikan ke seluruh slot panggilan dan hasil.\n- Gunakan kembali `context.lastComponent` ketika instance komponen yang sama dapat diperbarui di tempatnya.\n- Gunakan `renderShell: \"self\"` hanya ketika shell kotak default menghalangi. Dalam mode self-shell, alat ini bertanggung jawab atas framing, padding, dan latar belakangnya sendiri.\n\n#### Penggantian\n\nJika penyaji slot tidak ditentukan atau muncul:\n- `renderCall`: Menampilkan nama alat\n- `renderResult`: Menampilkan teks mentah dari `content`\n\n### Pemuatan Alat Dinamis\n\nExtensions dapat mendaftarkan banyak alat sambil tetap mengaktifkan set awal kecil. Sebuah alat kemudian dapat menambahkan lebih banyak alat dengan `pi.setActiveTools()` selama eksekusi. Pi mendeteksi perubahan aditif murni, mencatat nama alat yang baru tersedia pada hasil alat tersebut, dan menerapkan set aktif yang diperbarui sebelum permintaan model berikutnya.\n\nIni berfungsi pada setiap model. Models dengan dukungan pemuatan tertunda asli mempertahankan awalan prompt stabil dan memuat definisi baru pada posisi hasil pahat. Model lain menggunakan fallback yang dijelaskan di bawah.\n\nSiklus hidupnya adalah:\n\n1. Daftarkan setiap alat dengan `pi.registerTool()` sehingga muncul di `pi.getAllTools()`.\n2. Biarkan alat pemuat, seperti `search_tools`, tetap aktif dan biarkan alat yang dapat dicari tidak aktif.\n3. Selama eksekusi loader, panggil `pi.setActiveTools([...currentTools,...matchingTools])`. Perubahannya harus bersifat tambahan: jangan menghapus alat yang sedang aktif dalam panggilan yang sama.\n4. Pi mencatat alat mana yang ditambahkan pada hasil alat pemuat.\n5. Sebelum respons model berikutnya, Pi memaparkan definisi tambahan menggunakan pemuatan asli yang ditangguhkan jika didukung, atau daftar alat aktif normal sebaliknya.\n\nAnda tidak perlu mengembalikan referensi alat khusus penyedia atau menandai pemuat sebagai alat pencarian khusus. Perubahan alat aktif adalah sinyalnya. Nama yang diteruskan ke `pi.setActiveTools()` harus sudah terdaftar; nama yang tidak diketahui diabaikan.\n\n#### Models dengan pemuatan tertunda asli\n\n- **Antropik**\n  - **Models:** Soneta, Opus, Fable versi 4.5 atau lebih baru (tanpa Haiku)\n  - **Representasi asli:** Definisi yang ditangguhkan menggunakan `defer_loading`; titik muat menggunakan konten `tool_reference`.\n- **BukaAI**\n  - **Models:** `gpt-5.4` dan keluarga baru\n  - **Representasi asli:** Pi menambahkan item klien `tool_search_call` dan `tool_search_output` yang telah selesai pada titik pemuatan.\n\nUntuk model atau proksi khusus yang terverifikasi, penanganan asli dapat diaktifkan dengan `compat.supportsToolReferences: true` untuk `anthropic-messages`, atau `compat.supportsToolSearch: true` untuk `openai-responses` dan `openai-codex-responses`. Biarkan ini dinonaktifkan kecuali titik akhir dan model menerima protokol asli yang sesuai.\n\n#### Perilaku mundur\n\nUntuk semua model dan penyedia lainnya, aktivasi dinamis masih berfungsi: Pi mengirimkan daftar lengkap alat aktif saat ini secara normal pada permintaan berikutnya. Model dapat memanggil alat yang baru diaktifkan, namun menambahkan definisinya dapat membuat awalan prompt cache penyedia menjadi tidak valid.\n\nPi juga menggunakan fallback aman ini ketika set aktif tidak murni bersifat aditif, seperti mengganti satu kelompok alat dengan kelompok alat lainnya. Oleh karena itu, penghapusan alat dapat dilakukan, tetapi tidak menggunakan pemuatan yang ditangguhkan.\n\nUntuk perilaku cache terbaik, biarkan alat pemuat tetap aktif sepanjang sesi dan tambahkan alat alih-alih mengganti set yang aktif. Perhatikan juga bahwa mengaktifkan alat dengan `promptSnippet` atau `promptGuidelines` akan membangun kembali prompt sistem; perubahan yang dilakukan segera oleh sistem dapat membatalkan awalan meskipun penyedia mendukung skema yang ditangguhkan. Alat yang dimuat dengan lambat biasanya harus mengandalkan alatnya `description` dan menghilangkan metadata prompt yang hanya aktif.\n\n#### Contoh alat pencarian\n\nEkstensi berikut mendaftarkan dua alat yang dapat dicari, menghapusnya dari set aktif awal, dan hanya menyimpan `search_tools` sebagai pemuatnya. Contohnya menggunakan pencocokan kata kunci sederhana, namun penerapan penelusuran dapat menggunakan BM25, penyematan, katalog jarak jauh, atau perutean khusus proyek.\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\nSaat `search_tools` menambahkan kecocokan, model menerima definisi tersebut segera setelah permintaan. Pada model berkemampuan asli, definisi tersebut ditetapkan setelah hasil pencarian tanpa mengubah awalan skema alat awal. Pada model lain, alat ini muncul dalam daftar alat normal berdasarkan permintaan berikut yang sama.\n\n## UI khusus\n\nExtensions dapat berinteraksi dengan pengguna melalui metode `ctx.ui` dan menyesuaikan cara pesan/alat ditampilkan.\n\n**Untuk komponen khusus, lihat [tui.md](tui.md)** yang memiliki pola salin-tempel untuk:\n- Dialog pemilihan (SelectList)\n- Operasi asinkron dengan pembatalan (BorderedLoader)\n- Pengalih pengaturan (Daftar Pengaturan)\n- Indikator status (setStatus)\n- Pesan, visibilitas, dan indikator yang berfungsi selama streaming (`setWorkingMessage`, `setWorkingVisible`, `setWorkingIndicator`)\n- Widget di atas/di bawah editor (setWidget)\n- Penyedia pelengkapan otomatis yang berlapis di atas garis miring/penyelesaian jalur bawaan (addAutocompleteProvider)\n- Footer khusus (setFooter)\n\n### Dialog\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#### Dialog Jangka Waktu dengan Hitung Mundur\n\nDialog mendukung opsi `timeout` yang ditutup secara otomatis dengan tampilan hitung mundur langsung:\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**Nilai pengembalian saat batas waktu habis:**\n- `select()` mengembalikan `undefined`\n- `confirm()` mengembalikan `false`\n- `input()` mengembalikan `undefined`\n\n#### Pemberhentian Manual dengan AbortSignal\n\nUntuk kontrol lebih lanjut (misalnya, untuk membedakan batas waktu dari pembatalan pengguna), gunakan `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\nLihat [examples/extensions/timed-confirm.ts](../examples/extensions/timed-confirm.ts) untuk contoh lengkap.\n\n### Widget, Status, dan Footer\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\nBingkai indikator kerja khusus ditampilkan secara verbatim. Jika Anda menginginkan warna, tambahkan sendiri warna tersebut ke string bingkai, misalnya dengan `ctx.ui.theme.fg(...)`.\n\n### Pelengkapan otomatis Providers\n\nGunakan `ctx.ui.addAutocompleteProvider()` untuk menumpuk logika pelengkapan otomatis khusus di atas perintah garis miring dan penyedia jalur bawaan. Setel `triggerCharacters` untuk pemicu alami khusus seperti `Gunakan `ctx.ui.addAutocompleteProvider()` untuk menumpuk logika pelengkapan otomatis khusus di atas perintah garis miring dan penyedia jalur bawaan. Setel `triggerCharacters` untuk pemicu alami khusus seperti.\n\nPola khas:\n\n- periksa teks sebelum kursor\n- kembalikan saran Anda sendiri ketika sintaks khusus ekstensi Anda cocok\n- jika tidak, delegasikan ke `current.getSuggestions(...)`\n- delegasikan `applyCompletion(...)` kecuali Anda memerlukan perilaku penyisipan khusus\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\nLihat [github-issue-autocomplete.ts](../examples/extensions/github-issue-autocomplete.ts) untuk contoh lengkap yang memuat masalah GitHub terbuka terbaru dengan `gh issue list` dan memfilternya secara lokal untuk penyelesaian `#...` yang cepat. Ini memerlukan GitHub CLI (`gh`) dan checkout repositori GitHub.\n\n### Komponen Khusus\n\nUntuk UI yang kompleks, gunakan `ctx.ui.custom()`. Ini untuk sementara menggantikan editor dengan komponen Anda hingga `done()` dipanggil:\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\nPanggilan balik menerima:\n- `tui` - TUI contoh (untuk dimensi layar, manajemen fokus)\n- `theme` - Tema penataan gaya saat ini\n- `keybindings` - Manajer pengikat tombol aplikasi (untuk memeriksa pintasan)\n- `done(value)` - Panggilan untuk menutup komponen dan mengembalikan nilai\n\nLihat [tui.md](tui.md) untuk komponen lengkap API.\n\n#### Mode Hamparan (Eksperimental)\n\nTeruskan `{ overlay: true }` untuk merender komponen sebagai modal mengambang di atas konten yang sudah ada, tanpa mengosongkan layar:\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\nUntuk pemosisian lanjutan (jangkar, margin, persentase, visibilitas responsif), teruskan `overlayOptions`. Gunakan `onHandle` untuk mengontrol fokus atau visibilitas secara terprogram:\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\nHamparan terlihat terfokus dapat memperoleh kembali masukan setelah UI khusus non-hamparan sementara ditutup. Jika Anda sengaja ingin komponen lain tetap memasukkan input sementara overlay tetap terlihat, panggil `handle.unfocus({ target })`. Melewati `{ target: null }` akan melepaskan overlay tanpa memfokuskan komponen lain.\n\nLihat [tui.md](tui.md) untuk `OverlayOptions` lengkap dan `OverlayHandle` API dan [overlay-qa-tests.ts](../examples/extensions/overlay-qa-tests.ts) sebagai contoh.\n\n### Editor Kustom\n\nGanti editor masukan utama dengan implementasi khusus (mode vim, mode emacs, dll.):\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**Poin-poin penting:**\n- Perpanjang `CustomEditor` (bukan basis `Editor`) untuk mendapatkan ikatan kunci aplikasi (escape untuk membatalkan, ctrl+d, peralihan model)\n- Hubungi `super.handleInput(data)` untuk kunci yang tidak Anda tangani\n- Pabrik menerima `tui`, `theme`, dan `keybindings` dari aplikasi\n- Gunakan `ctx.ui.getEditorComponent()` sebelum `setEditorComponent()` untuk menggabungkan editor khusus yang telah dikonfigurasi sebelumnya\n- Lewati `undefined` untuk mengembalikan default: `ctx.ui.setEditorComponent(undefined)`\n\nUntuk menulis dengan ekstensi lain yang telah menggantikan editor, ambil pabrik sebelumnya sebelum mengatur milik Anda:\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\nLihat [tui.md](tui.md) Pola 7 untuk contoh lengkap dengan indikator mode.\n\n### Rendering Pesan dan Entri\n\nDaftarkan penyaji khusus untuk pesan dengan `customType` Anda. Gunakan penyaji pesan untuk konten yang harus berpartisipasi dalam konteks 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\nPesan dikirim melalui `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\nUntuk konten khusus TUI yang tidak boleh dikirim ke LLM, render entri khusus sebagai gantinya:\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### Warna Tema\n\nSemua fungsi render menerima objek `theme`. Lihat [themes.md](themes.md) untuk membuat tema khusus dan palet warna lengkap.\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\nUntuk penyorotan sintaksis pada perender alat khusus:\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## Penanganan Kesalahan\n\n- Kesalahan ekstensi dicatat, agen melanjutkan\n- `tool_call` kesalahan memblokir alat (aman dari kegagalan)\n- Kesalahan alat `execute` harus ditandai dengan melempar; kesalahan yang terjadi ditangkap, dilaporkan ke LLM dengan `isError: true`, dan eksekusi dilanjutkan\n\n## Modus Perilaku\n\n| Mode | `ctx.mode` | `ctx.hasUI` | Catatan |\n|------|------------|-------------|-------|\n| Interaktif | `\"tui\"` | `true` | Penuh TUI dengan rendering terminal |\n| RPC (`--mode rpc`) | `\"rpc\"` | `true` | Dialog dan pemberitahuan melalui protokol JSON; `custom()` mengembalikan `undefined`. Lihat [rpc.md](rpc.md) |\n| JSON (`--mode json`) | `\"json\"` | `false` | Aliran acara ke stdout; Metode UI tidak boleh dilakukan |\n| Cetak (`-p`) | `\"print\"` | `false` | Extensions dijalankan tetapi tidak dapat meminta |\n\nGunakan `ctx.mode === \"tui\"` sebelum TUI fitur khusus (`custom()`, pabrik komponen, input terminal). Gunakan `ctx.hasUI` sebelum dialog dan metode pemberitahuan yang berfungsi dalam mode TUI dan RPC.\n\n## Contoh Referensi\n\nSemua contoh di [examples/extensions/](../examples/extensions/).\n\n| Contoh | Keterangan | Kunci APIs |\n|---------|-------------|----------|\n| **Peralatan** |  |  |\n| `hello.ts` | Registrasi alat minimal | `registerTool` |\n| `question.ts` | Alat dengan interaksi pengguna | `registerTool`, `ui.select` |\n| `questionnaire.ts` | Alat penyihir multi-langkah | `registerTool`, `ui.custom` |\n| `todo.ts` | Alat stateful dengan ketekunan | `registerTool`, `appendEntry`, `renderResult`, acara sesi |\n| `dynamic-tools.ts` | Daftarkan alat setelah startup dan selama perintah | `registerTool`, `session_start`, `registerCommand` |\n| `structured-output.ts` | Alat keluaran terstruktur akhir dengan `terminate: true` | `registerTool`, menghentikan hasil alat |\n| `truncated-tool.ts` | Contoh pemotongan keluaran | `registerTool`, `truncateHead` |\n| `tool-override.ts` | Ganti alat baca bawaan | `registerTool` (nama yang sama dengan bawaan) |\n| **Perintah** |  |  |\n| `pirate.ts` | Ubah perintah sistem per putaran | `registerCommand`, `before_agent_start` |\n| `summarize.ts` | Perintah ringkasan percakapan | `registerCommand`, `ui.custom` |\n| `handoff.ts` | Penyerahan model lintas penyedia | `registerCommand`, `ui.editor`, `ui.custom` |\n| `qna.ts` | Tanya Jawab dengan UI khusus | `registerCommand`, `ui.custom`, `setEditorText` |\n| `send-user-message.ts` | Menyuntikkan pesan pengguna | `registerCommand`, `sendUserMessage` |\n| `reload-runtime.ts` | Muat ulang perintah dan handoff alat LLM | `registerCommand`, `ctx.reload()`, `sendUserMessage` |\n| `shutdown-command.ts` | Perintah mematikan dengan baik | `registerCommand`, `shutdown()` |\n| **Acara & Gerbang** |  |  |\n| `permission-gate.ts` | Blokir perintah berbahaya | `on(\"tool_call\")`, `ui.confirm` |\n| `project-trust.ts` | Memutuskan atau menunda kepercayaan proyek dari pengguna/ekstensi global atau CLI | `on(\"project_trust\")`, percayai UI, diperlukan hasil kepercayaan |\n| `protected-paths.ts` | Blokir penulisan ke jalur tertentu | `on(\"tool_call\")` |\n| `confirm-destructive.ts` | Konfirmasikan perubahan sesi | `on(\"session_before_switch\")`, `on(\"session_before_fork\")` |\n| `dirty-repo-guard.ts` | Peringatkan tentang repo git yang kotor | `on(\"session_before_*\")`, `exec` |\n| `input-transform.ts` | Ubah masukan pengguna | `on(\"input\")` |\n| `input-transform-streaming.ts` | Transformasi masukan yang sadar streaming | `on(\"input\")`, `streamingBehavior` |\n| `model-status.ts` | React untuk memodelkan perubahan | `on(\"model_select\")`, `setStatus` |\n| `provider-payload.ts` | Periksa muatan dan header respons penyedia | `on(\"before_provider_request\")`, `on(\"after_provider_response\")` |\n| `system-prompt-header.ts` | Menampilkan info cepat sistem | `on(\"agent_start\")`, `getSystemPrompt` |\n| `claude-rules.ts` | Muat aturan dari file | `on(\"session_start\")`, `on(\"before_agent_start\")` |\n| `prompt-customizer.ts` | Tambahkan panduan alat peka konteks menggunakan `systemPromptOptions` | `on(\"before_agent_start\")`, `BuildSystemPromptOptions` |\n| `file-trigger.ts` | Pengamat file memicu pesan | `sendMessage` |\n| **Pemadatan & Sesi** |  |  |\n| `custom-compaction.ts` | Ringkasan pemadatan khusus | `on(\"session_before_compact\")` |\n| `trigger-compact.ts` | Memicu pemadatan secara manual | `compact()` |\n| `git-checkpoint.ts` | Git simpanan di tikungan | `on(\"turn_start\")`, `on(\"session_before_fork\")`, `exec` |\n| `git-merge-and-resolve.ts` | Ambil, gabungkan, dan selesaikan konflik | `on(\"agent_end\")`, `exec`, `sendUserMessage` |\n| `auto-commit-on-exit.ts` | Berkomitmen untuk mematikan | `on(\"session_shutdown\")`, `exec` |\n| **Komponen UI** |  |  |\n| `status-line.ts` | Indikator status catatan kaki | `setStatus`, acara sesi |\n| `working-indicator.ts` | Sesuaikan indikator kerja streaming | `setWorkingIndicator`, `registerCommand` |\n| `github-issue-autocomplete.ts` | Tambahkan `#1234` penyelesaian masalah di atas pelengkapan otomatis bawaan dengan memuat terlebih dahulu masalah terbuka terkini dari `gh issue list` | `addAutocompleteProvider`, `on(\"session_start\")`, `exec` |\n| `custom-footer.ts` | Ganti footer seluruhnya | `registerCommand`, `setFooter` |\n| `custom-header.ts` | Ganti header permulaan | `on(\"session_start\")`, `setHeader` |\n| `modal-editor.ts` | Editor modal bergaya Vim | `setEditorComponent`, `CustomEditor` |\n| `rainbow-editor.ts` | Gaya editor khusus | `setEditorComponent` |\n| `widget-placement.ts` | Widget di atas/di bawah editor | `setWidget` |\n| `overlay-test.ts` | Komponen hamparan | `ui.custom` dengan opsi hamparan |\n| `overlay-qa-tests.ts` | Tes overlay yang komprehensif | `ui.custom`, semua opsi hamparan |\n| `notify.ts` | Pemberitahuan sederhana | `ui.notify` |\n| `timed-confirm.ts` | Dialog dengan batas waktu | `ui.confirm` dengan batas waktu/sinyal |\n| `mac-system-theme.ts` | Beralih tema secara otomatis | `setTheme`, `exec` |\n| **Kompleks Extensions** |  |  |\n| `plan-mode/` | Implementasi mode rencana penuh | Semua jenis acara, `registerCommand`, `registerShortcut`, `registerFlag`, `setStatus`, `setWidget`, `sendMessage`, `setActiveTools` |\n| `preset.ts` | Preset yang dapat disimpan (model, alat, pemikiran) | `registerCommand`, `registerShortcut`, `registerFlag`, `setModel`, `setActiveTools`, `setThinkingLevel`, `appendEntry` |\n| `tools.ts` | Mengaktifkan/menonaktifkan UI alat | `registerCommand`, `setActiveTools`, `SettingsList`, acara sesi |\n| **Jarak Jauh & Kotak Pasir** |  |  |\n| `ssh.ts` | SSH eksekusi jarak jauh | `registerFlag`, `on(\"user_bash\")`, `on(\"before_agent_start\")`, pengoperasian alat |\n| `interactive-shell.ts` | Sesi shell yang persisten | `on(\"user_bash\")` |\n| `sandbox/` | Eksekusi alat dalam kotak pasir | Operasi alat |\n| `gondolin/` | Rutekan alat bawaan dan perintah `!` ke dalam Gondolin mikro-VM | Pengoperasian alat, penggantian alat bawaan, `on(\"user_bash\")` |\n| `subagent/` | Memunculkan sub-agen | `registerTool`, `exec` |\n| **Pertandingan** |  |  |\n| `snake.ts` | Permainan ular | `registerCommand`, `ui.custom`, penanganan keyboard |\n| `space-invaders.ts` | Permainan Penjajah Luar Angkasa | `registerCommand`, `ui.custom` |\n| `doom-overlay/` | Malapetaka dalam hamparan | `ui.custom` dengan hamparan |\n| **Providers** |  |  |\n| `custom-provider-anthropic/` | Proksi Antropik Kustom | `registerProvider` |\n| `custom-provider-gitlab-duo/` | GitIntegrasi Lab Duo | `registerProvider` dengan OAuth |\n| **Pesan & Komunikasi** |  |  |\n| `message-renderer.ts` | Render pesan khusus | `registerMessageRenderer`, `sendMessage` |\n| `entry-renderer.ts` | TUI-hanya rendering entri khusus | `registerEntryRenderer`, `appendEntry` |\n| `event-bus.ts` | Acara antar-ekstensi | `pi.events` |\n| **Metadata Sesi** |  |  |\n| `session-name.ts` | Sesi nama untuk pemilih | `setSessionName`, `getSessionName` |\n| `bookmark.ts` | Tandai entri untuk /pohon | `setLabel` |\n| **Lain-lain** |  |  |\n| `inline-bash.ts` | Sebaris bash dalam panggilan alat | `on(\"tool_call\")` |\n| `bash-spawn-hook.ts` | Sesuaikan perintah bash, cwd, dan env sebelum dieksekusi | `createBashTool`, `spawnHook` |\n| `with-deps/` | Ekstensi dengan dependensi npm | Struktur paket dengan `package.json` |","sourceFile":"extensions.md"},"index":{"title":"Pi Dokumentasi","markdown":"Pi adalah memanfaatkan pengkodean terminal minimal. Ini dirancang untuk tetap kecil pada intinya sambil diperluas melalui TypeScript ekstensi, keterampilan, prompt templates, tema, dan paket pi.\n\n## Mulai cepat\n\nInstal Pi dengan npm:\n\n```bash\nnpm install -g --ignore-scripts @earendil-works/pi-coding-agent\n```\n\n`--ignore-scripts` menonaktifkan skrip siklus hidup ketergantungan selama instalasi. Pi tidak memerlukan skrip instalasi untuk instalasi npm normal.\n\nDi Linux atau macOS, Anda juga dapat menggunakan penginstal:\n\n```bash\ncurl -fsSL https://pi.dev/install.sh | sh\n```\n\nUntuk menghapus instalasi pi itu sendiri, gunakan npm untuk curl dan npm instalasi:\n\n```bash\nnpm uninstall -g @earendil-works/pi-coding-agent\n```\n\nUntuk pemasangan pnpm, Yarn, atau Bun, gunakan perintah hapus global yang cocok: `pnpm remove -g @earendil-works/pi-coding-agent`, `yarn global remove @earendil-works/pi-coding-agent`, atau `bun uninstall -g @earendil-works/pi-coding-agent`.\n\nKemudian jalankan di direktori proyek:\n\n```bash\npi\n```\n\nOtentikasi dengan `/login` untuk subscription providers, atau atur API key seperti `ANTHROPIC_API_KEY` sebelum memulai pi.\n\nUntuk alur putaran pertama selengkapnya, lihat [Quickstart](quickstart.md).\n\n## Mulai di sini\n\n- [Quickstart](quickstart.md) - instal, autentikasi, dan jalankan sesi pertama.\n- [Using Pi](usage.md) - mode interaktif, referensi slash commands, context files, dan CLI.\n- [Providers](providers.md) - langganan dan pengaturan kunci API untuk penyedia bawaan.\n- [llama.cpp](llama-cpp.md) - jalankan router lokal dan kelola model dengan `/llama`.\n- [Security](security.md) - kepercayaan proyek, sandbox batasan, dan pelaporan kerentanan.\n- [Containerization](containerization.md) - sandbox pi dengan Gondolin, Docker, atau OpenShell.\n- [Settings](settings.md) - pengaturan global dan proyek.\n- [Keybindings](keybindings.md) - pintasan default dan pengikatan tombol khusus.\n- [Sessions](sessions.md) - manajemen sesi, percabangan, dan navigasi pohon.\n- [Compaction](compaction.md) - context compaction dan branch summarization.\n\n## Kustomisasi\n\n- [Extensions](extensions.md) - TypeScript modul untuk alat, perintah, acara, dan UI khusus.\n- [Skills](skills.md) - Agen Skills untuk kemampuan sesuai permintaan yang dapat digunakan kembali.\n- [Prompt templates](prompt-templates.md) - perintah yang dapat digunakan kembali yang diperluas dari slash commands.\n- [Themes](themes.md) - bawaan dan khusus terminal themes.\n- [Pi packages](packages.md) - gabungkan dan bagikan ekstensi, keterampilan, petunjuk, dan tema.\n- [Custom models](models.md) - tambahkan entri model untuk penyedia yang didukung APIs.\n- [Custom providers](custom-provider.md) - menerapkan alur API dan OAuth khusus.\n\n## Penggunaan terprogram\n\n- [SDK](sdk.md) - sematkan pi di Node.js aplikasi.\n- [RPC mode](rpc.md) - integrasikan melalui stdin/stdout JSONL.\n- [JSON event stream mode](json.md) - mode cetak dengan acara terstruktur.\n- [TUI components](tui.md) - buat UI terminal khusus untuk ekstensi.\n\n## Referensi\n\n- [Environment variables](environment-variables.md) - Pi konfigurasi proses dan metadata sesi tersedia untuk bash alat.\n- [Session format](session-format.md) - JSONL format file sesi, jenis entri, dan SessionManager API.\n\n## Pengaturan platform\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## Perkembangan\n\n- [Development](development.md) - pengaturan lokal, struktur proyek, dan debugging.","sourceFile":"index.md"},"json":{"title":"JSON Mode Aliran Acara","markdown":"```bash\npi --mode json \"Your prompt\"\n```\n\nMenghasilkan semua acara sesi sebagai baris JSON ke stdout. Berguna untuk mengintegrasikan pi ke alat lain atau UI khusus.\n\n## Jenis Acara\n\nAcara kawat menggunakan `JsonAgentSessionEvent`. Itu cocok\n[`AgentSessionEvent`](https://github.com/earendil-works/pi-mono/blob/main/packages/coding-agent/src/core/agent-session.ts)\nkecuali pembaruan pesan streaming menghilangkan snapshot kumulatif:\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` memancarkan kemudi penuh yang tertunda dan antrean tindak lanjut setiap kali berubah. `compaction_start` dan `compaction_end` mencakup pemadatan manual dan otomatis.\n\nAcara dasar lainnya berasal\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## Jenis Pesan\n\nPesan dasar dari [`packages/ai/src/types.ts`](https://github.com/earendil-works/pi-mono/blob/main/packages/ai/src/types.ts#L134):\n- `UserMessage` (baris 134)\n- `AssistantMessage` (baris 140)\n- `ToolResultMessage` (baris 152)\n\nPesan tambahan dari [`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` (baris 29)\n- `CustomMessage` (baris 46)\n- `BranchSummaryMessage` (baris 55)\n- `CompactionSummaryMessage` (baris 62)\n\n## Format Keluaran\n\nSetiap baris adalah objek JSON. Baris pertama adalah header sesi:\n\n```json\n{\"type\":\"session\",\"version\":3,\"id\":\"uuid\",\"timestamp\":\"...\",\"cwd\":\"/path\"}\n```\n\nDiikuti oleh peristiwa-peristiwa yang terjadi:\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` catatan hanya untuk delta. Mereka menghilangkan bidang kumulatif `message` dan\n`assistantMessageEvent.partial` untuk menjaga ukuran aliran tetap linier. Gunakan `contentIndex` dan `delta`\nuntuk menyusun teks langsung, pemikiran, atau argumen panggilan alat jika diperlukan. `message_end` berisi\npesan otoritatif terakhir.\n\n## Contoh\n\n```bash\npi --mode json \"List files\" 2>/dev/null | jq -c 'select(.type == \"message_end\")'\n```","sourceFile":"json.md"},"keybindings":{"title":"Pengikatan kunci","markdown":"Semua pintasan keyboard dapat disesuaikan melalui `~/.pi/agent/keybindings.json`. Setiap tindakan dapat terikat pada satu atau lebih kunci.\n\nFile konfigurasi menggunakan id pengikat kunci dengan spasi nama yang sama yang digunakan pi secara internal dan yang digunakan penulis ekstensi di `keyHint()` dan menyuntikkan manajer `keybindings`.\n\nKonfigurasi lama yang menggunakan id dengan spasi nama sebelumnya seperti `cursorUp` atau `expandTools` dimigrasikan secara otomatis ke id dengan spasi nama saat startup.\n\nSetelah mengedit `keybindings.json`, jalankan `/reload` di pi untuk menerapkan perubahan tanpa memulai ulang sesi.\n\n## Format Kunci\n\n`modifier+key` dengan pengubah `ctrl`, `shift`, `alt`, `super` (dapat digabungkan) dan kuncinya adalah:\n\n- **Huruf:** `a-z`\n- **Digit:** `0-9`\n- **Tombol khusus:** `escape`, `esc`, `enter`, `return`, `tab`, `space`, `backspace`, `delete`, `insert`, `clear`, `home`, `end`, `pageUp`, `pageDown`, `up`, `down`, `left`, `right`\n- **Tombol fungsi:** `f1`-`f12`\n- **Simbol:** `` ` ``, `-`, `=`, `[`, `]`, `\\`, `;`, `'`, `,`, `.`, `/`, `!`, `@`, `#`, `$`, `%`, `^`, `&`, `*`, `(`, `)`, `_`, `+`, `|`, `~`, `{`, `}`, `:`, `<`, `>`, `?`\n\nKombinasi pengubah: `ctrl+shift+x`, `alt+ctrl+x`, `ctrl+shift+alt+x`, `super+k`, `ctrl+super+k`, `ctrl+1`, dll.\n\nPengikatan `super` memerlukan terminal yang melaporkan pengubah secara terpisah, biasanya melalui protokol keyboard Kitty. Mereka mungkin tidak dapat bekerja di terminal tanpa dukungan tersebut.\n\n## Semua Tindakan\n\n### TUI Gerakan Kursor Editor\n\n| ID pengikatan kunci | Bawaan | Keterangan |\n|--------|---------|-------------|\n| `tui.editor.cursorUp` | `up` | Pindahkan kursor ke atas, telusuri riwayat lama di bagian atas |\n| `tui.editor.cursorDown` | `down` | Pindahkan kursor ke bawah, telusuri riwayat baru di bagian bawah |\n| `tui.editor.historyPrevious` | *(tidak ada)* | Pilih entri riwayat prompt sebelumnya |\n| `tui.editor.historyNext` | *(tidak ada)* | Pilih entri riwayat prompt berikutnya |\n| `tui.editor.cursorLeft` | `left`, `ctrl+b` | Gerakkan kursor ke kiri |\n| `tui.editor.cursorRight` | `right`, `ctrl+f` | Gerakkan kursor ke kanan |\n| `tui.editor.cursorWordLeft` | `alt+left`, `ctrl+left`, `alt+b` | Pindahkan kata kursor ke kiri |\n| `tui.editor.cursorWordRight` | `alt+right`, `ctrl+right`, `alt+f` | Pindahkan kata kursor ke kanan |\n| `tui.editor.cursorLineStart` | `home`, `ctrl+home`, `ctrl+a` | Pindah ke baris awal |\n| `tui.editor.cursorLineEnd` | `end`, `ctrl+end`, `ctrl+e` | Pindah ke akhir baris |\n| `tui.editor.jumpForward` | `ctrl+]` | Lompat ke depan ke karakter |\n| `tui.editor.jumpBackward` | `ctrl+alt+]` | Lompat mundur ke karakter |\n| `tui.editor.pageUp` | `pageUp`, `ctrl+pageUp` | Gulir ke atas berdasarkan halaman |\n| `tui.editor.pageDown` | `pageDown`, `ctrl+pageDown` | Gulir ke bawah berdasarkan halaman |\n\nTindakan riwayat khusus selalu mengubah entri riwayat, terlepas dari posisi kursor dalam perintah multiline. Pengikatan riwayat eksplisit lebih diutamakan daripada tindakan aplikasi saat editor utama fokus, jadi pengikatan `tui.editor.historyPrevious` ke `ctrl+p` akan mengesampingkan perputaran model dalam konteks tersebut tanpa mengubah `Ctrl+P` pada penyeleksi.\n\n### TUI Penghapusan Editor\n\n| ID pengikatan kunci | Bawaan | Keterangan |\n|--------|---------|-------------|\n| `tui.editor.deleteCharBackward` | `backspace` | Hapus karakter mundur |\n| `tui.editor.deleteCharForward` | `delete`, `ctrl+d` | Hapus karakter ke depan |\n| `tui.editor.deleteWordBackward` | `ctrl+w`, `alt+backspace` | Hapus kata mundur |\n| `tui.editor.deleteWordForward` | `alt+d`, `alt+delete` | Hapus kata maju |\n| `tui.editor.deleteToLineStart` | `ctrl+u` | Hapus ke baris awal |\n| `tui.editor.deleteToLineEnd` | `ctrl+k` | Hapus sampai akhir baris |\n\n### TUI Masukan\n\n| ID pengikatan kunci | Bawaan | Keterangan |\n|--------|---------|-------------|\n| `tui.input.newLine` | `shift+enter`, `ctrl+j` | Sisipkan baris baru |\n| `tui.input.submit` | `enter` | Kirim masukan |\n| `tui.input.tab` | `tab` | Tab / pelengkapan otomatis |\n\n### TUI Bunuh Cincin\n\n| ID pengikatan kunci | Bawaan | Keterangan |\n|--------|---------|-------------|\n| `tui.editor.yank` | `ctrl+y` | Tempel teks yang terakhir dihapus |\n| `tui.editor.yankPop` | `alt+y` | Telusuri teks yang dihapus setelah penarikan |\n| `tui.editor.undo` | `ctrl+-` | Membatalkan pengeditan terakhir |\n\n### TUI Papan Klip dan Seleksi\n\n| ID pengikatan kunci | Bawaan | Keterangan |\n|--------|---------|-------------|\n| `tui.input.copy` | `ctrl+c` | Salin pilihan |\n| `tui.select.up` | `up` | Pindahkan pilihan ke atas |\n| `tui.select.down` | `down` | Pindahkan pilihan ke bawah |\n| `tui.select.pageUp` | `pageUp` | Halaman dalam daftar |\n| `tui.select.pageDown` | `pageDown` | Halaman bawah dalam daftar |\n| `tui.select.confirm` | `enter` | Konfirmasikan pilihan |\n| `tui.select.cancel` | `escape`, `ctrl+c` | Batalkan pilihan |\n\n### TUI Area Pandang Layar Penuh\n\nTindakan ini berlaku ketika mode interaktif menggunakan `--tui-mode fullscreen` dan menargetkan wilayah gulir transkrip utama. Trackpad dua jari dan input roda mouse menggulir wilayah di bawah penunjuk, kembali ke transkrip melalui dok editor/status/footer tetap. Mengklik hyperlink OSC 8 akan membukanya di handler default. Menyeret dengan tombol mouse utama akan memilih teks dan menyalinnya ke clipboard; memegang di tepi atas atau bawah transkrip akan otomatis menggulir ke konten di luar layar.\n\nPengikatan transkrip layar penuh lebih diutamakan daripada pengikatan editor. Oleh karena itu, tombol navigasi default yang tidak dimodifikasi mengontrol transkrip dalam mode layar penuh, sementara varian `ctrl` terus mengontrol editor. Di luar mode layar penuh, kedua varian mengontrol editor.\n\n| Kunci | Modus bawaan | Modus layar penuh |\n|-----|--------------|-----------------|\n| `home`, `end` | Editor | Salinan |\n| `ctrl+home`, `ctrl+end` | Editor | Editor |\n| `pageUp`, `pageDown` | Editor | Salinan |\n| `ctrl+pageUp`, `ctrl+pageDown` | Editor | Editor |\n\nPerutean ini tetap dapat dikonfigurasi melalui pengikatan tindakan biasa. Misalnya, `\"tui.altScreen.pageUp\": \"ctrl+pageUp\"` membuat `pageUp` mengontrol editor dan `ctrl+pageUp` mengontrol transkrip dalam mode layar penuh. Ikat `tui.altScreen.halfPageUp` dan `tui.altScreen.halfPageDown` untuk langkah transkrip yang lebih kecil sambil mempertahankan penjilidan satu halaman penuh. Pengaturan `\"tui.altScreen.pageUp\": []` menonaktifkan pintasan transkrip itu sepenuhnya. Pengikatan pengguna menggantikan default untuk tindakan itu.\n\n| ID pengikatan kunci | Bawaan | Keterangan |\n|--------|---------|-------------|\n| `tui.altScreen.pageUp` | `pageUp` | Gulir transkrip ke atas satu halaman |\n| `tui.altScreen.pageDown` | `pageDown` | Gulir transkrip ke bawah satu halaman |\n| `tui.altScreen.halfPageUp` | *(tidak ada)* | Gulir transkrip ke atas setengah halaman |\n| `tui.altScreen.halfPageDown` | *(tidak ada)* | Gulir transkrip ke bawah setengah halaman |\n| `tui.altScreen.previousPrompt` | `ctrl+shift+up` | Lompat ke pesan yang ditandai sebelumnya |\n| `tui.altScreen.nextPrompt` | `ctrl+shift+down` | Lompat ke pesan yang ditandai berikutnya |\n| `tui.altScreen.top` | `home` | Gulir ke awal transkrip |\n| `tui.altScreen.bottom` | `end` | Gulir ke akhir transkrip dan ikuti keluaran baru |\n\n### Aplikasi\n\n| ID pengikatan kunci | Bawaan | Keterangan |\n|--------|---------|-------------|\n| `app.interrupt` | `escape` | Batalkan / batalkan |\n| `app.clear` | `ctrl+c` | Hapus editor (pertama) / keluar (kedua) |\n| `app.exit` | `ctrl+d` | Keluar (ketika editor kosong) |\n| `app.suspend` | `ctrl+z` (tidak ada di Windows) | Tangguhkan ke latar belakang |\n| `app.editor.external` | `ctrl+g` | Buka di editor eksternal (`externalEditor`, `$VISUAL`, `$EDITOR`, Notepad di Windows, atau `nano` di tempat lain) |\n| `app.clipboard.pasteImage` | `ctrl+v` (`alt+v` di Windows) | Tempel gambar atau teks dari clipboard |\n\n### Sesi\n\n| ID pengikatan kunci | Bawaan | Keterangan |\n|--------|---------|-------------|\n| `app.session.new` | *(tidak ada)* | Mulai sesi baru (`/new`) |\n| `app.session.tree` | *(tidak ada)* | Buka session tree navigator (`/tree`) |\n| `app.session.fork` | *(tidak ada)* | Garpu sesi saat ini (`/fork`) |\n| `app.session.resume` | *(tidak ada)* | Buka pemilih resume sesi (`/resume`) |\n| `app.session.togglePath` | `ctrl+p` | Alihkan tampilan jalur |\n| `app.session.toggleSort` | `ctrl+s` | Alihkan mode pengurutan |\n| `app.session.toggleNamedFilter` | `ctrl+n` | Alihkan filter khusus nama |\n| `app.session.rename` | `ctrl+r` | Ganti nama sesi |\n| `app.session.delete` | `ctrl+d` | Hapus sesi |\n| `app.session.deleteNoninvasive` | `ctrl+backspace` | Hapus sesi saat kueri kosong |\n\n### Models dan Berpikir\n\n| ID pengikatan kunci | Bawaan | Keterangan |\n|--------|---------|-------------|\n| `app.model.select` | `ctrl+l` | Buka pemilih model |\n| `app.model.cycleForward` | `ctrl+p` | Putar ke model berikutnya |\n| `app.model.cycleBackward` | `shift+ctrl+p` | Siklus ke model sebelumnya |\n| `app.thinking.cycle` | `shift+tab` | Tingkat berpikir siklus |\n| `app.thinking.toggle` | `ctrl+t` | Perkecil atau perluas blok pemikiran |\n\n### Tampilan dan Antrean Pesan\n\n| ID pengikatan kunci | Bawaan | Keterangan |\n|--------|---------|-------------|\n| `app.tools.expand` | `ctrl+o` | Perkecil atau perluas keluaran alat |\n| `app.message.copy` | `ctrl+x` | Salin pesan asisten terakhir, atau pesan yang dipilih di `/tree` |\n| `app.message.followUp` | `alt+enter` | Pesan tindak lanjut antrian |\n| `app.message.dequeue` | `alt+up` | Pulihkan pesan yang antri ke editor |\n\n### Navigasi Pohon\n\n| ID pengikatan kunci | Bawaan | Keterangan |\n|--------|---------|-------------|\n| `app.tree.foldOrUp` | `ctrl+left`, `alt+left` | Lipat segmen cabang saat ini, atau lompat ke awal segmen sebelumnya |\n| `app.tree.unfoldOrDown` | `ctrl+right`, `alt+right` | Buka segmen cabang saat ini, atau lompat ke awal segmen atau ujung cabang berikutnya |\n| `app.tree.editLabel` | `shift+l` | Edit label pada simpul pohon yang dipilih |\n| `app.tree.toggleLabelTimestamp` | `shift+t` | Alihkan stempel waktu label di pohon |\n| `app.tree.filter.default` | `ctrl+d` | Setel filter pohon ke tampilan default |\n| `app.tree.filter.noTools` | `ctrl+t` | Alihkan filter pohon yang menyembunyikan hasil alat |\n| `app.tree.filter.userOnly` | `ctrl+u` | Alihkan filter pohon yang hanya menampilkan pesan pengguna |\n| `app.tree.filter.labeledOnly` | `ctrl+l` | Alihkan filter pohon yang hanya menampilkan entri berlabel |\n| `app.tree.filter.all` | `ctrl+a` | Alihkan filter pohon yang menampilkan semua entri |\n| `app.tree.filter.cycleForward` | `ctrl+o` | Filter pohon siklus maju |\n| `app.tree.filter.cycleBackward` | `shift+ctrl+o` | Siklus filter pohon mundur |\n\n### Cakupan Models Pemilih\n\nDigunakan di dalam pemilih model tercakup (dibuka melalui `/scoped-models`).\n\n| ID pengikatan kunci | Bawaan | Keterangan |\n|--------|---------|-------------|\n| `app.models.save` | `ctrl+s` | Simpan pilihan model saat ini ke pengaturan |\n| `app.models.enableAll` | `ctrl+a` | Aktifkan semua model (atau semua yang cocok dengan pencarian saat ini) |\n| `app.models.clearAll` | `ctrl+x` | Hapus semua model (atau semua yang cocok dengan pencarian saat ini) |\n| `app.models.toggleProvider` | `ctrl+p` | Alihkan semua model untuk penyedia saat ini |\n| `app.models.reorderUp` | `alt+up` | Pindahkan model yang dipilih ke atas dalam urutan siklus |\n| `app.models.reorderDown` | `alt+down` | Pindahkan model yang dipilih ke bawah dalam urutan siklus |\n\n## Konfigurasi Kustom\n\nBuat `~/.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\nSetiap tindakan dapat memiliki satu kunci atau serangkaian kunci. Konfigurasi pengguna mengesampingkan default.\n\nPada Windows asli, `app.suspend` tidak memiliki pengikatan default karena terminal Windows tidak mendukung kontrol pekerjaan Unix. Jika Anda mengikatnya secara manual, pi menampilkan pesan status alih-alih menangguhkan. Di WSL, perilaku normal Linux `ctrl+z`/`fg` masih berlaku.\n\n### Contoh Emacs\n\n```json\n{\n  \"tui.editor.historyPrevious\": \"ctrl+p\",\n  \"tui.editor.historyNext\": \"ctrl+n\",\n  \"tui.editor.cursorLeft\": [\"left\", \"ctrl+b\"],\n  \"tui.editor.cursorRight\": [\"right\", \"ctrl+f\"],\n  \"tui.editor.cursorWordLeft\": [\"alt+left\", \"alt+b\"],\n  \"tui.editor.cursorWordRight\": [\"alt+right\", \"alt+f\"],\n  \"tui.editor.deleteCharForward\": [\"delete\", \"ctrl+d\"],\n  \"tui.editor.deleteCharBackward\": [\"backspace\", \"ctrl+h\"],\n  \"tui.input.newLine\": [\"shift+enter\", \"ctrl+j\"]\n}\n```\n\n### Contoh 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 mendukung server router [llama.cpp](https://github.com/ggml-org/llama.cpp). Router menemukan beberapa model GGUF dan memuat atau mengeluarkannya sesuai permintaan.\n\nGunakan build llama.cpp saat ini dengan dukungan router. Ikuti [build instructions](https://github.com/ggml-org/llama.cpp/blob/master/docs/build.md) atau instal [prebuilt release](https://github.com/ggml-org/llama.cpp/releases) untuk platform Anda.\n\n## Mulai perute\n\nMulai `llama-server` tanpa `--model` atau `-m`. Melewati model akan memulai mode model tunggal, bukan mode router.\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\nOpsi penting:\n\n- `--models-dir ~/models` menemukan file GGUF lokal.\n- `--no-models-autoload` terus memuat secara eksplisit melalui `/llama`.\n- `--jinja` mengaktifkan templat obrolan dan panggilan alat yang kompatibel.\n- `-ngl 999` memindahkan sebanyak mungkin lapisan ke GPU.\n- `-c 32768` menyetel jendela konteks untuk setiap model yang dimuat. Abaikan untuk menggunakan konteks asli model, yang mungkin memerlukan lebih banyak memori.\n\nModel file tunggal dapat ditempatkan langsung di direktori model. Letakkan model multimodal dan multi-shard di subdirektori terpisah:\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\nMulai ulang router setelah menambahkan file secara manual. Untuk ukuran konteks per model dan opsi lainnya, gunakan [llama.cpp model presets](https://github.com/ggml-org/llama.cpp/blob/master/tools/server/README.md#model-presets).\n\n## Konfigurasikan Pi\n\nMulai Pi dan konfigurasikan penyedia:\n\n```text\n/login llama.cpp\n```\n\nMasukkan URL router dan opsional API key. URL bawaannya adalah `http://127.0.0.1:8080`.\n\nVariabel lingkungan dapat mengonfigurasi nilai yang sama tanpa `/login`:\n\n```bash\nexport LLAMA_BASE_URL=http://127.0.0.1:8080\nexport LLAMA_API_KEY=optional-secret\npi\n```\n\nJika server menggunakan API key, mulailah `llama-server` dengan nilai `--api-key` yang cocok. Pertahankan `--host 127.0.0.1` untuk akses lokal saja.\n\n## Kelola model\n\nBerlari:\n\n```text\n/llama\n```\n\n- Pilih model yang dibongkar untuk memuatnya.\n- Pilih model yang dimuat untuk membongkarnya.\n- Pilih **Unduh model…**, cari Hugging Face, lalu pilih repositori dan kuantisasi. Nilai `owner/repository[:quant]` yang tepat juga berfungsi.\n- Tekan Escape saat memuat atau mengunduh untuk mengonfirmasi pembatalan.\n\nPencarian Hugging Face menggunakan `HF_TOKEN` saat disetel, lalu centang `$HF_TOKEN_PATH`, `$HF_HOME/token`, `$XDG_CACHE_HOME/huggingface/token`, dan `~/.cache/huggingface/token`. Pencarian juga berfungsi tanpa autentikasi, dengan tunduk pada batas kecepatan yang lebih rendah. Pi memperingatkan sebelum mengunduh repositori yang terjaga keamanannya dan tautan ke halaman aksesnya. Server llama.cpp melakukan pengunduhan, jadi prosesnya juga harus memiliki `HF_TOKEN` ketika repositori yang dipilih memerlukan akses.\n\nJika model lain dimuat, Pi menanyakan apakah akan membongkarnya terlebih dahulu atau tetap memuatnya. Pi tidak membongkar model secara diam-diam dan tidak pernah menghapus file model. Router mungkin digunakan bersama dengan klien lain, jadi `/llama` selalu menampilkan status router saat ini.\n\nHanya model yang dimuat yang muncul di `/model`. Setelah memuat model, jalankan `/model` untuk memilihnya untuk sesi Pi saat ini.\n\nJika router terputus, `/llama` menampilkan **Coba lagi** dan **Tutup**. Coba lagi sambungkan kembali dan segarkan status model tanpa memutar ulang operasi yang terputus.\n\n## Pemecahan masalah\n\nPeriksa apakah router dapat dijangkau:\n\n```bash\ncurl http://127.0.0.1:8080/health\ncurl http://127.0.0.1:8080/models\n```\n\n- **Tidak ada model di `/llama`:** Periksa `--models-dir`, tata letak direktori, dan mulai ulang router.\n- **Model hilang dari `/model`:** Muat dengan `/llama` terlebih dahulu.\n- **Pemuatan gagal atau menggunakan terlalu banyak memori:** Turunkan `-c` atau bongkar model lain.\n- **Server tidak dalam mode router:** Mulai tanpa `--model`, `-m`, atau `-hf`.","sourceFile":"llama-cpp.md"},"models":{"title":"Kustom Models","markdown":"Tambahkan penyedia dan model khusus (Ollama, vLLM, LM Studio, proxy) melalui `~/.pi/agent/models.json`.\n\n## Daftar isi\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## Contoh Minimal\n\nUntuk model lokal (Ollama, LM Studio, vLLM), hanya `id` yang diperlukan per model:\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\nNilai `apiKey` adalah pengganti karena Ollama mengabaikannya. pi masih menganggap model memerlukan autentikasi sebelum muncul di `/model`, jadi server lokal tanpa kunci harus menyimpan nilai dummy, menyimpan kunci untuk penyedia tersebut dengan `/login`, atau meneruskan `--api-key` saat memilih model.\n\nBeberapa server yang kompatibel dengan OpenAI tidak memahami peran `developer` yang digunakan untuk model berkemampuan penalaran. Untuk penyedia tersebut, setel `compat.supportsDeveloperRole` ke `false` sehingga pi mengirimkan perintah sistem sebagai pesan `system`. Jika server juga tidak mendukung `reasoning_effort`, setel `compat.supportsReasoningEffort` ke `false` juga.\n\nAnda dapat mengatur `compat` di tingkat penyedia untuk diterapkan ke semua model, atau di tingkat model untuk mengganti model tertentu. Hal ini biasanya berlaku untuk Ollama, vLLM, SGLang, dan server serupa yang kompatibel dengan OpenAI.\n\n```json\n{\n  \"providers\": {\n    \"ollama\": {\n      \"baseUrl\": \"http://localhost:11434/v1\",\n      \"api\": \"openai-completions\",\n      \"apiKey\": \"ollama\",\n      \"compat\": {\n        \"supportsDeveloperRole\": false,\n        \"supportsReasoningEffort\": false\n      },\n      \"models\": [\n        {\n          \"id\": \"gpt-oss:20b\",\n          \"reasoning\": true\n        }\n      ]\n    }\n  }\n}\n```\n\n## Contoh Lengkap\n\nGanti default saat Anda memerlukan nilai spesifik:\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\nFile dimuat ulang setiap kali Anda membuka `/model`. Edit selama sesi; tidak perlu memulai ulang.\n\n## Contoh Google AI Studio\n\nGunakan `google-generative-ai` dengan `baseUrl` untuk menambahkan model dari Google AI Studio, termasuk entri Gemma 4 khusus:\n\n```json\n{\n  \"providers\": {\n    \"my-google\": {\n      \"baseUrl\": \"https://generativelanguage.googleapis.com/v1beta\",\n      \"api\": \"google-generative-ai\",\n      \"apiKey\": \"$GEMINI_API_KEY\",\n      \"models\": [\n        {\n          \"id\": \"gemma-4-31b-it\",\n          \"name\": \"Gemma 4 31B\",\n          \"input\": [\"text\", \"image\"],\n          \"contextWindow\": 262144,\n          \"reasoning\": true\n        }\n      ]\n    }\n  }\n}\n```\n\n`baseUrl` diperlukan saat menambahkan model khusus ke tipe `google-generative-ai` API.\n\n## Didukung APIs\n\n| API | Keterangan |\n|-----|-------------|\n| `openai-completions` | Penyelesaian Obrolan OpenAI (paling kompatibel) |\n| `openai-responses` | Respons OpenAI API |\n| `anthropic-messages` | Pesan Antropis API |\n| `google-generative-ai` | AI Generatif Google |\n\nTetapkan `api` di tingkat penyedia (default untuk semua model) atau tingkat model (penggantian per model).\n\n## Konfigurasi Penyedia\n\n| Bidang | Keterangan |\n|-------|-------------|\n| `baseUrl` | API URL titik akhir |\n| `api` | Tipe API (lihat di atas) |\n| `apiKey` | Konfigurasi API key opsional (lihat resolusi nilai di bawah). Hilangkan jika autentikasi disediakan oleh `/login`/`auth.json` atau CLI `--api-key`. |\n| `oauth` | Jenis penyedia OAuth dinamis. Saat ini mendukung `\"radius\"`; membutuhkan gateway `baseUrl`. |\n| `headers` | Header khusus (lihat resolusi nilai di bawah) |\n| `authHeader` | Atur `true` untuk menambahkan `Authorization: Bearer <apiKey>` secara otomatis |\n| `models` | Array konfigurasi model |\n| `modelOverrides` | Penggantian per model untuk model bawaan atau model yang terdaftar ekstensi pada penyedia ini |\n\nUntuk penyedia dengan `models`, konfigurasi penyedia non-bawaan memerlukan `baseUrl` dan nilai `api` di tingkat penyedia atau model. `apiKey` tidak diperlukan untuk memuat file: model akan tersedia ketika autentikasi dikonfigurasi melalui `/login`/`auth.json`, CLI `--api-key`, atau penyedia `apiKey`. Jika tidak ada autentikasi yang dikonfigurasi, model akan dimuat tetapi tetap tidak tersedia di `/model` dan `--list-models`.\n\n### Resolusi Nilai\n\nBidang `apiKey` dan `headers` mendukung eksekusi perintah, interpolasi lingkungan, dan literal:\n\n- **Perintah shell:** `\"!command\"` di awal mengeksekusi seluruh nilai sebagai perintah dan menggunakan stdout\n  ```json\n  \"apiKey\": \"!security find-generic-password -ws 'anthropic'\"\n  \"apiKey\": \"!op read 'op://vault/item/credential'\"\n  ```\n- **Interpolasi lingkungan:** `\"$ENV_VAR\"` atau `\"${ENV_VAR}\"` menggunakan nilai variabel bernama. Interpolasi berfungsi di dalam literal yang lebih besar.\n  ```json\n  \"apiKey\": \"$MY_API_KEY\"\n  \"apiKey\": \"${KEY_PREFIX}_${KEY_SUFFIX}\"\n  ```\n  `$FOO_BAR` adalah variabel `FOO_BAR`; gunakan `${FOO}_BAR` ketika `BAR` adalah teks literal. Variabel lingkungan yang hilang membuat nilai tidak terselesaikan.\n- **Lolos:** `\"$\"` memancarkan `\"$\"` literal; `\"$!\"` memancarkan `\"!\"` literal tanpa memicu eksekusi perintah.\n  ```json\n  \"apiKey\": \"$$literal-dollar-prefix\"\n  \"apiKey\": \"$!literal-bang-prefix\"\n  ```\n- **Nilai literal:** Digunakan secara langsung. String huruf besar biasa seperti `MY_API_KEY` bersifat literal; gunakan `$MY_API_KEY` untuk variabel lingkungan.\n  ```json\n  \"apiKey\": \"sk-...\"\n  ```\n\nUntuk `models.json`, perintah shell diselesaikan pada waktu permintaan. pi sengaja tidak menerapkan TTL bawaan, penggunaan kembali yang basi, atau logika pemulihan untuk perintah sewenang-wenang. Perintah yang berbeda memerlukan strategi caching dan kegagalan yang berbeda, dan pi tidak dapat menyimpulkan perintah yang tepat.\n\nJika perintah Anda lambat, mahal, terbatas pada kecepatan, atau harus tetap menggunakan nilai sebelumnya pada kegagalan sementara, gabungkan perintah tersebut dalam skrip atau perintah Anda sendiri yang mengimplementasikan perilaku caching atau TTL yang Anda inginkan.\n\n`/model` pemeriksaan ketersediaan menggunakan kehadiran autentikasi yang dikonfigurasi dan tidak menjalankan perintah shell.\n\n### Header Kustom\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## Konfigurasi Model\n\n| Bidang | Diperlukan | Bawaan | Keterangan |\n|-------|----------|---------|-------------|\n| `id` | Ya | — | Pengidentifikasi model (diteruskan ke API) |\n| `name` | TIDAK | `id` | Label model yang dapat dibaca manusia. Digunakan untuk mencocokkan (pola `--model`) dan ditampilkan sebagai teks detail model sekunder. |\n| `api` | TIDAK | penyedia `api` | Ganti API penyedia untuk model ini |\n| `reasoning` | TIDAK | `false` | Mendukung pemikiran yang diperluas |\n| `thinkingLevelMap` | TIDAK | dihilangkan | Memetakan tingkat pemikiran pi ke nilai-nilai penyedia dan menandai tingkat yang tidak didukung (lihat di bawah) |\n| `input` | TIDAK | `[\"text\"]` | Jenis masukan: `[\"text\"]` atau `[\"text\", \"image\"]` |\n| `contextWindow` | TIDAK | `128000` | Ukuran jendela konteks dalam token |\n| `maxTokens` | TIDAK | `16384` | Token keluaran maksimum |\n| `samplingParams` | TIDAK | dihilangkan | Parameter pengambilan sampel digabungkan kata demi kata ke dalam setiap isi permintaan (lihat di bawah) |\n| `cost` | TIDAK | semua nol | Tarif per juta token dengan tingkat harga input opsional untuk seluruh permintaan |\n| `compat` | TIDAK | penyedia `compat` | Penggantian kompatibilitas penyedia. Digabung dengan tingkat penyedia `compat` ketika keduanya disetel. |\n\nTingkat biaya menyediakan kumpulan tarif alternatif lengkap dan berlaku untuk permintaan penuh ketika total penggunaan input (`input + cacheRead + cacheWrite`) melebihi `inputTokensAbove`. Jika beberapa tingkatan cocok, ambang batas tertinggilah yang menang.\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\nPerilaku saat ini:\n- `/model`, `--list-models`, dan entri tampilan footer interaktif berdasarkan model `id`.\n- `name` yang dikonfigurasi digunakan untuk pencocokan model dan teks detail model sekunder. Itu tidak menggantikan id model footer/bilah status.\n\n### Parameter Pengambilan Sampel\n\n`samplingParams` adalah objek bentuk bebas yang digabungkan kata demi kata ke dalam setiap isi permintaan untuk model, setelah bidang pi menetapkan dirinya sendiri, sehingga kuncinya menang. Gunakan untuk mengirim parameter pengambilan sampel yang tidak dimodelkan oleh pi — termasuk parameter khusus server seperti llama.cpp `min_p` atau `top_k` 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\nHanya API yang kompatibel dengan OpenAI yang menerapkannya (`openai-completions`, `openai-responses`, `azure-openai-responses`); API lainnya abaikan saja. Kunci menggantikan bidang permintaan bernama pi (misalnya kunci `temperature` di sini mengalahkan suhu tingkat permintaan), jadi pilihlah kunci tersebut sebagai satu-satunya sumber kebenaran pengambilan sampel untuk suatu model. Dalam `modelOverrides`, `samplingParams` digabungkan per kunci dengan nilai model dasar.\n\n### Peta Tingkat Berpikir\n\nGunakan `thinkingLevelMap` pada model untuk mendeskripsikan kontrol pemikiran khusus model. Kuncinya adalah tingkat berpikir pi: `off`, `minimal`, `low`, `medium`, `high`, `xhigh`, `max`. Peta mungkin berisi lubang; misalnya, model dapat mengekspos `high` dan `max` tanpa mengekspos `xhigh`.\n\nNilai-nilai bersifat tristate:\n\n| Nilai | Arti |\n|-------|---------|\n| dihilangkan | Level standar hingga `high` menggunakan pemetaan default penyedia; level `xhigh` dan `max` yang diperluas tidak didukung |\n| rangkaian | Level didukung dan nilai ini dikirim ke penyedia |\n| `null` | Level tidak didukung dan disembunyikan/dilewati/dijepit |\n\nContoh model yang hanya mendukung penalaran off, high, dan max:\n\n```json\n{\n  \"id\": \"deepseek-v4-pro\",\n  \"reasoning\": true,\n  \"thinkingLevelMap\": {\n    \"minimal\": null,\n    \"low\": null,\n    \"medium\": null,\n    \"high\": \"high\",\n    \"xhigh\": null,\n    \"max\": \"max\"\n  }\n}\n```\n\nContoh model di mana pemikiran tidak dapat dinonaktifkan:\n\n```json\n{\n  \"id\": \"always-thinking-model\",\n  \"reasoning\": true,\n  \"thinkingLevelMap\": {\n    \"off\": null\n  }\n}\n```\n\nMigrasi: konfigurasi lama yang menggunakan `compat.reasoningEffortMap` harus memindahkan pemetaan tersebut ke tingkat model `thinkingLevelMap`. Gunakan `null` untuk level yang tidak seharusnya muncul di UI.\n\n## Mengganti Bawaan Providers\n\nRutekan penyedia bawaan melalui proxy tanpa mendefinisikan ulang model:\n\n```json\n{\n  \"providers\": {\n    \"anthropic\": {\n      \"baseUrl\": \"https://my-proxy.example.com/v1\"\n    }\n  }\n}\n```\n\nSemua model Anthropic bawaan tetap tersedia. OAuth atau API key autentikasi yang ada terus berfungsi.\n\nUntuk menggabungkan model kustom ke dalam penyedia bawaan, sertakan array `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\nGabungkan semantik:\n- Model bawaan tetap dipertahankan.\n- Model khusus di-upserd sebesar `id` dalam penyedia.\n- Jika model khusus `id` cocok dengan model bawaan `id`, model khusus akan menggantikan model bawaan tersebut.\n- Jika model khusus `id` baru, model tersebut akan ditambahkan bersama model bawaan.\n\n## Penggantian Per model\n\nGunakan `modelOverrides` untuk menyesuaikan model bawaan dan mencocokkan model terdaftar ekstensi tanpa mengganti daftar model lengkap penyedia.\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` mendukung bidang berikut per model: `name`, `reasoning`, `thinkingLevelMap`, `input`, `cost` (sebagian), `contextWindow`, `maxTokens`, `samplingParams` (digabung per kunci), `headers`, `compat`.\n\nMengarahkan OpenAI GPT-5.6 Sol, Terra, dan Luna secara default ke jendela konteks `272000` sehingga permintaan tetap berada dalam tingkat harga konteks pendek OpenAI. Untuk ikut serta dalam jendela konteks 1,05 juta OpenAI, tingkatkan jendela tersebut untuk setiap model yang Anda gunakan:\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\nPenggantian ini mempertahankan metadata harga bawaan. Permintaan dengan total lebih dari 272 ribu token masukan menggunakan tarif konteks panjang GPT-5.6 untuk keseluruhan permintaan. Terapkan penggantian yang sama ke `gpt-5.6-terra` atau `gpt-5.6-luna` bila diperlukan.\n\nCatatan perilaku:\n- `modelOverrides` diterapkan pada model penyedia bawaan dan model penyedia terdaftar ekstensi yang cocok.\n- ID model yang tidak dikenal diabaikan.\n- Anda dapat menggabungkan `baseUrl`/`headers` tingkat penyedia dengan `modelOverrides`.\n- Mengganti `name` hanya mengubah pencocokan model dan teks detail sekunder; daftar footer dan model utama terus menampilkan model `id`.\n- Jika `models` juga ditentukan untuk penyedia, model kustom akan digabungkan setelah penggantian bawaan. Model khusus dengan `id` yang sama menggantikan entri model bawaan yang diganti.\n\n## Kompatibilitas Pesan Antropis\n\nUntuk penyedia atau proxy yang menggunakan `api: \"anthropic-messages\"`, gunakan `compat` untuk mengontrol kompatibilitas permintaan khusus Antropik.\n\nSecara default pi mengirimkan per alat `eager_input_streaming: true`. Jika proxy atau backend yang kompatibel dengan Anthropic menolak kolom tersebut, setel `supportsEagerToolInputStreaming` ke `false`. Pi akan menghilangkan `tools[].eager_input_streaming` dan mengirimkan header beta `fine-grained-tool-streaming-2025-05-14` lama untuk permintaan yang mendukung alat.\n\nBeberapa model Antropik memerlukan pemikiran adaptif (`thinking.type: \"adaptive\"` plus `output_config.effort`) dibandingkan muatan pemikiran berbasis anggaran yang lama. Model bawaan mengatur ini secara otomatis. Untuk penyedia khusus atau alias yang merutekan ke model tersebut, setel `forceAdaptiveThinking` ke `true`.\n\nBeberapa penyedia yang kompatibel dengan Anthropic mengeluarkan blok pemikiran dengan tanda tangan kosong dan masih mengharapkannya diputar ulang. Tetapkan `allowEmptySignature` ke `true` hanya untuk penyedia tersebut; Anthropic sejati menolak tanda-tanda pemikiran kosong.\n\nModel Antropik bawaan mengaktifkan `supportsStrictTools` dalam metadata modelnya. Model kustom yang kompatibel dengan Anthropic harus menyetelnya ke `true` ketika titik akhirnya menerima definisi alat skema JSON yang ketat.\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| Bidang | Keterangan |\n|-------|-------------|\n| `supportsEagerToolInputStreaming` | Apakah penyedia menerima per alat `eager_input_streaming`. Bawaan: `true`. Setel ke `false` untuk menghilangkan kolom tersebut dan gunakan header beta streaming alat canggih yang lama pada permintaan yang mendukung alat. |\n| `supportsLongCacheRetention` | Apakah penyedia menerima retensi cache panjang Antropik (`cache_control.ttl: \"1h\"`) ketika retensi cache `long`. Bawaan: `true`. |\n| `sendSessionAffinityHeaders` | Apakah akan mengirim `x-session-affinity` dari id sesi saat caching diaktifkan. Default: terdeteksi otomatis untuk penyedia yang dikenal. |\n| `supportsCacheControlOnTools` | Apakah penyedia menerima penanda `cache_control` gaya Antropik pada definisi alat. Bawaan: `true`. |\n| `forceAdaptiveThinking` | Apakah akan mengirimkan pemikiran adaptif (`thinking.type: \"adaptive\"` plus `output_config.effort`) untuk model ini. Model adaptif bawaan mengatur ini secara otomatis. Bawaan: `false`. |\n| `allowEmptySignature` | Apakah akan memutar ulang tanda tangan berpikir kosong sebagai `signature: \"\"` alih-alih mengubah pemikiran menjadi teks. Bawaan: `false`. |\n| `supportsStrictTools` | Apakah penyedia menerima definisi alat skema JSON yang ketat. Bawaan: `false`; model Antropik bawaan mengaktifkannya dalam metadata yang dihasilkan. |\n\n## Kompatibilitas OpenAI\n\nUntuk penyedia dengan kompatibilitas OpenAI parsial, gunakan kolom `compat`.\n\n- Tingkat penyedia `compat` menerapkan default ke semua model di bawah penyedia tersebut.\n- Tingkat model `compat` menggantikan nilai tingkat penyedia untuk model tersebut.\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| Bidang | Keterangan |\n|-------|-------------|\n| `supportsStore` | Penyedia mendukung bidang `store` |\n| `supportsDeveloperRole` | Gunakan peran `developer` vs `system` |\n| `supportsReasoningEffort` | Dukungan untuk parameter `reasoning_effort` |\n| `supportsUsageInStreaming` | Mendukung `stream_options: { include_usage: true }` (default: `true`) |\n| `supportsFinishReason` | Apakah respons yang dialirkan mencakup `finish_reason`. Saat `false`, pi menyimpulkan `stop` atau `toolUse` saat streaming berakhir. Bawaan: `true`. |\n| `maxTokensField` | Gunakan `max_completion_tokens` atau `max_tokens` |\n| `requiresToolResultName` | Sertakan `name` pada pesan hasil alat |\n| `requiresAssistantAfterToolResult` | Sisipkan pesan asisten sebelum pesan pengguna setelah hasil alat |\n| `requiresThinkingAsText` | Ubah blok pemikiran menjadi teks biasa |\n| `requiresReasoningContentOnAssistantMessages` | Sertakan `reasoning_content` kosong pada semua pesan asisten yang diputar ulang saat penalaran diaktifkan |\n| `thinkingFormat` | Gunakan parameter berpikir `reasoning_effort`, `openrouter`, `deepseek`, `together`, `baseten`, `zai`, `qwen`, `chat-template`, atau `qwen-chat-template` |\n| `chatTemplateKwargs` | `chat_template_kwargs` nilai untuk `thinkingFormat: \"chat-template\"`; gunakan `{ \"$var\": \"thinking.enabled\" }` atau `{ \"$var\": \"thinking.effort\" }` untuk nilai berpikir yang dikontrol pi |\n| `chatTemplateArgs` | `chat_template_args` nilai untuk `thinkingFormat: \"baseten\"`; gunakan `{ \"$var\": \"thinking.enabled\" }` atau `{ \"$var\": \"thinking.effort\" }` untuk nilai berpikir yang dikontrol pi |\n| `cacheControlFormat` | Gunakan penanda `cache_control` bergaya Antropis pada perintah sistem, definisi alat terakhir, dan konten teks pengguna, asisten, atau hasil alat terakhir. Saat ini hanya `anthropic` yang didukung. |\n| `sendSessionAffinityHeaders` | Untuk `openai-completions`, kirim header afinitas sesi dari id sesi saat cache diaktifkan. Bawaan: `false`. |\n| `sessionAffinityFormat` | Untuk `openai-completions` dan `openai-responses`, format header afinitas sesi: `openai` mengirimkan `session_id`/`x-client-request-id` (penyelesaian juga `x-session-affinity`), `openai-nosession` menghilangkan header `session_id` yang berisi garis bawah, `openrouter` mengirimkan `x-session-id`. Tidak mempengaruhi parameter tubuh `prompt_cache_key`. Default: terdeteksi otomatis. |\n| `supportsStrictMode` | Apakah penyedia menerima definisi alat fungsi skema JSON yang ketat. Defaultnya bergantung pada API; model OpenAI bawaan membawa metadata kemampuan eksplisit. |\n| `supportsOpenAIGrammarTools` | Apakah API yang kompatibel dengan OpenAI memancarkan alat tata bahasa Lark/regex khusus. Ketika `false`, alat dengan batasan tata bahasa akan kembali ke alat dengan fungsi normal. Bawaan: `false`; katalog model bawaan memungkinkannya untuk model GPT-5+ di OpenAI, OpenAI Codex, Azure OpenAI, GitHub Copilot, opencode, dan Cloudflare AI Gateway. |\n| `deferredToolsMode` | Gunakan serialisasi alat yang ditangguhkan khusus penyedia. Saat ini hanya `\"kimi\"` yang didukung untuk format Penyelesaian Obrolan Kimi yang kompatibel dengan OpenAI. |\n| `supportsLongCacheRetention` | Apakah penyedia menerima retensi cache yang lama ketika retensi cache adalah `long`: `prompt_cache_retention: \"24h\"` untuk cache cepat OpenAI, atau `cache_control.ttl: \"1h\"` ketika `cacheControlFormat` adalah `anthropic`. Bawaan: `true`. |\n| `openRouterRouting` | Preferensi perutean penyedia OpenRouter. Objek ini dikirim apa adanya di bidang `provider` di [OpenRouter API request](https://openrouter.ai/docs/guides/routing/provider-selection). |\n| `vercelGatewayRouting` | Konfigurasi perutean Vercel AI Gateway untuk pemilihan penyedia (`only`, `order`) |\n\n`openrouter` menggunakan `reasoning: { effort }`. `together` menggunakan `reasoning: { enabled }` dan juga `reasoning_effort` ketika `supportsReasoningEffort` diaktifkan. `qwen` menggunakan `enable_thinking` tingkat atas. Gunakan `qwen-chat-template` untuk server lokal yang kompatibel dengan Qwen yang memerlukan `chat_template_kwargs.enable_thinking` dan `preserve_thinking`. Gunakan `chat-template` untuk templat obrolan vLLM/Hugging Face yang memerlukan `chat_template_kwargs` yang dapat dikonfigurasi, seperti `chatTemplateKwargs: { \"thinking\": { \"$var\": \"thinking.enabled\" } }` untuk templat DeepSeek V3.x. Gunakan `thinkingFormat: \"baseten\"` dengan `chatTemplateArgs` untuk penyedia yang mengekspos kontrol peralihan melalui `chat_template_args` dan secara opsional mendukung `reasoning_effort` tingkat atas.\n\n`cacheControlFormat: \"anthropic\"` ditujukan untuk penyedia yang kompatibel dengan OpenAI yang mengekspos cache cepat bergaya Antropik melalui penanda `cache_control` pada konten teks dan definisi alat.\n\nContoh:\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\nContoh Vercel AI Gerbang:\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 dapat membantu Anda membuat paket pi. Mintalah untuk menggabungkan ekstensi, keterampilan, prompt templates, atau tema Anda.\n\n\nPi mengemas ekstensi bundel, keterampilan, prompt templates, dan tema sehingga Anda dapat membagikannya melalui npm atau git. Sebuah paket dapat mendeklarasikan sumber daya di `package.json` di bawah kunci `pi`, atau menggunakan direktori konvensional.\n\n## Daftar isi\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## Instal dan Kelola\n\n> **Keamanan:** Pi paket dijalankan dengan akses sistem penuh. Extensions mengeksekusi kode arbitrer, dan keterampilan dapat menginstruksikan model untuk melakukan tindakan apa pun termasuk menjalankan executable. Tinjau kode sumber sebelum menginstal paket pihak ketiga.\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\nPerintah ini mengelola paket pi dan `pi update` dapat memperbarui instalasi pi CLI. Untuk menghapus instalasi pi itu sendiri, lihat [Quickstart](quickstart.md#uninstall).\n\nSecara default, `install` dan `remove` menulis ke pengaturan pengguna (`~/.pi/agent/settings.json`). Gunakan `-l` untuk menulis ke pengaturan proyek (`.pi/settings.json`). Pengaturan proyek dapat dibagikan dengan tim Anda, dan pi menginstal paket apa pun yang hilang secara otomatis saat startup setelah proyek dipercaya.\n\nUntuk mencoba suatu paket tanpa menginstalnya, gunakan `--extension` atau `-e`. Ini menginstal ke direktori sementara untuk proses saat ini saja:\n\n```bash\npi -e npm:@foo/bar\npi -e git:github.com/user/repo\n```\n\n## Sumber Paket\n\nPi menerima tiga jenis sumber dalam pengaturan dan `pi install`.\n\n### npm\n\n```\nnpm:@scope/pkg@1.2.3\nnpm:pkg\n```\n\n- Spesifikasi berversi disematkan dan dilewati oleh pembaruan paket (`pi update --extensions`, `pi update --all`).\n- Penginstalan pengguna berada di bawah `~/.pi/agent/npm/`.\n- Penginstalan proyek berada di bawah `.pi/npm/`.\n- Atur `npmCommand` di `settings.json` untuk menyematkan npm pencarian paket dan operasi instalasi ke perintah pembungkus tertentu seperti `mise` atau `asdf`.\n\nContoh:\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- Tanpa awalan `git:`, hanya URL protokol yang diterima (`https://`, `http://`, `ssh://`, `git://`).\n- Dengan awalan `git:`, format steno diterima, termasuk `github.com/user/repo` dan `git@github.com:user/repo`.\n- URL HTTPS dan SSH keduanya didukung.\n- SSH URL menggunakan kunci SSH yang dikonfigurasi secara otomatis (menghormati `~/.ssh/config`).\n- Untuk proses non-interaktif (misalnya CI), Anda dapat mengatur `GIT_TERMINAL_PROMPT=0` untuk menonaktifkan perintah kredensial dan mengatur `GIT_SSH_COMMAND` (misalnya `ssh -o BatchMode=yes -o ConnectTimeout=5`) agar gagal dengan cepat.\n- Referensi adalah tag atau komitmen yang disematkan. `pi update --extensions` dan `pi update --all` tidak memindahkannya ke referensi yang lebih baru, tetapi mereka merekonsiliasi klon yang ada ke referensi yang dikonfigurasi.\n- Gunakan `pi install git:host/user/repo@new-ref` untuk memperbarui pengaturan dan memindahkan paket yang ada ke referensi baru yang dipasangi pin.\n- Diklon ke `~/.pi/agent/git/<host>/<path>` (global) atau `.pi/git/<host>/<path>` (proyek).\n- Ketika rekonsiliasi mengubah checkout, pi mengatur ulang dan membersihkan klon, lalu menjalankan `npm install` jika `package.json` ada.\n\n**SSH contoh:**\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### Jalur Lokal\n\n```\n/absolute/path/to/package\n./relative/path/to/package\n```\n\nJalur lokal menunjuk ke file atau direktori pada disk dan ditambahkan ke pengaturan tanpa menyalin. Jalur relatif diselesaikan berdasarkan file pengaturan tempat jalur tersebut muncul. Jika jalur tersebut berupa file, maka jalur tersebut dimuat sebagai ekstensi tunggal. Jika itu adalah sebuah direktori, pi memuat sumber daya menggunakan aturan paket.\n\n## Membuat Paket Pi\n\nTambahkan manifes `pi` ke `package.json` atau gunakan direktori konvensional. Sertakan kata kunci `pi-package` agar dapat ditemukan.\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\nJalur relatif terhadap root paket. Array mendukung pola glob dan `!exclusions`.\n\n### Metadata Galeri\n\n[package gallery](https://pi.dev/packages) menampilkan paket yang diberi tag `pi-package`. Tambahkan kolom `video` atau `image` untuk menampilkan pratinjau:\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**: hanya MP4. Di desktop, putar otomatis saat mengarahkan kursor. Mengklik akan membuka pemutar layar penuh.\n- **gambar**: PNG, JPEG, GIF, atau WebP. Ditampilkan sebagai pratinjau statis.\n\nJika keduanya disetel, video akan diutamakan.\n\n## Struktur Paket\n\n### Direktori Konvensi\n\nJika tidak ada manifes `pi`, pi otomatis menemukan sumber daya dari direktori berikut:\n\n- `extensions/` memuat file `.ts` dan `.js`\n- `skills/` secara rekursif menemukan `SKILL.md` folder dan memuat file `.md` tingkat atas sebagai keterampilan\n- `prompts/` memuat `.md` file\n- `themes/` memuat `.json` file\n\n## Ketergantungan\n\nDependensi runtime pihak ketiga termasuk dalam `dependencies` di `package.json`. Dependensi yang tidak mendaftarkan ekstensi, keterampilan, prompt templates, atau tema juga termasuk dalam `dependencies`. Saat pi menginstal paket dari npm atau git, ia menjalankan `npm install`, sehingga dependensi tersebut diinstal secara otomatis.\n\nPi menggabungkan paket inti untuk ekstensi dan keterampilan. Jika Anda mengimpor salah satu dari ini, cantumkan dalam `peerDependencies` dengan rentang `\"*\"` dan jangan gabungkan: `@earendil-works/pi-ai`, `@earendil-works/pi-agent-core`, `@earendil-works/pi-coding-agent`, `@earendil-works/pi-tui`, `typebox`.\n\nPaket pi lainnya harus dibundel dalam tarball Anda. Tambahkan sumber daya tersebut ke `dependencies` dan `bundledDependencies`, lalu rujuk sumber dayanya melalui jalur `node_modules/`. Pi memuat paket dengan akar modul terpisah, sehingga instalasi terpisah tidak bertabrakan atau berbagi modul.\n\nContoh:\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## Penyaringan Paket\n\nFilter apa yang dimuat paket menggunakan bentuk objek di pengaturan:\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` dan `-path` adalah jalur persis yang berhubungan dengan root paket.\n\n- Hilangkan kunci untuk memuat semua jenis itu.\n- Gunakan `[]` untuk tidak memuat jenis tersebut.\n- `!pattern` tidak termasuk kecocokan.\n- `+path` kekuatan-termasuk jalur yang tepat.\n- `-path` paksa-tidak termasuk jalur yang tepat.\n- Filter lapisan di atas manifes. Mereka mempersempit apa yang sudah diperbolehkan.\n\n## Aktifkan dan Nonaktifkan Sumber Daya\n\nGunakan `pi config` untuk mengaktifkan atau menonaktifkan ekstensi, keterampilan, prompt templates, dan tema dari paket yang diinstal dan direktori lokal. `pi config` dimulai dalam pengaturan global (`~/.pi/agent/settings.json`); tekan Tab untuk beralih antara mode global dan proyek-lokal. Gunakan `pi config -l` untuk memulai penggantian proyek (`.pi/settings.json`) dengan sumber daya global yang diwarisi diredupkan.\n\n## Ruang Lingkup dan Deduplikasi\n\nPaket dapat muncul di pengaturan global dan proyek. Jika paket yang sama muncul di keduanya, entri proyek menang kecuali entri proyek memiliki `autoload: false`, dalam hal ini diterapkan sebagai delta di atas entri global. Identitas ditentukan oleh:\n\n- npm: nama paket\n- git: URL repositori tanpa referensi\n- lokal: jalur absolut terselesaikan","sourceFile":"packages.md"},"prompt-templates":{"title":"Templat Cepat","markdown":"> pi dapat membuat prompt templates. Mintalah untuk membuatkannya untuk alur kerja Anda.\n\n\nTemplat prompt adalah Markdown cuplikan yang diperluas menjadi perintah penuh. Ketik `/name` di editor untuk memanggil templat, dengan `name` adalah nama file tanpa `.md`.\n\n## Lokasi\n\nPi memuat prompt templates dari:\n\n- Global: `~/.pi/agent/prompts/*.md`\n- Proyek: `.pi/prompts/*.md` (hanya setelah proyek dipercaya)\n- Paket: `prompts/` direktori atau `pi.prompts` entri di `package.json`\n- Pengaturan: `prompts` array dengan file atau direktori\n- CLI: `--prompt-template <path>` (dapat diulang)\n\nNonaktifkan penemuan dengan `--no-prompt-templates`.\n\n## Format\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- Nama file menjadi nama perintah. `review.md` menjadi `/review`.\n- `description` adalah opsional. Jika tidak ada, baris pertama yang tidak kosong digunakan.\n- `argument-hint` adalah opsional. Jika disetel, petunjuk akan ditampilkan sebelum deskripsi di dropdown pelengkapan otomatis.\n\n### Petunjuk Argumen\n\nGunakan `argument-hint` di frontmatter untuk menampilkan argumen yang diharapkan dalam pelengkapan otomatis. Gunakan `<angle brackets>` untuk argumen wajib dan `[square brackets]` untuk argumen opsional:\n\n```markdown\n---\ndescription: Review PRs from URLs with structured issue and code analysis\nargument-hint: \"<PR-URL>\"\n---\n```\n\nIni ditampilkan dalam dropdown pelengkapan otomatis sebagai:\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## Penggunaan\n\nKetik `/` diikuti dengan nama templat di editor. Pelengkapan otomatis menampilkan templat yang tersedia dengan deskripsi.\n\n```\n/review                           # Expands review.md\n/component Button                 # Expands with argument\n/component Button \"click handler\" # Multiple arguments\n```\n\n## Argumen\n\nTemplat mendukung argumen posisi, default, dan pemotongan sederhana:\n\n- `$1`, `$2`,... argumen posisi\n- `$@` atau `$ARGUMENTS` untuk semua argumen yang digabungkan\n- `${1:-default}` menggunakan arg 1 saat ada/tidak kosong, jika tidak `default`\n- `${@:-default}` atau `${ARGUMENTS:-default}` menggunakan semua argumen saat ada/tidak kosong, jika tidak `default`\n- `${@:N}` untuk argumen dari posisi ke-N (terindeks 1)\n- `${@:N:L}` untuk `L` argumen dimulai dari N\n\nContoh:\n\n```markdown\n---\ndescription: Create a component\n---\nCreate a React component named $1 with features: $@\n```\n\nNilai default berguna untuk argumen opsional:\n\n```markdown\nSummarize the current state in ${1:-7} bullet points.\n```\n\nPenggunaan: `/component Button \"onClick handler\" \"disabled support\"`\n\n## Memuat Aturan\n\n- Penemuan templat di `prompts/` bersifat non-rekursif.\n- Jika Anda ingin templat dalam subdirektori, tambahkan secara eksplisit melalui pengaturan `prompts` atau manifes paket.","sourceFile":"prompt-templates.md"},"providers":{"title":"Providers","markdown":"Pi mendukung penyedia berbasis langganan melalui penyedia OAuth dan API key melalui variabel lingkungan atau file autentikasi. Katalog bawaan dikirimkan dengan pi; penyedia yang dikonfigurasi dapat menyegarkan katalog baru dan menyimpannya dalam cache di `~/.pi/agent/models-store.json` untuk penggunaan offline.\n\n## Daftar isi\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## Langganan\n\nGunakan `/login` dalam mode interaktif, lalu pilih penyedia:\n\n- ObrolanGPT Plus/Pro (Kodeks)\n- Claude Pro/Maks\n- GitHub Kopilot\n- xAI (langganan Grok/X)\n- OpenRouter (OAuth-dicetak API key ditagih dari kredit OpenRouter)\n- Radius\n\nGunakan `/logout` untuk menghapus kredensial. Token disimpan di `~/.pi/agent/auth.json` dan disegarkan secara otomatis ketika habis masa berlakunya. OpenRouter malah mencetak API key yang dikontrol pengguna yang tidak kedaluwarsa secara otomatis.\n\n### Kodeks OpenAI\n\n- Memerlukan langganan ChatGPT Plus atau Pro\n- Secara resmi didukung oleh OpenAI: [Codex for OSS](https://developers.openai.com/community/codex-for-oss)\n\n### Claude Pro/Maks\n\nAutentikasi langganan antropik aktif untuk akun Claude Pro/Max. Penggunaan harness pihak ketiga dimulai dari [extra usage](https://claude.ai/settings/usage) dan ditagih per token, tidak berdasarkan batas paket Claude.\n\n### GitHub Kopilot\n\n- Tekan Enter untuk github.com, atau masukkan domain GitHub Server Perusahaan Anda\n- Jika Anda mendapatkan \"model tidak didukung\", aktifkan di VS Code: Copilot Chat → model selector → pilih model → \"Enable\"\n\n### xAI (langganan Grok/X)\n\n- Jalankan `/login xai`, lalu pilih **Gunakan langganan**\n- `XAI_API_KEY` tetap tersedia melalui **Gunakan API key**\n\n### BukaRouter\n\n- Jalankan `/login openrouter`, lalu pilih **Masuk dengan OpenRouter** untuk membuka alur otorisasi OpenRouter PKCE\n- Otorisasi ini membuat OpenRouter API key yang dikontrol pengguna ditagih dari kredit OpenRouter Anda\n- Pada mesin jarak jauh/tanpa kepala (misalnya lebih dari SSH) browser tidak dapat menjangkau panggilan balik loopback; tempelkan URL pengalihan terakhir (atau kode otorisasi) ke dalam perintah login\n- `OPENROUTER_API_KEY` tetap tersedia melalui **Gunakan API key**\n\n### Radius\n\nRadius adalah gerbang `pi-messages` yang dinamis. `/login radius` menyimpan OAuth token di `auth.json`; katalog gateway disegarkan secara independen dan disimpan dalam cache di `models-store.json`. Gateway Radius khusus dapat dideklarasikan di `models.json` dengan `\"oauth\": \"radius\"` dan gateway `baseUrl`.\n\n## API Kunci\n\n### Variabel Lingkungan atau File Auth\n\nGunakan `/login` dalam mode interaktif dan pilih penyedia untuk menyimpan API key di `auth.json`, atau atur kredensial melalui variabel lingkungan:\n\n```bash\nexport ANTHROPIC_API_KEY=sk-ant-...\npi\n```\n\n| Penyedia | Variabel Lingkungan | `auth.json` kunci |\n|----------|----------------------|------------------|\n| Antropis | `ANTHROPIC_API_KEY` | `anthropic` |\n| Semut Ling | `ANT_LING_API_KEY` | `ant-ling` |\n| Respons Azure OpenAI | `AZURE_OPENAI_API_KEY` | `azure-openai-responses` |\n| OpenAI | `OPENAI_API_KEY` | `openai` |\n| Pencarian Mendalam | `DEEPSEEK_API_KEY` | `deepseek` |\n| NVIDIA NIM | `NVIDIA_API_KEY` | `nvidia` |\n| Google Gemini | `GEMINI_API_KEY` | `google` |\n| Batuan Dasar Amazon | `AWS_BEARER_TOKEN_BEDROCK` | `amazon-bedrock` |\n| Mistral | `MISTRAL_API_KEY` | `mistral` |\n| Bagus | `GROQ_API_KEY` | `groq` |\n| otak besar | `CEREBRAS_API_KEY` | `cerebras` |\n| Gerbang AI Cloudflare | `CLOUDFLARE_API_KEY` (+ `CLOUDFLARE_ACCOUNT_ID`, `CLOUDFLARE_GATEWAY_ID`) | `cloudflare-ai-gateway` |\n| AI Pekerja Cloudflare | `CLOUDFLARE_API_KEY` (+ `CLOUDFLARE_ACCOUNT_ID`) | `cloudflare-workers-ai` |\n| xAI | `XAI_API_KEY` | `xai` |\n| BukaRouter | `OPENROUTER_API_KEY` | `openrouter` |\n| Gerbang AI Vercel | `AI_GATEWAY_API_KEY` | `vercel-ai-gateway` |\n| Paket Pengkodean ZAI (Global) | `ZAI_API_KEY` | `zai` |\n| Paket Pengkodean ZAI (Tiongkok) | `ZAI_CODING_CN_API_KEY` | `zai-coding-cn` |\n| Kode Terbuka Zen | `OPENCODE_API_KEY` | `opencode` |\n| OpenCode Pergi | `OPENCODE_API_KEY` | `opencode-go` |\n| Radius | `RADIUS_API_KEY` | `radius` |\n| Hugging Face | `HF_TOKEN` | `huggingface` |\n| Kembang api | `FIREWORKS_API_KEY` | `fireworks` |\n| Bersama AI | `TOGETHER_API_KEY` | `together` |\n| Dasar | `BASETEN_API_KEY` | `baseten` |\n| Kimi Untuk Pengkodean | `KIMI_API_KEY` | `kimi-coding` |\n| Mini Maks | `MINIMAX_API_KEY` | `minimax` |\n| MiniMax (Cina) | `MINIMAX_CN_API_KEY` | `minimax-cn` |\n| Paket Token Qwen (katalog yang ada) | `QWEN_TOKEN_PLAN_API_KEY` | `qwen-token-plan` |\n| Paket Token Qwen (Individu) | `QWEN_TOKEN_PLAN_API_KEY` | `qwen-token-plan-individual` |\n| Paket Token Qwen (Tiongkok) | `QWEN_TOKEN_PLAN_CN_API_KEY` | `qwen-token-plan-cn` |\n| Xiaomi MiMo | `XIAOMI_API_KEY` | `xiaomi` |\n| Paket Token Xiaomi MiMo (Tiongkok) | `XIAOMI_TOKEN_PLAN_CN_API_KEY` | `xiaomi-token-plan-cn` |\n| Paket Token Xiaomi MiMo (Amsterdam) | `XIAOMI_TOKEN_PLAN_AMS_API_KEY` | `xiaomi-token-plan-ams` |\n| Paket Token Xiaomi MiMo (Singapura) | `XIAOMI_TOKEN_PLAN_SGP_API_KEY` | `xiaomi-token-plan-sgp` |\n\nReferensi untuk variabel lingkungan dan kunci `auth.json`: [`const envMap`](https://github.com/earendil-works/pi-mono/blob/main/packages/ai/src/env-api-keys.ts) di [`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#### File Otentikasi\n\nSimpan kredensial di `~/.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` menggunakan titik akhir internasional yang sama dan `QWEN_TOKEN_PLAN_API_KEY` seperti\n`qwen-token-plan`, namun membatasi pemilih pada model yang didokumentasikan untuk langganan Perorangan. Yang ada\npenyedia menyimpan katalognya yang lebih luas untuk kompatibilitas ke belakang. Saat menggunakan `auth.json`, simpan\nkredensial di bawah penyedia yang Anda pilih; variabel lingkungan digunakan bersama oleh kedua penyedia internasional.\n\nFile ini dibuat dengan izin `0600` (hanya baca/tulis pengguna). Kredensial file autentikasi lebih diprioritaskan daripada variabel lingkungan.\n\nKredensial API key juga dapat mencakup nilai lingkungan cakupan penyedia. Nilai-nilai ini digunakan sebelum variabel lingkungan proses ketika menyelesaikan kunci kredensial, header penyedia/model, dan konfigurasi penyedia seperti ID akun Cloudflare, pengaturan Azure OpenAI, proyek/lokasi Vertex, pengaturan Batuan Dasar, `PI_CACHE_RETENTION`, dan `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\nGunakan ini ketika pi harus menggunakan pengaturan penyedia yang berbeda dari lingkungan shell proyek.\n\n### Resolusi Kunci\n\nBidang `key` mendukung eksekusi perintah, interpolasi lingkungan, dan literal:\n\n- **Perintah shell:** `\"!command\"` di awal mengeksekusi seluruh nilai sebagai perintah dan menggunakan stdout (di-cache untuk masa proses)\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- **Interpolasi lingkungan:** `\"$ENV_VAR\"` atau `\"${ENV_VAR}\"` menggunakan nilai variabel bernama. Interpolasi berfungsi di dalam literal yang lebih besar.\n  ```json\n  { \"type\": \"api_key\", \"key\": \"$MY_ANTHROPIC_KEY\" }\n  { \"type\": \"api_key\", \"key\": \"${KEY_PREFIX}_${KEY_SUFFIX}\" }\n  ```\n  `$FOO_BAR` adalah variabel `FOO_BAR`; gunakan `${FOO}_BAR` ketika `BAR` adalah teks literal. Variabel lingkungan yang hilang membuat nilai tidak terselesaikan.\n- **Lolos:** `\"$\"` memancarkan `\"$\"` literal; `\"$!\"` memancarkan `\"!\"` literal tanpa memicu eksekusi perintah.\n  ```json\n  { \"type\": \"api_key\", \"key\": \"$$literal-dollar-prefix\" }\n  { \"type\": \"api_key\", \"key\": \"$!literal-bang-prefix\" }\n  ```\n- **Nilai literal:** Digunakan secara langsung. String huruf besar biasa seperti `MY_API_KEY` bersifat literal; gunakan `$MY_API_KEY` untuk variabel lingkungan.\n  ```json\n  { \"type\": \"api_key\", \"key\": \"sk-ant-...\" }\n  { \"type\": \"api_key\", \"key\": \"public\" }\n  ```\n\nKredensial OAuth juga disimpan di sini setelah `/login` dan dikelola secara otomatis.\n\n## Awan 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### Batuan Dasar Amazon\n\nGunakan `/login amazon-bedrock` untuk menyimpan Batuan Dasar API key, atau konfigurasikan salah satu sumber kredensial AWS ambien di bawah:\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\nJuga mendukung peran tugas ECS (`AWS_CONTAINER_CREDENTIALS_*`) dan IRSA (`AWS_WEB_IDENTITY_TOKEN_FILE`).\n\n```bash\npi --provider amazon-bedrock --model us.anthropic.claude-sonnet-4-20250514-v1:0\n```\n\nCaching cepat diaktifkan secara otomatis untuk model Claude yang ID-nya berisi nama model yang dapat dikenali (model dasar dan profil inferensi yang ditentukan sistem). Untuk profil inferensi aplikasi (yang ARN-nya tidak berisi nama model), setel `AWS_BEDROCK_FORCE_CACHE=1` untuk mengaktifkan titik cache:\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\nJika Anda menyambung ke proksi Bedrock API, variabel lingkungan berikut dapat digunakan:\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### Gerbang AI Cloudflare\n\n`CLOUDFLARE_API_KEY` dapat diatur melalui `/login`. ID akun dan slug gateway dapat ditetapkan sebagai variabel lingkungan atau dalam objek `env` kredensial API key di `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\nRute ke OpenAI, Anthropic, dan Workers AI melalui Cloudflare AI Gateway. AI Pekerja menggunakan ID model Terpadu API (`/compat`) dan awalan (`workers-ai/@cf/...`). OpenAI menggunakan rute passthrough OpenAI (`/openai`) dengan ID model OpenAI asli seperti `gpt-5.1`. Anthropic menggunakan rute passthrough Anthropic (`/anthropic`) dengan ID model Anthropic asli seperti `claude-sonnet-4-5`.\n\nOtentikasi AI Gateway menggunakan `CLOUDFLARE_API_KEY` sebagai `cf-aig-authorization`. Otentikasi upstream dapat berupa salah satu dari:\n\n| Mode | Minta autentikasi | Otentikasi hulu |\n|------|--------------|---------------|\n| AI pekerja | Hanya token Cloudflare | Cloudflare-asli |\n| Penagihan terpadu | Hanya token Cloudflare | Cloudflare menangani autentikasi upstream dan mengurangi kredit |\n| Disimpan BYOK | Hanya token Cloudflare | Cloudflare menyuntikkan kunci penyedia yang disimpan di dasbor AI Gateway |\n| BYOK sebaris | Token Cloudflare ditambah header `Authorization` hulu | Permintaan tersebut menyediakan kunci penyedia hulu |\n\nUntuk penggunaan pi normal, pilih penagihan terpadu atau BYOK tersimpan. BYOK sebaris memerlukan konfigurasi header `Authorization` upstream tambahan untuk penyedia Cloudflare AI Gateway, misalnya melalui penggantian penyedia/model `models.json`.\n\n### AI Pekerja Cloudflare\n\n`CLOUDFLARE_API_KEY` dapat diatur melalui `/login`. `CLOUDFLARE_ACCOUNT_ID` dapat ditetapkan sebagai variabel lingkungan atau dalam objek `env` kredensial API key di `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 secara otomatis menetapkan `x-session-affinity` untuk [prefix caching](https://developers.cloudflare.com/workers-ai/features/prompt-caching/) diskon.\n\n### Google Vertex AI\n\nMenggunakan Kredensial Default Aplikasi:\n\n```bash\ngcloud auth application-default login\nexport GOOGLE_CLOUD_PROJECT=your-project\nexport GOOGLE_CLOUD_LOCATION=us-central1\n```\n\nAtau atur `GOOGLE_APPLICATION_CREDENTIALS` ke file kunci akun layanan.\n\n## llama.cpp\n\nPi mendukung server router llama.cpp. Konfigurasikan dengan `/login llama.cpp`, kelola model yang dimuat dengan `/llama`, dan pilih model yang dimuat dengan `/model`.\n\nLihat [llama.cpp](llama-cpp.md) untuk pengaturan server, tata letak direktori model, variabel lingkungan, dan penggunaan perintah.\n\n## Kustom Providers\n\n**Melalui models.json:** Tambahkan Ollama, LM Studio, vLLM, atau penyedia apa pun yang menggunakan API yang didukung (Penyelesaian OpenAI, Respons OpenAI, Pesan Antropik, AI Generatif Google). Lihat [models.md](models.md).\n\n**Melalui ekstensi:** Untuk penyedia yang memerlukan penerapan API atau alur OAuth khusus, buat ekstensi. Lihat [custom-provider.md](custom-provider.md) dan [examples/extensions/custom-provider-gitlab-duo](../examples/extensions/custom-provider-gitlab-duo/).\n\n## Perintah Resolusi\n\nSaat menyelesaikan kredensial untuk penyedia:\n\n1. CLI `--api-key` bendera\n2. `auth.json` entri (API key atau OAuth token)\n3. Variabel lingkungan\n4. Kunci penyedia khusus dari `models.json`","sourceFile":"providers.md"},"quickstart":{"title":"Mulai cepat","markdown":"Halaman ini membawa Anda dari instalasi ke sesi pi pertama yang berguna.\n\n## Memasang\n\nPi didistribusikan sebagai paket npm:\n\n```bash\nnpm install -g --ignore-scripts @earendil-works/pi-coding-agent\n```\n\n`--ignore-scripts` menonaktifkan skrip siklus hidup ketergantungan selama instalasi. Pi tidak memerlukan skrip instalasi untuk instalasi npm normal.\n\n### Copot pemasangan\n\nGunakan manajer paket yang menginstal pi. Pemasang curl menggunakan npm secara global, sehingga pemasangan curl dan npm dihapus dengan 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\nMenghapus instalasi pi akan meninggalkan pengaturan, kredensial, sesi, dan paket pi yang diinstal di `~/.pi/agent/`.\n\nKemudian mulai pi di direktori proyek yang Anda inginkan:\n\n```bash\ncd /path/to/project\npi\n```\n\n## Otentikasi\n\nPi dapat menggunakan subscription providers melalui `/login`, atau API-penyedia kunci melalui variabel lingkungan atau file autentikasi.\n\n### Opsi 1: login berlangganan\n\nMulai pi dan jalankan:\n\n```text\n/login\n```\n\nKemudian pilih penyedia. Login berlangganan bawaan termasuk Claude Pro/Max, ChatGPT Plus/Pro (Codex), dan GitHub Copilot.\n\n### Opsi 2: API key\n\nTetapkan API key sebelum meluncurkan pi:\n\n```bash\nexport ANTHROPIC_API_KEY=sk-ant-...\npi\n```\n\nAnda juga dapat menjalankan `/login` dan memilih penyedia kunci API untuk menyimpan kunci di `~/.pi/agent/auth.json`.\n\nLihat [Providers](providers.md) untuk semua penyedia yang didukung, variabel lingkungan, dan pengaturan penyedia cloud.\n\n## Sesi pertama\n\nSetelah pi dimulai, ketik permintaan dan tekan Enter:\n\n```text\nSummarize this repository and tell me how to run its checks.\n```\n\nSecara default, pi memberikan model empat alat:\n\n- `read` - membaca file\n- `write` - membuat atau menimpa file\n- `edit` - menambal file\n- `bash` - jalankan perintah shell\n\nAlat baca-saja bawaan tambahan (`grep`, `find`, `ls`) tersedia melalui opsi alat. Pi berjalan di direktori kerja Anda saat ini dan dapat mengubah file di sana. Gunakan git atau alur kerja pos pemeriksaan lainnya jika Anda ingin pengembalian yang mudah.\n\n## Berikan instruksi proyek pi\n\nPi memuat context files saat startup. Tambahkan file `AGENTS.md` untuk memberitahukannya cara bekerja dalam sebuah proyek:\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 memuat:\n\n- `~/.pi/agent/AGENTS.md` untuk instruksi global\n- `AGENTS.md` atau `CLAUDE.md` dari direktori induk dan direktori saat ini\n\nJika direktori berisi `AGENTS.override.md`, Pi akan memuatnya, bukan `AGENTS.md` atau `CLAUDE.md` dari direktori tersebut.\n\nMulai ulang pi, atau jalankan `/reload`, setelah mengubah context files.\n\n## Hal umum untuk dicoba\n\n### File referensi\n\nKetik `@` di editor untuk mencari file secara fuzzy, atau teruskan file di baris perintah:\n\n```bash\npi @README.md \"Summarize this\"\npi @src/app.ts @src/app.test.ts \"Review these together\"\n```\n\nGambar atau teks dapat ditempel dengan Ctrl+V (Alt+V di Windows); gambar juga dapat diseret ke terminal yang didukung.\n\n### Jalankan perintah shell\n\nDalam mode interaktif:\n\n```text\n!npm run lint\n```\n\nOutput perintah dikirim ke model. Gunakan `!!command` untuk menjalankan perintah tanpa menambahkan outputnya ke konteks model.\n\n### Ganti model\n\nGunakan `/model` atau Ctrl+L untuk memilih model. Gunakan Shift+Tab untuk memutar tingkat pemikiran. Gunakan Ctrl+P / Shift+Ctrl+P untuk menelusuri model cakupan.\n\n### Lanjutkan nanti\n\nSesi disimpan secara otomatis:\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\nDi dalam pi, gunakan `/resume`, `/new`, `/tree`, `/fork`, dan `/clone` untuk mengelola sesi.\n\n### Mode non-interaktif\n\nUntuk perintah sekali pakai:\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\nGunakan `--mode json` untuk JSON keluaran acara atau `--mode rpc` untuk integrasi proses.\n\n## Langkah selanjutnya\n\n- [Using Pi](usage.md) - mode interaktif, slash commands, sesi, context files, dan referensi CLI.\n- [Providers](providers.md) - otentikasi dan pengaturan model.\n- [Settings](settings.md) - konfigurasi global dan proyek.\n- [Keybindings](keybindings.md) - pintasan dan penyesuaian.\n- [Pi Packages](packages.md) - instal ekstensi, keterampilan, petunjuk, dan tema bersama.\n\nCatatan platform: [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 Modus","markdown":"Mode RPC memungkinkan pengoperasian agen pengkode tanpa kepala melalui protokol JSON melalui stdin/stdout. Ini berguna untuk menyematkan agen di aplikasi lain, IDE, atau UI kustom.\n\n**Catatan untuk pengguna Node.js/TypeScript**: Jika Anda membuat aplikasi Node.js, pertimbangkan untuk menggunakan `AgentSession` langsung dari `@earendil-works/pi-coding-agent` daripada membuat subproses. Lihat [`src/core/agent-session.ts`](../src/core/agent-session.ts) untuk API. Untuk klien TypeScript berbasis subproses, lihat [`src/modes/rpc/rpc-client.ts`](../src/modes/rpc/rpc-client.ts).\n\n## Memulai Mode RPC\n\n```bash\npi --mode rpc [options]\n```\n\nOpsi umum:\n- `--provider <name>`: Tetapkan penyedia LLM (anthropic, openai, google, dll.)\n- `--model <pattern>`: Pola atau ID model (mendukung `provider/id` dan opsional `:<thinking>`)\n- `--name <name>` / `-n <name>`: Mengatur nama tampilan sesi saat startup\n- `--no-session`: Nonaktifkan persistensi sesi\n- `--session-dir <path>`: Direktori penyimpanan sesi khusus\n\n## Ikhtisar Protokol\n\n- **Perintah**: JSON objek dikirim ke stdin, satu per baris\n- **Respon**: JSON objek dengan `type: \"response\"` yang menunjukkan keberhasilan/kegagalan perintah\n- **Acara**: Acara agen dialirkan ke stdout sebagai baris JSON\n\nSemua perintah mendukung bidang opsional `id` untuk korelasi permintaan/respons. Jika disediakan, respons terkait akan mencakup `id` yang sama. Peristiwa `bash_execution_update` juga menyertakan `id` dari perintah `bash` asalnya.\n\n### Pembingkaian\n\nMode RPC menggunakan semantik JSONL yang ketat dengan LF (`\\n`) sebagai satu-satunya pembatas rekaman.\n\nIni penting bagi klien:\n- Pisahkan catatan hanya pada `\\n`\n- Terima masukan opsional `\\r\\n` dengan menghilangkan tanda `\\r`\n- Jangan gunakan pembaca baris umum yang memperlakukan pemisah Unicode sebagai baris baru\n\nSecara khusus, Node `readline` tidak sesuai protokol untuk mode RPC karena ia juga terbagi menjadi `U+2028` dan `U+2029`, yang valid di dalam string JSON.\n\n## Perintah\n\n### Dorongan\n\n#### mengingatkan\n\nKirimkan perintah pengguna ke agen. Respons perintah dikeluarkan setelah prompt diterima, dimasukkan dalam antrean, atau ditangani. Acara terus mengalir secara asinkron setelah penerimaan.\n\n```json\n{\"id\": \"req-1\", \"type\": \"prompt\", \"message\": \"Hello, world!\"}\n```\n\nDengan gambar:\n```json\n{\"type\": \"prompt\", \"message\": \"What's in this image?\", \"images\": [{\"type\": \"image\", \"data\": \"base64-encoded-data\", \"mimeType\": \"image/png\"}]}\n```\n\n**Selama streaming**: Jika agen sudah melakukan streaming, Anda harus menentukan `streamingBehavior` untuk mengantri pesan:\n\n```json\n{\"type\": \"prompt\", \"message\": \"New instruction\", \"streamingBehavior\": \"steer\"}\n```\n\n- `\"steer\"`: Mengantri pesan saat agen sedang berjalan. Ini dikirimkan setelah giliran asisten saat ini selesai menjalankan panggilan alatnya, sebelum panggilan LLM berikutnya.\n- `\"followUp\"`: Tunggu hingga agen selesai. Pesan dikirimkan hanya ketika agen berhenti.\n\nJika agen sedang streaming dan tidak ada `streamingBehavior` yang ditentukan, perintah akan mengembalikan kesalahan.\n\n**Perintah ekstensi**: Jika pesannya berupa perintah ekstensi (misalnya `/mycommand`), pesan akan langsung dijalankan bahkan selama streaming. Perintah ekstensi mengelola interaksi LLM mereka sendiri melalui `pi.sendMessage()`.\n\n**Perluasan input**: Perintah keterampilan (`/skill:name`) dan prompt templates (`/template`) diperluas sebelum dikirim/antrian.\n\nTanggapan:\n```json\n{\"id\": \"req-1\", \"type\": \"response\", \"command\": \"prompt\", \"success\": true}\n```\n\n`success: true` berarti perintah diterima, dimasukkan dalam antrean, atau ditangani dengan segera. `success: false` berarti perintah ditolak sebelum diterima. Kegagalan setelah penerimaan dilaporkan melalui peristiwa normal dan aliran pesan, bukan sebagai `response` kedua untuk id permintaan yang sama.\n\nBidang `images` bersifat opsional. Setiap gambar menggunakan format `ImageContent`: `{\"type\": \"image\", \"data\": \"base64-encoded-data\", \"mimeType\": \"image/png\"}`.\n\n#### mengarahkan\n\nAntri pesan kemudi saat agen sedang berjalan. Ini dikirimkan setelah giliran asisten saat ini selesai menjalankan panggilan alatnya, sebelum panggilan LLM berikutnya. Perintah keterampilan dan prompt templates diperluas. Perintah ekstensi tidak diperbolehkan (sebagai gantinya gunakan `prompt`).\n\n```json\n{\"type\": \"steer\", \"message\": \"Stop and do this instead\"}\n```\n\nDengan gambar:\n```json\n{\"type\": \"steer\", \"message\": \"Look at this instead\", \"images\": [{\"type\": \"image\", \"data\": \"base64-encoded-data\", \"mimeType\": \"image/png\"}]}\n```\n\nBidang `images` bersifat opsional. Setiap gambar menggunakan format `ImageContent` (sama seperti `prompt`).\n\nTanggapan:\n```json\n{\"type\": \"response\", \"command\": \"steer\", \"success\": true}\n```\n\nLihat [set_steering_mode](#set_steering_mode) untuk mengontrol bagaimana pesan pengarah diproses.\n\n#### menindaklanjuti\n\nAntrian pesan tindak lanjut untuk diproses setelah agen selesai. Dikirim hanya ketika agen tidak lagi memiliki panggilan alat atau pesan pengarah. Perintah keterampilan dan prompt templates diperluas. Perintah ekstensi tidak diperbolehkan (sebagai gantinya gunakan `prompt`).\n\n```json\n{\"type\": \"follow_up\", \"message\": \"After you're done, also do this\"}\n```\n\nDengan gambar:\n```json\n{\"type\": \"follow_up\", \"message\": \"Also check this image\", \"images\": [{\"type\": \"image\", \"data\": \"base64-encoded-data\", \"mimeType\": \"image/png\"}]}\n```\n\nBidang `images` bersifat opsional. Setiap gambar menggunakan format `ImageContent` (sama seperti `prompt`).\n\nTanggapan:\n```json\n{\"type\": \"response\", \"command\": \"follow_up\", \"success\": true}\n```\n\nLihat [set_follow_up_mode](#set_follow_up_mode) untuk mengontrol bagaimana pesan tindak lanjut diproses.\n\n#### menggugurkan\n\nBatalkan operasi agen saat ini.\n\n```json\n{\"type\": \"abort\"}\n```\n\nTanggapan:\n```json\n{\"type\": \"response\", \"command\": \"abort\", \"success\": true}\n```\n\n#### sesi_baru\n\nMulailah sesi baru. Dapat dibatalkan oleh pengendali acara ekstensi `session_before_switch`.\n\n```json\n{\"type\": \"new_session\"}\n```\n\nDengan pelacakan sesi orang tua opsional:\n```json\n{\"type\": \"new_session\", \"parentSession\": \"/path/to/parent-session.jsonl\"}\n```\n\nTanggapan:\n```json\n{\"type\": \"response\", \"command\": \"new_session\", \"success\": true, \"data\": {\"cancelled\": false}}\n```\n\nJika perpanjangan dibatalkan:\n```json\n{\"type\": \"response\", \"command\": \"new_session\", \"success\": true, \"data\": {\"cancelled\": true}}\n```\n\n### Negara\n\n#### dapatkan_status\n\nDapatkan status sesi saat ini.\n\n```json\n{\"type\": \"get_state\"}\n```\n\nTanggapan:\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\nBidang `model` adalah objek [Model](#model) penuh atau `null`. Bidang `sessionName` adalah nama tampilan yang diatur melalui `set_session_name`, atau dihilangkan jika tidak diatur.\n\n#### dapatkan_pesan\n\nDapatkan semua pesan dalam percakapan.\n\n```json\n{\"type\": \"get_messages\"}\n```\n\nTanggapan:\n```json\n{\n  \"type\": \"response\",\n  \"command\": \"get_messages\",\n  \"success\": true,\n  \"data\": {\"messages\": [...]}\n}\n```\n\nPesan adalah `AgentMessage` objek (lihat [Message Types](#message-types)).\n\n### Model\n\n#### set_model\n\nBeralih ke model tertentu.\n\n```json\n{\"type\": \"set_model\", \"provider\": \"anthropic\", \"modelId\": \"claude-sonnet-4-20250514\"}\n```\n\nRespons berisi objek [Model](#model) lengkap:\n```json\n{\n  \"type\": \"response\",\n  \"command\": \"set_model\",\n  \"success\": true,\n  \"data\": {...}\n}\n```\n\n#### siklus_model\n\nBeralih ke model berikutnya yang tersedia. Mengembalikan `null` data jika hanya satu model yang tersedia.\n\n```json\n{\"type\": \"cycle_model\"}\n```\n\nTanggapan:\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\nBidang `model` adalah objek [Model](#model) penuh.\n\n#### dapatkan_tersedia_model\n\nDaftar semua model yang dikonfigurasi.\n\n```json\n{\"type\": \"get_available_models\"}\n```\n\nRespons berisi serangkaian objek [Model](#model) penuh:\n```json\n{\n  \"type\": \"response\",\n  \"command\": \"get_available_models\",\n  \"success\": true,\n  \"data\": {\n    \"models\": [...]\n  }\n}\n```\n\n### Pemikiran\n\n#### set_thinking_level\n\nTetapkan tingkat penalaran/berpikir untuk model yang mendukungnya.\n\n```json\n{\"type\": \"set_thinking_level\", \"level\": \"high\"}\n```\n\nTingkat: `\"off\"`, `\"minimal\"`, `\"low\"`, `\"medium\"`, `\"high\"`, `\"xhigh\"`, `\"max\"`\n\n`\"xhigh\"` dan `\"max\"` hanya terekspos bila didukung oleh model yang dipilih. Beberapa model, termasuk GPT-5.6, menampilkan keduanya.\n\nTanggapan:\n```json\n{\"type\": \"response\", \"command\": \"set_thinking_level\", \"success\": true}\n```\n\n#### tingkat_pemikiran_siklus\n\nTelusuri tingkat berpikir yang tersedia. Mengembalikan `null` data jika model tidak mendukung pemikiran.\n\n```json\n{\"type\": \"cycle_thinking_level\"}\n```\n\nTanggapan:\n```json\n{\n  \"type\": \"response\",\n  \"command\": \"cycle_thinking_level\",\n  \"success\": true,\n  \"data\": {\"level\": \"high\"}\n}\n```\n\n#### dapatkan_tersedia_tingkat_pemikiran\n\nBuat daftar tingkat berpikir yang didukung oleh model saat ini. Mengembalikan `[\"off\"]` untuk model tanpa dukungan alasan.\n\n```json\n{\"type\": \"get_available_thinking_levels\"}\n```\n\nTanggapan:\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### Mode Antrian\n\n#### set_steering_mode\n\nKontrol bagaimana pesan pengarah (dari `steer`) dikirimkan.\n\n```json\n{\"type\": \"set_steering_mode\", \"mode\": \"one-at-a-time\"}\n```\n\nMode:\n- `\"all\"`: Mengirimkan semua pesan kemudi setelah giliran asisten saat ini selesai menjalankan panggilan alatnya\n- `\"one-at-a-time\"`: Mengirimkan satu pesan kemudi per giliran asisten yang selesai (default)\n\nTanggapan:\n```json\n{\"type\": \"response\", \"command\": \"set_steering_mode\", \"success\": true}\n```\n\n#### set_follow_up_mode\n\nKontrol bagaimana pesan tindak lanjut (dari `follow_up`) dikirimkan.\n\n```json\n{\"type\": \"set_follow_up_mode\", \"mode\": \"one-at-a-time\"}\n```\n\nMode:\n- `\"all\"`: Kirimkan semua pesan tindak lanjut setelah agen selesai\n- `\"one-at-a-time\"`: Mengirimkan satu pesan tindak lanjut per penyelesaian agen (default)\n\nTanggapan:\n```json\n{\"type\": \"response\", \"command\": \"set_follow_up_mode\", \"success\": true}\n```\n\n### Pemadatan\n\n#### kompak\n\nRingkas konteks percakapan secara manual untuk mengurangi penggunaan token.\n\n```json\n{\"type\": \"compact\"}\n```\n\nDengan instruksi khusus:\n```json\n{\"type\": \"compact\", \"customInstructions\": \"Focus on code changes\"}\n```\n\nTanggapan:\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` adalah perkiraan heuristik atas konteks pesan yang dibangun kembali segera setelah pemadatan, bukan jumlah token persis penyedia. `usage` melaporkan panggilan LLM atau panggilan yang menghasilkan ringkasan dan dapat dihilangkan oleh penangan pemadatan khusus.\n\n#### set_auto_compaction\n\nMengaktifkan atau menonaktifkan pemadatan otomatis ketika konteks hampir penuh.\n\n```json\n{\"type\": \"set_auto_compaction\", \"enabled\": true}\n```\n\nTanggapan:\n```json\n{\"type\": \"response\", \"command\": \"set_auto_compaction\", \"success\": true}\n```\n\n### Mencoba kembali\n\n#### set_auto_coba lagi\n\nMengaktifkan atau menonaktifkan percobaan ulang otomatis pada kesalahan sementara (kelebihan beban, batas kecepatan, 5xx).\n\n```json\n{\"type\": \"set_auto_retry\", \"enabled\": true}\n```\n\nTanggapan:\n```json\n{\"type\": \"response\", \"command\": \"set_auto_retry\", \"success\": true}\n```\n\n#### batalkan_coba lagi\n\nBatalkan percobaan ulang yang sedang berlangsung (batalkan penundaan dan hentikan percobaan ulang).\n\n```json\n{\"type\": \"abort_retry\"}\n```\n\nTanggapan:\n```json\n{\"type\": \"response\", \"command\": \"abort_retry\", \"success\": true}\n```\n\n### Pesta\n\n#### bash\n\nJalankan perintah shell dan tambahkan output ke konteks percakapan. Aliran keluaran sebagai `bash_execution_update` peristiwa saat perintah dijalankan; tanggapannya berisi hasil akhir.\n\n```json\n{\"id\": \"req-1\", \"type\": \"bash\", \"command\": \"ls -la\"}\n```\n\nSertakan `id` untuk mengaitkan acara `bash_execution_update` yang dialirkan dengan perintah ini.\n\nTanggapan:\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\nJika keluaran terpotong, termasuk `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**Bagaimana hasil bash mencapai LLM:**\n\nPerintah `bash` segera dijalankan dan mengembalikan `BashResult`. Secara internal, `BashExecutionMessage` dibuat dan disimpan dalam status pesan agen.\n\nKetika perintah `prompt` berikutnya dikirim, semua pesan (termasuk `BashExecutionMessage`) diubah sebelum dikirim ke LLM. `BashExecutionMessage` diubah menjadi `UserMessage` dengan format ini:\n\n````\nRan `ls -la`\n```\njumlah 48\ndrwxr-xr-x...\n```\n````\n\nArtinya:\n1. Output Bash disertakan dalam konteks LLM pada **prompt berikutnya**, tidak langsung\n2. Beberapa perintah bash dapat dijalankan sebelum prompt; semua output akan disertakan\n\n#### batalkan_bash\n\nBatalkan perintah bash yang sedang berjalan.\n\n```json\n{\"type\": \"abort_bash\"}\n```\n\nTanggapan:\n```json\n{\"type\": \"response\", \"command\": \"abort_bash\", \"success\": true}\n```\n\n### Sidang\n\n#### dapatkan_session_stats\n\nDapatkan penggunaan token, statistik biaya, dan penggunaan jendela konteks saat ini.\n\n```json\n{\"type\": \"get_session_stats\"}\n```\n\nTanggapan:\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` dan `cost` mencakup pesan asisten, laporan penggunaan alat, dan pembuatan ringkasan/ringkasan cabang di seluruh sesi. `contextUsage` berisi perkiraan jendela konteks terkini yang digunakan untuk pemadatan dan tampilan footer.\n\n`contextUsage` dihilangkan jika tidak ada model atau jendela konteks yang tersedia. `contextUsage.tokens` dan `contextUsage.percent` adalah `null` segera setelah pemadatan hingga respons asisten pasca pemadatan yang baru memberikan data penggunaan yang valid.\n\n#### ekspor_html\n\nEkspor sesi ke file HTML.\n\n```json\n{\"type\": \"export_html\"}\n```\n\nDengan jalur khusus:\n```json\n{\"type\": \"export_html\", \"outputPath\": \"/tmp/session.html\"}\n```\n\nTanggapan:\n```json\n{\n  \"type\": \"response\",\n  \"command\": \"export_html\",\n  \"success\": true,\n  \"data\": {\"path\": \"/tmp/session.html\"}\n}\n```\n\n#### saklar_sesi\n\nMuat file sesi yang berbeda. Dapat dibatalkan oleh pengendali acara ekstensi `session_before_switch`.\n\n```json\n{\"type\": \"switch_session\", \"sessionPath\": \"/path/to/session.jsonl\"}\n```\n\nTanggapan:\n```json\n{\"type\": \"response\", \"command\": \"switch_session\", \"success\": true, \"data\": {\"cancelled\": false}}\n```\n\nJika ekstensi membatalkan peralihan:\n```json\n{\"type\": \"response\", \"command\": \"switch_session\", \"success\": true, \"data\": {\"cancelled\": true}}\n```\n\n#### garpu\n\nBuat fork baru dari pesan pengguna sebelumnya di cabang aktif. Dapat dibatalkan oleh pengendali acara ekstensi `session_before_fork`. Mengembalikan teks pesan yang dicabangkan.\n\n```json\n{\"type\": \"fork\", \"entryId\": \"abc123\"}\n```\n\nTanggapan:\n```json\n{\n  \"type\": \"response\",\n  \"command\": \"fork\",\n  \"success\": true,\n  \"data\": {\"text\": \"The original prompt text...\", \"cancelled\": false}\n}\n```\n\nJika ekstensi membatalkan percabangan:\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#### klon\n\nGandakan cabang aktif saat ini ke dalam sesi baru di posisi saat ini. Dapat dibatalkan oleh pengendali acara ekstensi `session_before_fork`.\n\n```json\n{\"type\": \"clone\"}\n```\n\nTanggapan:\n```json\n{\n  \"type\": \"response\",\n  \"command\": \"clone\",\n  \"success\": true,\n  \"data\": {\"cancelled\": false}\n}\n```\n\nJika ekstensi membatalkan kloning:\n```json\n{\n  \"type\": \"response\",\n  \"command\": \"clone\",\n  \"success\": true,\n  \"data\": {\"cancelled\": true}\n}\n```\n\n#### dapatkan_fork_messages\n\nDapatkan pesan pengguna tersedia untuk forking.\n\n```json\n{\"type\": \"get_fork_messages\"}\n```\n\nTanggapan:\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#### dapatkan_entries\n\nDapatkan semua entri sesi dalam urutan penambahan (tidak termasuk header sesi). Sesi ini adalah pohon entri tambahan saja dengan id stabil, sehingga id entri berfungsi sebagai kursor yang tahan lama: teruskan id entri terakhir yang Anda lihat sebagai `since` untuk hanya mendapatkan entri setelahnya, bahkan saat klien dimulai ulang. Berbeda dengan `get_messages`, ini mencakup riwayat sebelum pemadatan dan cabang yang ditinggalkan.\n\n```json\n{\"type\": \"get_entries\"}\n```\n\nDengan kursor:\n```json\n{\"type\": \"get_entries\", \"since\": \"abc123\"}\n```\n\nTanggapan:\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` adalah id entri daun saat ini (`null` untuk sesi kosong), sehingga klien dapat mengetahui dalam satu perjalanan apakah cabang aktif telah berpindah. Jika `since` tidak cocok dengan id entri mana pun, responsnya adalah `success: false`.\n\n#### dapatkan_pohon\n\nDapatkan sesi sebagai pohon entri. Setiap node adalah `{entry, children, label?, labelTimestamp?}`. Sesi yang terbentuk dengan baik memiliki satu root; entri yatim piatu (rantai induk yang rusak) juga muncul sebagai akar.\n\n```json\n{\"type\": \"get_tree\"}\n```\n\nTanggapan:\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#### dapatkan_last_assistant_text\n\nDapatkan konten teks dari pesan asisten terakhir.\n\n```json\n{\"type\": \"get_last_assistant_text\"}\n```\n\nTanggapan:\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\nMengembalikan `{\"text\": null}` jika tidak ada pesan asisten.\n\n#### set_sesi_nama\n\nTetapkan nama tampilan untuk sesi saat ini. Nama tersebut muncul dalam daftar sesi dan membantu mengidentifikasi sesi.\n\n```json\n{\"type\": \"set_session_name\", \"name\": \"my-feature-work\"}\n```\n\nTanggapan:\n```json\n{\n  \"type\": \"response\",\n  \"command\": \"set_session_name\",\n  \"success\": true\n}\n```\n\nNama sesi saat ini tersedia melalui `get_state` di kolom `sessionName`. Untuk mengatur nama awal saat memulai mode RPC, teruskan `--name <name>` atau `-n <name>` ke proses `pi --mode rpc`.\n\n### Perintah\n\n#### dapatkan_perintah\n\nDapatkan perintah yang tersedia (perintah ekstensi, prompt templates, dan keterampilan). Ini dapat dipanggil melalui perintah `prompt` dengan mengawali dengan `/`.\n\n```json\n{\"type\": \"get_commands\"}\n```\n\nTanggapan:\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\nSetiap perintah memiliki:\n- `name`: Nama perintah (dipanggil dengan `/name`)\n- `description`: Deskripsi yang dapat dibaca manusia (opsional untuk perintah ekstensi)\n- `source`: Perintah seperti apa:\n  - `\"extension\"`: Terdaftar melalui `pi.registerCommand()` di ekstensi\n  - `\"prompt\"`: Dimuat dari file templat cepat `.md`\n  - `\"skill\"`: Dimuat dari direktori keterampilan (nama diawali dengan `skill:`)\n- `location`: Dari mana file tersebut dimuat (opsional, tidak ada untuk ekstensi):\n  - `\"user\"`: Tingkat pengguna (`~/.pi/agent/`)\n  - `\"project\"`: Tingkat proyek (`./.pi/agent/`)\n  - `\"path\"`: Jalur eksplisit melalui CLI atau pengaturan\n- `path`: Jalur file absolut ke sumber perintah (opsional)\n\n**Catatan**: Perintah TUI bawaan (`/settings`, `/hotkeys`, dll.) tidak disertakan. Mereka hanya ditangani dalam mode interaktif dan tidak akan dijalankan jika dikirim melalui `prompt`.\n\n## Acara\n\nAcara dialirkan ke baris stdout sebagai JSON selama operasi agen. Peristiwa umumnya tidak menyertakan kolom `id`; `bash_execution_update` termasuk `id` dari perintah asal `bash` ketika diberikan.\n\n### Jenis Acara\n\n| Peristiwa | Keterangan |\n|-------|-------------|\n| `agent_start` | Agen mulai memproses |\n| `agent_end` | Satu proses agen tingkat rendah selesai (mungkin masih diikuti dengan percobaan ulang, pemadatan, atau kelanjutan antrean) |\n| `agent_settled` | Pengoperasian agen telah diselesaikan sepenuhnya; tidak ada percobaan ulang otomatis, percobaan pemadatan, atau kelanjutan antrean yang tersisa |\n| `turn_start` | Giliran baru dimulai |\n| `turn_end` | Putaran selesai (termasuk pesan asisten dan hasil alat) |\n| `message_start` | Pesan dimulai |\n| `message_update` | Pembaruan streaming (teks/pemikiran/delta panggilan alat) |\n| `message_end` | Pesan selesai |\n| `bash_execution_update` | Potongan keluaran perintah langsung RPC bash |\n| `tool_execution_start` | Alat memulai eksekusi |\n| `tool_execution_update` | Kemajuan eksekusi alat (output streaming) |\n| `tool_execution_end` | Alat selesai |\n| `queue_update` | Antrean kemudi/tindak lanjut yang tertunda diubah |\n| `compaction_start` | Pemadatan dimulai |\n| `compaction_end` | Pemadatan selesai |\n| `auto_retry_start` | Coba ulang otomatis dimulai (setelah kesalahan sementara) |\n| `auto_retry_end` | Coba ulang otomatis selesai (berhasil atau gagal akhir) |\n| `summarization_retry_scheduled` | Percobaan ulang dijadwalkan untuk kesalahan pemadatan sementara atau peringkasan ringkasan cabang |\n| `summarization_retry_attempt_start` | Permintaan peringkasan ulang dimulai |\n| `summarization_retry_finished` | Perulangan percobaan ulang peringkasan selesai |\n| `extension_error` | Ekstensi menimbulkan kesalahan |\n\n### agen_mulai\n\nDipancarkan saat agen mulai memproses perintah.\n\n```json\n{\"type\": \"agent_start\"}\n```\n\n### agen_akhir\n\nDipancarkan ketika satu agen tingkat rendah dijalankan selesai. Berisi semua pesan yang dihasilkan selama proses ini. Jika `willRetry` benar, percobaan ulang otomatis akan dilakukan.\n\n```json\n{\n  \"type\": \"agent_end\",\n  \"messages\": [...],\n  \"willRetry\": false\n}\n```\n\n### agen_menyelesaikan\n\nDipancarkan setelah proses tingkat sesi penuh diselesaikan. Pada titik ini Pi tidak akan dilanjutkan secara otomatis melalui percobaan ulang, percobaan pemadatan, atau pesan tindak lanjut dalam antrean.\n\n```json\n{\"type\": \"agent_settled\"}\n```\n\n### turn_start / turn_end\n\nSatu giliran terdiri dari satu respons asisten ditambah panggilan alat dan hasil apa pun yang dihasilkan.\n\n```json\n{\"type\": \"turn_start\"}\n```\n\n```json\n{\n  \"type\": \"turn_end\",\n  \"message\": {...},\n  \"toolResults\": [...]\n}\n```\n\n### pesan_mulai / pesan_akhir\n\nDipancarkan saat pesan dimulai dan selesai. Bidang `message` berisi `AgentMessage`.\n\n```json\n{\"type\": \"message_start\", \"message\": {...}}\n{\"type\": \"message_end\", \"message\": {...}}\n```\n\n### pesan_perbarui (Streaming)\n\nDipancarkan selama streaming pesan asisten. Berisi peristiwa delta tanpa snapshot pesan kumulatif.\n\n```json\n{\n  \"type\": \"message_update\",\n  \"assistantMessageEvent\": {\n    \"type\": \"text_delta\",\n    \"contentIndex\": 0,\n    \"delta\": \"Hello \"\n  }\n}\n```\n\nBidang `assistantMessageEvent` berisi salah satu tipe delta berikut:\n\n| Jenis | Keterangan |\n|------|-------------|\n| `text_start` | Blok konten teks dimulai |\n| `text_delta` | Potongan konten teks |\n| `text_end` | Blok konten teks berakhir |\n| `thinking_start` | Blok berpikir dimulai |\n| `thinking_delta` | Memikirkan potongan konten |\n| `thinking_end` | Blok berpikir berakhir |\n| `toolcall_start` | Panggilan alat dimulai |\n| `toolcall_delta` | Potongan argumen pemanggilan alat |\n| `toolcall_end` | Panggilan alat berakhir (termasuk objek `toolCall` penuh) |\n\nContoh streaming respons teks:\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` sengaja menghilangkan bidang kumulatif `message` sebelumnya dan\n`assistantMessageEvent.partial`. Klien yang memerlukan pesan parsial langsung harus merakitnya\ndari `message_start` dan kejadian selanjutnya menggunakan `contentIndex`. Perlakukan `message_end.message`\nsebagai berwibawa. Untuk pemanggilan alat, buffer `toolcall_delta.delta`; `toolcall_end.toolCall`\nberisi panggilan yang telah selesai.\n\n### bash_execution_update\n\nDipancarkan satu kali untuk setiap potongan keluaran dari perintah langsung `bash`. `id` cocok dengan `id` perintah, memungkinkan klien mengaitkan output dengan perintah yang benar.\n\nPeristiwa mengalirkan semua output saat perintah dijalankan, meskipun respons `bash` akhir `output` terpotong.\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\nDipancarkan saat alat dimulai, mengalirkan kemajuan, dan menyelesaikan eksekusi.\n\n```json\n{\n  \"type\": \"tool_execution_start\",\n  \"toolCallId\": \"call_abc123\",\n  \"toolName\": \"bash\",\n  \"args\": {\"command\": \"ls -la\"}\n}\n```\n\nSelama eksekusi, `tool_execution_update` peristiwa mengalirkan sebagian hasil (misalnya, bash keluaran saat tiba):\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\nKetika selesai:\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\nGunakan `toolCallId` untuk menghubungkan peristiwa. `partialResult` di `tool_execution_update` berisi akumulasi keluaran sejauh ini (bukan hanya delta), memungkinkan klien untuk dengan mudah mengganti tampilan mereka pada setiap pembaruan.\n\n### antrian_perbarui\n\nDipancarkan setiap kali kemudi yang tertunda atau antrian tindak lanjut berubah.\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### pemadatan_mulai / pemadatan_akhir\n\nDikeluarkan saat pemadatan berjalan, baik manual maupun otomatis.\n\n```json\n{\"type\": \"compaction_start\", \"reason\": \"threshold\"}\n```\n\nBidang `reason` adalah `\"manual\"`, `\"threshold\"`, atau `\"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\nJika `reason` adalah `\"overflow\"` dan pemadatan berhasil, `willRetry` adalah `true` dan agen akan secara otomatis mencoba kembali perintah tersebut.\n\nJika pemadatan dibatalkan, `result` adalah `null` dan `aborted` adalah `true`.\n\nJika pemadatan gagal (misalnya, API melebihi kuota), `result` adalah `null`, `aborted` adalah `false`, dan `errorMessage` berisi deskripsi kesalahan.\n\n### auto_retry_start / auto_retry_end\n\nDipancarkan ketika percobaan ulang otomatis dipicu setelah kesalahan sementara (kelebihan beban, batas kecepatan, 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\nPada kegagalan terakhir (percobaan ulang maksimal terlampaui):\n```json\n{\n  \"type\": \"auto_retry_end\",\n  \"success\": false,\n  \"attempt\": 3,\n  \"finalError\": \"529 overloaded_error: Overloaded\"\n}\n```\n\n### ringkasan_retry_scheduled / ringkasan_retry_attempt_start / ringkasan_retry_finished\n\nDipancarkan saat pemadatan atau peringkasan ringkasan cabang dicoba lagi setelah kesalahan penyedia sementara. Peristiwa ini menggunakan pengaturan percobaan ulang yang sama seperti percobaan ulang asisten otomatis.\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\nUntuk ringkasan cabang, `source` adalah `\"branchSummary\"` dan tidak ada `reason`.\n\n```json\n{\n  \"type\": \"summarization_retry_finished\"\n}\n```\n\n### ekstensi_kesalahan\n\nDipancarkan saat ekstensi menimbulkan kesalahan.\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## Protokol UI Ekstensi\n\nExtensions dapat meminta interaksi pengguna melalui `ctx.ui.select()`, `ctx.ui.confirm()`, dll. Dalam mode RPC, ini diterjemahkan ke dalam sub-protokol permintaan/respons di atas aliran perintah/peristiwa dasar.\n\nAda dua kategori metode UI ekstensi:\n\n- **Metode dialog** (`select`, `confirm`, `input`, `editor`): memancarkan `extension_ui_request` pada stdout dan memblokir hingga klien mengirimkan kembali `extension_ui_response` pada stdin dengan `id` yang cocok.\n- **Metode api-dan-lupakan** (`notify`, `setStatus`, `setWidget`, `setTitle`, `set_editor_text`): pancarkan `extension_ui_request` pada stdout tetapi jangan mengharapkan respons. Klien dapat menampilkan informasi atau mengabaikannya.\n\nJika metode dialog menyertakan kolom `timeout`, sisi agen akan menyelesaikan secara otomatis dengan nilai default ketika batas waktu habis. Klien tidak perlu melacak batas waktu.\n\nBeberapa metode `ExtensionUIContext` tidak didukung atau terdegradasi dalam mode RPC karena memerlukan akses langsung TUI:\n- `custom()` mengembalikan `undefined`\n- `setWorkingMessage()`, `setWorkingIndicator()`, `setFooter()`, `setHeader()`, `setEditorComponent()`, `setToolsExpanded()` tidak boleh dijalankan\n- `getEditorText()` mengembalikan `\"\"`\n- `getToolsExpanded()` mengembalikan `false`\n- `pasteToEditor()` delegasi ke `setEditorText()` (tidak ada penanganan tempel/ciutkan)\n- `getAllThemes()` mengembalikan `[]`\n- `getTheme()` mengembalikan `undefined`\n- `setTheme()` mengembalikan `{ success: false, error: \"...\" }`\n\nCatatan: `ctx.mode` adalah `\"rpc\"` dan `ctx.hasUI` adalah `true` dalam mode RPC karena metode dialog dan api-dan-lupa berfungsi melalui sub-protokol UI ekstensi. Gunakan `ctx.mode === \"tui\"` untuk menjaga TUI fitur khusus seperti `custom()` yang memerlukan terminal nyata.\n\n### Permintaan Ekstensi UI (stdout)\n\nSemua permintaan memiliki bidang `type: \"extension_ui_request\"`, bidang unik `id`, dan `method`.\n\n#### memilih\n\nMinta pengguna untuk memilih dari daftar. Metode dialog dengan kolom `timeout` menyertakan batas waktu dalam milidetik; agen menyelesaikan secara otomatis dengan `undefined` jika klien tidak merespons tepat waktu.\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\nRespons yang diharapkan: `extension_ui_response` dengan `value` (string opsi yang dipilih) atau `cancelled: true`.\n\n#### mengonfirmasi\n\nMeminta pengguna untuk konfirmasi ya/tidak.\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\nRespons yang diharapkan: `extension_ui_response` dengan `confirmed: true/false` atau `cancelled: true`.\n\n#### masukan\n\nMeminta pengguna untuk teks bentuk bebas.\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\nRespons yang diharapkan: `extension_ui_response` dengan `value` (teks yang dimasukkan) atau `cancelled: true`.\n\n#### editor\n\nBuka editor teks multi-baris dengan konten opsional yang telah diisi sebelumnya.\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\nRespons yang diharapkan: `extension_ui_response` dengan `value` (teks yang diedit) atau `cancelled: true`.\n\n#### memberitahu\n\nTampilkan pemberitahuan. Api-dan-lupakan, tidak ada respons yang diharapkan.\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\nBidang `notifyType` adalah `\"info\"`, `\"warning\"`, atau `\"error\"`. Defaultnya adalah `\"info\"` jika dihilangkan.\n\n#### setStatus\n\nMengatur atau menghapus entri status di footer/bilah status. Api-dan-lupakan.\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\nKirim `statusText: undefined` (atau hilangkan) untuk menghapus entri status untuk kunci tersebut.\n\n#### setWidget\n\nMengatur atau menghapus widget (blok baris teks) yang ditampilkan di atas atau di bawah editor. Api-dan-lupakan.\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\nKirim `widgetLines: undefined` (atau hilangkan) untuk menghapus widget. Bidang `widgetPlacement` adalah `\"aboveEditor\"` (default) atau `\"belowEditor\"`. Hanya array string yang didukung dalam mode RPC; pabrik komponen diabaikan.\n\n#### setJudul\n\nAtur judul jendela/tab terminal. Api-dan-lupakan.\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_teks\n\nAtur teks di editor input. Api-dan-lupakan.\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### Respons UI Ekstensi (stdin)\n\nRespons dikirim hanya untuk metode dialog (`select`, `confirm`, `input`, `editor`). `id` harus sesuai dengan permintaan.\n\n#### Respon nilai (pilih, masukan, editor)\n\n```json\n{\"type\": \"extension_ui_response\", \"id\": \"uuid-1\", \"value\": \"Allow\"}\n```\n\n#### Tanggapan konfirmasi (konfirmasi)\n\n```json\n{\"type\": \"extension_ui_response\", \"id\": \"uuid-2\", \"confirmed\": true}\n```\n\n#### Respons pembatalan (dialog apa pun)\n\nTutup metode dialog apa pun. Ekstensi menerima `undefined` (untuk pilih/input/editor) atau `false` (untuk konfirmasi).\n\n```json\n{\"type\": \"extension_ui_response\", \"id\": \"uuid-3\", \"cancelled\": true}\n```\n\n## Penanganan Kesalahan\n\nPerintah yang gagal mengembalikan respons dengan `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\nKesalahan penguraian:\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## Jenis\n\nFile sumber:\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 tipe perintah/respons, tipe permintaan/respons UI ekstensi\n\n### Model\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### Pesan Pengguna\n\n```json\n{\n  \"role\": \"user\",\n  \"content\": \"Hello!\",\n  \"timestamp\": 1733234567890,\n  \"attachments\": []\n}\n```\n\nBidang `content` dapat berupa string atau larik blok `TextContent`/`ImageContent`.\n\n### Pesan Asisten\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\nAlasan berhenti: `\"stop\"`, `\"length\"`, `\"toolUse\"`, `\"error\"`, `\"aborted\"`\n\n### AlatHasilPesan\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` bersifat opsional dan melaporkan pekerjaan LLM bertingkat yang dilakukan oleh alat tersebut. Saat ini, ini berkontribusi pada token sesi dan total biaya.\n\n### Pesan Eksekusi Bash\n\nDibuat dengan perintah `bash` RPC (bukan dengan panggilan alat 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### Lampiran\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## Contoh: Klien Dasar (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## Contoh: Klien Interaktif (Node.js)\n\nLihat [`test/rpc-example.ts`](../test/rpc-example.ts) untuk contoh interaktif lengkap, atau [`src/modes/rpc/rpc-client.ts`](../src/modes/rpc/rpc-client.ts) untuk implementasi klien yang diketik.\n\nUntuk contoh lengkap penanganan protokol UI ekstensi, lihat [`examples/rpc-extension-ui.ts`](../examples/rpc-extension-ui.ts) yang berpasangan dengan ekstensi [`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 dapat membantu Anda menggunakan SDK. Mintalah untuk membangun integrasi untuk kasus penggunaan Anda.\n\n\nSDK menyediakan akses terprogram ke kemampuan agen pi. Gunakan untuk menyematkan pi di aplikasi lain, membuat antarmuka khusus, atau berintegrasi dengan alur kerja otomatis.\n\n**Contoh kasus penggunaan:**\n- Bangun UI khusus (web, desktop, seluler)\n- Integrasikan kemampuan agen ke dalam aplikasi yang ada\n- Buat saluran pipa otomatis dengan alasan agen\n- Bangun alat khusus yang menghasilkan sub-agen\n- Uji perilaku agen secara terprogram\n\nLihat [examples/sdk/](../examples/sdk/) untuk contoh kerja dari kontrol minimal hingga kontrol penuh.\n\n## Mulai Cepat\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## Instalasi\n\n```bash\nnpm install @earendil-works/pi-coding-agent\n```\n\nSDK disertakan dalam paket utama. Tidak diperlukan instalasi terpisah.\n\n## Konsep Inti\n\n### buatAgentSession()\n\nFungsi pabrik utama untuk satu `AgentSession`.\n\n`createAgentSession()` menggunakan `ResourceLoader` untuk menyediakan ekstensi, keterampilan, prompt templates, tema, dan context files. Jika Anda tidak menyediakannya, ia akan menggunakan `DefaultResourceLoader` dengan penemuan standar.\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### AgenSesi\n\nSesi ini mengelola siklus hidup agen, riwayat pesan, status model, pemadatan, dan streaming peristiwa.\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\nPenggantian sesi API seperti sesi baru, resume, fork, dan impor langsung di `AgentSessionRuntime`, bukan di `AgentSession`.\n\n### createAgentSessionRuntime() dan AgentSessionRuntime\n\nGunakan runtime API ketika Anda perlu mengganti sesi aktif dan membangun kembali status runtime yang terikat cwd.\nIni adalah lapisan yang sama yang digunakan oleh mode interaktif, cetak, dan RPC bawaan.\n\n`createAgentSessionRuntime()` membutuhkan pabrik runtime ditambah target cwd/sesi awal. Pabrik menutup input tetap global proses, membuat ulang layanan terikat cwd untuk cwd yang efektif, menyelesaikan opsi sesi terhadap layanan tersebut, dan mengembalikan hasil runtime penuh.\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` memiliki penggantian runtime aktif di:\n\n- `newSession()`\n- `switchSession()`\n- `fork()`\n- aliran klon melalui `fork(entryId, { position: \"at\" })`\n- `importFromJsonl()`\n\nPerilaku penting:\n\n- `runtime.session` perubahan setelah operasi tersebut\n- langganan acara terikat pada `AgentSession` tertentu, jadi berlangganan kembali setelah penggantian\n- jika Anda menggunakan ekstensi, panggil `runtime.session.bindExtensions(...)` lagi untuk sesi baru\n- kreasi mengembalikan diagnostik pada `runtime.diagnostics`\n- jika pembuatan atau penggantian runtime gagal, metode akan dilempar dan pemanggil memutuskan bagaimana menanganinya\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### Anjuran dan Antrian Pesan\n\n`PromptOptions` mengontrol perluasan cepat, perilaku antrian saat streaming, dan pemberitahuan pra-penerbangan cepat:\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` dipanggil sekali per `prompt()` pemanggilan:\n\n- `true` ketika perintah diterima, dimasukkan dalam antrean, atau ditangani dengan segera\n- `false` ketika preflight cepat ditolak sebelum diterima\n\nIni menyala sebelum `prompt()` terselesaikan. `prompt()` masih terselesaikan hanya setelah proses yang diterima sepenuhnya selesai, termasuk percobaan ulang. Kegagalan setelah penerimaan dilaporkan melalui peristiwa normal dan aliran pesan, bukan melalui `preflightResult(false)`.\n\nMetode `prompt()` menangani prompt templates, perintah ekstensi, dan pengiriman pesan:\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**Perilaku:**\n- **Perintah ekstensi** (mis., `/mycommand`): Jalankan segera, bahkan saat streaming. Mereka mengelola interaksi LLM mereka sendiri melalui `pi.sendMessage()`.\n- **Berbasis file prompt templates** (dari `.md` file): Diperluas ke kontennya sebelum mengirim atau mengantri.\n- **Selama streaming tanpa `streamingBehavior`**: Terjadi kesalahan. Gunakan `steer()` atau `followUp()` secara langsung, atau tentukan opsinya.\n- **`preflightResult(true)`**: Berarti perintah diterima, dimasukkan dalam antrean, atau ditangani dengan segera.\n- **`preflightResult(false)`**: Berarti preflight ditolak sebelum diterima.\n\nUntuk antrian eksplisit selama streaming:\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\nBaik `steer()` dan `followUp()` memperluas berbasis file prompt templates tetapi kesalahan pada perintah ekstensi (perintah ekstensi tidak dapat dimasukkan dalam antrean).\n\n### Agen dan AgentState\n\nKelas `Agent` (dari `@earendil-works/pi-agent-core`) menangani interaksi inti LLM. Akses melalui `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### Acara\n\nBerlangganan acara untuk menerima keluaran streaming dan pemberitahuan siklus hidup.\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## Referensi Pilihan\n\n### Direktori\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` digunakan oleh `DefaultResourceLoader` untuk:\n- Ekstensi proyek (`.pi/extensions/`)\n- Keterampilan proyek:\n  - `.pi/skills/`\n  - `.agents/skills/` di `cwd` dan direktori leluhur (hingga git repo root, atau root sistem file jika tidak ada dalam repo)\n- Perintah proyek (`.pi/prompts/`)\n- File konteks (`AGENTS.md` berjalan dari cwd)\n- Penamaan direktori sesi\n\n`agentDir` digunakan oleh `DefaultResourceLoader` untuk:\n- Ekstensi global (`extensions/`)\n- Keterampilan global:\n  - `skills/` di bawah `agentDir` (misalnya `~/.pi/agent/skills/`)\n  - `~/.agents/skills/`\n- Perintah global (`prompts/`)\n- File konteks global (`AGENTS.md`)\n- Pengaturan (`settings.json`)\n- Model khusus (`models.json`)\n- Kredensial (`auth.json`)\n- Sesi (`sessions/`)\n\nSaat Anda meneruskan `ResourceLoader` khusus, `cwd` dan `agentDir` tidak lagi mengontrol penemuan sumber daya. Mereka masih mempengaruhi penamaan sesi dan resolusi jalur alat.\n\n### Model\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\nJika tidak ada model yang disediakan:\n1. Mencoba memulihkan dari sesi (jika melanjutkan)\n2. Menggunakan default dari pengaturan\n3. Kembali ke model pertama yang tersedia\n\nUntuk mencocokkan penguraian model CLI, gunakan bantuan penyelesai yang diekspor:\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()` menggunakan semua model terdaftar sehingga pengaturan pertama kali gaya `--api-key` dapat menyelesaikan model sebelum autentikasi yang disimpan ada. `resolveModelScopeWithDiagnostics()` cocok dengan semantik `--models` dan `enabledModels` sambil mengembalikan peringatan alih-alih mencetaknya.\n\n> Lihat [examples/sdk/02-custom-model.ts](../examples/sdk/02-custom-model.ts)\n\n### API Kunci dan OAuth\n\nPrioritas resolusi autentikasi (ditangani oleh `ModelRuntime`):\n1. Penggantian waktu proses (melalui `setRuntimeApiKey`, tidak dipertahankan)\n2. Kredensial yang disimpan dalam `auth.json` (API keys atau OAuth token)\n3. Variabel lingkungan (`ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, dll.)\n4. Penyelesai cadangan (untuk kunci penyedia khusus dari `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()`, dan `removeRuntimeApiKey()` diselesaikan setelah katalog, komposisi, dan snapshot ketersediaan yang di-cache/bawaan dari penyedia yang terpengaruh konsisten secara lokal. Mereka tidak menunggu kesegaran katalog yang jauh. Jika kredensial telah diterapkan tetapi sinkronisasi lokal gagal, kredensial akan ditolak dengan `CredentialSynchronizationError` yang diekspor; periksa bidang `providerId`, `operation`, `credential`, dan `cause` daripada mencoba ulang mutasi kredensial secara membabi buta.\n\nOperasi model publik/autentikasi dan `ModelRuntime.create({ signal })` menerima sinyal pembatalan opsional dan tidak dibatasi saat dihilangkan. SDK aplikasi memiliki kebijakan tenggat waktu untuk kesegaran katalog jarak jauh:\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\nPenyegaran jaringan yang gagal atau habis waktunya tidak membatalkan operasi kredensial yang berhasil. `refresh()` memulai generasi penyedia baru, sehingga tidak menunggu penyegaran lama yang terhenti dan generasi lama tidak dapat mempublikasikannya setelahnya.\n\n> Lihat [examples/sdk/09-api-keys-and-oauth.ts](../examples/sdk/09-api-keys-and-oauth.ts)\n\n### Perintah Sistem\n\nGunakan `ResourceLoader` untuk mengganti perintah sistem:\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> Lihat [examples/sdk/03-custom-prompt.ts](../examples/sdk/03-custom-prompt.ts)\n\n### Peralatan\n\nTentukan alat bawaan mana yang akan diaktifkan:\n\n- Nama alat bawaan: `read`, `bash`, `edit`, `write`, `grep`, `find`, `ls`\n- Bawaan bawaan: `read`, `bash`, `edit`, `write`\n- `noTools: \"all\"` menonaktifkan semua alat\n- `noTools: \"builtin\"` menonaktifkan bawaan bawaan sambil tetap mengaktifkan ekstensi dan alat khusus\n- `excludeTools` menonaktifkan nama alat bawaan, ekstensi, atau khusus tertentu setelah `tools` daftar yang diizinkan diterapkan\n\nAlat `edit` mengembalikan `details.diff` untuk tampilan Pi TUI dan `details.patch` sebagai patch terpadu standar untuk SDK konsumen.\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#### Alat dengan Custom cwd\n\nSaat Anda meneruskan `cwd` khusus, `createAgentSession()` membuat alat bawaan yang dipilih untuk cwd tersebut.\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> Lihat [examples/sdk/05-tools.ts](../examples/sdk/05-tools.ts)\n\n### Alat Kustom\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\nGunakan `defineTool()` untuk definisi mandiri dan array seperti `customTools: [myTool]`. Inline `pi.registerTool({... })` sudah menyimpulkan tipe parameter dengan benar.\n\nAlat khusus yang diteruskan melalui `customTools` digabungkan dengan alat yang terdaftar dengan ekstensi. Extensions yang dimuat oleh ResourceLoader juga dapat mendaftarkan alat melalui `pi.registerTool()`.\n\nJika Anda meneruskan `tools`, sertakan setiap nama alat khusus atau ekstensi yang ingin Anda aktifkan, misalnya `tools: [\"read\", \"bash\", \"my_tool\"]`.\n\n> Lihat [examples/sdk/05-tools.ts](../examples/sdk/05-tools.ts)\n\n### Extensions\n\nExtensions dimuat oleh `ResourceLoader`. `DefaultResourceLoader` menemukan ekstensi dari `~/.pi/agent/extensions/`, `.pi/extensions/`, dan sumber ekstensi 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 dapat mendaftarkan alat, berlangganan acara, menambahkan perintah, dan banyak lagi. Lihat [extensions.md](extensions.md) untuk API selengkapnya.\n\n**Ekstensi inline yang diberi nama:** Secara default, pabrik inline ditampilkan sebagai `<inline:1>`, `<inline:2>`, dll. di daftar startup Extensions. Untuk menampilkan nama deskriptif, bungkus pabriknya:\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\nIni ditampilkan sebagai `<inline:my-provider>` bukannya `<inline:1>`. Fungsi pabrik yang kosong masih diterima untuk kompatibilitas ke belakang.\n\n**Bus Acara:** Extensions dapat berkomunikasi melalui `pi.events`. Berikan `eventBus` ke `DefaultResourceLoader` bersama jika Anda perlu memancarkan atau mendengarkan dari luar:\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> Lihat [examples/sdk/06-extensions.ts](../examples/sdk/06-extensions.ts) dan [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> Lihat [examples/sdk/04-skills.ts](../examples/sdk/04-skills.ts)\n\n### File Konteks\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> Lihat [examples/sdk/07-context-files.ts](../examples/sdk/07-context-files.ts)\n\n### Perintah Tebas\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> Lihat [examples/sdk/08-prompt-templates.ts](../examples/sdk/08-prompt-templates.ts)\n\n### Manajemen Sesi\n\nSesi menggunakan struktur pohon dengan tautan `id`/`parentId`, sehingga memungkinkan percabangan di tempat.\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**Pohon Manajer Sesi 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> Lihat [examples/sdk/11-sessions.ts](../examples/sdk/11-sessions.ts) dan [Session Format](session-format.md)\n\n### Manajemen Pengaturan\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**Pabrik statis:**\n- `SettingsManager.create(cwd?, agentDir?)` - Muat dari file\n- `SettingsManager.inMemory(settings?)` - Tidak ada I/O berkas\n\n**Setelan spesifik proyek:**\n\nPengaturan dimuat dari dua lokasi dan digabungkan:\n1. Global: `~/.pi/agent/settings.json`\n2. Proyek: `<cwd>/.pi/settings.json`\n\nProyek menggantikan global. Objek bersarang menggabungkan kunci. Setter mengubah pengaturan global secara default.\n\n**Semantik penanganan kesalahan dan persistensi:**\n\n- Pengambil/penyetel pengaturan sinkron untuk status dalam memori.\n- Penyetel membuat persistensi antrean menulis secara asinkron.\n- Panggil `await settingsManager.flush()` ketika Anda memerlukan batas ketahanan (misalnya, sebelum proses keluar atau sebelum menegaskan konten file dalam pengujian).\n- `SettingsManager` tidak mencetak kesalahan pengaturan I/O. Gunakan `settingsManager.drainErrors()` dan laporkan di lapisan aplikasi Anda.\n\n> Lihat [examples/sdk/10-settings.ts](../examples/sdk/10-settings.ts)\n\n## ResourceLoader\n\nGunakan `DefaultResourceLoader` untuk menemukan ekstensi, keterampilan, petunjuk, tema, dan 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## Nilai Pengembalian\n\n`createAgentSession()` kembali:\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## Contoh Lengkap\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## Jalankan Mode\n\nUtilitas mode proses ekspor SDK untuk membangun antarmuka khusus di atas `createAgentSession()`:\n\n### Mode Interaktif\n\nMode interaktif TUI penuh dengan editor, riwayat obrolan, dan semua perintah bawaan:\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### jalankanPrintMode\n\nMode pengambilan tunggal: kirim petunjuk, hasil keluaran, keluar:\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### jalankanRpcMode\n\nMode JSON-RPC untuk integrasi subproses:\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\nLihat [RPC documentation](rpc.md) untuk protokol JSON.\n\n## RPC Mode Alternatif\n\nUntuk integrasi berbasis subproses tanpa membangun dengan SDK, gunakan CLI secara langsung:\n\n```bash\npi --mode rpc --no-session\n```\n\nLihat [RPC documentation](rpc.md) untuk protokol JSON.\n\nSDK lebih disukai ketika:\n- Anda ingin mengetik keamanan\n- Anda berada dalam proses Node.js yang sama\n- Anda memerlukan akses langsung ke negara agen\n- Anda ingin menyesuaikan alat/ekstensi secara terprogram\n\nMode RPC lebih disukai ketika:\n- Anda mengintegrasikan dari bahasa lain\n- Anda ingin proses isolasi\n- Anda sedang membangun klien tanpa bahasa\n\n## Ekspor\n\nEkspor titik masuk utama:\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\nUntuk jenis ekstensi, lihat [extensions.md](extensions.md) untuk API selengkapnya.","sourceFile":"sdk.md"},"security":{"title":"Keamanan","markdown":"Pi adalah agen pengkodean lokal. Ini berjalan dengan izin dari akun pengguna yang memulainya, dan memperlakukan file yang dapat ditulis oleh pengguna tersebut seperti berada di dalam batas kepercayaan lokal yang sama.\n\n## Kepercayaan Proyek\n\nKepercayaan proyek mengontrol apakah pi memuat pengaturan lokal proyek, sumber daya, paket, dan ekstensi. Ini bukan sandbox dan tidak membatasi apa yang diminta model untuk dilakukan alat setelah Anda mulai bekerja di direktori.\n\nPi menganggap proyek memiliki sumber daya yang memerlukan kepercayaan ketika menemukan salah satu dari ini dari direktori kerja saat ini:\n\n- `.pi/settings.json`\n- `.pi/extensions`, `.pi/skills`, `.pi/prompts`, atau `.pi/themes`\n- `.pi/SYSTEM.md` atau `.pi/APPEND_SYSTEM.md`\n- proyek `.agents/skills` di direktori saat ini atau direktori leluhur\n\nDirektori `.pi` yang kosong tidak dihitung sebagai sumber daya proyek yang memerlukan kepercayaan.\n\nKetika sesi interaktif dimulai dalam sebuah proyek dengan sumber daya yang memerlukan kepercayaan dan tidak ada keputusan tersimpan untuk direktori saat ini atau direktori induk, pi mengikuti `defaultProjectTrust` dari pengaturan global. Nilai defaultnya adalah `\"ask\"`, yang menanyakan apakah akan memercayai proyek saat UI tersedia. Keputusan tersimpan disimpan oleh direktori kanonik di `~/.pi/agent/trust.json`, dan keputusan tersimpan terdekat pada jalur saat ini atau jalur induk berlaku sebelum default global.\n\nMempercayai suatu proyek memungkinkan pi memuat sumber daya proyek yang memerlukan kepercayaan, termasuk:\n\n- `.pi/settings.json`\n- `.pi` sumber daya seperti ekstensi, keterampilan, prompt templates, tema, dan file prompt sistem\n- paket proyek yang hilang dikonfigurasi melalui pengaturan proyek\n- ekstensi proyek-lokal dan ekstensi yang dikelola paket proyek\n\nMenurunnya kepercayaan mengabaikan sumber daya yang dilindungi. File konteks seperti `AGENTS.override.md`, `AGENTS.md`, dan `CLAUDE.md` dimuat terlepas dari kepercayaan proyek kecuali pemuatan konteks dinonaktifkan. Sebelum kepercayaan diselesaikan, pi hanya memuat context files, ekstensi pengguna/global, dan CLI `-e` ekstensi. Ekstensi pengguna/global dan CLI dapat menangani peristiwa `project_trust`; ekstensi pertama yang mengembalikan keputusan ya/tidak adalah pemilik keputusan tersebut.\n\nMode non-interaktif (`-p`, `--mode json`, dan `--mode rpc`) tidak menampilkan prompt kepercayaan. Tanpa keputusan perwalian tersimpan yang dapat diterapkan, `defaultProjectTrust: \"ask\"` dan `\"never\"` mengabaikan sumber daya tersebut, sementara `\"always\"` memercayai sumber daya tersebut. Gunakan `--approve`/`-a` atau `--no-approve`/`-na` untuk mengganti kepercayaan proyek untuk satu kali proses.\n\n## Tidak Ada Kotak Pasir Bawaan\n\nPi tidak termasuk sandbox bawaan. Alat bawaan dapat membaca file, menulis file, mengedit file, dan menjalankan perintah shell dengan izin proses pi. Extensions adalah TypeScript modul yang dijalankan dengan izin yang sama. Penginstalan paket, perintah shell, server bahasa, perintah pengujian, dan alat pengembang lainnya berperilaku seperti proses lokal biasa.\n\nIni disengaja. Pi dirancang untuk beroperasi pada pohon sumber lokal, menggunakan rantai alat proyek, dan berintegrasi dengan lingkungan pengembangan pengguna yang ada. Sebagian proses sandbox akan mudah disalahpahami sebagai batas keamanan namun tetap bergantung pada shell host, sistem file, manajer paket, kredensial, dan kode ekstensi. Isolasi nyata perlu datang dari sistem operasi atau batas virtualisasi/wadah.\n\nKepercayaan proyek hanyalah penjaga pemuatan masukan. Ini mencegah repositori mengubah pengaturan atau ekstensi pi secara diam-diam sebelum Anda menyetujuinya. Itu tidak membuat kode yang tidak tepercaya, perintah yang tidak tepercaya, atau keluaran model yang tidak tepercaya menjadi aman. Injeksi cepat dari file repositori, komentar, dokumentasi, context files, atau keluaran build diperkirakan merupakan risiko agen lokal dan tidak dapat dicegah dengan pi.\n\n## Menjalankan Pekerjaan yang Tidak Dipercaya atau Tidak Dipantau\n\nUntuk repositori yang tidak tepercaya, kode yang dihasilkan tidak ingin Anda pantau secara ketat, atau otomatisasi tanpa pengawasan, jalankan pi di lingkungan yang tertampung. Gunakan kontainer, VM, mikro-VM, sandbox jarak jauh, atau sandbox yang dikontrol kebijakan dengan hanya file dan kredensial yang diperlukan untuk tugas tersebut.\n\nPola umum didokumentasikan di [Containerization](containerization.md):\n\n- jalankan seluruh proses `pi` di dalam wadah/sandbox\n- jalankan host pi sambil merutekan eksekusi alat bawaan ke Gondolin mikro-VM\n- pasang hanya jalur ruang kerja yang harus diakses agen\n- hindari pemasangan host `~/.pi/agent` kecuali kontainer harus mengakses sesi host, pengaturan, dan kredensial\n- lulus persyaratan minimum API key atau menggunakan kredensial berumur pendek\n- membatasi akses jaringan ketika tugas tidak memerlukannya\n- meninjau perbedaan dan keluaran sebelum menyalin hasilnya kembali ke sistem tepercaya\n\nJika Anda mengikat-mount ruang kerja host untuk membaca/menulis, menulis dari dalam kontainer, atau VM masih dapat mengubah file host. Gunakan mount read-only atau salin file ke dalam dan keluar dari sandbox ketika Anda memerlukan perlindungan yang lebih kuat dari penulisan yang tidak diinginkan.\n\n## Melaporkan Masalah Keamanan\n\nUntuk melaporkan masalah keamanan, ikuti repositori [Security Policy](https://github.com/earendil-works/pi-mono/blob/main/SECURITY.md). Jangan membuka terbitan publik untuk laporan yang sensitif terhadap keamanan.\n\nPerilaku agen lokal yang diharapkan, kurangnya sandbox bawaan, injeksi cepat dari konten yang tidak tepercaya, dan perilaku ekstensi atau keterampilan yang dipasang pengguna umumnya berada di luar batas keamanan kecuali jika laporan menunjukkan bypass batas hak istimewa yang nyata atau menunjukkan bagaimana pi memberikan akses yang belum dimiliki pengguna lokal.","sourceFile":"security.md"},"session-format":{"title":"Format File Sesi","markdown":"Sesi disimpan sebagai file JSONL (JSON Baris). Setiap baris adalah objek JSON dengan bidang `type`. Entri sesi membentuk struktur pohon melalui kolom `id`/`parentId`, memungkinkan percabangan di tempat tanpa membuat file baru.\n\n## Lokasi Berkas\n\n```\n~/.pi/agent/sessions/--<path>--/<timestamp>_<uuid>.jsonl\n```\n\nDimana `<path>` adalah direktori kerja dengan `/` digantikan oleh `-`.\n\n## Menghapus Sesi\n\nSesi dapat dihapus dengan menghapus file `.jsonl` di bawah `~/.pi/agent/sessions/`.\n\nPi juga mendukung penghapusan sesi secara interaktif dari `/resume` (pilih sesi dan tekan `Ctrl+D`, lalu konfirmasi). Jika tersedia, pi menggunakan `trash` CLI untuk menghindari penghapusan permanen.\n\n## Versi Sesi\n\nSesi memiliki kolom versi di header:\n\n- **Versi 1**: Urutan entri linier (lama, dimigrasi otomatis saat dimuat)\n- **Versi 2**: Struktur pohon dengan tautan `id`/`parentId`\n- **Versi 3**: Mengganti nama peran `hookMessage` menjadi `custom` (penyatuan ekstensi)\n\nSesi yang ada secara otomatis dimigrasikan ke versi saat ini (v3) saat dimuat.\n\n## File Sumber\n\nSumber di 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) - Jenis entri sesi dan SessionManager\n- [`packages/coding-agent/src/core/messages.ts`](https://github.com/earendil-works/pi-mono/blob/main/packages/coding-agent/src/core/messages.ts) - Jenis pesan yang diperluas (BashExecutionMessage, CustomMessage, dll.)\n- [`packages/ai/src/types.ts`](https://github.com/earendil-works/pi-mono/blob/main/packages/ai/src/types.ts) - Jenis pesan dasar (UserMessage, AssistantMessage, ToolResultMessage)\n- [`packages/agent/src/types.ts`](https://github.com/earendil-works/pi-mono/blob/main/packages/agent/src/types.ts) - Jenis gabungan AgentMessage\n\nUntuk definisi TypeScript dalam proyek Anda, periksa `node_modules/@earendil-works/pi-coding-agent/dist/` dan `node_modules/@earendil-works/pi-ai/dist/`.\n\n## Jenis Pesan\n\nEntri sesi berisi objek `AgentMessage`. Memahami jenis ini penting untuk sesi penguraian dan penulisan ekstensi.\n\n### Blok Konten\n\nPesan berisi array blok konten yang diketik:\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### Jenis Pesan Dasar (dari 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\nJenis pi-ai `StopReason` yang diekspor juga mencakup `\"pending\"`, tetapi nilai tersebut dicadangkan untuk sebagian pesan dalam acara streaming. Pesan terminal `done`/`error` menggantinya dengan alasan penyelesaian sebelum pi mempertahankan pesan asisten, jadi `\"pending\"` tidak akan pernah muncul di sesi JSONL.\n\n### Jenis Pesan yang Diperluas (dari 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### Persatuan AgenPesan\n\n```typescript\ntype AgentMessage =\n  | UserMessage\n  | AssistantMessage\n  | ToolResultMessage\n  | BashExecutionMessage\n  | CustomMessage\n  | BranchSummaryMessage\n  | CompactionSummaryMessage;\n```\n\n## Basis Masuk\n\nSemua entri (kecuali `SessionHeader`) diperpanjang `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## Jenis Entri\n\n### SessionHeader\n\nBaris pertama file. Hanya metadata, bukan bagian dari pohon (tidak ada `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\nUntuk sesi dengan orang tua (dibuat melalui `/fork`, `/clone`, atau `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### Entri Pesan Sesi\n\nSebuah pesan dalam percakapan. Bidang `message` berisi `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### Entri Perubahan Model\n\nDipancarkan saat pengguna mengganti model di tengah sesi.\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### Entri Perubahan Tingkat Berpikir\n\nDipancarkan ketika pengguna mengubah tingkat berpikir/penalaran.\n\n```json\n{\"type\":\"thinking_level_change\",\"id\":\"e5f6g7h8\",\"parentId\":\"d4e5f6g7\",\"timestamp\":\"2024-12-03T14:06:00.000Z\",\"thinkingLevel\":\"high\"}\n```\n\n### Entri Pemadatan\n\nDibuat ketika konteks dipadatkan. Menyimpan ringkasan pesan sebelumnya.\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\nPemadatan yang dihasilkan oleh harness yang lebih baru menyematkan konteks pasca-pemadatan yang dipertahankan langsung pada entri, bukan `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\nBidang opsional:\n- `usage`: Penggunaan LLM dari pembuatan ringkasan; termasuk dalam token sesi dan total biaya\n- `retainedTail`: Terwujud `AgentMessage[]` disimpan setelah pemadatan. Ini opsional hanya untuk kompatibilitas dengan sesi yang lebih lama. Pemadatan yang dihasilkan oleh harness yang lebih baru menyertakannya sehingga kami dapat membangun kembali konteks dari pos pemeriksaan ini tanpa harus menjalankan entri lama sebelum entri pemadatan.\n- `details`: Data spesifik implementasi (misalnya, `{ readFiles: string[], modifiedFiles: string[] }` untuk default, atau data khusus untuk ekstensi)\n- `fromHook`: `true` jika dihasilkan oleh ekstensi, `false`/`undefined` jika dihasilkan oleh pi (nama kolom lama)\n- `firstKeptEntryId`: untuk kompatibilitas dengan format entri lama.\n\n### Entri Ringkasan Cabang\n\nDibuat saat berpindah cabang melalui `/tree` dengan ringkasan yang dihasilkan LLM dari cabang kiri hingga nenek moyang yang sama. Menangkap konteks dari jalur yang ditinggalkan.\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\nBidang opsional:\n- `usage`: Penggunaan LLM dari pembuatan ringkasan; termasuk dalam token sesi dan total biaya\n- `details`: Data pelacakan file (`{ readFiles: string[], modifiedFiles: string[] }`) untuk default, atau data khusus untuk ekstensi\n- `fromHook`: `true` jika dihasilkan oleh ekstensi, `false`/`undefined` jika dihasilkan oleh pi (nama kolom lama)\n\n### Entri Kustom\n\nPersistensi status ekstensi. TIDAK berpartisipasi dalam konteks 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\nGunakan `customType` untuk mengidentifikasi entri ekstensi Anda saat memuat ulang. Mode interaktif dapat merender entri khusus melalui `pi.registerEntryRenderer(customType, renderer)`, tetapi entri tersebut tetap tidak berpartisipasi dalam konteks LLM.\n\n### Entri Pesan Khusus\n\nPesan yang dimasukkan ekstensi yang DO berpartisipasi dalam konteks 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\nBidang:\n- `content`: String atau `(TextContent | ImageContent)[]` (sama seperti UserMessage)\n- `display`: `true` = tampilkan di TUI dengan gaya berbeda, `false` = tersembunyi\n- `details`: Metadata khusus ekstensi opsional (tidak dikirim ke LLM)\n\n### LabelEntri\n\nBookmark/penanda yang ditentukan pengguna pada sebuah entri.\n\n```json\n{\"type\":\"label\",\"id\":\"j0k1l2m3\",\"parentId\":\"i9j0k1l2\",\"timestamp\":\"2024-12-03T14:30:00.000Z\",\"targetId\":\"a1b2c3d4\",\"label\":\"checkpoint-1\"}\n```\n\nSetel `label` ke `undefined` untuk menghapus label.\n\n### Entri Info Sesi\n\nMetadata sesi (misalnya, nama tampilan yang ditentukan pengguna). Diatur melalui `/name`, `--name` / `-n`, atau `pi.setSessionName()` dalam ekstensi.\n\n```json\n{\"type\":\"session_info\",\"id\":\"k1l2m3n4\",\"parentId\":\"j0k1l2m3\",\"timestamp\":\"2024-12-03T14:35:00.000Z\",\"name\":\"Refactor auth module\"}\n```\n\nNama sesi ditampilkan di pemilih sesi (`/resume`) dan bukan di pesan pertama saat disetel.\n\n## Struktur Pohon\n\nEntri membentuk pohon:\n- Entri pertama memiliki `parentId: null`\n- Setiap entri berikutnya menunjuk ke induknya melalui `parentId`\n- Percabangan menciptakan anak baru dari entri sebelumnya\n- \"Daun\" adalah posisi saat ini di pohon\n\n```\n[user msg] ─── [assistant] ─── [user msg] ─── [assistant] ─┬─ [user msg] ← current leaf\n                                                            │\n                                                            └─ [branch_summary] ─── [user msg] ← alternate branch\n```\n\n## Membangun Konteks\n\n`buildContextEntries()` berjalan dari daun saat ini ke akar, menghasilkan daftar entri aktif sambil melakukan pemadatan:\n\n1. Mengumpulkan semua entri di jalur\n2. Jika `CompactionEntry` ada di jalur:\n   - Termasuk entri pemadatan terlebih dahulu\n   - Jika ada `retainedTail`, maka pos tersebut berfungsi sebagai pos pemeriksaan mandiri dan entri setelah pemadatan disertakan\n   - Jika tidak, entri dari `firstKeptEntryId` hingga pemadatan akan disertakan\n   - Kemudian entri setelah pemadatan dimasukkan\n3. Mempertahankan entri non-pesan dalam rentang yang dipilih sehingga mode interaktif dapat merendernya\n\n`buildSessionContext()` dibangun berdasarkan daftar entri tersebut untuk menghasilkan daftar pesan untuk LLM:\n\n1. Mengekstrak model saat ini dan pengaturan tingkat pemikiran dari jalur lengkap\n2. Mengonversi entri yang dipilih menjadi pesan:\n   - `message` -> disimpan `AgentMessage`\n   - `compaction` -> `compactionSummary` ditambah `retainedTail` saat ada\n   - `branch_summary` -> `branchSummary`\n   - `custom_message` -> `CustomMessage`\n   - `custom` -> tidak ada pesan konteks\n\nHal ini membuat pemadatan yang lebih baru berfungsi seperti pos pemeriksaan mandiri. `retainedTail` bersifat opsional sehingga sesi lama yang hanya menyimpan `firstKeptEntryId` terus dimuat dengan benar.\n\n## Contoh Penguraian\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## Manajer Sesi API\n\nMetode utama untuk bekerja dengan sesi secara terprogram.\n\n### Metode Penciptaan Statis\n- `SessionManager.create(cwd, sessionDir?)` - Sesi baru\n- `SessionManager.open(path, sessionDir?)` - Buka file sesi yang ada\n- `SessionManager.continueRecent(cwd, sessionDir?)` - Lanjutkan yang terbaru atau buat yang baru\n- `SessionManager.inMemory(cwd?)` - Tidak ada persistensi file\n- `SessionManager.forkFrom(sourcePath, targetCwd, sessionDir?)` - Sesi fork dari proyek lain\n\n### Metode Daftar Statis\n- `SessionManager.list(cwd, sessionDir?, onProgress?)` - Daftar sesi untuk direktori\n- `SessionManager.listAll(onProgress?)` - Daftar semua sesi di semua proyek\n\n### Metode Instance - Manajemen Sesi\n- `newSession(options?)` - Memulai sesi baru (opsi: `{ parentSession?: string }`)\n- `setSessionFile(path)` - Beralih ke file sesi lain\n- `createBranchedSession(leafId)` - Ekstrak cabang ke file sesi baru\n\n### Metode Instance - Menambahkan (semua ID entri kembali)\n- `appendMessage(message)` - Tambahkan pesan\n- `appendThinkingLevelChange(level)` - Rekam perubahan pemikiran\n- `appendModelChange(provider, modelId)` - Rekam perubahan model\n- `appendCompaction(summary, firstKeptEntryId, tokensBefore, details?, fromHook?)` - Tambahkan pemadatan\n- `appendCustomEntry(customType, data?)` - Status ekstensi (tidak dalam konteks)\n- `appendSessionInfo(name)` - Tetapkan nama tampilan sesi\n- `appendCustomMessageEntry(customType, content, display, details?)` - Pesan ekstensi (dalam konteks)\n- `appendLabelChange(targetId, label)` - Setel/hapus label\n\n### Metode Instance - Navigasi Pohon\n- `getLeafId()` - Posisi saat ini\n- `getLeafEntry()` - Dapatkan entri daun saat ini\n- `getEntry(id)` - Dapatkan entri berdasarkan ID\n- `getBranch(fromId?)` - Berjalan dari entri ke root\n- `getTree()` - Dapatkan struktur pohon lengkap\n- `getChildren(parentId)` - Dapatkan anak langsung\n- `getLabel(id)` - Dapatkan label untuk masuk\n- `branch(entryId)` - Pindahkan daun ke entri sebelumnya\n- `resetLeaf()` - Setel ulang daun ke nol (sebelum entri apa pun)\n- `branchWithSummary(entryId, summary, details?, fromHook?)` - Cabang dengan ringkasan konteks\n\n### Metode Instance - Konteks & Info\n- `buildContextEntries()` - Dapatkan entri cabang aktif dengan pemadatan diterapkan\n- `buildSessionContext()` - Dapatkan pesan, Tingkat berpikir, dan model untuk LLM\n- `getEntries()` - Semua entri (tidak termasuk header)\n- `getHeader()` - Metadata header sesi\n- `getSessionName()` - Dapatkan nama tampilan dari entri session_info terbaru\n- `getCwd()` - Direktori kerja\n- `getSessionDir()` - Direktori penyimpanan sesi\n- `getSessionId()` - Sesi UUID\n- `getSessionFile()` - Jalur file sesi (tidak ditentukan untuk dalam memori)\n- `isPersisted()` - Apakah sesi disimpan ke disk","sourceFile":"session-format.md"},"sessions":{"title":"Sesi","markdown":"Pi menyimpan percakapan sebagai sesi sehingga Anda dapat melanjutkan pekerjaan, bercabang dari belokan sebelumnya, dan mengunjungi kembali jalur sebelumnya.\n\n## Penyimpanan Sesi\n\nSesi disimpan otomatis ke `~/.pi/agent/sessions/`, diatur berdasarkan direktori kerja. Setiap sesi adalah file JSONL dengan struktur pohon.\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\nGunakan `/session` dalam mode interaktif untuk melihat file sesi saat ini, ID sesi, jumlah pesan, token, dan biaya.\n\nUntuk format file JSONL dan SessionManager API, lihat [Session Format](session-format.md).\n\n## Perintah Sesi\n\n| Memerintah | Keterangan |\n|---------|-------------|\n| `/resume` | Telusuri dan pilih sesi sebelumnya |\n| `/new` | Mulai sesi baru |\n| `/name <name>` | Tetapkan nama tampilan sesi saat ini |\n| `/session` | Tampilkan info sesi |\n| `/tree` | Navigasikan session tree saat ini |\n| `/fork` | Buat sesi baru dari pesan pengguna sebelumnya |\n| `/clone` | Gandakan cabang aktif saat ini ke dalam sesi baru |\n| `/compact [prompt]` | Ringkaslah konteks lama; lihat [Compaction](compaction.md) |\n| `/export [file]` | Ekspor sesi ke HTML |\n| `/share` | Unggah sebagai inti GitHub pribadi dengan tautan HTML yang dapat dibagikan |\n\n## Melanjutkan dan Menghapus Sesi\n\n`/resume` membuka pemilih sesi interaktif untuk proyek saat ini. `pi -r` membuka pemilih yang sama saat startup.\n\nDi pemilih Anda dapat:\n\n- mencari dengan mengetik\n- beralih tampilan jalur dengan Ctrl+P\n- beralih mode pengurutan dengan Ctrl+S\n- filter ke sesi bernama dengan Ctrl+N\n- ganti nama dengan Ctrl+R\n- hapus dengan Ctrl+D, lalu konfirmasi\n\nJika tersedia, pi menggunakan `trash` CLI untuk menghapus alih-alih menghapus file secara permanen.\n\n## Sesi Penamaan\n\nGunakan `/name <name>` untuk menetapkan nama sesi yang dapat dibaca manusia:\n\n```text\n/name Refactor auth module\n```\n\nTetapkan nama saat startup dengan `--name` atau `-n`:\n\n```bash\npi --name \"Refactor auth module\"\npi --name \"CI audit\" -p \"Review this build failure\"\n```\n\nSesi bernama lebih mudah ditemukan di `/resume` dan `pi -r`.\n\n## Bercabang dengan `/tree`\n\nSesi disimpan sebagai pohon. Setiap entri memiliki `id` dan `parentId`, dan posisi saat ini adalah daun aktif. `/tree` memungkinkan Anda melompat ke titik sebelumnya dan melanjutkan dari sana tanpa membuat file baru.\n\n<p align=\"center\"><img src=\"images/tree-view.png\" alt=\"Tree View\" width=\"600\"></p>\n\nContoh bentuk:\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### Kontrol Pohon\n\n| Kunci | Tindakan |\n|-----|--------|\n| ↑/↓ | Navigasi entri yang terlihat |\n| ←/→ | Halaman atas/bawah |\n| Ctrl+←/Ctrl+→ atau Alt+←/Alt+→ | Lipat/buka atau lompat di antara segmen cabang |\n| Shift+L | Menetapkan atau menghapus label pada entri yang dipilih |\n| Shift+T | Alihkan stempel waktu label |\n| Memasuki | Pilih entri |\n| Melarikan diri/Ctrl+C | Membatalkan |\n| Ctrl+O | Mode filter siklus |\n\nMode filter adalah: default, tanpa alat, hanya pengguna, hanya berlabel, dan semuanya. Konfigurasikan default dengan `treeFilterMode` di [Settings](settings.md).\n\n### Perilaku Seleksi\n\nMemilih pengguna atau pesan khusus:\n\n1. Memindahkan daun ke induk pesan yang dipilih.\n2. Menempatkan teks pesan yang dipilih di editor.\n3. Memungkinkan Anda mengedit dan mengirim ulang, membuat cabang baru.\n\nMemilih asisten, alat, pemadatan, atau entri non-pengguna lainnya:\n\n1. Memindahkan daun ke entri itu.\n2. Biarkan editor kosong.\n3. Biarkan Anda melanjutkan dari titik itu.\n\nMemilih pesan pengguna akar akan menyetel ulang daun ke percakapan kosong dan menempatkan perintah asli di editor.\n\n## `/tree`, `/fork`, dan `/clone`\n\n| Fitur | `/tree` | `/fork` | `/clone` |\n|---------|---------|---------|----------|\n| Keluaran | File sesi yang sama | File sesi baru | File sesi baru |\n| Melihat | Pohon penuh | Pemilih pesan pengguna | Cabang aktif saat ini |\n| Penggunaan yang umum | Jelajahi alternatif yang ada | Mulai sesi baru dari perintah sebelumnya | Gandakan pekerjaan saat ini sebelum melanjutkan |\n| Ringkasan | Ringkasan cabang opsional | Tidak ada | Tidak ada |\n\nGunakan `/tree` ketika Anda ingin menyatukan alternatif. Gunakan `/fork` atau `/clone` bila Anda menginginkan file sesi terpisah.\n\n## Ringkasan Cabang\n\nKetika `/tree` berpindah dari satu cabang ke cabang lainnya, pi dapat meringkas cabang yang ditinggalkan dan melampirkan ringkasan tersebut di posisi baru. Ini mempertahankan konteks penting dari jalur yang Anda tinggalkan tanpa memutar ulang keseluruhan cabang.\n\nSaat diminta, pilih salah satu dari:\n\n1. tidak ada ringkasan\n2. rangkum dengan prompt default\n3. rangkum dengan instruksi fokus khusus\n\nLihat [Compaction](compaction.md) untuk branch summarization kait internal dan ekstensi.\n\n## Format Sesi\n\nFile sesi berukuran JSONL dan berisi entri pesan, perubahan model, perubahan tingkat pemikiran, label, pemadatan, ringkasan cabang, dan entri ekstensi.\n\nUntuk parser, ekstensi, penggunaan SDK, dan SessionManager API lengkap, lihat [Session Format](session-format.md).","sourceFile":"sessions.md"},"settings":{"title":"Pengaturan","markdown":"Pi menggunakan file pengaturan JSON dengan pengaturan proyek menggantikan pengaturan global.\n\n| Lokasi | Cakupan |\n|----------|-------|\n| `~/.pi/agent/settings.json` | Global (semua proyek) |\n| `.pi/settings.json` | Proyek (direktori saat ini) |\n\nEdit secara langsung atau gunakan `/settings` untuk opsi umum.\n\n## Kepercayaan Proyek\n\nPada startup interaktif, pi bertanya sebelum memercayai folder proyek yang berisi pengaturan lokal proyek, sumber daya, atau proyek `.agents/skills` dan tidak memiliki keputusan tersimpan untuk folder atau folder induk di `~/.pi/agent/trust.json`. Mempercayai suatu proyek memungkinkan pi memuat sumber daya `.pi/settings.json` dan `.pi`, menginstal paket proyek yang hilang, dan menjalankan ekstensi proyek.\n\nMode non-interaktif (`-p`, `--mode json`, dan `--mode rpc`) tidak menampilkan prompt kepercayaan. Tanpa keputusan perwalian tersimpan yang dapat diterapkan, mereka menggunakan `defaultProjectTrust` dari pengaturan global: `ask` (default) dan `never` mengabaikan sumber daya proyek tersebut, sementara `always` memercayainya. Lewati `--approve`/`-a` atau `--no-approve`/`-na` untuk mengesampingkan kepercayaan proyek dalam sekali proses.\n\nJika tidak ada perpanjangan atau keputusan tersimpan yang berlaku, `defaultProjectTrust` mengontrol perilaku fallback. Setel ke `\"ask\"`, `\"always\"`, atau `\"never\"` di `~/.pi/agent/settings.json`, atau ubah dengan `/settings`.\n\n`pi config` dan perintah paket menggunakan aliran kepercayaan proyek yang sama, kecuali `pi update` tidak pernah diminta. Lewati `--approve` untuk memercayai pengaturan proyek-lokal untuk satu perintah atau `--no-approve` untuk mengabaikannya.\n\nGunakan `/trust` dalam mode interaktif untuk menyimpan keputusan kepercayaan proyek untuk sesi mendatang, termasuk kepercayaan untuk folder induk langsung. Ia hanya menulis `~/.pi/agent/trust.json`; sesi saat ini tidak dimuat ulang, jadi mulai ulang pi agar perubahan diterapkan.\n\n## Semua Pengaturan\n\n### Model & Pemikiran\n\n| Pengaturan | Jenis | Bawaan | Keterangan |\n|---------|------|---------|-------------|\n| `defaultProvider` | rangkaian | - | Penyedia default (mis., `\"anthropic\"`, `\"openai\"`) |\n| `defaultModel` | rangkaian | - | ID model bawaan |\n| `defaultThinkingLevel` | rangkaian | - | `\"off\"`, `\"minimal\"`, `\"low\"`, `\"medium\"`, `\"high\"`, `\"xhigh\"`, `\"max\"` |\n| `hideThinkingBlock` | boolean | `false` | Sembunyikan blok pemikiran dalam keluaran |\n| `showCacheMissNotices` | boolean | `false` | Tampilkan pemberitahuan transkrip untuk kesalahan cache cepat yang signifikan |\n| `thinkingBudgets` | obyek | - | Anggaran token khusus per tingkat pemikiran |\n\n#### berpikir Anggaran\n\n```json\n{\n  \"thinkingBudgets\": {\n    \"minimal\": 1024,\n    \"low\": 4096,\n    \"medium\": 10240,\n    \"high\": 32768\n  }\n}\n```\n\n### UI & Tampilan\n\n| Pengaturan | Jenis | Bawaan | Keterangan |\n|---------|------|---------|-------------|\n| `theme` | rangkaian | `\"dark\"` | Nama tema (`\"dark\"`, `\"light\"`, atau khusus) |\n| `externalEditor` | rangkaian | `$VISUAL`, lalu `$EDITOR`, lalu Notepad di Windows atau `nano` di tempat lain | Perintah untuk Ctrl+G editor eksternal; lebih diutamakan daripada variabel lingkungan |\n| `quietStartup` | boolean | `false` | Sembunyikan tajuk permulaan |\n| `defaultProjectTrust` | rangkaian | `\"ask\"` | Perilaku kepercayaan proyek cadangan: `\"ask\"`, `\"always\"`, atau `\"never\"`. Hanya pengaturan global |\n| `collapseChangelog` | boolean | `false` | Tampilkan log perubahan ringkas setelah pembaruan |\n| `enableInstallTelemetry` | boolean | `true` | Kirim ping versi instalasi/perbarui anonim setelah instalasi pertama atau pembaruan yang terdeteksi log perubahan. Ini tidak mengontrol pemeriksaan pembaruan |\n| `enableAnalytics` | boolean | `false` | Ikut serta dalam berbagi data analitik. Saat ini hanya diminta selama percobaan pengaturan pertama kali (`PI_EXPERIMENTAL=1`) |\n| `trackingId` | rangkaian | - | Pengidentifikasi pelacakan Analytics, dihasilkan ketika `enableAnalytics` diaktifkan |\n| `doubleEscapeAction` | rangkaian | `\"tree\"` | Tindakan untuk pelarian ganda: `\"tree\"`, `\"fork\"`, atau `\"none\"` |\n| `treeFilterMode` | rangkaian | `\"default\"` | Filter bawaan untuk `/tree`: `\"default\"`, `\"no-tools\"`, `\"user-only\"`, `\"labeled-only\"`, `\"all\"` |\n| `editorPaddingX` | nomor | `0` | Padding horizontal untuk editor masukan (0-3) |\n| `outputPad` | nomor | `1` | Padding horizontal untuk pesan pengguna, pesan asisten, dan pemikiran (0 atau 1) |\n| `autocompleteMaxVisible` | nomor | `5` | Maksimum item yang terlihat di dropdown pelengkapan otomatis (3-20) |\n| `showHardwareCursor` | boolean | `false` | Tampilkan kursor terminal sambil TUI memposisikannya untuk dukungan IME |\n| `tuiMode` | rangkaian | `\"regular\"` | Mode TUI interaktif: `\"regular\"` atau eksperimental `\"fullscreen\"`. Perubahan dari `/settings` berlaku segera; `--tui-mode` mengesampingkan pengaturan ini saat startup |\n| `fullscreenExitOutput` | rangkaian | `\"transcript\"` | Keluaran layar penuh: `\"transcript\"` mencetak transkrip akhir dan petunjuk melanjutkan, sedangkan `\"resume-hint\"` memulihkan layar sebelumnya dan hanya mencetak petunjuk melanjutkan. Tidak berpengaruh dalam mode TUI biasa |\n| `fullscreenScrollbar` | rangkaian | `\"auto\"` | Scrollbar transkrip layar penuh: `\"auto\"` menampilkannya sementara saat menggulir, `\"always\"` menyimpan kolom paling kanan dan membuatnya tetap terlihat, dan `\"hidden\"` menyembunyikannya. Tidak berpengaruh dalam mode TUI biasa |\n\nUntuk VS Code, sertakan `--wait` sehingga pi dilanjutkan setelah editor keluar:\n\n```json\n{\n  \"externalEditor\": \"code --wait\"\n}\n```\n\n### Pemeriksaan telemetri dan pembaruan\n\n`enableInstallTelemetry` hanya mengontrol ping pemasangan/pembaruan anonim ke `https://pi.dev/api/report-install`. Menolak telemetri tidak menonaktifkan pemeriksaan pembaruan; Pi masih dapat mengambil `https://pi.dev/api/latest-version` untuk mencari versi terbaru.\n\nSetel `PI_SKIP_VERSION_CHECK=1` untuk menonaktifkan pemeriksaan pembaruan versi Pi. Gunakan `--offline` atau `PI_OFFLINE=1` untuk menonaktifkan semua operasi jaringan startup yang dijelaskan di sini, termasuk pemeriksaan pembaruan, pemeriksaan pembaruan paket, dan pemasangan/perbarui telemetri.\n\n### Jaringan\n\n| Pengaturan | Jenis | Bawaan | Keterangan |\n|---------|------|---------|-------------|\n| `httpProxy` | rangkaian | - | URL proksi HTTP diterapkan sebagai `HTTP_PROXY` dan `HTTPS_PROXY`. Hanya pengaturan global. |\n\n```json\n{\n  \"httpProxy\": \"http://127.0.0.1:7890\"\n}\n```\n\n### Peringatan\n\n| Pengaturan | Jenis | Bawaan | Keterangan |\n|---------|------|---------|-------------|\n| `warnings.anthropicExtraUsage` | boolean | `true` | Tampilkan peringatan ketika autentikasi langganan Anthropic mungkin menggunakan penggunaan ekstra berbayar |\n\n```json\n{\n  \"warnings\": {\n    \"anthropicExtraUsage\": false\n  }\n}\n```\n\n### Pemadatan\n\n| Pengaturan | Jenis | Bawaan | Keterangan |\n|---------|------|---------|-------------|\n| `compaction.enabled` | boolean | `true` | Aktifkan pemadatan otomatis |\n| `compaction.reserveTokens` | nomor | `16384` | Token dicadangkan untuk respons LLM |\n| `compaction.keepRecentTokens` | nomor | `20000` | Token terkini yang harus disimpan (tidak diringkas) |\n\n```json\n{\n  \"compaction\": {\n    \"enabled\": true,\n    \"reserveTokens\": 16384,\n    \"keepRecentTokens\": 20000\n  }\n}\n```\n\n### Ringkasan Cabang\n\n| Pengaturan | Jenis | Bawaan | Keterangan |\n|---------|------|---------|-------------|\n| `branchSummary.reserveTokens` | nomor | `16384` | Token dicadangkan untuk branch summarization |\n| `branchSummary.skipPrompt` | boolean | `false` | Lewati \"Ringkas cabang?\" prompt pada navigasi `/tree` (defaultnya tidak ada ringkasan) |\n\n### Mencoba kembali\n\n| Pengaturan | Jenis | Bawaan | Keterangan |\n|---------|------|---------|-------------|\n| `retry.enabled` | boolean | `true` | Aktifkan percobaan ulang tingkat agen otomatis pada kesalahan sementara |\n| `retry.maxRetries` | nomor | `3` | Upaya percobaan ulang tingkat agen maksimum |\n| `retry.baseDelayMs` | nomor | `2000` | Penundaan dasar untuk backoff eksponensial tingkat agen (2 detik, 4 detik, 8 detik) |\n| `retry.provider.timeoutMs` | nomor | SDK bawaan | Batas waktu permintaan Penyedia/SDK dalam milidetik |\n| `retry.provider.maxRetries` | nomor | `0` | Penyedia/SDK percobaan ulang |\n| `retry.provider.maxRetryDelayMs` | nomor | `60000` | Penundaan maksimum yang diminta server sebelum gagal (60 detik) |\n\nKetika penyedia meminta penundaan percobaan ulang lebih lama dari `retry.provider.maxRetryDelayMs`, permintaan tersebut langsung gagal dengan kesalahan informatif alih-alih menunggu diam-diam. Setel ke `0` untuk menonaktifkan batas.\n\nPertahankan `retry.provider.maxRetries` di `0` kecuali percobaan ulang di tingkat penyedia secara eksplisit diperlukan. Menyetelnya di atas `0` dapat membuat SDK/percobaan ulang penyedia menangani kesalahan di luar batas penggunaan sebelum Pi melihatnya, yang dapat memblokir agen hingga kuota penyedia direset dalam beberapa keadaan.\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### Pengiriman Pesan\n\n| Pengaturan | Jenis | Bawaan | Keterangan |\n|---------|------|---------|-------------|\n| `steeringMode` | rangkaian | `\"one-at-a-time\"` | Cara pesan pengarah dikirim: `\"all\"` atau `\"one-at-a-time\"` |\n| `followUpMode` | rangkaian | `\"one-at-a-time\"` | Cara pesan tindak lanjut dikirim: `\"all\"` atau `\"one-at-a-time\"` |\n| `transport` | rangkaian | `\"auto\"` | Transportasi pilihan untuk penyedia yang mendukung banyak transportasi: `\"sse\"`, `\"websocket\"`, `\"websocket-cached\"`, atau `\"auto\"` |\n| `httpIdleTimeoutMs` | nomor | `300000` | Batas waktu idle header/body HTTP dalam milidetik, juga digunakan oleh penyedia dengan batas waktu idle streaming eksplisit. Setel ke `0` untuk menonaktifkan. |\n| `websocketConnectTimeoutMs` | nomor | `15000` | Batas waktu jabat tangan koneksi/buka WebSocket dalam milidetik untuk penyedia yang mendukung transportasi WebSocket. Setel ke `0` untuk menonaktifkan. |\n\n### Terminal & Gambar\n\n| Pengaturan | Jenis | Bawaan | Keterangan |\n|---------|------|---------|-------------|\n| `terminal.showImages` | boolean | `true` | Tampilkan gambar di terminal (jika didukung) |\n| `terminal.imageWidthCells` | nomor | `60` | Lebar gambar sebaris yang disukai di sel terminal |\n| `terminal.clearOnShrink` | boolean | `false` | Hapus baris kosong saat konten menyusut (dapat menyebabkan kedipan) |\n| `images.autoResize` | boolean | `true` | Ubah ukuran gambar menjadi maksimal 2000x2000. Berlaku untuk `@file` lampiran, `read`, dan gambar yang dikembalikan oleh alat |\n| `images.blockImages` | boolean | `false` | Blokir semua gambar agar tidak dikirim ke LLM |\n\n### Kerang\n\n| Pengaturan | Jenis | Bawaan | Keterangan |\n|---------|------|---------|-------------|\n| `shellPath` | rangkaian | - | Jalur shell khusus (misalnya, untuk Cygwin di Windows); mendukung `~` terdepan untuk direktori home |\n| `shellCommandPrefix` | rangkaian | - | Awalan untuk setiap perintah bash (mis., `\"shopt -s expand_aliases\"`) |\n| `npmCommand` | rangkaian[] | - | Perintah argv digunakan untuk npm operasi pencarian/pemasangan paket (misalnya, `[\"mise\", \"exec\", \"node@20\", \"--\", \"npm\"]`) |\n\n```json\n{\n  \"npmCommand\": [\"mise\", \"exec\", \"node@20\", \"--\", \"npm\"]\n}\n```\n\n`npmCommand` digunakan untuk semua npm operasi pengelola paket, termasuk pemasangan, pencopotan pemasangan, dan pemasangan ketergantungan di dalam paket git. Paket npm cakupan pengguna dipasang di bawah `~/.pi/agent/npm/`; paket npm lingkup proyek dipasang di bawah `.pi/npm/`. Gunakan entri bergaya argv persis seperti proses yang harus diluncurkan. Saat `npmCommand` dikonfigurasi, penginstalan ketergantungan paket git menggunakan `install` biasa untuk menghindari tanda khusus npm di wrapper atau pengelola paket alternatif.\n\n### Sesi\n\n| Pengaturan | Jenis | Bawaan | Keterangan |\n|---------|------|---------|-------------|\n| `sessionDir` | rangkaian | - | Direktori tempat file sesi disimpan. Menerima jalur absolut atau relatif, ditambah `~`. |\n\n```json\n{ \"sessionDir\": \".pi/sessions\" }\n```\n\nJika beberapa sumber menentukan direktori sesi, prioritasnya adalah `--session-dir`, `PI_CODING_AGENT_SESSION_DIR`, lalu `sessionDir` di settings.json.\n\n### Model Bersepeda\n\n| Pengaturan | Jenis | Bawaan | Keterangan |\n|---------|------|---------|-------------|\n| `enabledModels` | rangkaian[] | - | Pola model untuk bersepeda Ctrl+P (format yang sama dengan bendera `--models` CLI) |\n\n```json\n{\n  \"enabledModels\": [\"claude-*\", \"gpt-4o\", \"gemini-2*\"]\n}\n```\n\n### Markdown\n\n| Pengaturan | Jenis | Bawaan | Keterangan |\n|---------|------|---------|-------------|\n| `markdown.codeBlockIndent` | rangkaian | `\"  \"` | Indentasi untuk blok kode |\n| `markdown.mermaid` | rangkaian | `\"streaming\"` | Mode rendering putri duyung: `\"off\"`, `\"final\"`, atau `\"streaming\"` |\n\n### Sumber daya\n\nPengaturan ini menentukan tempat memuat ekstensi, keterampilan, petunjuk, dan tema.\n\nJalur di `~/.pi/agent/settings.json` terselesaikan relatif terhadap `~/.pi/agent`. Jalur di `.pi/settings.json` terselesaikan relatif terhadap `.pi`. Jalur absolut dan `~` didukung.\n\n| Pengaturan | Jenis | Bawaan | Keterangan |\n|---------|------|---------|-------------|\n| `packages` | susunan | `[]` | npm/git paket untuk memuat sumber daya |\n| `extensions` | rangkaian[] | `[]` | Jalur atau direktori file ekstensi lokal |\n| `skills` | rangkaian[] | `[]` | Jalur atau direktori file keterampilan lokal |\n| `prompts` | rangkaian[] | `[]` | Jalur atau direktori template cepat lokal |\n| `themes` | rangkaian[] | `[]` | Jalur atau direktori file tema lokal |\n| `enableSkillCommands` | boolean | `true` | Daftarkan keterampilan sebagai perintah `/skill:name` |\n\nArray mendukung pola dan pengecualian glob. Gunakan `!pattern` untuk mengecualikan. Gunakan `+path` untuk memaksa memasukkan jalur yang tepat dan `-path` untuk memaksa mengecualikan jalur yang tepat.\n\n#### paket\n\nFormulir string memuat semua sumber daya dari sebuah paket:\n\n```json\n{\n  \"packages\": [\"pi-skills\", \"@org/my-extension\"]\n}\n```\n\nFormulir objek memfilter sumber daya mana yang akan dimuat:\n\n```json\n{\n  \"packages\": [\n    {\n      \"source\": \"pi-skills\",\n      \"skills\": [\"brave-search\", \"transcribe\"],\n      \"extensions\": []\n    }\n  ]\n}\n```\n\nLihat [packages.md](packages.md) untuk detail manajemen paket.\n\n## Contoh\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## Penggantian Proyek\n\nPengaturan proyek (`.pi/settings.json`) menggantikan pengaturan global. Objek bersarang digabungkan:\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":"Alias ​​Kerang","markdown":"Pi menjalankan bash dalam mode non-interaktif (`bash -c`), yang tidak memperluas alias secara default.\n\nUntuk mengaktifkan alias shell Anda, tambahkan ke `~/.pi/agent/settings.json`:\n\n```json\n{\n  \"shellCommandPrefix\": \"shopt -s expand_aliases\\neval \\\"$(grep '^alias ' ~/.zshrc)\\\"\"\n}\n```\n\nSesuaikan jalur (`~/.zshrc`, `~/.bashrc`, dll.) agar sesuai dengan konfigurasi shell Anda.","sourceFile":"shell-aliases.md"},"skills":{"title":"Skills","markdown":"> pi dapat menciptakan keterampilan. Mintalah untuk membuat satu untuk kasus penggunaan Anda.\n\n\nSkills adalah paket kemampuan mandiri yang dimuat agen sesuai permintaan. Keterampilan menyediakan alur kerja khusus, instruksi pengaturan, skrip pembantu, dan dokumentasi referensi untuk tugas tertentu.\n\nPi menerapkan [Agent Skills standard](https://agentskills.io/specification), memperingatkan tentang sebagian besar pelanggaran namun tetap bersikap lunak. Pi memperbolehkan nama keterampilan berbeda dari direktori induknya meskipun standar tidak mengizinkannya; aturan tersebut kurang optimal untuk direktori keterampilan bersama yang digunakan di beberapa kelompok agen.\n\n## Daftar isi\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## Lokasi\n\n> **Keamanan:** Skills dapat memerintahkan model untuk melakukan tindakan apa pun dan mungkin menyertakan kode yang dapat dieksekusi yang dipanggil model. Tinjau konten keterampilan sebelum digunakan.\n\nPi memuat keterampilan dari:\n\n- Global:\n  - `~/.pi/agent/skills/`\n  - `~/.agents/skills/`\n- Proyek (hanya setelah proyek dipercaya):\n  - `.pi/skills/`\n  - `.agents/skills/` di `cwd` dan direktori leluhur (hingga git repo root, atau root sistem file jika tidak ada dalam repo)\n- Paket: `skills/` direktori atau `pi.skills` entri di `package.json`\n- Pengaturan: `skills` array dengan file atau direktori\n- CLI: `--skill <path>` (dapat diulang, bahkan ditambah dengan `--no-skills`)\n\nAturan penemuan:\n- Dalam `~/.pi/agent/skills/` dan `.pi/skills/`, file root langsung `.md` ditemukan sebagai keterampilan individu\n- Di semua lokasi keterampilan, direktori yang berisi `SKILL.md` ditemukan secara rekursif\n- Di `~/.agents/skills/` dan proyek `.agents/skills/`, file root `.md` diabaikan\n\nNonaktifkan penemuan dengan `--no-skills` (jalur eksplisit `--skill` masih dimuat).\n\n### Menggunakan Skills dari Harnes Lainnya\n\nUntuk menggunakan keterampilan dari Claude Code atau OpenAI Codex, tambahkan direktorinya ke pengaturan:\n\n```json\n{\n  \"skills\": [\n    \"~/.claude/skills\",\n    \"~/.codex/skills\"\n  ]\n}\n```\n\nUntuk skill Claude Code tingkat proyek, tambahkan ke `.pi/settings.json`:\n\n```json\n{\n  \"skills\": [\"../.claude/skills\"]\n}\n```\n\n## Bagaimana Skills Bekerja\n\n1. Saat startup, pi memindai lokasi keterampilan dan mengekstrak nama dan deskripsi\n2. Perintah sistem mencakup keterampilan yang tersedia dalam format XML per [specification](https://agentskills.io/integrate-skills)\n3. Ketika tugas cocok, agen menggunakan `read` untuk memuat SKILL.md lengkap (model tidak selalu melakukan ini; gunakan perintah atau `/skill:name` untuk memaksanya)\n4. Agen mengikuti instruksi, menggunakan jalur relatif ke skrip referensi dan aset\n\nIni adalah pengungkapan progresif: hanya deskripsi yang selalu sesuai konteks, instruksi lengkap dimuat sesuai permintaan.\n\n## Perintah Keterampilan\n\nSkills mendaftar sebagai perintah `/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\nArgumen setelah perintah ditambahkan ke konten keterampilan sebagai `User: <args>`.\n\nAlihkan perintah keterampilan melalui `/settings` dalam mode interaktif atau di `settings.json`:\n\n```json\n{\n  \"enableSkillCommands\": true\n}\n```\n\n## Struktur Keterampilan\n\nKeterampilan adalah direktori dengan file `SKILL.md`. Segala sesuatu yang lain berbentuk bebas.\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### Format KETERAMPILAN.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/ke/skill && npm instal\n```\n\n## Usage\n\n```bash\n./scripts/process.sh <input>\n```\n````\n\nGunakan jalur relatif dari direktori keterampilan:\n\n```markdown\nSee [the reference guide](references/REFERENCE.md) for details.\n```\n\n## Masalah depan\n\nBerdasarkan [Agent Skills specification](https://agentskills.io/specification#frontmatter-required):\n\n| Bidang | Diperlukan | Keterangan |\n|-------|----------|-------------|\n| `name` | Ya | Maks 64 karakter. Huruf kecil a-z, 0-9, tanda hubung. Berbeda dengan standar, Pi tidak mengharuskan ini cocok dengan direktori induk karena persyaratan standar tersebut kurang optimal untuk direktori keterampilan bersama. |\n| `description` | Ya | Maks 1024 karakter. Apa yang dilakukan keterampilan itu dan kapan menggunakannya. |\n| `license` | TIDAK | Nama lisensi atau referensi ke file yang dibundel. |\n| `compatibility` | TIDAK | Maks 500 karakter. Persyaratan lingkungan. |\n| `metadata` | TIDAK | Pemetaan nilai kunci sewenang-wenang. |\n| `allowed-tools` | TIDAK | Daftar alat yang telah disetujui sebelumnya dan dibatasi spasi (eksperimental). |\n| `disable-model-invocation` | TIDAK | Ketika `true`, keterampilan disembunyikan dari prompt sistem. Pengguna harus menggunakan `/skill:name`. |\n\n### Aturan Nama\n\n- 1-64 karakter\n- Huruf kecil, angka, tanda hubung saja\n- Tidak ada tanda hubung awal/akhir\n- Tidak ada tanda hubung berturut-turut\nPi tidak memerlukan nama yang cocok dengan direktori induk. Standar Agen Skills dapat melakukannya, namun persyaratan tersebut kurang optimal untuk direktori keterampilan bersama yang digunakan oleh banyak alat.\n\nSah: `pdf-processing`, `data-analysis`, `code-review`\nTidak valid: `PDF-Processing`, `-pdf`, `pdf--processing`\n\n### Deskripsi Praktik Terbaik\n\nDeskripsi menentukan kapan agen memuat keterampilannya. Bersikaplah spesifik.\n\nBagus:\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\nMiskin:\n```yaml\ndescription: Helps with PDFs.\n```\n\n## Validasi\n\nPi memvalidasi keterampilan terhadap standar Agen Skills. Sebagian besar masalah menghasilkan peringatan tetapi masih memuat keterampilan:\n\n- Nama melebihi 64 karakter atau mengandung karakter tidak valid\n- Nama dimulai/diakhiri dengan tanda hubung atau memiliki tanda hubung yang berurutan\n- Deskripsi melebihi 1024 karakter\n\nBidang frontmatter yang tidak diketahui akan diabaikan.\n\n**Pengecualian:** Skills dengan deskripsi yang hilang tidak dimuat.\n\nTabrakan nama (nama yang sama dari lokasi berbeda) memperingatkan dan menjaga agar keterampilan pertama tetap ditemukan.\n\n## Contoh\n\n```\nbrave-search/\n├── SKILL.md\n├── search.js\n└── content.js\n```\n\n** KETERAMPILAN.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/ke/brave-search && npm instal\n```\n\n## Search\n\n```bash\n./search.js \"query\" # Pencarian dasar\n./search.js \"query\" --content # Sertakan konten halaman\n```\n\n## Extract Page Content\n\n```bash\n./content.js https://example.com\n```\n````\n\n## Repositori Keterampilan\n\n- [Anthropic Skills](https://github.com/anthropics/skills) - Pemrosesan dokumen (docx, pdf, pptx, xlsx), pengembangan web\n- [Pi Skills](https://github.com/badlogic/pi-skills) - Pencarian web, otomatisasi browser, Google APIs, transkripsi","sourceFile":"skills.md"},"terminal-setup":{"title":"Pengaturan Terminal","markdown":"Pi menggunakan [Kitty keyboard protocol](https://sw.kovidgoyal.net/kitty/keyboard-protocol/) untuk deteksi kunci pengubah yang andal. Kebanyakan terminal modern mendukung protokol ini, namun beberapa memerlukan konfigurasi.\n\n## Kitty, iTerm2\n\nBekerja di luar kotak.\n\n## Terminal Apel\n\nPi mengaktifkan pelaporan kunci yang ditingkatkan bila tersedia. Jika Terminal.app masih mengirimkan Return biasa untuk `Shift+Enter`, pi menggunakan fallback pengubah macOS lokal untuk memperlakukan Return tersebut sebagai `Shift+Enter`.\n\nPenggantian ini hanya berfungsi ketika pi berjalan di Mac yang sama dengan Terminal.app. Itu tidak dapat mendeteksi keyboard lokal melalui jarak jauh SSH.\n\n## Hantu\n\nTambahkan ke konfigurasi Ghostty Anda (`~/Library/Application Support/com.mitchellh.ghostty/config` di macOS, `~/.config/ghostty/config` di Linux):\n\n```\nkeybind = alt+backspace=text:\\x1b\\x7f\n```\n\nVersi Kode Claude yang lebih lama mungkin telah menambahkan pemetaan Ghostty ini:\n\n```\nkeybind = shift+enter=text:\\n\n```\n\nPemetaan itu mengirimkan byte linefeed mentah. Di dalam pi, itu tidak dapat dibedakan dari `Ctrl+J`, jadi tmux dan pi tidak lagi melihat peristiwa penting `shift+enter` yang sebenarnya.\n\nJika Kode Claude 2.x atau yang lebih baru adalah satu-satunya alasan Anda menambahkan pemetaan tersebut, Anda dapat menghapusnya, kecuali Anda ingin menggunakan Kode Claude di tmux, yang masih memerlukan pemetaan Ghostty tersebut.\n\nPi mengikat `Ctrl+J` sebagai alias baris baru default, jadi `Shift+Enter` tetap bekerja di tmux melalui remap tersebut tanpa konfigurasi pi tambahan.\n\n## WezTerm\n\nWezTerm biasanya berfungsi langsung untuk `Shift+Enter` melalui xterm memodifikasiOtherKeys. Untuk menggunakan protokol keyboard Kitty secara eksplisit, buat `~/.wezterm.lua`:\n\n```lua\nlocal wezterm = require 'wezterm'\nlocal config = wezterm.config_builder()\nconfig.enable_kitty_keyboard = true\nreturn config\n```\n\nDi macOS, WezTerm mengikat `Option+Enter` ke layar penuh secara default. Untuk menggunakan `Option+Enter` untuk antrian tindak lanjut pi, tambahkan penggantian kunci ini:\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\nJika Anda sudah memiliki tabel `config.keys`, tambahkan entri ke dalamnya.\n\nDi WSL, WezTerm mungkin memerlukan kursor perangkat keras yang terlihat untuk penentuan posisi jendela kandidat IME. Jika kandidat IME CJK tidak mengikuti kursor teks, setel `PI_HARDWARE_CURSOR=1` sebelum menjalankan pi atau setel `showHardwareCursor` ke `true` di pengaturan.\n\n## Sigap\n\nAlacritty biasanya bekerja di luar kotak untuk `Shift+Enter`. Di macOS, `Option+Enter` mungkin muncul sebagai `Enter` biasa. Untuk menggunakan `Option+Enter` untuk antrian tindak lanjut pi, tambahkan ke `~/.config/alacritty/alacritty.toml`:\n\n```toml\n[[keyboard.bindings]]\nkey = \"Enter\"\nmods = \"Alt\"\nchars = \"\\u001b[13;3u\"\n```\n\nMulai ulang Alacritty setelah mengubah konfigurasi.\n\n## VS Code (Terminal Terintegrasi)\n\nVS Code 1.109.5 dan yang lebih baru mengaktifkan protokol keyboard Kitty di terminal terintegrasi secara default, jadi `Shift+Enter` akan langsung berfungsi.\n\nVersi VS Code yang lebih lama dari 1.109.5 memerlukan pengikatan kunci terminal eksplisit untuk `Shift+Enter`.\n\n`keybindings.json` lokasi:\n- macOS: `~/Library/Application Support/Code/User/keybindings.json`\n- Linux: `~/.config/Code/User/keybindings.json`\n- Jendela: `%APPDATA%\\\\Code\\\\User\\\\keybindings.json`\n\nTambahkan ke `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## Terminal Windows\n\nTambahkan ke `settings.json` (Ctrl+Shift+, atau Pengaturan → Buka file JSON) untuk meneruskan tombol Enter yang dimodifikasi yang digunakan pi:\n\n```json\n{\n  \"actions\": [\n    {\n      \"command\": { \"action\": \"sendInput\", \"input\": \"\\u001b[13;2u\" },\n      \"keys\": \"shift+enter\"\n    },\n    {\n      \"command\": { \"action\": \"sendInput\", \"input\": \"\\u001b[13;3u\" },\n      \"keys\": \"alt+enter\"\n    }\n  ]\n}\n```\n\n- `Shift+Enter` menyisipkan baris baru.\n- Terminal Windows mengikat `Alt+Enter` ke layar penuh secara default. Itu mencegah pi menerima `Alt+Enter` untuk antrian tindak lanjut.\n- Memetakan ulang `Alt+Enter` ke `sendInput` meneruskan kunci kunci sebenarnya ke pi.\n\nJika Anda sudah memiliki array `actions`, tambahkan objek ke dalamnya. Jika perilaku layar penuh lama masih berlanjut, tutup sepenuhnya dan buka kembali Terminal Windows.\n\n## xfce4-terminal, terminator\n\nTerminal-terminal ini mempunyai dukungan escape sequence yang terbatas. Tombol Enter yang dimodifikasi seperti `Ctrl+Enter` dan `Shift+Enter` tidak dapat dibedakan dari `Enter` biasa, sehingga mencegah pengikatan tombol khusus seperti `submit: [\"ctrl+enter\"]` berfungsi.\n\nUntuk pengalaman terbaik, gunakan terminal yang mendukung protokol keyboard 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) (memerlukan kompilasi dengan dukungan protokol Kitty)\n\n## IntelliJ IDEA (Terminal Terintegrasi)\n\nTerminal internal memiliki dukungan urutan escape yang terbatas. Shift+Enter tidak dapat dibedakan dari Enter di terminal IntelliJ.\n\nJika Anda ingin kursor perangkat keras terlihat, setel `PI_HARDWARE_CURSOR=1` sebelum menjalankan pi (dinonaktifkan secara default untuk kompatibilitas).\n\nPertimbangkan untuk menggunakan emulator terminal khusus untuk pengalaman terbaik.","sourceFile":"terminal-setup.md"},"termux":{"title":"Termux (Android) Pengaturan","markdown":"Pi berjalan di Android melalui [Termux](https://termux.dev/), emulator terminal dan lingkungan Linux untuk Android.\n\n## Prasyarat\n\n1. Instal [Termux](https://github.com/termux/termux-app#installation) dari GitHub atau F-Droid (bukan Google Play, versi tersebut tidak digunakan lagi)\n2. Instal [Termux:API](https://github.com/termux/termux-api#installation) dari GitHub atau F-Droid untuk clipboard dan integrasi perangkat lainnya\n\n## Instalasi\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## Dukungan Papan Klip\n\nOperasi papan klip menggunakan `termux-clipboard-set` dan `termux-clipboard-get` saat dijalankan di Termux. Aplikasi Termux:API harus diinstal agar dapat berfungsi.\n\nPapan klip gambar tidak didukung di Termux (fitur tempel gambar `ctrl+v` tidak akan berfungsi).\n\n## Contoh AGEN.md untuk Termux\n\nBuat `~/.pi/agent/AGENTS.md` untuk membantu agen memahami lingkungan 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 # Dibuka dengan aplikasi default\ntermux-open --chooser image.jpg # Pilih aplikasi\n```\n\n## Clipboard\n```bash\ntermux-clipboard-set \"teks\" # Salin\ntermux-clipboard-dapatkan #Tempel\n```\n\n## Notifications\n```bash\ntermux-notification -t \"Judul\" -c \"Isi\"\n```\n\n## Device Info\n```bash\ntermux-battery-status # Info baterai\ntermux-wifi-connectioninfo #info WiFi\ntermux-telephony-deviceinfo # Info perangkat\n```\n\n## Sharing\n```bash\ntermux-share -a kirim file.txt # Bagikan file\n```\n\n## Other Useful Commands\n```bash\ntermux-toast \"pesan\" # Popup roti panggang cepat\ntermux-vibrate # Perangkat getar\ntermux-tts-speak \"halo\" # Teks ke ucapan\ntermux-camera-photo out.jpg # Ambil foto\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## Keterbatasan\n\n- **Tidak ada papan klip gambar**: Termux papan klip API hanya mendukung teks\n- **Tidak ada biner asli**: Beberapa dependensi asli opsional (seperti modul clipboard) tidak tersedia di Android ARM64 dan dilewati selama instalasi\n- **Akses penyimpanan**: Untuk mengakses file di `/storage/emulated/0` (Unduhan, dll.), jalankan `termux-setup-storage` sekali untuk memberikan izin\n\n## Pemecahan masalah\n\n### Papan klip tidak berfungsi\n\nPastikan kedua aplikasi diinstal:\n1. Termux (dari GitHub atau F-Droid)\n2. Termux:API (dari GitHub atau F-Droid)\n\nKemudian instal alat CLI:\n```bash\npkg install termux-api\n```\n\n### Izin ditolak untuk penyimpanan bersama\n\nJalankan sekali untuk memberikan izin penyimpanan:\n```bash\ntermux-setup-storage\n```\n\n### Node.js masalah instalasi\n\nJika npm gagal, coba bersihkan cache:\n```bash\nnpm cache clean --force\n```","sourceFile":"termux.md"},"themes":{"title":"Tema","markdown":"> pi dapat membuat tema. Mintalah untuk membuatkannya untuk pengaturan Anda.\n\n\nTema adalah file JSON yang menentukan warna untuk TUI.\n\n## Daftar isi\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## Lokasi\n\nPi memuat tema dari:\n\n- Bawaan: `dark`, `light`\n- Global: `~/.pi/agent/themes/*.json`\n- Proyek: `.pi/themes/*.json` (hanya setelah proyek dipercaya)\n- Paket: `themes/` direktori atau `pi.themes` entri di `package.json`\n- Pengaturan: `themes` array dengan file atau direktori\n- CLI: `--theme <path>` (dapat diulang)\n\nNonaktifkan penemuan dengan `--no-themes`.\n\n## Memilih Tema\n\nPilih tema melalui `/settings` atau di `settings.json`:\n\n```json\n{\n  \"theme\": \"my-theme\"\n}\n```\n\nSaat pertama kali dijalankan, pi mendeteksi latar belakang terminal Anda dan defaultnya adalah `dark` atau `light`.\n\n## Membuat Tema Kustom\n\n1. Buat file tema:\n\n```bash\nmkdir -p ~/.pi/agent/themes\nvim ~/.pi/agent/themes/my-theme.json\n```\n\n2. Tentukan tema dengan semua warna yang diperlukan (lihat [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. Pilih tema melalui `/settings`.\n\n**Hot reload:** Saat Anda mengedit file tema khusus yang sedang aktif, pi memuat ulang secara otomatis untuk mendapatkan masukan visual langsung.\n\n## Format Tema\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` wajib diisi, harus unik, dan tidak boleh mengandung `/`.\n- `vars` adalah opsional. Tentukan warna yang dapat digunakan kembali di sini, lalu referensikan di `colors`.\n- `colors` harus mendefinisikan seluruh 51 token yang diperlukan. `thinkingMax` bersifat opsional dan kembali ke `thinkingXhigh`; `scrollbarThumb` bersifat opsional dan kembali ke `selectedBg`.\n\nBidang `$schema` mengaktifkan pelengkapan dan validasi otomatis editor.\n\n## Token Warna\n\nSetiap tema harus menentukan 51 token warna yang diperlukan. `thinkingMax` dan `scrollbarThumb` bersifat opsional untuk kompatibilitas dengan tema yang ada; jika dihilangkan, masing-masing menggunakan `thinkingXhigh` dan `selectedBg`.\n\n### UI Inti (11 warna)\n\n| Token | Tujuan |\n|-------|---------|\n| `accent` | Aksen utama (logo, item yang dipilih, kursor) |\n| `border` | Perbatasan biasa |\n| `borderAccent` | Perbatasan yang disorot |\n| `borderMuted` | Batas halus (editor) |\n| `success` | Status sukses |\n| `error` | Status kesalahan |\n| `warning` | Status peringatan |\n| `muted` | Teks sekunder |\n| `dim` | Teks tersier |\n| `text` | Teks default (biasanya `\"\"`) |\n| `thinkingText` | Teks blok berpikir |\n\n### Latar Belakang & Konten (11 wajib, 1 opsional)\n\n| Token | Tujuan |\n|-------|---------|\n| `selectedBg` | Latar belakang garis yang dipilih |\n| `scrollbarThumb` | Latar belakang jempol scrollbar layar penuh; opsional, kembali ke `selectedBg` |\n| `userMessageBg` | Latar belakang pesan pengguna |\n| `userMessageText` | Teks pesan pengguna |\n| `customMessageBg` | Latar belakang pesan ekstensi |\n| `customMessageText` | Teks pesan ekstensi |\n| `customMessageLabel` | Label pesan ekstensi |\n| `toolPendingBg` | Kotak alat (menunggu keputusan) |\n| `toolSuccessBg` | Kotak alat (sukses) |\n| `toolErrorBg` | Kotak alat (kesalahan) |\n| `toolTitle` | Judul alat |\n| `toolOutput` | Teks keluaran alat |\n\n### Markdown (10 warna)\n\n| Token | Tujuan |\n|-------|---------|\n| `mdHeading` | Judul |\n| `mdLink` | Teks tautan |\n| `mdLinkUrl` | URL tautan |\n| `mdCode` | Kode sebaris |\n| `mdCodeBlock` | Konten blok kode |\n| `mdCodeBlockBorder` | Pagar blok kode |\n| `mdQuote` | Teks kutipan blok |\n| `mdQuoteBorder` | Batas blokquote |\n| `mdHr` | Aturan horisontal |\n| `mdListBullet` | Daftar poin-poin |\n\n### Perbedaan Alat (3 warna)\n\n| Token | Tujuan |\n|-------|---------|\n| `toolDiffAdded` | Menambahkan baris |\n| `toolDiffRemoved` | Garis yang dihapus |\n| `toolDiffContext` | Garis konteks |\n\n### Penyorotan Sintaks (9 warna)\n\n| Token | Tujuan |\n|-------|---------|\n| `syntaxComment` | Komentar |\n| `syntaxKeyword` | Kata kunci |\n| `syntaxFunction` | Nama fungsi |\n| `syntaxVariable` | Variabel |\n| `syntaxString` | string |\n| `syntaxNumber` | Angka |\n| `syntaxType` | Jenis |\n| `syntaxOperator` | Operator |\n| `syntaxPunctuation` | tanda baca |\n\n### Batas Tingkat Berpikir (6 wajib, 1 opsional)\n\nWarna batas editor menunjukkan tingkat berpikir (hierarki visual dari halus hingga menonjol):\n\n| Token | Tujuan |\n|-------|---------|\n| `thinkingOff` | Berpikir |\n| `thinkingMinimal` | Minimal berpikir |\n| `thinkingLow` | Berpikir rendah |\n| `thinkingMedium` | Pemikiran sedang |\n| `thinkingHigh` | Berpikir tinggi |\n| `thinkingXhigh` | Pemikiran ekstra tinggi |\n| `thinkingMax` | Pemikiran maksimal; opsional, kembali ke `thinkingXhigh` |\n\n### Mode Pesta (1 warna)\n\n| Token | Tujuan |\n|-------|---------|\n| `bashMode` | Batas editor dalam mode bash (awalan `!`) |\n\n### Ekspor HTML (opsional)\n\nBagian `export` mengontrol warna untuk `/export` keluaran HTML. Jika dihilangkan, warna berasal dari `userMessageBg`.\n\n```json\n{\n  \"export\": {\n    \"pageBg\": \"#18181e\",\n    \"cardBg\": \"#1e1e24\",\n    \"infoBg\": \"#3c3728\"\n  }\n}\n```\n\n## Nilai Warna\n\nEmpat format yang didukung:\n\n| Format | Contoh | Keterangan |\n|--------|---------|-------------|\n| kutukan | `\"#ff0000\"` | RGB heksa 6 digit |\n| 256 warna | `39` | xterm indeks palet 256 warna (0-255) |\n| Variabel | `\"primary\"` | Referensi ke entri `vars` |\n| Bawaan | `\"\"` | Warna default terminal |\n\n### Palet 256 Warna\n\n- `0-15`: Warna dasar ANSI (tergantung terminal)\n- `16-231`: 6×6×6 kubus RGB (`16 + 36×R + 6×G + B` dengan R,G,B adalah 0-5)\n- `232-255`: Jalan skala abu-abu\n\n### Kompatibilitas Terminal\n\nPi menggunakan warna RGB 24-bit. Kebanyakan terminal modern mendukung ini (iTerm2, Kitty, WezTerm, Terminal Windows, VS Code). Untuk terminal lama yang hanya mendukung 256 warna, pi kembali ke perkiraan terdekat.\n\nPeriksa dukungan warna asli:\n\n```bash\necho $COLORTERM  # Should output \"truecolor\" or \"24bit\"\n```\n\n## Kiat\n\n**Terminal gelap:** Gunakan warna cerah dan jenuh dengan kontras lebih tinggi.\n\n**Terminal terang:** Gunakan warna yang lebih gelap dan kalem dengan kontras lebih rendah.\n\n**Harmoni warna:** Mulailah dengan palet dasar (Nord, Gruvbox, Tokyo Night), tentukan di `vars`, dan rujuk secara konsisten.\n\n**Pengujian:** Periksa tema Anda dengan berbagai jenis pesan, status alat, konten penurunan harga, dan teks panjang.\n\n**Kode VS:** Setel `terminal.integrated.minimumContrastRatio` ke `1` untuk warna yang akurat.\n\n## Contoh\n\nLihat tema bawaan:\n- [dark.json](../src/modes/interactive/theme/dark.json)\n- [light.json](../src/modes/interactive/theme/light.json)","sourceFile":"themes.md"},"tmux":{"title":"tmux Pengaturan","markdown":"Pi berfungsi di dalam tmux, tetapi tmux menghapus informasi pengubah dari kunci tertentu secara default. Tanpa konfigurasi, `Shift+Enter` dan `Ctrl+Enter` biasanya tidak dapat dibedakan dari `Enter` biasa.\n\n## Konfigurasi yang Direkomendasikan\n\nTambahkan ke `~/.tmux.conf`:\n\n```tmux\nset -g extended-keys on\nset -g extended-keys-format csi-u\n```\n\nKemudian mulai ulang tmux sepenuhnya:\n\n```bash\ntmux kill-server\ntmux\n```\n\nPi meminta pelaporan kunci yang diperluas secara otomatis ketika protokol keyboard Kitty tidak tersedia. Dengan `extended-keys-format csi-u`, tmux meneruskan kunci yang dimodifikasi dalam format CSI-u, yang merupakan konfigurasi paling andal. Opsi `extended-keys-format` memerlukan tmux 3.5 atau lebih baru.\n\n## Mengapa `csi-u` Direkomendasikan\n\nHanya dengan:\n\n```tmux\nset -g extended-keys on\n```\n\ntmux defaultnya adalah `extended-keys-format xterm`. Ketika aplikasi meminta pelaporan kunci yang diperluas, kunci yang dimodifikasi diteruskan dalam format xterm `modifyOtherKeys` seperti:\n\n- `Ctrl+C` → `\\x1b[27;5;99~`\n- `Ctrl+D` → `\\x1b[27;5;100~`\n- `Ctrl+Enter` → `\\x1b[27;5;13~`\n\nDengan `extended-keys-format csi-u`, kunci yang sama diteruskan seperti:\n\n- `Ctrl+C` → `\\x1b[99;5u`\n- `Ctrl+D` → `\\x1b[100;5u`\n- `Ctrl+Enter` → `\\x1b[13;5u`\n\nPi mendukung kedua format, namun `csi-u` adalah pengaturan tmux yang direkomendasikan.\n\n## Apa yang Diperbaiki Ini\n\nTanpa kunci tambahan tmux, kunci Enter yang dimodifikasi akan diciutkan ke urutan lama:\n\n| Kunci | Tanpa extkey | Dengan `csi-u` |\n|-----|-----------------|--------------|\n| Memasuki | `\\r` | `\\r` |\n| Shift+Masuk | `\\r` | `\\x1b[13;2u` |\n| Ctrl+Masuk | `\\r` | `\\x1b[13;5u` |\n| Alt/Opsi+Enter | `\\x1b\\r` | `\\x1b[13;3u` |\n\nHal ini mempengaruhi pengikatan kunci default (`Enter` untuk mengirimkan, `Shift+Enter` untuk baris baru) dan pengikatan kunci khusus apa pun yang menggunakan Enter yang dimodifikasi.\n\n## Persyaratan\n\n- tmux 3.5 atau lebih baru untuk `extended-keys-format csi-u` (jalankan `tmux -V` untuk memeriksa)\n- Emulator terminal yang mendukung kunci tambahan (Ghostty, Kitty, iTerm2, WezTerm, Windows Terminal)\n\nDengan tmux 3.2 hingga 3.4, hilangkan `extended-keys-format csi-u`; Pi masih mendukung format xterm `modifyOtherKeys` default tmux.","sourceFile":"tmux.md"},"tui":{"title":"TUI Komponen","markdown":"> pi dapat membuat komponen TUI. Mintalah untuk membuat satu untuk kasus penggunaan Anda.\n\n\nExtensions dan alat khusus dapat merender komponen TUI khusus untuk antarmuka pengguna interaktif. Halaman ini mencakup sistem komponen dan blok penyusun yang tersedia.\n\n**Sumber:** [`@earendil-works/pi-tui`](https://github.com/earendil-works/pi-mono/tree/main/packages/tui)\n\n## Antarmuka Komponen\n\nSemua komponen menerapkan:\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| Metode | Keterangan |\n|--------|-------------|\n| `render(width)` | Kembalikan array string (satu per baris). Setiap baris **tidak boleh melebihi `width`**. |\n| `handleInput?(data)` | Menerima input keyboard saat komponen memiliki fokus. |\n| `wantsKeyRelease?` | Jika benar, komponen menerima peristiwa rilis penting (protokol Kitty). Bawaan: salah. |\n| `invalidate()` | Hapus status render yang di-cache. Dipanggil pada perubahan tema. |\n\nTUI menambahkan reset SGR penuh dan reset OSC 8 di akhir setiap baris yang dirender. Gaya tidak bersifat lintas batas. Jika Anda memancarkan teks multi-baris dengan gaya, terapkan kembali gaya per baris atau gunakan `wrapTextWithAnsi()` sehingga gaya dipertahankan untuk setiap baris yang dibungkus.\n\n## Antarmuka yang Dapat Difokuskan (Dukungan IME)\n\nKomponen yang menampilkan kursor teks dan memerlukan dukungan IME (Input Method Editor) harus mengimplementasikan antarmuka `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\nKetika komponen `Focusable` memiliki fokus, TUI:\n1. Menyetel `focused = true` pada komponen\n2. Memindai keluaran yang diberikan untuk `CURSOR_MARKER` (urutan escape APC dengan lebar nol)\n3. Memposisikan kursor terminal perangkat keras di lokasi itu\n4. Menampilkan kursor perangkat keras hanya ketika `showHardwareCursor` diaktifkan\n\nKursor tetap tersembunyi secara default. Hal ini menjaga rendering kursor palsu, sambil tetap memposisikan kursor perangkat keras untuk terminal yang melacak jendela kandidat IME dengan kursor tersembunyi. Beberapa terminal memerlukan kursor perangkat keras yang terlihat untuk penentuan posisi IME; aktifkan dengan `showHardwareCursor`, `setShowHardwareCursor(true)`, atau `PI_HARDWARE_CURSOR=1`. Komponen bawaan `Editor` dan `Input` sudah mengimplementasikan antarmuka ini.\n\n### Komponen Kontainer dengan Input Tersemat\n\nKetika komponen container (dialog, selector, dll.) berisi turunan `Input` atau `Editor`, container harus mengimplementasikan `Focusable` dan menyebarkan status fokus ke turunan tersebut. Jika tidak, kursor perangkat keras tidak akan ditempatkan dengan benar untuk input 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\nTanpa propagasi ini, mengetik dengan IME (China, Jepang, Korea, dll.) akan menampilkan jendela kandidat pada posisi yang salah di layar.\n\n## Menggunakan Komponen\n\n**Dalam ekstensi** melalui `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**Dalam alat khusus** melalui `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## Hamparan\n\nOverlay merender komponen di atas konten yang ada tanpa membersihkan layar. Teruskan `{ overlay: true }` ke `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\nUntuk penentuan posisi dan ukuran, gunakan `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### Fokus Hamparan\n\nHamparan terlihat terfokus mempertahankan kepemilikan masukan di seluruh UI non-hamparan sementara. Jika overlay membuka komponen `ctx.ui.custom()` lain tanpa `{ overlay: true }`, UI pengganti tersebut menerima input saat sedang aktif; ketika ditutup, hamparan terfokus dapat memperoleh kembali masukan.\n\nGunakan `handle.unfocus()` ketika overlay yang terlihat tidak lagi memiliki masukan dan biarkan TUI kembali ke overlay pengambilan lain yang terlihat atau target fokus sebelumnya. Gunakan `handle.unfocus({ target })` ketika komponen tertentu harus menerima masukan sementara overlay tetap terlihat. Melewati `{ target: null }` dengan sengaja tidak meninggalkan komponen fokus hingga fokus diatur kembali.\n\n### Siklus Hidup Hamparan\n\nKomponen overlay dibuang saat ditutup. Jangan gunakan kembali referensi - buatlah instance baru:\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\nLihat [overlay-qa-tests.ts](../examples/extensions/overlay-qa-tests.ts) untuk contoh komprehensif yang mencakup jangkar, margin, penumpukan, visibilitas responsif, dan animasi.\n\n## Komponen Bawaan\n\nImpor dari `@earendil-works/pi-tui`:\n\n```typescript\nimport { Text, Box, Container, Spacer, Markdown } from \"@earendil-works/pi-tui\";\n```\n\n### Teks\n\nTeks multi-baris dengan pembungkusan kata.\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### Kotak\n\nWadah dengan padding dan warna latar belakang.\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### Wadah\n\nMengelompokkan komponen anak secara vertikal.\n\n```typescript\nconst container = new Container();\ncontainer.addChild(component1);\ncontainer.addChild(component2);\ncontainer.removeChild(component1);\n```\n\n### pengatur jarak\n\nRuang vertikal kosong.\n\n```typescript\nconst spacer = new Spacer(2);  // 2 empty lines\n```\n\n### Markdown\n\nMerender penurunan harga dengan penyorotan sintaksis.\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### Gambar\n\nMerender gambar di terminal yang didukung (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## Masukan Papan Ketik\n\nGunakan `matchesKey()` untuk deteksi kunci:\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**Pengidentifikasi kunci** (gunakan `Key.*` untuk pelengkapan otomatis, atau literal string):\n- Kunci dasar: `Key.enter`, `Key.escape`, `Key.tab`, `Key.space`, `Key.backspace`, `Key.delete`, `Key.home`, `Key.end`\n- Tombol panah: `Key.up`, `Key.down`, `Key.left`, `Key.right`\n- Dengan pengubah: `Key.ctrl(\"c\")`, `Key.shift(\"tab\")`, `Key.alt(\"left\")`, `Key.ctrlShift(\"p\")`\n- Format string juga berfungsi: `\"enter\"`, `\"ctrl+c\"`, `\"shift+tab\"`, `\"ctrl+shift+p\"`\n\n## Lebar Garis\n\n**Kritis:** Setiap baris dari `render()` tidak boleh melebihi parameter `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\nUtilitas:\n- `visibleWidth(str)` - Dapatkan lebar tampilan (abaikan kode ANSI)\n- `truncateToWidth(str, width, ellipsis?)` - Potong dengan elipsis opsional\n- `wrapTextWithAnsi(str, width)` - Bungkus kata yang menyimpan kode ANSI\n\n## Membuat Komponen Khusus\n\nContoh: Pemilih interaktif\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\nPenggunaan dalam ekstensi:\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## Tema\n\nKomponen menerima objek tema untuk penataan gaya.\n\n**Di `renderCall`/`renderResult`**, gunakan parameter `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**Warna latar depan** (`theme.fg(color, text)`):\n\n| Kategori | Warna |\n|----------|--------|\n| Umum | `text`, `accent`, `muted`, `dim` |\n| Status | `success`, `error`, `warning` |\n| Perbatasan | `border`, `borderAccent`, `borderMuted` |\n| Pesan | `userMessageText`, `customMessageText`, `customMessageLabel` |\n| Peralatan | `toolTitle`, `toolOutput` |\n| Perbedaan | `toolDiffAdded`, `toolDiffRemoved`, `toolDiffContext` |\n| Markdown | `mdHeading`, `mdLink`, `mdLinkUrl`, `mdCode`, `mdCodeBlock`, `mdCodeBlockBorder`, `mdQuote`, `mdQuoteBorder`, `mdHr`, `mdListBullet` |\n| Sintaksis | `syntaxComment`, `syntaxKeyword`, `syntaxFunction`, `syntaxVariable`, `syntaxString`, `syntaxNumber`, `syntaxType`, `syntaxOperator`, `syntaxPunctuation` |\n| Pemikiran | `thinkingOff`, `thinkingMinimal`, `thinkingLow`, `thinkingMedium`, `thinkingHigh`, `thinkingXhigh`, `thinkingMax` |\n| Mode | `bashMode` |\n\n**Warna latar belakang** (`theme.bg(color, text)`):\n\n`selectedBg`, `userMessageBg`, `customMessageBg`, `toolPendingBg`, `toolSuccessBg`, `toolErrorBg`\n\n**Untuk Markdown**, gunakan `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**Untuk komponen khusus**, tentukan antarmuka tema Anda sendiri:\n\n```typescript\ninterface MyTheme {\n  selected: (s: string) => string;\n  normal: (s: string) => string;\n}\n```\n\n## Pencatatan debug\n\nSetel `PI_TUI_WRITE_LOG` untuk menangkap aliran ANSI mentah yang ditulis ke stdout.\n\n```bash\nPI_TUI_WRITE_LOG=/tmp/tui-ansi.log npx tsx packages/tui/test/chat-simple.ts\n```\n\n## Pertunjukan\n\nOutput yang di-cache jika memungkinkan:\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\nPanggil `invalidate()` ketika status berubah, lalu gunakan `tui.requestRender()` yang disuntikkan untuk memicu rendering ulang.\n\n## Pembatalan dan Perubahan Tema\n\nSaat tema berubah, TUI memanggil `invalidate()` pada semua komponen untuk menghapus cache-nya. Komponen harus menerapkan `invalidate()` dengan benar untuk memastikan perubahan tema diterapkan.\n\n### Masalahnya\n\nJika komponen membuat warna tema menjadi string terlebih dahulu (melalui `theme.fg()`, `theme.bg()`, dll.) dan menyimpannya dalam cache, string yang di-cache berisi kode escape ANSI dari tema lama. Menghapus cache render saja tidak cukup jika komponen menyimpan konten bertema secara terpisah.\n\n**Pendekatan yang salah** (warna tema tidak akan diperbarui):\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### Solusinya\n\nKomponen yang membuat konten dengan warna tema harus membangun kembali konten tersebut ketika `invalidate()` dipanggil:\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### Pola: Membangun Kembali saat Tidak Valid\n\nUntuk komponen dengan konten kompleks:\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### Kapan Ini Penting\n\nPola ini diperlukan ketika:\n\n1. **Warna tema sebelum dipanggang** - Menggunakan `theme.fg()` atau `theme.bg()` untuk membuat string bergaya yang disimpan di komponen anak\n2. **Penyorotan sintaksis** - Menggunakan `highlightCode()` yang menerapkan warna sintaksis berbasis tema\n3. **Tata letak kompleks** - Membangun pohon komponen anak yang menyematkan warna tema\n\nPola ini TIDAK diperlukan ketika:\n\n1. **Menggunakan panggilan balik tema** - Meneruskan fungsi seperti `(text) => theme.fg(\"accent\", text)` yang dipanggil selama render\n2. **Wadah sederhana** - Cukup mengelompokkan komponen lain tanpa menambahkan konten bertema\n3. **Render tanpa status** - Menghitung keluaran bertema baru di setiap panggilan `render()` (tanpa caching)\n\n## Pola Umum\n\nPola-pola ini mencakup kebutuhan UI yang paling umum dalam ekstensi. **Salin pola ini daripada membuat dari awal.**\n\n### Pola 1: Dialog Seleksi (SelectList)\n\nUntuk membiarkan pengguna memilih dari daftar opsi. Gunakan `SelectList` dari `@earendil-works/pi-tui` dengan `DynamicBorder` untuk pembingkaian.\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**Contoh:** [preset.ts](../examples/extensions/preset.ts), [tools.ts](../examples/extensions/tools.ts)\n\n### Pola 2: Operasi Asinkron dengan Pembatalan (BorderedLoader)\n\nUntuk operasi yang memakan waktu dan harus dibatalkan. `BorderedLoader` menunjukkan pemintal dan pegangan escape untuk membatalkan.\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**Contoh:** [qna.ts](../examples/extensions/qna.ts), [handoff.ts](../examples/extensions/handoff.ts)\n\n### Pola 3: Pengaturan/Toggles (Daftar Pengaturan)\n\nUntuk mengubah beberapa pengaturan. Gunakan `SettingsList` dari `@earendil-works/pi-tui` dengan `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**Contoh:** [tools.ts](../examples/extensions/tools.ts)\n\n### Pola 4: Indikator Status Persisten\n\nTampilkan status di footer yang tetap ada di seluruh render. Cocok untuk indikator mode.\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**Contoh:** [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### Pola 4b: Kustomisasi Indikator Kerja\n\nSesuaikan indikator kerja sebaris yang ditampilkan saat pi mengalirkan respons.\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\nIni hanya mempengaruhi indikator kerja streaming normal. Loader pemadatan dan percobaan ulang mempertahankan gaya bawaannya. Bingkai khusus ditampilkan kata demi kata, jadi ekstensi harus menambahkan warnanya sendiri bila diperlukan.\n\n**Contoh:** [working-indicator.ts](../examples/extensions/working-indicator.ts)\n\n### Pola 5: Widget Di Atas/Di Bawah Editor\n\nTampilkan konten persisten di atas atau di bawah editor masukan. Bagus untuk daftar tugas, kemajuan.\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**Contoh:** [plan-mode/index.ts](../examples/extensions/plan-mode/index.ts)\n\n### Pola 6: Footer Kustom\n\nGanti footernya. `footerData` memaparkan data yang tidak dapat diakses oleh ekstensi.\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\nStatistik token tersedia melalui `ctx.sessionManager.getBranch()` dan `ctx.model`.\n\n**Contoh:** [custom-footer.ts](../examples/extensions/custom-footer.ts)\n\n### Pola 7: Editor Kustom (mode vim, dll.)\n\nGanti editor masukan utama dengan implementasi khusus. Berguna untuk pengeditan modal (vim), pengikatan tombol yang berbeda (emacs), atau penanganan masukan khusus.\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**Poin-poin penting:**\n\n- **Perluas `CustomEditor`** (bukan basis `Editor`) untuk mendapatkan pengikatan kunci aplikasi (escape untuk membatalkan, ctrl+d untuk keluar, peralihan model, dll.)\n- **Hubungi `super.handleInput(data)`** untuk kunci yang tidak Anda tangani\n- **Pola pabrik**: `setEditorComponent` menerima fungsi pabrik yang mendapatkan `tui`, `theme`, dan `keybindings`\n- **Lewati `undefined`** untuk memulihkan editor default: `ctx.ui.setEditorComponent(undefined)`\n\n**Contoh:** [modal-editor.ts](../examples/extensions/modal-editor.ts)\n\n## Aturan Utama\n\n1. **Selalu gunakan tema dari panggilan balik** - Jangan mengimpor tema secara langsung. Gunakan `theme` dari panggilan balik `ctx.ui.custom((tui, theme, keybindings, done) =>...)`.\n\n2. **Selalu ketik parameter warna DynamicBorder** - Tulis `(s: string) => theme.fg(\"accent\", s)`, bukan `(s) => theme.fg(\"accent\", s)`.\n\n3. **Panggil tui.requestRender() setelah status berubah** - Di `handleInput`, hubungi `tui.requestRender()` setelah memperbarui status.\n\n4. **Kembalikan objek tiga metode** - Komponen khusus memerlukan `{ render, invalidate, handleInput }`.\n\n5. **Gunakan komponen yang ada** - `SelectList`, `SettingsList`, `BorderedLoader` mencakup 90% kasus. Jangan membangunnya kembali.\n\n## Contoh\n\n- **Seleksi UI**: [examples/extensions/preset.ts](../examples/extensions/preset.ts) - SelectList dengan framing DynamicBorder\n- **Async dengan pembatalan**: [examples/extensions/qna.ts](../examples/extensions/qna.ts) - BorderedLoader untuk panggilan LLM\n- **Pengaturan beralih**: [examples/extensions/tools.ts](../examples/extensions/tools.ts) - Daftar Pengaturan untuk mengaktifkan/menonaktifkan alat\n- **Indikator status**: [examples/extensions/plan-mode/index.ts](../examples/extensions/plan-mode/index.ts) - setStatus dan setWidget\n- **Indikator kerja**: [examples/extensions/working-indicator.ts](../examples/extensions/working-indicator.ts) - setIndikator Kerja\n- **Footer khusus**: [examples/extensions/custom-footer.ts](../examples/extensions/custom-footer.ts) - setFooter dengan statistik\n- **Editor khusus**: [examples/extensions/modal-editor.ts](../examples/extensions/modal-editor.ts) - Pengeditan modal seperti Vim\n- **Permainan ular**: [examples/extensions/snake.ts](../examples/extensions/snake.ts) - Game lengkap dengan input keyboard, game loop\n- **Render alat khusus**: [examples/extensions/todo.ts](../examples/extensions/todo.ts) - renderCall dan renderResult","sourceFile":"tui.md"},"usage":{"title":"Menggunakan Pi","markdown":"Halaman ini mengumpulkan detail penggunaan sehari-hari yang tidak sesuai dengan halaman mulai cepat.\n\n## Mode Interaktif\n\n<p align=\"center\"><img src=\"images/interactive-mode.png\" alt=\"Interactive Mode\" width=\"600\"></p>\n\nAntarmuka memiliki empat area utama:\n\n- **Startup header** - pintasan, memuat context files, prompt templates, keterampilan, dan ekstensi\n- **Pesan** - pesan pengguna, tanggapan asisten, panggilan alat, hasil alat, pemberitahuan, kesalahan, dan UI ekstensi\n- **Editor** - tempat Anda mengetik; warna batas menunjukkan tingkat berpikir saat ini\n- **Footer** - direktori kerja, nama sesi, penggunaan token/cache, biaya, penggunaan konteks, dan model saat ini. Totalnya mencakup respons asisten, penggunaan yang dilaporkan oleh alat, dan pembuatan ringkasan.\n\nEditor dapat diganti sementara dengan UI bawaan seperti `/settings` atau dengan UI ekstensi khusus.\n\n### Fitur Penyunting\n\n| Fitur | Bagaimana |\n|---------|-----|\n| Referensi berkas | Ketik `@` untuk mencari file proyek secara fuzzy |\n| Penyelesaian jalur | Tekan Tab untuk menyelesaikan jalur |\n| Masukan multi-baris | Shift+Enter, atau Ctrl+Enter di Terminal Windows |\n| Salin respons | Ctrl+X menyalin pesan asisten terakhir; di `/tree`, ini menyalin pesan yang dipilih |\n| Gambar | Tempel dengan Ctrl+V, Alt+V di Windows, atau seret ke terminal |\n| Perintah cangkang | `!command` berjalan dan mengirimkan output ke model |\n| Perintah shell tersembunyi | `!!command` berjalan tanpa mengirimkan output ke model |\n| Editor eksternal | Ctrl+G membuka `externalEditor`, `$VISUAL`, `$EDITOR`, Notepad di Windows, atau `nano` di tempat lain |\n\nLihat [Keybindings](keybindings.md) untuk semua pintasan dan penyesuaian.\n\n## Perintah Tebas\n\nKetik `/` di editor untuk membuka penyelesaian perintah. Extensions dapat mendaftarkan perintah khusus, keterampilan tersedia sebagai `/skill:name`, dan prompt templates diperluas melalui `/templatename`.\n\n| Memerintah | Keterangan |\n|---------|-------------|\n| `/login`, `/logout` | Kelola kredensial kunci OAuth atau API |\n| [`/llama`](llama-cpp.md) | Unduh, muat, dan keluarkan model router llama.cpp |\n| `/model` | Ganti model |\n| `/scoped-models` | Mengaktifkan/menonaktifkan model untuk bersepeda Ctrl+P |\n| `/settings` | Tingkat berpikir, tema, penyampaian pesan, transportasi |\n| `/resume` | Pick dari sesi sebelumnya |\n| `/new` | Mulai sesi baru |\n| `/name <name>` | Tetapkan nama tampilan sesi |\n| `/session` | Tampilkan file sesi, ID, pesan, token, dan biaya |\n| `/tree` | Lompat ke titik mana pun dalam sesi dan lanjutkan dari sana |\n| `/trust` | Simpan keputusan kepercayaan proyek untuk sesi mendatang |\n| `/fork` | Buat sesi baru dari pesan pengguna sebelumnya |\n| `/clone` | Gandakan cabang aktif saat ini ke dalam sesi baru |\n| `/compact [prompt]` | Konteks ringkas secara manual, opsional dengan instruksi khusus |\n| `/copy` | Salin pesan asisten terakhir ke papan klip |\n| `/export [file]` | Ekspor sesi ke HTML atau JSONL |\n| `/import <file>` | Impor dan lanjutkan sesi dari file JSONL |\n| `/share` | Unggah sebagai inti GitHub pribadi dengan tautan HTML yang dapat dibagikan |\n| `/reload` | Muat ulang pengikatan kunci, ekstensi, keterampilan, perintah, tema, dan context files |\n| `/hotkeys` | Tampilkan semua pintasan keyboard |\n| `/changelog` | Tampilkan riwayat versi |\n| `/quit` | Keluar dari pi |\n\n## Antrian Pesan\n\nAnda dapat mengirimkan pesan saat agen masih bekerja:\n\n- **Masuk** mengantri pesan kemudi, dikirimkan setelah giliran asisten saat ini selesai menjalankan panggilan alatnya.\n- **Alt+Enter** mengantrekan pesan tindak lanjut, dikirimkan setelah agen menyelesaikan semua pekerjaan.\n- **Escape** membatalkan dan memulihkan antrean pesan ke editor.\n- **Alt+Up** mengambil pesan antrean kembali ke editor.\n\nDi Terminal Windows, Alt+Enter adalah layar penuh secara default. Petakan ulang seperti yang dijelaskan dalam [Terminal setup](terminal-setup.md) jika Anda ingin pi menerima pintasan.\n\nKonfigurasikan pengiriman dalam [Settings](settings.md) dengan `steeringMode` dan `followUpMode`.\n\n## Sesi\n\nSesi disimpan secara otomatis ke `~/.pi/agent/sessions/`, diatur berdasarkan direktori kerja.\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\nPerintah sesi yang berguna:\n\n- `/session` menunjukkan file dan ID sesi saat ini.\n- `/tree` menavigasi dalam file session tree dan dapat meringkas cabang yang ditinggalkan.\n- `/fork` membuat sesi baru dari pesan pengguna sebelumnya.\n- `/clone` menduplikasi cabang aktif saat ini ke dalam file sesi baru.\n- `/compact` merangkum pesan-pesan lama ke dalam konteks bebas.\n\nLihat [Sessions](sessions.md) dan [Compaction](compaction.md) untuk detailnya.\n\n## File Konteks\n\nPi memuat `AGENTS.md` atau `CLAUDE.md` saat startup dari:\n\n- `~/.pi/agent/AGENTS.md` untuk instruksi global\n- direktori induk, berjalan dari direktori kerja saat ini\n- direktori saat ini\n\nJika direktori berisi `AGENTS.override.md`, Pi akan memuatnya, bukan `AGENTS.md` atau `CLAUDE.md` dari direktori tersebut. File konteks dari direktori lain masih berlapis secara normal.\n\nGunakan context files untuk konvensi proyek, perintah, aturan keselamatan, dan preferensi. Nonaktifkan pemuatan dengan `--no-context-files` atau `-nc`.\n\n### File Perintah Sistem\n\nGanti prompt sistem default dengan:\n\n- `.pi/SYSTEM.md` untuk sebuah proyek\n- `~/.pi/agent/SYSTEM.md` secara global\n\nTambahkan ke prompt default tanpa menggantinya dengan `APPEND_SYSTEM.md` di salah satu lokasi.\n\n### Kepercayaan Proyek\n\nPada startup interaktif, pi bertanya sebelum memercayai folder proyek yang berisi pengaturan lokal proyek, sumber daya, atau proyek `.agents/skills` dan tidak memiliki keputusan tersimpan untuk folder atau folder induk di `~/.pi/agent/trust.json`. Mempercayai suatu proyek memungkinkan pi memuat sumber daya `.pi/settings.json` dan `.pi`, menginstal paket proyek yang hilang, dan menjalankan ekstensi proyek.\n\nSebelum keputusan kepercayaan, pi hanya memuat context files, ekstensi pengguna/global, dan ekstensi CLI `-e` sehingga dapat menangani peristiwa `project_trust`. Ekstensi proyek-lokal, ekstensi proyek yang dikelola paket, dan pengaturan proyek dimuat hanya setelah proyek dipercaya. Pemisahan ini juga berlaku ketika berpindah ke sesi dari cwd berbeda yang kepercayaannya belum terselesaikan dalam proses saat ini.\n\nMode non-interaktif (`-p`, `--mode json`, dan `--mode rpc`) tidak menampilkan prompt kepercayaan. Tanpa keputusan perwalian tersimpan yang dapat diterapkan, mereka menggunakan `defaultProjectTrust` dari pengaturan global: `ask` (default) dan `never` mengabaikan sumber daya proyek tersebut, sementara `always` memercayainya. Lewati `--approve`/`-a` atau `--no-approve`/`-na` untuk mengesampingkan kepercayaan proyek dalam sekali proses.\n\nJika tidak ada perpanjangan atau keputusan tersimpan yang berlaku, `defaultProjectTrust` mengontrol perilaku fallback. Setel ke `\"ask\"`, `\"always\"`, atau `\"never\"` di `~/.pi/agent/settings.json`, atau ubah dengan `/settings`.\n\n`pi config` dan perintah paket menggunakan aliran kepercayaan proyek yang sama, kecuali `pi update` tidak pernah diminta. Lewati `--approve` untuk memercayai pengaturan proyek-lokal untuk satu perintah atau `--no-approve` untuk mengabaikannya.\n\nGunakan `/trust` dalam mode interaktif untuk menyimpan keputusan kepercayaan proyek untuk sesi mendatang, termasuk kepercayaan untuk folder induk langsung. Ia hanya menulis `~/.pi/agent/trust.json`; sesi saat ini tidak dimuat ulang, jadi mulai ulang pi agar perubahan diterapkan.\n\n\n## Mengekspor dan Berbagi Sesi\n\nGunakan `/export [file]` untuk menulis sesi ke HTML.\n\nGunakan `/share` untuk mengunggah inti GitHub pribadi dengan tautan HTML yang dapat dibagikan.\n\nJika Anda menggunakan pi untuk pekerjaan sumber terbuka dan ingin mempublikasikan sesi untuk penelitian model, prompt, alat, dan evaluasi, lihat [`badlogic/pi-share-hf`](https://github.com/badlogic/pi-share-hf). Ini menerbitkan sesi ke Hugging Face kumpulan data.\n\n## CLI Referensi\n\n```bash\npi [options] [@files...] [messages...]\n```\n\n### Perintah Paket\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\nPerintah ini mengelola paket pi dan `pi update` dapat memperbarui instalasi pi CLI. Untuk menghapus instalasi pi itu sendiri, lihat [Quickstart](quickstart.md#uninstall). `pi config` dan perintah paket proyek menerima `--approve`/`--no-approve` untuk mempercayai atau mengabaikan pengaturan lokal proyek untuk satu perintah. `pi update` tidak pernah meminta kepercayaan proyek.\n\nLihat [Pi Packages](packages.md) untuk sumber paket dan catatan keamanan.\n\n### Mode\n\n| Bendera | Keterangan |\n|------|-------------|\n| bawaan | Modus interaktif |\n| `-p`, `--print` | Cetak respons dan keluar |\n| `--mode json` | Keluarkan semua peristiwa sebagai baris JSON; lihat [JSON mode](json.md) |\n| `--mode rpc` | mode RPC di atas stdin/stdout; lihat [RPC mode](rpc.md) |\n| `--export <in> [out]` | Ekspor sesi ke HTML |\n\nDalam mode cetak, pi juga membaca pipa stdin dan menggabungkannya ke dalam prompt awal:\n\n```bash\ncat README.md | pi -p \"Summarize this text\"\n```\n\n### Pilihan Model\n\n| Pilihan | Keterangan |\n|--------|-------------|\n| `--provider <name>` | Penyedia, seperti `anthropic`, `openai`, atau `google` |\n| `--model <pattern>` | Pola atau ID model; mendukung `provider/id` dan opsional `:<thinking>` |\n| `--api-key <key>` | API key, mengesampingkan variabel lingkungan |\n| `--thinking <level>` | `off`, `minimal`, `low`, `medium`, `high`, `xhigh`, `max` |\n| `--models <patterns>` | Pola yang dipisahkan koma untuk bersepeda Ctrl+P |\n| `--list-models [search]` | Daftar model yang tersedia |\n\n### Opsi Sesi\n\n| Pilihan | Keterangan |\n|--------|-------------|\n| `-c`, `--continue` | Lanjutkan sesi terbaru |\n| `-r`, `--resume` | Telusuri dan pilih sesi |\n| `--sesi <jalur\\ | tanda pengenal>` | Gunakan file sesi tertentu atau UUID parsial |\n| `--garpu <jalur\\ | tanda pengenal>` | Garpu file sesi atau sebagian UUID ke dalam sesi baru |\n| `--session-dir <dir>` | Direktori penyimpanan sesi khusus |\n| `--no-session` | Modus sementara; jangan simpan |\n| `--name <name>`, `-n <name>` | Tetapkan nama tampilan sesi saat startup |\n\n### Opsi Alat\n\n| Pilihan | Keterangan |\n|--------|-------------|\n| `--tools <list>`, `-t <list>` | Izinkan alat bawaan, ekstensi, dan khusus tertentu yang diizinkan |\n| `--exclude-tools <list>`, `-xt <list>` | Nonaktifkan alat bawaan, ekstensi, dan khusus tertentu |\n| `--no-builtin-tools`, `-nbt` | Nonaktifkan alat bawaan tetapi tetap aktifkan ekstensi/alat khusus |\n| `--no-tools`, `-nt` | Nonaktifkan semua alat |\n\nAlat bawaan: `read`, `bash`, `edit`, `write`, `grep`, `find`, `ls`.\n\n### Opsi Sumber Daya\n\n| Pilihan | Keterangan |\n|--------|-------------|\n| `-e`, `--extension <source>` | Muat ekstensi dari jalur, npm, atau git; dapat diulang |\n| `--no-extensions` | Nonaktifkan penemuan ekstensi |\n| `--skill <path>` | Muat keterampilan; dapat diulang |\n| `--no-skills` | Nonaktifkan penemuan keterampilan |\n| `--prompt-template <path>` | Muat template prompt; dapat diulang |\n| `--no-prompt-templates` | Nonaktifkan penemuan template cepat |\n| `--theme <path>` | Muat tema; dapat diulang |\n| `--no-themes` | Nonaktifkan penemuan tema |\n| `--no-context-files`, `-nc` | Nonaktifkan penemuan `AGENTS.md` dan `CLAUDE.md` |\n\nGabungkan `--no-*` dengan tanda eksplisit untuk memuat apa yang Anda butuhkan, abaikan pengaturan. Contoh:\n\n```bash\npi --no-extensions -e ./my-extension.ts\n```\n\n### Opsi Lainnya\n\n| Pilihan | Keterangan |\n|--------|-------------|\n| `--system-prompt <text>` | Ganti perintah default; context files dan keterampilan masih ditambahkan |\n| `--append-system-prompt <text>` | Tambahkan ke perintah sistem |\n| `--tui-mode <mode>` | Mode TUI: `regular` (default) atau eksperimental `fullscreen` |\n| `--verbose` | Paksa startup yang panjang lebar |\n| `-a`, `--approve` | Percayai file lokal proyek untuk proses ini |\n| `-na`, `--no-approve` | Abaikan file lokal proyek untuk proses ini |\n| `-h`, `--help` | Tunjukkan bantuan |\n| `-v`, `--version` | Tampilkan versi |\n\nDalam mode `fullscreen`, transkrip bergulir di dalam area pandang terminal sementara pesan dalam antrean, status kerja, widget ekstensi, editor, dan footer tetap berada di bagian bawah. Input mouse/trackpad akan menggulir wilayah di bawah penunjuk; tindakan area pandang keyboard selalu tersedia. Gambar sebaris berfungsi di terminal yang mendukung protokol grafis Kitty, termasuk Kitty dan Ghostty. Di iTerm2 mereka dirender sebagai placeholder teks karena protokol gambar sebarisnya tidak dapat menghapus atau memotong penempatan selama pengguliran milik aplikasi. Dalam mode `regular`, pi menggunakan layar utama dan scrollback milik terminal, dan gambar sebaris iTerm2 terus ditampilkan secara normal.\n\nAtur **TUI mode** di `/settings` untuk segera beralih antara `regular` dan `fullscreen` dan pilih default untuk sesi berikutnya. **Output keluar layar penuh** mengontrol apakah keluar dari layar penuh akan mencetak transkrip akhir atau memulihkan layar sebelumnya dan hanya mencetak petunjuk melanjutkan sesi.\n\n### Argumen File\n\nAwali file dengan `@` untuk memasukkannya ke dalam pesan:\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### Contoh\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## Prinsip Desain\n\nPi menjaga inti tetap kecil dan mendorong perilaku spesifik alur kerja ke dalam ekstensi, keterampilan, prompt templates, dan paket.\n\nIni sengaja tidak menyertakan MCP bawaan, sub-agen, pop-up izin, mode rencana, tugas, atau latar belakang bash. Anda dapat membangun atau menginstal alur kerja tersebut sebagai ekstensi atau paket, atau menggunakan alat eksternal seperti container dan tmux.\n\nUntuk alasan selengkapnya, baca [blog post](https://mariozechner.at/posts/2025-11-30-pi-coding-agent/).","sourceFile":"usage.md"},"windows":{"title":"Pengaturan Windows","markdown":"Pi memerlukan shell bash di Windows. Lokasi yang diperiksa (secara berurutan):\n\n1. Jalur khusus dari `~/.pi/agent/settings.json`\n2. Git Pesta (`C:\\Program Files\\Git\\bin\\bash.exe`)\n3. `bash.exe` di PATH (Cygwin, MSYS2, WSL)\n\nBagi sebagian besar pengguna, [Git for Windows](https://git-scm.com/download/win) sudah cukup.\n\n## Jalur Shell Kustom\n\n```json\n{\n  \"shellPath\": \"C:\\\\cygwin64\\\\bin\\\\bash.exe\"\n}\n```","sourceFile":"windows.md"}}},"navigation":{"id":[{"title":"Mulai di sini","items":[{"title":"Pi Dokumentasi","path":"/docs/latest","slug":"index"},{"title":"Mulai cepat","path":"/docs/latest/quickstart","slug":"quickstart"},{"title":"Menggunakan Pi","path":"/docs/latest/usage","slug":"usage"},{"title":"Providers","path":"/docs/latest/providers","slug":"providers"},{"title":"Keamanan","path":"/docs/latest/security","slug":"security"},{"title":"Kontainerisasi","path":"/docs/latest/containerization","slug":"containerization"},{"title":"Pengaturan","path":"/docs/latest/settings","slug":"settings"},{"title":"Pengikatan kunci","path":"/docs/latest/keybindings","slug":"keybindings"},{"title":"Sesi","path":"/docs/latest/sessions","slug":"sessions"},{"title":"Pemadatan & Peringkasan Cabang","path":"/docs/latest/compaction","slug":"compaction"}]},{"title":"Kustomisasi","items":[{"title":"Extensions","path":"/docs/latest/extensions","slug":"extensions"},{"title":"Skills","path":"/docs/latest/skills","slug":"skills"},{"title":"Templat Cepat","path":"/docs/latest/prompt-templates","slug":"prompt-templates"},{"title":"Tema","path":"/docs/latest/themes","slug":"themes"},{"title":"Pi Packages","path":"/docs/latest/packages","slug":"packages"},{"title":"Kustom Models","path":"/docs/latest/models","slug":"models"},{"title":"Kustom Providers","path":"/docs/latest/custom-provider","slug":"custom-provider"}]},{"title":"Referensi","items":[{"title":"Format File Sesi","path":"/docs/latest/session-format","slug":"session-format"}]},{"title":"Penggunaan programatik","items":[{"title":"SDK","path":"/docs/latest/sdk","slug":"sdk"},{"title":"RPC Modus","path":"/docs/latest/rpc","slug":"rpc"},{"title":"JSON Mode Aliran Acara","path":"/docs/latest/json","slug":"json"},{"title":"TUI Komponen","path":"/docs/latest/tui","slug":"tui"}]},{"title":"Pengaturan platform","items":[{"title":"Pengaturan Windows","path":"/docs/latest/windows","slug":"windows"},{"title":"Termux (Android) Pengaturan","path":"/docs/latest/termux","slug":"termux"},{"title":"tmux Pengaturan","path":"/docs/latest/tmux","slug":"tmux"},{"title":"Pengaturan Terminal","path":"/docs/latest/terminal-setup","slug":"terminal-setup"},{"title":"Alias ​​Kerang","path":"/docs/latest/shell-aliases","slug":"shell-aliases"}]},{"title":"Pengembangan","items":[{"title":"Perkembangan","path":"/docs/latest/development","slug":"development"}]}]}}
