拡張機能
pi は拡張機能を作成できます。あなたのユースケースに合わせて構築するよう依頼してください。
拡張機能は、pi の動作を拡張する TypeScript モジュールです。ライフサイクル イベントのサブスクライブ、LLM によって呼び出し可能なカスタム ツールの登録、コマンドの追加などを行うことができます。
/reload の配置: 自動検出のために、拡張機能を
~/.pi/agent/extensions/(グローバル) または.pi/extensions/(プロジェクトローカル) に配置します。pi -e ./path.tsは簡単なテストの場合にのみ使用してください。自動検出された場所にある拡張機能は、/reloadを使用してホットリロードできます。
主な機能:
- カスタム ツール - LLM が
pi.registerTool()経由で呼び出すことができるツールを登録します。 - イベントインターセプト - ツール呼び出しのブロックまたは変更、コンテキストの挿入、圧縮のカスタマイズ
- ユーザー操作 -
ctx.ui経由でユーザーにプロンプトを表示します (選択、確認、入力、通知) - カスタム UI コンポーネント - 複雑な対話のための
ctx.ui.custom()経由のキーボード入力を備えた完全な TUI コンポーネント - カスタム コマンド -
pi.registerCommand()経由で/mycommandのようなコマンドを登録します - セッションの永続性 -
pi.appendEntry()による再起動後も保存されるストア状態 - カスタム レンダリング - ツールの呼び出し/結果およびメッセージが TUI にどのように表示されるかを制御します
ユースケース例:
- 許可ゲート(
rm -rf、sudoなどの前で確認) - Git チェックポイント設定 (各ターンにスタッシュ、ブランチ時に復元)
- パス保護 (
.env、node_modules/への書き込みをブロック) - カスタム圧縮 (会話を自分の方法で要約)
- 会話の要約 (
summarize.tsの例を参照) - インタラクティブなツール (質問、ウィザード、カスタム ダイアログ)
- ステートフル ツール (ToDo リスト、接続プール)
- 外部統合 (ファイル ウォッチャー、Webhook、CI トリガー)
- 待っている間のゲーム (
snake.tsの例を参照)
動作する実装については、examples/extensions/ を参照してください。
- クイックスタート
- 拡張機能の場所
- 利用可能なインポート
- 拡張機能の作成
- イベント
- ExtensionContext
- ExtensionCommandContext
- ExtensionAPI メソッド
- 状態管理
- カスタムツール
- カスタム UI
- エラー処理
- モードの動作
- サンプルリファレンス
クイックスタート
Section titled “クイックスタート”~/.pi/agent/extensions/my-extension.ts を作成します。
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";import { Type } from "typebox";
export default function (pi: ExtensionAPI) { // React to events pi.on("session_start", async (_event, ctx) => { ctx.ui.notify("Extension loaded!", "info"); });
pi.on("tool_call", async (event, ctx) => { if (event.toolName === "bash" && event.input.command?.includes("rm -rf")) { const ok = await ctx.ui.confirm("Dangerous!", "Allow rm -rf?"); if (!ok) return { block: true, reason: "Blocked by user" }; } });
// Register a custom tool pi.registerTool({ name: "greet", label: "Greet", description: "Greet someone by name", parameters: Type.Object({ name: Type.String({ description: "Name to greet" }), }), async execute(toolCallId, params, signal, onUpdate, ctx) { return { content: [{ type: "text", text: `Hello, ${params.name}!` }], details: {}, }; }, });
// Register a command pi.registerCommand("hello", { description: "Say hello", handler: async (args, ctx) => { ctx.ui.notify(`Hello ${args || "world"}!`, "info"); }, });}--extension (または -e) フラグを使用してテストします。
pi -e ./my-extension.ts拡張機能の場所
Section titled “拡張機能の場所”セキュリティ: 拡張機能は完全なシステム権限で実行され、任意のコードを実行できます。信頼できるソースからのみインストールしてください。
拡張機能は信頼できる場所から自動的に検出されます。プロジェクト ローカルの .pi/extensions エントリは、プロジェクトが信頼された後にのみロードされます。
| 場所 | スコープ |
|---|---|
~/.pi/agent/extensions/*.ts | グローバル(すべてのプロジェクト) |
~/.pi/agent/extensions/*/index.ts | グローバル(サブディレクトリ) |
.pi/extensions/*.ts | プロジェクトローカル |
.pi/extensions/*/index.ts | プロジェクトローカル(サブディレクトリ) |
settings.json 経由の追加パス:
{ "packages": [ "git:github.com/user/repo@v1" ], "extensions": [ "/path/to/local/extension.ts", "/path/to/local/extension/dir" ]}npm または git を介して拡張機能を pi パッケージとして共有するには、packages.md を参照してください。
利用可能なインポート
Section titled “利用可能なインポート”| パッケージ | 目的 |
|---|---|
@earendil-works/pi-coding-agent | 拡張タイプ(ExtensionAPI、ExtensionContext、イベント) |
typebox | ツールパラメータのスキーマ定義 |
@earendil-works/pi-ai | AI ユーティリティ(Google 互換列挙型の StringEnum) |
@earendil-works/pi-tui | カスタムレンダリング用 TUI コンポーネント |
npm の依存関係も機能します。拡張機能の隣 (または親ディレクトリ) に package.json を追加し、npm install を実行すると、node_modules/ からのインポートが自動的に解決されます。
pi install (npm または git) でインストールされた分散 pi パッケージの場合、ランタイム deps は dependencies にある必要があります。パッケージのインストールでは、デフォルトで実稼働インストール (npm install --omit=dev) が使用されるため、実行時には devDependencies を使用できません。 npmCommand が設定されている場合、git パッケージはラッパーとの互換性のためにプレーン install を使用します。
Node.js ビルトイン (node:fs、node:path など) も利用できます。
拡張機能の作成
Section titled “拡張機能の作成”拡張機能は、ExtensionAPI を受け取るデフォルトのファクトリ関数をエクスポートします。ファクトリは同期または非同期にすることができます。
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
export default function (pi: ExtensionAPI) { // Subscribe to events pi.on("event_name", async (event, ctx) => { // ctx.ui for user interaction const ok = await ctx.ui.confirm("Title", "Are you sure?"); ctx.ui.notify("Done!", "info"); ctx.ui.setStatus("my-ext", "Processing..."); // Footer status ctx.ui.setWidget("my-ext", ["Line 1", "Line 2"]); // Widget above editor (default) });
// Register tools, commands, shortcuts, flags pi.registerTool({ ... }); pi.registerCommand("name", { ... }); pi.registerShortcut("ctrl+x", { ... }); pi.registerFlag("my-flag", { ... });}拡張機能は jiti 経由でロードされるため、TypeScript はコンパイルなしで動作します。
ファクトリが Promise を返した場合、pi は起動を続行する前にそれを待ちます。つまり、非同期初期化は、session_start より前、resources_discover より前、そして pi.registerProvider() 経由でキューに入れられたプロバイダー登録がフラッシュされる前に完了します。
非同期ファクトリー関数
Section titled “非同期ファクトリー関数”リモート構成の取得や利用可能なモデルの動的検出などの 1 回限りの起動作業には、非同期ファクトリーを使用します。
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
export default async function (pi: ExtensionAPI) { const response = await fetch("http://localhost:1234/v1/models"); const payload = (await response.json()) as { data: Array<{ id: string; name?: string; context_window?: number; max_tokens?: number; }>; };
pi.registerProvider("local-openai", { baseUrl: "http://localhost:1234/v1", apiKey: "$LOCAL_OPENAI_API_KEY", api: "openai-completions", models: payload.data.map((model) => ({ id: model.id, name: model.name ?? model.id, reasoning: false, input: ["text"], cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, contextWindow: model.context_window ?? 128000, maxTokens: model.max_tokens ?? 4096, })), });}このパターンにより、フェッチされたモデルが通常の起動中および pi --list-models で使用可能になります。
有効期間の長いリソースとシャットダウン
Section titled “有効期間の長いリソースとシャットダウン”拡張機能ファクトリーは、セッションを開始しない呼び出しで実行される場合があります。プロセス、ソケット、ファイル ウォッチャー、タイマーなどのバックグラウンド リソースを工場出荷時に開始しないでください。
session_start またはリソースを必要とするコマンド/ツール/イベントが実行されるまで、バックグラウンド リソースの起動を延期します。べき等 session_shutdown ハンドラーを登録して、開始したセッション スコープのリソースを閉じます。
拡張スタイル
Section titled “拡張スタイル”単一ファイル - 最も単純で、小さな拡張子の場合:
~/.pi/agent/extensions/└── my-extension.tsindex.ts を持つディレクトリ - 複数のファイル拡張子の場合:
~/.pi/agent/extensions/└── my-extension/ ├── index.ts # Entry point (exports default function) ├── tools.ts # Helper module └── utils.ts # Helper module依存関係のあるパッケージ - npm パッケージを必要とする拡張機能の場合:
~/.pi/agent/extensions/└── my-extension/ ├── package.json # Declares dependencies and entry points ├── package-lock.json ├── node_modules/ # After npm install └── src/ └── index.ts{ "name": "my-extension", "dependencies": { "zod": "^3.0.0", "chalk": "^5.0.0" }, "pi": { "extensions": ["./src/index.ts"] }}拡張機能ディレクトリで npm install を実行すると、node_modules/ からのインポートが自動的に機能します。
ライフサイクルの概要
Section titled “ライフサイクルの概要”pi starts │ ├─► project_trust (user/global and CLI extensions only, before project resources load) ├─► session_start { reason: "startup" } └─► resources_discover { reason: "startup" } │ ▼user sends prompt ─────────────────────────────────────────┐ │ │ ├─► (extension commands checked first, bypass if found) │ ├─► input (can intercept, transform, or handle) │ ├─► (skill/template expansion if not handled) │ ├─► before_agent_start (can inject message, modify system prompt) ├─► agent_start │ ├─► message_start / message_update / message_end │ │ │ │ ┌─── turn (repeats while LLM calls tools) ───┐ │ │ │ │ │ │ ├─► turn_start │ │ │ ├─► context (can modify messages) │ │ │ ├─► before_provider_request (can inspect or replace payload) │ ├─► after_provider_response (status + headers, before stream consume) │ │ │ │ │ │ LLM responds, may call tools: │ │ │ │ ├─► tool_execution_start │ │ │ │ ├─► tool_call (can block) │ │ │ │ ├─► tool_execution_update │ │ │ │ ├─► tool_result (can modify) │ │ │ │ └─► tool_execution_end │ │ │ │ │ │ │ └─► turn_end │ │ │ │ └─► agent_end │ │user sends another prompt ◄────────────────────────────────┘
/new (new session) or /resume (switch session) ├─► session_before_switch (can cancel) ├─► session_shutdown ├─► session_start { reason: "new" | "resume", previousSessionFile? } └─► resources_discover { reason: "startup" }
/fork or /clone ├─► session_before_fork (can cancel) ├─► session_shutdown ├─► session_start { reason: "fork", previousSessionFile } └─► resources_discover { reason: "startup" }
/compact or auto-compaction ├─► session_before_compact (can cancel or customize) └─► session_compact
/tree navigation ├─► session_before_tree (can cancel or customize) └─► session_tree
/model or Ctrl+P (model selection/cycling) ├─► thinking_level_select (if model change changes/clamps thinking level) └─► model_select
thinking level changes (settings, keybinding, pi.setThinkingLevel()) └─► thinking_level_select
exit (Ctrl+C, Ctrl+D, SIGHUP, SIGTERM) └─► session_shutdown起動イベント
Section titled “起動イベント”project_trust
Section titled “project_trust”pi が動的構成 (.pi または .agents/skills) を持つプロジェクトを信頼するかどうかを決定する前に起動されます。これは起動時と、現在のプロセスで信頼が解決されていない cwd にセッション置換 (/resume など) が入ったときに実行されます。ユーザー/グローバル拡張機能と CLI -e 拡張機能のみが参加します。プロジェクトローカル拡張機能は、信頼が解決されるまでロードされません。
pi.on("project_trust", async (event, ctx) => { // event.cwd - current working directory // ctx has a limited trust context: cwd, mode, hasUI, and select/confirm/input/notify UI helpers if (await ctx.ui.confirm("Trust project?", event.cwd)) { return { trusted: "yes", remember: true }; } return { trusted: "undecided" };});project_trust ハンドラーは { trusted: "yes" | "no" | "undecided" } を返す必要があります。 "yes" または "no" を返すユーザー/グローバルまたは CLI 拡張機能が決定を所有します。最初の「はい/いいえ」の決定が優先され、組み込みの信頼プロンプトが抑制されます。 remember: true を使用して、はい/いいえの決定を永続化します。それ以外の場合は、現在のプロセスにのみ適用されます。 "undecided" を返して、後のハンドラーまたは組み込みの信頼フローに決定させます。プロンプトを表示する前に、ctx.hasUI を確認してください。どのハンドラーも Yes/No を返さない場合、通常の信頼解決が続行されます。保存された trust.json 決定が最初に適用され、次に defaultProjectTrust がデフォルトで pi が質問するか、信頼するか、拒否するかを制御します。
リソースイベント
Section titled “リソースイベント”resources_discover
Section titled “resources_discover”session_start の後に起動されるため、拡張機能は追加のスキル、プロンプト、テーマのパスを提供できます。
起動パスは reason: "startup" を使用します。リロードでは reason: "reload" を使用します。
pi.on("resources_discover", async (event, _ctx) => { // event.cwd - current working directory // event.reason - "startup" | "reload" return { skillPaths: ["/path/to/skills"], promptPaths: ["/path/to/prompts"], themePaths: ["/path/to/themes"], };});セッションイベント
Section titled “セッションイベント”セッションストレージの内部と SessionManager API については、セッション形式 を参照してください。
session_start
Section titled “session_start”セッションの開始時、ロード時、またはリロード時に発生します。
pi.on("session_start", async (event, ctx) => { // event.reason - "startup" | "reload" | "new" | "resume" | "fork" // event.previousSessionFile - present for "new", "resume", and "fork" ctx.ui.notify(`Session: ${ctx.sessionManager.getSessionFile() ?? "ephemeral"}`, "info");});session_before_switch
Section titled “session_before_switch”新しいセッションを開始する前 (/new)、またはセッションを切り替える前 (/resume) に発生します。
pi.on("session_before_switch", async (event, ctx) => { // event.reason - "new" or "resume" // event.targetSessionFile - session we're switching to (only for "resume")
if (event.reason === "new") { const ok = await ctx.ui.confirm("Clear?", "Delete all messages?"); if (!ok) return { cancel: true }; }});切り替えまたは新しいセッションのアクションが成功した後、pi は古い拡張機能インスタンスに対して session_shutdown を発行し、新しいセッションに対して拡張機能をリロードおよび再バインドしてから、reason: "new" | "resume" および previousSessionFile を含む session_start を発行します。
session_shutdown でクリーンアップ作業を実行し、その後 session_start でメモリ内の状態を再確立します。
session_before_fork
Section titled “session_before_fork”/fork 経由でフォークするとき、または /clone 経由でクローン作成するときに発生します。
pi.on("session_before_fork", async (event, ctx) => { // event.entryId - ID of the selected entry // event.position - "before" for /fork, "at" for /clone return { cancel: true }; // Cancel fork/clone // OR return { skipConversationRestore: true }; // Reserved for future conversation restore control});フォークまたはクローンが成功した後、pi は古い拡張機能インスタンスに対して session_shutdown を発行し、新しいセッションに対して拡張機能をリロードおよび再バインドしてから、reason: "fork" および previousSessionFile を含む session_start を発行します。
session_shutdown でクリーンアップ作業を実行し、その後 session_start でメモリ内の状態を再確立します。
session_before_compact / session_compact
Section titled “session_before_compact / session_compact”圧縮時に発火します。詳細については、compaction.md を参照してください。
pi.on("session_before_compact", async (event, ctx) => { const { preparation, branchEntries, customInstructions, signal } = event;
// Cancel: return { cancel: true };
// Custom summary: return { compaction: { summary: "...", firstKeptEntryId: preparation.firstKeptEntryId, tokensBefore: preparation.tokensBefore, } };});
pi.on("session_compact", async (event, ctx) => { // event.compactionEntry - the saved compaction // event.fromExtension - whether extension provided it});セッション前ツリー / セッションツリー
Section titled “セッション前ツリー / セッションツリー”/tree ナビゲーションで発生します。ツリー ナビゲーションの概念については、セッション を参照してください。
pi.on("session_before_tree", async (event, ctx) => { const { preparation, signal } = event; return { cancel: true }; // OR provide custom summary: return { summary: { summary: "...", details: {} } };});
pi.on("session_tree", async (event, ctx) => { // event.newLeafId, oldLeafId, summaryEntry, fromExtension});セッションシャットダウン
Section titled “セッションシャットダウン”開始されたセッション ランタイムが破棄される前に発生します。これを使用して、session_start または他のセッション スコープのフックから開かれたリソースをクリーンアップします。
pi.on("session_shutdown", async (event, ctx) => { // event.reason - "quit" | "reload" | "new" | "resume" | "fork" // event.targetSessionFile - destination session for session replacement flows // Cleanup, save state, etc.});エージェントイベント
Section titled “エージェントイベント”before_agent_start
Section titled “before_agent_start”ユーザーがプロンプトを送信した後、エージェント ループの前に発生します。メッセージを挿入したり、システム プロンプトを変更したりできます。
pi.on("before_agent_start", async (event, ctx) => { // event.prompt - user's prompt text // event.images - attached images (if any) // event.systemPrompt - current chained system prompt for this handler // (includes changes from earlier before_agent_start handlers) // event.systemPromptOptions - structured options used to build the system prompt // .customPrompt - any custom system prompt (from --system-prompt, SYSTEM.md, or custom templates) // .selectedTools - tools currently active in the prompt // .toolSnippets - one-line descriptions for each tool // .promptGuidelines - custom guideline bullets // .appendSystemPrompt - text from --append-system-prompt flags // .cwd - working directory // .contextFiles - AGENTS.md files and other loaded context files // .skills - loaded skills
return { // Inject a persistent message (stored in session, sent to LLM) message: { customType: "my-extension", content: "Additional context for the LLM", display: true, }, // Replace the system prompt for this turn (chained across extensions) systemPrompt: event.systemPrompt + "\n\nExtra instructions for this turn...", };});systemPromptOptions フィールドにより、拡張機能は Pi がシステム プロンプトを構築するために使用するのと同じ構造化データにアクセスできるようになります。これにより、リソースを再検出したりフラグを再解析したりすることなく、Pi がロードしたもの (カスタム プロンプト、ガイドライン、ツール スニペット、コンテキスト ファイル、スキル) を検査できます。これは、拡張機能がユーザー指定の設定を尊重しながら、情報に基づいてシステム プロンプトに詳細な変更を加える必要がある場合に使用します。
before_agent_start 内では、event.systemPrompt および ctx.getSystemPrompt() は両方とも、現在のハンドラーの時点でのチェーンされたシステム プロンプトを反映しています。その後の before_agent_start ハンドラーは、引き続きこれを再度変更できます。
agent_start / agent_end
Section titled “agent_start / agent_end”ユーザープロンプトごとに 1 回起動されます。
pi.on("agent_start", async (_event, ctx) => {});
pi.on("agent_end", async (event, ctx) => { // event.messages - messages from this prompt});turn_start / turn_end
Section titled “turn_start / turn_end”各ターン(LLM 応答 1 回+ツール呼び出し)ごとに起動されます。
pi.on("turn_start", async (event, ctx) => { // event.turnIndex, event.timestamp});
pi.on("turn_end", async (event, ctx) => { // event.turnIndex, event.message, event.toolResults});message_start / message_update / message_end
Section titled “message_start / message_update / message_end”メッセージのライフサイクル更新のために発生します。
message_startおよびmessage_endは、ユーザー、アシスタント、toolResult メッセージに対して起動されます。message_updateは、アシスタントのストリーミング更新のために起動します。message_endハンドラーは、確定されたメッセージを置き換えるために{ message }を返すことができます。置き換えるメッセージは同じroleを維持する必要があります。
pi.on("message_start", async (event, ctx) => { // event.message});
pi.on("message_update", async (event, ctx) => { // event.message // event.assistantMessageEvent (token-by-token stream event)});
pi.on("message_end", async (event, ctx) => { if (event.message.role !== "assistant") return;
return { message: { ...event.message, usage: { ...event.message.usage, cost: { ...event.message.usage.cost, total: 0.123, }, }, }, };});tool_execution_start / tool_execution_update / tool_execution_end
Section titled “tool_execution_start / tool_execution_update / tool_execution_end”ツール実行ライフサイクルの更新のために発生します。
パラレルツールモード:
tool_execution_startは、プリフライト フェーズ中にアシスタント ソースの順序で発行されます。tool_execution_updateイベントはツール間でインターリーブされる可能性がありますtool_execution_endは、各ツールが完了した後、ツールの完了順に出力されます。- 最終
toolResultメッセージ イベントは、引き続きアシスタント ソースの順序で後から出力されます。
pi.on("tool_execution_start", async (event, ctx) => { // event.toolCallId, event.toolName, event.args});
pi.on("tool_execution_update", async (event, ctx) => { // event.toolCallId, event.toolName, event.args, event.partialResult});
pi.on("tool_execution_end", async (event, ctx) => { // event.toolCallId, event.toolName, event.result, event.isError});context
Section titled “context”各 LLM 呼び出しの前に発生します。メッセージを非破壊的に変更します。メッセージの種類については、セッション形式 を参照してください。
pi.on("context", async (event, ctx) => { // event.messages - deep copy, safe to modify const filtered = event.messages.filter(m => !shouldPrune(m)); return { messages: filtered };});before_provider_request
Section titled “before_provider_request”プロバイダー固有のペイロードが構築された後、リクエストが送信される直前に起動されます。ハンドラーは拡張機能のロード順序で実行されます。 undefined を返すと、ペイロードは変更されません。他の値を返すと、後のハンドラーと実際のリクエストのペイロードが置き換えられます。
このフックは、プロバイダーレベルのシステム命令を書き換えたり、完全に削除したりできます。これらのペイロード レベルの変更は、最終的なシリアル化されたプロバイダー ペイロードではなく Pi のシステム プロンプト文字列を報告する ctx.getSystemPrompt() には反映されません。
pi.on("before_provider_request", (event, ctx) => { console.log(JSON.stringify(event.payload, null, 2));
// Optional: replace payload // return { ...event.payload, temperature: 0 };});これは主に、プロバイダーのシリアル化とキャッシュの動作をデバッグするのに役立ちます。
after_provider_response
Section titled “after_provider_response”HTTP 応答を受信した後、そのストリーム本体が消費される前に発生します。ハンドラーは拡張機能のロード順序で実行されます。
pi.on("after_provider_response", (event, ctx) => { // event.status - HTTP status code // event.headers - normalized response headers if (event.status === 429) { console.log("rate limited", event.headers["retry-after"]); }});ヘッダーが利用できるかどうかは、プロバイダーとトランスポートによって異なります。 HTTP 応答を抽象化するプロバイダーはヘッダーを公開できない場合があります。
モデルイベント
Section titled “モデルイベント”model_select
Section titled “model_select”/model コマンド、モデルのサイクリング (Ctrl+P)、またはセッションの復元によってモデルが変更されたときに発生します。
pi.on("model_select", async (event, ctx) => { // event.model - newly selected model // event.previousModel - previous model (undefined if first selection) // event.source - "set" | "cycle" | "restore"
const prev = event.previousModel ? `${event.previousModel.provider}/${event.previousModel.id}` : "none"; const next = `${event.model.provider}/${event.model.id}`;
ctx.ui.notify(`Model changed (${event.source}): ${prev} -> ${next}`, "info");});これを使用して、UI 要素 (ステータス バー、フッター) を更新したり、アクティブなモデルが変更されたときにモデル固有の初期化を実行したりできます。
thinking_level_select
Section titled “thinking_level_select”思考レベルが変化したときに発生します。これは通知のみです。ハンドラーの戻り値は無視されます。
pi.on("thinking_level_select", async (event, ctx) => { // event.level - newly selected thinking level // event.previousLevel - previous thinking level
ctx.ui.setStatus("thinking", `thinking: ${event.level}`);});pi.setThinkingLevel()、モデルの変更、または組み込みの思考レベル コントロールによってアクティブな思考レベルが変更されたときに、これを使用して拡張機能 UI を更新します。
ツールイベント
Section titled “ツールイベント”tool_call
Section titled “tool_call”tool_execution_start の後、ツールが実行される前に発生します。ブロックできます。 isToolCallEventType を使用して型を絞り込み、型付きの入力を取得します。
tool_call を実行する前に、pi は、以前に発行されたエージェント イベントが AgentSession を介して排出し終わるのを待ちます。これは、現在のアシスタント ツール呼び出しメッセージを通じて ctx.sessionManager が最新であることを意味します。
デフォルトの並列ツール実行モードでは、同じアシスタント メッセージからの兄弟ツール呼び出しが順番にプリフライトされ、同時に実行されます。 tool_call では、ctx.sessionManager の同じアシスタント メッセージからの兄弟ツールの結果が表示されることは保証されていません。
event.input は変更可能です。実行前にツール引数にパッチを適用するために、適切な場所で変更します。
動作の保証:
event.inputへの変更は実際のツールの実行に影響します- 後の
tool_callハンドラーは、前のハンドラーによる変更を認識します - 変更後に再検証は実行されません
tool_callの戻り値は{ block: true, reason?: string }によるブロックのみを制御します
import { isToolCallEventType } from "@earendil-works/pi-coding-agent";
pi.on("tool_call", async (event, ctx) => { // event.toolName - "bash", "read", "write", "edit", etc. // event.toolCallId // event.input - tool parameters (mutable)
// Built-in tools: no type params needed if (isToolCallEventType("bash", event)) { // event.input is { command: string; timeout?: number } event.input.command = `source ~/.profile\n${event.input.command}`;
if (event.input.command.includes("rm -rf")) { return { block: true, reason: "Dangerous command" }; } }
if (isToolCallEventType("read", event)) { // event.input is { path: string; offset?: number; limit?: number } console.log(`Reading: ${event.input.path}`); }});カスタム ツール入力の入力
Section titled “カスタム ツール入力の入力”カスタム ツールは入力タイプをエクスポートする必要があります。
export type MyToolInput = Static<typeof myToolSchema>;isToolCallEventType を明示的な型パラメータとともに使用します。
import { isToolCallEventType } from "@earendil-works/pi-coding-agent";import type { MyToolInput } from "my-extension";
pi.on("tool_call", (event) => { if (isToolCallEventType<"my_tool", MyToolInput>("my_tool", event)) { event.input.action; // typed }});tool_result
Section titled “tool_result”ツールの実行終了後、tool_execution_end および最終的なツール結果メッセージイベントが発行される前に発生します。結果を変更できます。
パラレル ツール モードでは、tool_result と tool_execution_end がツールの完了順序でインターリーブする可能性がありますが、最終 toolResult メッセージ イベントは引き続きアシスタント ソース順序で後で発行されます。
tool_result ハンドラーはミドルウェアのように連鎖します。
- ハンドラーは拡張機能のロード順序で実行されます
- 各ハンドラーは、以前のハンドラー変更後の最新の結果を参照します。
- ハンドラーは部分的なパッチ (
content、details、またはisError) を返すことができます。省略されたフィールドは現在の値を保持します
ハンドラー内でネストされた非同期作業には ctx.signal を使用します。これにより、Esc でモデル呼び出し、fetch()、および拡張機能によって開始されたその他の中止対応操作をキャンセルできるようになります。
import { isBashToolResult } from "@earendil-works/pi-coding-agent";
pi.on("tool_result", async (event, ctx) => { // event.toolName, event.toolCallId, event.input // event.content, event.details, event.isError
if (isBashToolResult(event)) { // event.details is typed as BashToolDetails }
const response = await fetch("https://example.com/summarize", { method: "POST", body: JSON.stringify({ content: event.content }), signal: ctx.signal, });
// Modify result: return { content: [...], details: {...}, isError: false };});ユーザー Bash イベント
Section titled “ユーザー Bash イベント”user_bash
Section titled “user_bash”ユーザーが ! または !! コマンドを実行すると発生します。インターセプト可能。
import { createLocalBashOperations } from "@earendil-works/pi-coding-agent";
pi.on("user_bash", (event, ctx) => { // event.command - the bash command // event.excludeFromContext - true if !! prefix // event.cwd - working directory
// Option 1: Provide custom operations (e.g., SSH) return { operations: remoteBashOps };
// Option 2: Wrap pi's built-in local bash backend const local = createLocalBashOperations(); return { operations: { exec(command, cwd, options) { return local.exec(`source ~/.profile\n${command}`, cwd, options); } } };
// Option 3: Full replacement - return result directly return { result: { output: "...", exitCode: 0, cancelled: false, truncated: false } };});入力イベント
Section titled “入力イベント”拡張コマンドのチェック後、スキルとテンプレートの展開前に、ユーザー入力を受信したときに発生します。イベントは生の入力テキストを参照するため、/skill:foo と /template はまだ展開されていません。
処理順序:
- 拡張コマンド (
/cmd) が最初にチェックされます。見つかった場合、ハンドラーが実行され、入力イベントはスキップされます inputイベントが発生します。インターセプト、変換、または処理が可能- 処理されない場合: スキルコマンド (
/skill:name) がスキルコンテンツに展開されます - 処理されない場合: プロンプトテンプレート (
/template) がテンプレートコンテンツに展開されます - エージェント処理が開始されます (
before_agent_startなど)
pi.on("input", async (event, ctx) => { // event.text - raw input (before skill/template expansion) // event.images - attached images, if any // event.source - "interactive" (typed), "rpc" (API), or "extension" (via sendUserMessage) // event.streamingBehavior - "steer" | "followUp" | undefined // undefined when idle, "steer" for mid-stream interrupts, // "followUp" for messages queued until the agent finishes
// Transform: rewrite input before expansion if (event.text.startsWith("?quick ")) return { action: "transform", text: `Respond briefly: ${event.text.slice(7)}` };
// Handle: respond without LLM (extension shows its own feedback) if (event.text === "ping") { ctx.ui.notify("pong", "info"); return { action: "handled" }; }
// Route by source: skip processing for extension-injected messages if (event.source === "extension") return { action: "continue" };
// Intercept skill commands before expansion if (event.text.startsWith("/skill:")) { // Could transform, block, or let pass through }
return { action: "continue" }; // Default: pass through to expansion});結果:
continue- 変更せずに通過します (ハンドラーが何も返さない場合のデフォルト)transform- テキスト/画像を変更し、拡張を続行しますhandled- エージェントを完全にスキップします (これを返す最初のハンドラーが勝ちます)
ハンドラー間でチェーンを変換します。 streamingBehavior 対応ルーティングについては、input-transform.ts および input-transform-streaming.ts を参照してください。
ExtensionContext
Section titled “ExtensionContext”すべてのハンドラーは ctx: ExtensionContext を受け取ります。
ctx.ui
Section titled “ctx.ui”ユーザー対話のための UI メソッド。詳細については、カスタム UI を参照してください。
ctx.mode
Section titled “ctx.mode”現在の実行モード: "tui"、"rpc"、"json"、または "print"。ctx.mode === "tui" を使用して、custom()、コンポーネントファクトリ、ターミナル入力、ダイレクト TUI レンダリングなどのターミナル専用機能を保護します。
ctx.hasUI
Section titled “ctx.hasUI”TUI および RPC モードの true。印刷モード (-p) および JSON モードの false。これを使用して、TUI モードと RPC モードの両方で動作するダイアログ メソッド (select、confirm、input、editor) およびファイア アンド フォーゲット メソッド (notify、setStatus、setWidget、setTitle、setEditorText) を保護します。 RPC モードでは、一部の TUI 固有のメソッドは操作なし、またはデフォルトを返します (rpc.md を参照)。
ctx.cwd
Section titled “ctx.cwd”現在の作業ディレクトリ。
プロジェクトローカル構成パスを構築するときは、.pi をハードコーディングする代わりに CONFIG_DIR_NAME を使用してください。ブランド変更されたディストリビューションでは、別の構成ディレクトリ名を使用できます。
import { CONFIG_DIR_NAME, type ExtensionAPI } from "@earendil-works/pi-coding-agent";import { join } from "node:path";
export default function (pi: ExtensionAPI) { pi.on("session_start", (_event, ctx) => { const projectConfigPath = join(ctx.cwd, CONFIG_DIR_NAME, "my-extension.json"); // ... });}ctx.isProjectTrusted()
Section titled “ctx.isProjectTrusted()”現在のセッション コンテキストに対してプロジェクト ローカルの信頼がアクティブかどうかを返します。これには、グローバル トラスト ストアに保存された決定だけでなく、一時的な信頼の決定と CLI の信頼のオーバーライドも含まれます。
これは、信頼できるプロジェクトに対してのみ適用されるプロジェクト ローカル拡張機能の設定を読み取る前に使用してください。
ctx.sessionManager
Section titled “ctx.sessionManager”セッション状態への読み取り専用アクセス。完全な SessionManager API とエントリ タイプについては、セッション形式 を参照してください。
tool_call の場合、この状態はハンドラーが実行される前に、現在のアシスタント メッセージを通じて同期されます。並列ツール実行モードでは、同じアシスタント メッセージからの兄弟ツールの結果が含まれることはまだ保証されていません。
ctx.sessionManager.getEntries() // All entriesctx.sessionManager.getBranch() // Current branchctx.sessionManager.getLeafId() // Current leaf entry IDctx.modelRegistry / ctx.model
Section titled “ctx.modelRegistry / ctx.model”モデルと API キーへのアクセス。
ctx.signal
Section titled “ctx.signal”現在のエージェントの中止シグナル。アクティブなエージェントターンがない場合は undefined。
拡張ハンドラーから起動された中止対応のネスト処理で使用します。例:
fetch(..., { signal: ctx.signal })signalを受け入れるモデル呼び出しAbortSignalを受け入れるファイルまたはプロセスヘルパー
ctx.signal は通常、tool_call、tool_result、message_update、turn_end などのアクティブなターンイベント中に定義されます。
セッションイベント、拡張コマンド、pi がアイドル状態のときに起動されるショートカットなど、アイドルまたは非ターンコンテキストでは通常 undefined です。
pi.on("tool_result", async (event, ctx) => { const response = await fetch("https://example.com/api", { method: "POST", body: JSON.stringify(event), signal: ctx.signal, });
const data = await response.json(); return { details: data };});ctx.isIdle() / ctx.abort() / ctx.hasPendingMessages()
Section titled “ctx.isIdle() / ctx.abort() / ctx.hasPendingMessages()”制御フローヘルパー。
ctx.shutdown()
Section titled “ctx.shutdown()”pi の正常なシャットダウンを要求します。
- 対話型モード: エージェントがアイドル状態になるまで延期されます (キューに入れられたステアリングおよびフォローアップ メッセージをすべて処理した後)。
- RPC モード: 次のアイドル状態まで延期されます (現在のコマンド応答が完了した後、次のコマンドを待機しているとき)。
- 印刷モード: 何もしません。すべてのプロンプトが処理されると、プロセスは自動的に終了します。
終了する前に、すべての拡張機能に session_shutdown イベントを送信します。すべてのコンテキスト (イベント ハンドラー、ツール、コマンド、ショートカット) で使用できます。
pi.on("tool_call", (event, ctx) => { if (isFatal(event.input)) { ctx.shutdown(); }});ctx.getContextUsage()
Section titled “ctx.getContextUsage()”アクティブなモデルの現在のコンテキストの使用状況を返します。利用可能な場合は最後のアシスタントの使用状況を使用し、後続メッセージのトークンを推定します。
const usage = ctx.getContextUsage();if (usage && usage.tokens > 100_000) { // ...}ctx.compact()
Section titled “ctx.compact()”完了を待たずに圧縮をトリガーします。フォローアップアクションには onComplete および onError を使用します。
ctx.compact({ customInstructions: "Focus on recent changes", onComplete: (result) => { ctx.ui.notify("Compaction completed", "info"); }, onError: (error) => { ctx.ui.notify(`Compaction failed: ${error.message}`, "error"); },});ctx.getSystemPrompt()
Section titled “ctx.getSystemPrompt()”Pi の現在のシステム プロンプト文字列を返します。
before_agent_startの間、これは現在のターンでこれまでに行われた連鎖的なシステム プロンプトの変更を反映します。- これには、その後の
contextメッセージの変異は含まれません。 before_provider_requestペイロードの書き換えは含まれません。- 後からロードされた拡張機能が拡張機能の後に実行された場合でも、最終的に送信される内容が変更される可能性があります。
pi.on("before_agent_start", (event, ctx) => { const prompt = ctx.getSystemPrompt(); console.log(`System prompt length: ${prompt.length}`);});ExtensionCommandContext
Section titled “ExtensionCommandContext”コマンド ハンドラーは ExtensionCommandContext を受け取ります。これは、セッション制御メソッドで ExtensionContext を拡張します。これらはイベント ハンドラーから呼び出された場合にデッドロックが発生する可能性があるため、コマンドでのみ使用できます。
ctx.getSystemPromptOptions()
Section titled “ctx.getSystemPromptOptions()”Pi がシステム プロンプトを構築するために現在使用している基本入力を返します。
const options = ctx.getSystemPromptOptions();const contextPaths = options.contextFiles?.map((file) => file.path) ?? [];これは、before_agent_start event.systemPromptOptions と同じ形状および変更可能性を持ちます: カスタム プロンプト、アクティブ ツール、ツール スニペット、プロンプト ガイドライン、追加されたシステム プロンプト テキスト、CWD、ロードされたコンテキスト ファイル、およびロードされたスキル。これにはコンテキスト ファイルの完全な内容が含まれる可能性があるため、機密性の高い拡張機能ローカル データとして扱い、コマンド リスト、ログ、またはオートコンプリート メタデータを通じて公開することは避けてください。
これは、現在の基本プロンプト入力を報告します。これには、ターンごとの before_agent_start チェーンされたシステム プロンプトの変更、その後の context イベント メッセージの変更、または before_provider_request ペイロードの書き換えは含まれません。
ctx.waitForIdle()
Section titled “ctx.waitForIdle()”エージェントがストリーミングを終了するまで待ちます。
pi.registerCommand("my-cmd", { handler: async (args, ctx) => { await ctx.waitForIdle(); // Agent is now idle, safe to modify session },});ctx.newSession(オプション?)
Section titled “ctx.newSession(オプション?)”新しいセッションを作成します。
const parentSession = ctx.sessionManager.getSessionFile();const kickoff = "Continue in the replacement session";
const result = await ctx.newSession({ parentSession, setup: async (sm) => { sm.appendMessage({ role: "user", content: [{ type: "text", text: "Context from previous session..." }], timestamp: Date.now(), }); }, withSession: async (ctx) => { // Use only the replacement-session ctx here. await ctx.sendUserMessage(kickoff); },});
if (result.cancelled) { // An extension cancelled the new session}オプション:
parentSession: 新しいセッション ヘッダーに記録する親セッション ファイルsetup:withSessionが実行される前に、新しいセッションのSessionManagerを変更します。withSession: 新しい交換セッション コンテキストに対して切り替え後の作業を実行します。キャプチャされた古いpi/ コマンドctxは使用しないでください。 セッション交換のライフサイクルとフットガンを参照してください。
ctx.fork(entryId, オプション?)
Section titled “ctx.fork(entryId, オプション?)”特定のエントリからフォークして、新しいセッション ファイルを作成します。
const result = await ctx.fork("entry-id-123", { withSession: async (ctx) => { // Use only the replacement-session ctx here. ctx.ui.notify("Now in the forked session", "info"); },});if (result.cancelled) { // An extension cancelled the fork}
const cloneResult = await ctx.fork("entry-id-456", { position: "at" });if (cloneResult.cancelled) { // An extension cancelled the clone}オプション:
position:"before"(デフォルト) は、選択したユーザー メッセージの前でフォークし、そのプロンプトをエディターに復元します。position:"at"は、エディター テキストを復元せずに、選択したエントリを介してアクティブ パスを複製します。withSession: 新しい交換セッション コンテキストに対して切り替え後の作業を実行します。キャプチャされた古いpi/ コマンドctxは使用しないでください。 セッション交換のライフサイクルとフットガンを参照してください。
ctx.navigateTree(targetId、オプション?)
Section titled “ctx.navigateTree(targetId、オプション?)”セッション ツリー内の別のポイントに移動します。
const result = await ctx.navigateTree("entry-id-456", { summarize: true, customInstructions: "Focus on error handling changes", replaceInstructions: false, // true = replace default prompt entirely label: "review-checkpoint",});オプション:
summarize: 放棄されたブランチの概要を生成するかどうかcustomInstructions: サマライザーのカスタム命令replaceInstructions: true の場合、customInstructionsは追加されるのではなく、デフォルトのプロンプトを置き換えます。label: ブランチ概要エントリ (または概要でない場合はターゲット エントリ) に付けるラベル
ctx.switchSession(セッションパス、オプション?)
Section titled “ctx.switchSession(セッションパス、オプション?)”別のセッション ファイルに切り替えます。
const result = await ctx.switchSession("/path/to/session.jsonl", { withSession: async (ctx) => { await ctx.sendUserMessage("Resume work in the replacement session"); },});if (result.cancelled) { // An extension cancelled the switch via session_before_switch}オプション:
withSession: 新しい置換セッション コンテキストに対して切り替え後の作業を実行します。キャプチャされた古いpi/ コマンドctxは使用しないでください。 セッション交換のライフサイクルとフットガンを参照してください。
使用可能なセッションを検出するには、静的な SessionManager.list() メソッドまたは SessionManager.listAll() メソッドを使用します。
import { SessionManager } from "@earendil-works/pi-coding-agent";
pi.registerCommand("switch", { description: "Switch to another session", handler: async (args, ctx) => { const sessions = await SessionManager.list(ctx.cwd); if (sessions.length === 0) return; const choice = await ctx.ui.select( "Pick session:", sessions.map(s => s.file), ); if (choice) { await ctx.switchSession(choice, { withSession: async (ctx) => { ctx.ui.notify("Switched session", "info"); }, }); } },});セッション置換のライフサイクルとフットガン
Section titled “セッション置換のライフサイクルとフットガン”withSession は新しい ReplacedSessionContext を受け取り、置換セッションにバインドされた非同期 sendMessage() および sendUserMessage() ヘルパーで ExtensionCommandContext を拡張します。
ライフサイクルとフットガン:
withSessionは、古いセッションがsession_shutdownを発行し、古いランタイムが破棄され、代替セッションがバインドされ、新しい拡張機能インスタンスがすでにsession_startを受信した後にのみ実行されます。- コールバックは、新しい拡張機能インスタンス内ではなく、元のクロージャ内で引き続き実行されます。つまり、
withSessionが開始される前に、古い拡張機能インスタンスがすでにシャットダウン クリーンアップを実行している可能性があります。 - キャプチャされた古い
pi/ 古いコマンドctxセッション バインド オブジェクトは、置換後は古くなり、使用するとスローされます。セッションにバインドされた作業には、withSessionに渡されたctxのみを使用してください。 - 以前に抽出した生のオブジェクトは引き続きお客様の責任となります。たとえば、置換前に
const sm = ctx.sessionManagerをキャプチャした場合、smは古いSessionManagerオブジェクトのままです。交換後の再使用はしないでください。 withSessionのコードは、session_shutdownハンドラーによって無効化された状態はすでになくなっていると想定する必要があります。文字列、ID、シリアル化された構成など、シャットダウンしても問題なく残るプレーン データのみをキャプチャします。
安全なパターン:
pi.registerCommand("handoff", { handler: async (_args, ctx) => { const kickoff = "Continue from the replacement session"; await ctx.newSession({ withSession: async (ctx) => { await ctx.sendUserMessage(kickoff); }, }); },});安全でないパターン:
pi.registerCommand("handoff", { handler: async (_args, ctx) => { const oldSessionManager = ctx.sessionManager; await ctx.newSession({ withSession: async (_ctx) => { // stale old objects: do not do this oldSessionManager.getSessionFile(); pi.sendUserMessage("wrong"); }, }); },});ctx.reload()
Section titled “ctx.reload()”/reload と同じリロード フローを実行します。
pi.registerCommand("reload-runtime", { description: "Reload extensions, skills, prompts, and themes", handler: async (_args, ctx) => { await ctx.reload(); return; },});重要な行動:
await ctx.reload()は、現在の拡張機能ランタイムに対してsession_shutdownを発行します- その後、リソースをリロードし、
reason: "reload"を含むsession_startと"reload"を理由とするresources_discoverを発行します。 - 現在実行中のコマンド ハンドラーは引き続き古い呼び出しフレームを継続します。
await ctx.reload()以降のコードはリロード前のバージョンから引き続き実行されますawait ctx.reload()以降のコードは、古いメモリ内拡張機能の状態がまだ有効であると想定してはなりません- ハンドラーが戻った後、以降のコマンド/イベント/ツール呼び出しでは新しい拡張バージョンが使用されます。
予測可能な動作を実現するには、リロードをそのハンドラー (await ctx.reload(); return;) の端末として扱います。
ツールは ExtensionContext で実行されるため、ctx.reload() を直接呼び出すことはできません。コマンドをリロード エントリポイントとして使用し、そのコマンドをフォローアップ ユーザー メッセージとしてキューに入れるツールを公開します。
LLM がリロードをトリガーするために呼び出すことができるツールの例:
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";import { Type } from "typebox";
export default function (pi: ExtensionAPI) { pi.registerCommand("reload-runtime", { description: "Reload extensions, skills, prompts, and themes", handler: async (_args, ctx) => { await ctx.reload(); return; }, });
pi.registerTool({ name: "reload_runtime", label: "Reload Runtime", description: "Reload extensions, skills, prompts, and themes", parameters: Type.Object({}), async execute() { pi.sendUserMessage("/reload-runtime", { deliverAs: "followUp" }); return { content: [{ type: "text", text: "Queued /reload-runtime as a follow-up command." }], }; }, });}ExtensionAPI メソッド
Section titled “ExtensionAPI メソッド”pi.on(イベント、ハンドラー)
Section titled “pi.on(イベント、ハンドラー)”イベントを購読します。イベントの種類と戻り値については、イベント を参照してください。
pi.registerTool(定義)
Section titled “pi.registerTool(定義)”LLM によって呼び出し可能なカスタム ツールを登録します。詳細については、カスタム ツール を参照してください。
pi.registerTool() は、拡張機能のロード中と起動後の両方で機能します。これは、session_start、コマンド ハンドラー、またはその他のイベント ハンドラー内で呼び出すことができます。新しいツールは同じセッション内ですぐに更新されるため、pi.getAllTools() に表示され、/reload がなくても LLM から呼び出すことができます。
pi.setActiveTools() を使用して、実行時にツール (動的に追加されたツールを含む) を有効または無効にします。
カスタム ツールを Available tools の 1 行エントリに選択するには promptSnippet を使用し、ツールがアクティブなときにデフォルトの Guidelines セクションにツール固有の箇条書きを追加するには promptGuidelines を使用します。
重要: promptGuidelines の箇条書きは、ツール名のプレフィックスなしで Guidelines セクションにフラットに追加されます。各ガイドラインでは、参照するツールに名前を付ける必要があります。LLM は「これ」がどのツールを意味するかを判断できないため、「次の場合にこのツールを使用する」は避けてください。代わりに「次の場合に my_tool を使用する」と書きます。
完全な例については、dynamic-tools.ts を参照してください。
import { Type } from "typebox";import { StringEnum } from "@earendil-works/pi-ai";
pi.registerTool({ name: "my_tool", label: "My Tool", description: "What this tool does", promptSnippet: "Summarize or transform text according to action", promptGuidelines: ["Use my_tool when the user asks to summarize previously generated text."], parameters: Type.Object({ action: StringEnum(["list", "add"] as const), text: Type.Optional(Type.String()), }), prepareArguments(args) { // Optional compatibility shim. Runs before schema validation. // Return the current schema shape, for example to fold legacy fields // into the modern parameter object. return args; },
async execute(toolCallId, params, signal, onUpdate, ctx) { // Stream progress onUpdate?.({ content: [{ type: "text", text: "Working..." }] });
return { content: [{ type: "text", text: "Done" }], details: { result: "..." }, }; },
// Optional: Custom rendering renderCall(args, theme, context) { ... }, renderResult(result, options, theme, context) { ... },});pi.sendMessage(メッセージ、オプション?)
Section titled “pi.sendMessage(メッセージ、オプション?)”カスタム メッセージをセッションに挿入します。
pi.sendMessage({ customType: "my-extension", content: "Message text", display: true, details: { ... },}, { triggerTurn: true, deliverAs: "steer",});オプション:
deliverAs- 配信モード:"steer"(デフォルト) - ストリーミング中にメッセージをキューに入れます。現在のアシスタント ターンがツール呼び出しの実行を終了した後、次の LLM 呼び出しの前に配信されます。"followUp"- エージェントが終了するまで待機します。エージェントがツールを呼び出さなくなった場合にのみ配信されます。"nextTurn"- 次のユーザー プロンプトのキューに入れられました。何も中断したりトリガーしたりしません。
triggerTurn: true- エージェントがアイドル状態の場合、LLM 応答を直ちにトリガーします。"steer"および"followUp"モードにのみ適用されます ("nextTurn"の場合は無視されます)。
pi.sendUserMessage(コンテンツ、オプション?)
Section titled “pi.sendUserMessage(コンテンツ、オプション?)”ユーザーメッセージをエージェントに送信します。カスタム メッセージを送信する sendMessage() とは異なり、ユーザーが入力したかのように表示される実際のユーザー メッセージが送信されます。常にターンを誘発します。
// Simple text messagepi.sendUserMessage("What is 2+2?");
// With content array (text + images)pi.sendUserMessage([ { type: "text", text: "Describe this image:" }, { type: "image", source: { type: "base64", mediaType: "image/png", data: "..." } },]);
// During streaming - must specify delivery modepi.sendUserMessage("Focus on error handling", { deliverAs: "steer" });pi.sendUserMessage("And then summarize", { deliverAs: "followUp" });オプション:
deliverAs- エージェントがストリーミングしている場合に必要です:"steer"- 現在のアシスタント ターンがツール呼び出しの実行を終了した後、配信するメッセージをキューに入れます。"followUp"- エージェントがすべてのツールを完了するまで待機します
ストリーミングしていない場合、メッセージはすぐに送信され、新しいターンがトリガーされます。 deliverAs を使用せずにストリーミングすると、エラーがスローされます。
完全な例については、send-user-message.ts を参照してください。
pi.appendEntry(customType, data?)
Section titled “pi.appendEntry(customType, data?)”拡張機能の状態を保持します (LLM コンテキストには参加しません)。
pi.appendEntry("my-state", { count: 42 });
// Restore on reloadpi.on("session_start", async (_event, ctx) => { for (const entry of ctx.sessionManager.getEntries()) { if (entry.type === "custom" && entry.customType === "my-state") { // Reconstruct from entry.data } }});pi.setSessionName(名前)
Section titled “pi.setSessionName(名前)”セッションの表示名を設定します (最初のメッセージの代わりにセッション セレクターに表示されます)。
pi.setSessionName("Refactor auth module");pi.getSessionName()
Section titled “pi.getSessionName()”現在のセッション名が設定されている場合は、それを取得します。
const name = pi.getSessionName();if (name) { console.log(`Session: ${name}`);}pi.setLabel(entryId, label)
Section titled “pi.setLabel(entryId, label)”エントリのラベルを設定またはクリアします。ラベルは、ブックマークとナビゲーション用のユーザー定義のマーカーです (/tree セレクターに表示されます)。
// Set a labelpi.setLabel(entryId, "checkpoint-before-refactor");
// Clear a labelpi.setLabel(entryId, undefined);
// Read labels via sessionManagerconst label = ctx.sessionManager.getLabel(entryId);ラベルはセッション内に保持され、再起動しても存続します。これらを使用して、会話ツリー内の重要なポイント (ターン、チェックポイント) をマークします。
pi.registerCommand(名前, オプション)
Section titled “pi.registerCommand(名前, オプション)”コマンドを登録します。
複数の拡張機能が同じコマンド名を登録している場合、pi はそれらすべてを保持し、ロード順に数値呼び出しサフィックス (/review:1 と /review:2 など) を割り当てます。
pi.registerCommand("stats", { description: "Show session statistics", handler: async (args, ctx) => { const count = ctx.sessionManager.getEntries().length; ctx.ui.notify(`${count} entries`, "info"); }});オプション: /command ... の引数自動補完を追加します。
import type { AutocompleteItem } from "@earendil-works/pi-tui";
pi.registerCommand("deploy", { description: "Deploy to an environment", getArgumentCompletions: (prefix: string): AutocompleteItem[] | null => { const envs = ["dev", "staging", "prod"]; const items = envs.map((e) => ({ value: e, label: e })); const filtered = items.filter((i) => i.value.startsWith(prefix)); return filtered.length > 0 ? filtered : null; }, handler: async (args, ctx) => { ctx.ui.notify(`Deploying: ${args}`, "info"); },});pi.getCommands()
Section titled “pi.getCommands()”現在のセッションで prompt 経由で呼び出し可能なスラッシュ コマンドを取得します。拡張コマンド、プロンプト テンプレート、スキル コマンドが含まれます。
このリストは RPC get_commands の順序と一致します。最初に拡張機能、次にテンプレート、次にスキルです。
const commands = pi.getCommands();const bySource = commands.filter((command) => command.source === "extension");const userScoped = commands.filter((command) => command.sourceInfo.scope === "user");各エントリは次のような形状になっています。
{ name: string; // Invokable command name without the leading slash. May be suffixed like "review:1" description?: string; source: "extension" | "prompt" | "skill"; sourceInfo: { path: string; source: string; scope: "user" | "project" | "temporary"; origin: "package" | "top-level"; baseDir?: string; };}sourceInfo を正規の出所フィールドとして使用します。コマンド名やアドホック パス解析から所有権を推測しないでください。
組み込みの対話型コマンド (/model や /settings など) はここには含まれません。インタラクティブでのみ処理されます
モードであり、prompt 経由で送信された場合は実行されません。
pi.registerMessageRenderer(customType, レンダラー)
Section titled “pi.registerMessageRenderer(customType, レンダラー)”メッセージ用のカスタム TUI レンダラーを customType に登録します。 カスタム UI を参照してください。
pi.registerShortcut(ショートカット、オプション)
Section titled “pi.registerShortcut(ショートカット、オプション)”キーボードショートカットを登録します。ショートカットの形式と組み込みのキーバインディングについては、keybindings.md を参照してください。
pi.registerShortcut("ctrl+shift+p", { description: "Toggle plan mode", handler: async (ctx) => { ctx.ui.notify("Toggled!"); },});pi.registerFlag(名前, オプション)
Section titled “pi.registerFlag(名前, オプション)”CLIフラグを登録します。
pi.registerFlag("plan", { description: "Start in plan mode", type: "boolean", default: false,});
// Check valueif (pi.getFlag("plan")) { // Plan mode enabled}pi.exec(コマンド、引数、オプション?)
Section titled “pi.exec(コマンド、引数、オプション?)”シェルコマンドを実行します。
const result = await pi.exec("git", ["status"], { signal, timeout: 5000 });// result.stdout, result.stderr, result.code, result.killedpi.getActiveTools() / pi.getAllTools() / pi.setActiveTools(names)
Section titled “pi.getActiveTools() / pi.getAllTools() / pi.setActiveTools(names)”アクティブなツールを管理します。 This works for both built-in tools and dynamically registered tools. pi.getActiveTools() returns the active tool names as string[]; pi.getAllTools() returns metadata for all configured tools.
const active = pi.getActiveTools(); // ["read", "bash", ...]const all = pi.getAllTools();// all = [{// name: "read",// description: "Read file contents...",// parameters: ...,// promptGuidelines: ["Use read to examine files instead of cat or sed."],// sourceInfo: { path: "<builtin:read>", source: "builtin", scope: "temporary", origin: "top-level" }// }, ...]const builtinTools = all.filter((t) => t.sourceInfo.source === "builtin");const extensionTools = all.filter((t) => t.sourceInfo.source !== "builtin" && t.sourceInfo.source !== "sdk");pi.setActiveTools([...new Set([...active, "my_custom_tool"])]); // Keep current tools and enable my_custom_toolpi.setActiveTools(["read", "bash"]); // Switch to read-onlypi.getAllTools() は、name、description、parameters、promptGuidelines、および sourceInfo を返します。
Typical sourceInfo.source values:
builtinfor built-in toolssdkfor tools passed viacreateAgentSession({ customTools })- 拡張機能によって登録されたツールの拡張機能ソース メタデータ
pi.setModel(モデル)
Section titled “pi.setModel(モデル)”現在のモデルを設定します。モデルで使用できる API キーがない場合は、false を返します。カスタム モデルの構成については、models.md を参照してください。
const model = ctx.modelRegistry.find("anthropic", "claude-sonnet-4-5");if (model) { const success = await pi.setModel(model); if (!success) { ctx.ui.notify("No API key for this model", "error"); }}pi.getThinkingLevel() / pi.setThinkingLevel(レベル)
Section titled “pi.getThinkingLevel() / pi.setThinkingLevel(レベル)”思考レベルを取得または設定します。レベルはモデルの能力に固定されます (非推論モデルは常に「オフ」を使用します)。変更により thinking_level_select が発行されます。
const current = pi.getThinkingLevel(); // "off" | "minimal" | "low" | "medium" | "high" | "xhigh"pi.setThinkingLevel("high");pi.イベント
Section titled “pi.イベント”拡張機能間の通信用の共有イベント バス:
pi.events.on("my:event", (data) => { ... });pi.events.emit("my:event", { ... });pi.registerProvider(名前, 構成)
Section titled “pi.registerProvider(名前, 構成)”モデルプロバイダーを動的に登録またはオーバーライドします。プロキシ、カスタム エンドポイント、またはチーム全体のモデル構成に役立ちます。
拡張ファクトリー関数中に行われた呼び出しはキューに入れられ、ランナーが初期化されると適用されます。それ以降に行われた呼び出し (たとえば、ユーザー セットアップ フローに続くコマンド ハンドラーからの呼び出し) は、/reload を必要とせずにすぐに有効になります。
リモート エンドポイントからモデルを検出する必要がある場合は、フェッチを session_start に遅延させるよりも、非同期拡張ファクトリーを優先してください。 pi は起動を続行する前にファクトリーを待機するため、pi --list-models を含め、登録されたモデルはすぐに使用可能になります。
// Register a new provider with custom modelspi.registerProvider("my-proxy", { name: "My Proxy", baseUrl: "https://proxy.example.com", apiKey: "$PROXY_API_KEY", // env var reference api: "anthropic-messages", models: [ { id: "claude-sonnet-4-20250514", name: "Claude 4 Sonnet (proxy)", reasoning: false, input: ["text", "image"], cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, contextWindow: 200000, maxTokens: 16384 } ]});
// Override baseUrl for an existing provider (keeps all models)pi.registerProvider("anthropic", { baseUrl: "https://proxy.example.com"});
// Register provider with OAuth support for /loginpi.registerProvider("corporate-ai", { baseUrl: "https://ai.corp.com", api: "openai-responses", models: [...], oauth: { name: "Corporate AI (SSO)", async login(callbacks) { // Custom OAuth flow callbacks.onAuth({ url: "https://sso.corp.com/..." }); const code = await callbacks.onPrompt({ message: "Enter code:" }); return { refresh: code, access: code, expires: Date.now() + 3600000 }; }, async refreshToken(credentials) { // Refresh logic return credentials; }, getApiKey(credentials) { return credentials.access; } }});設定オプション:
name- UI でのプロバイダーの表示名 (/loginなど)。baseUrl- API エンドポイント URL。モデルを定義するときに必要です。apiKey- API キー リテラル、環境補間 ($ENV_VARまたは${ENV_VAR})、または先頭の!command。モデルを定義するときに必要です (oauthが提供されていない場合)。$$は$をエスケープし、$!はコマンドの実行をトリガーせずにリテラル!をエスケープします。api- API タイプ:"anthropic-messages"、"openai-completions"、"openai-responses"など。headers- リクエストに含めるカスタム ヘッダー。authHeader- true の場合、Authorization: Bearerヘッダーが自動的に追加されます。models- モデル定義の配列。指定すると、このプロバイダーの既存のモデルがすべて置き換えられます。モデル定義では、baseUrlを設定して、そのモデルのプロバイダー エンドポイントをオーバーライドできます。oauth-/loginサポートのための OAuth プロバイダー構成。プロバイダーを指定すると、ログイン メニューに表示されます。streamSimple- 非標準 API のカスタム ストリーミング実装。
カスタム ストリーミング API、OAuth の詳細、モデル定義リファレンスなどの高度なトピックについては、custom-provider.md を参照してください。
pi.unregisterProvider(名前)
Section titled “pi.unregisterProvider(名前)”以前に登録したプロバイダーとそのモデルを削除します。プロバイダーによってオーバーライドされた組み込みモデルが復元されます。プロバイダーが登録されていない場合は効果がありません。
registerProvider と同様、これは初期ロード フェーズの後に呼び出すとすぐに有効になるため、/reload は必要ありません。
pi.registerCommand("my-setup-teardown", { description: "Remove the custom proxy provider", handler: async (_args, _ctx) => { pi.unregisterProvider("my-proxy"); },});状態を含む拡張機能は、適切な分岐サポートのためにツールの結果 details にそれを保存する必要があります。
export default function (pi: ExtensionAPI) { let items: string[] = [];
// Reconstruct state from session pi.on("session_start", async (_event, ctx) => { items = []; for (const entry of ctx.sessionManager.getBranch()) { if (entry.type === "message" && entry.message.role === "toolResult") { if (entry.message.toolName === "my_tool") { items = entry.message.details?.items ?? []; } } } });
pi.registerTool({ name: "my_tool", // ... async execute(toolCallId, params, signal, onUpdate, ctx) { items.push("new item"); return { content: [{ type: "text", text: "Added" }], details: { items: [...items] }, // Store for reconstruction }; }, });}カスタムツール
Section titled “カスタムツール”LLM が pi.registerTool() 経由で呼び出すことができるツールを登録します。ツールはシステム プロンプトに表示され、カスタム レンダリングを行うことができます。
デフォルトのシステム プロンプトの Available tools セクションの短い 1 行エントリには、promptSnippet を使用します。省略した場合、カスタム ツールはそのセクションから除外されます。
promptGuidelines を使用して、ツール固有の箇条書きをデフォルトのシステム プロンプト Guidelines セクションに追加します。これらの箇条書きは、ツールがアクティブな間 (たとえば、pi.setActiveTools([...]) の後) にのみ含まれます。
重要: promptGuidelines の箇条書きは、ツール名のプレフィックスやグループ化なしで、フラットに Guidelines セクションに追加されます。各ガイドラインでは、参照するツールに名前を付ける必要があります。LLM は「これ」がどのツールを意味するかを判断できないため、「次の場合にこのツールを使用する」は避けてください。代わりに「次の場合に my_tool を使用する」と書きます。
注: 一部のモデルはツールパス引数に @ プレフィックスを含めることがあります。組み込みツールはパスを解決する前に先頭の @ を除去します。カスタムツールがパスを受け入れる場合は、先頭の @ も同様に正規化してください。
カスタム ツールがファイルを変更する場合は、withFileMutationQueue() を使用して、組み込みの edit および write と同じファイルごとのキューに参加させます。デフォルトではツール呼び出しが並行して実行されるため、これは重要です。キューがなければ、2 つのツールが同じ古いファイルの内容を読み取り、異なる更新を計算し、最後に書き込まれた方が他方を上書きする可能性があります。
失敗例: カスタム ツールは foo.ts を編集しますが、組み込みの edit も同じアシスタント ターンで foo.ts を変更します。ツールがキューに参加していない場合、両方が元の foo.ts を読み取り、別々の変更を適用することができ、それらの変更の 1 つが失われます。
raw ユーザー引数ではなく、実際のターゲット ファイル パスを withFileMutationQueue() に渡します。最初に、ctx.cwd またはツールの作業ディレクトリを基準とした絶対パスに解決します。既存のファイルの場合、ヘルパーは realpath() を通じて正規化するため、同じファイルのシンボリックリンク エイリアスは 1 つのキューを共有します。新しいファイルの場合は、realpath() にはまだ何もないため、解決された絶対パスに戻ります。
変更ウィンドウ全体をそのターゲット パス上でキューに入れます。これには、最終的な書き込みだけでなく、読み取り、変更、書き込みロジックも含まれます。
import { withFileMutationQueue } from "@earendil-works/pi-coding-agent";import { mkdir, readFile, writeFile } from "node:fs/promises";import { dirname, resolve } from "node:path";
async execute(_toolCallId, params, _signal, _onUpdate, ctx) { const absolutePath = resolve(ctx.cwd, params.path);
return withFileMutationQueue(absolutePath, async () => { await mkdir(dirname(absolutePath), { recursive: true }); const current = await readFile(absolutePath, "utf8"); const next = current.replace(params.oldText, params.newText); await writeFile(absolutePath, next, "utf8");
return { content: [{ type: "text", text: `Updated ${params.path}` }], details: {}, }; });}ツールの定義
Section titled “ツールの定義”import { Type } from "typebox";import { StringEnum } from "@earendil-works/pi-ai";import { Text } from "@earendil-works/pi-tui";
pi.registerTool({ name: "my_tool", label: "My Tool", description: "What this tool does (shown to LLM)", promptSnippet: "List or add items in the project todo list", promptGuidelines: [ "Use my_tool for todo planning instead of direct file edits when the user asks for a task list." ], parameters: Type.Object({ action: StringEnum(["list", "add"] as const), // Use StringEnum for Google compatibility text: Type.Optional(Type.String()), }), prepareArguments(args) { if (!args || typeof args !== "object") return args; const input = args as { action?: string; oldAction?: string }; if (typeof input.oldAction === "string" && input.action === undefined) { return { ...input, action: input.oldAction }; } return args; },
async execute(toolCallId, params, signal, onUpdate, ctx) { // Check for cancellation if (signal?.aborted) { return { content: [{ type: "text", text: "Cancelled" }] }; }
// Stream progress updates onUpdate?.({ content: [{ type: "text", text: "Working..." }], details: { progress: 50 }, });
// Run commands via pi.exec (captured from extension closure) const result = await pi.exec("some-command", [], { signal });
// Return result return { content: [{ type: "text", text: "Done" }], // Sent to LLM details: { data: result }, // For rendering & state // Optional: stop after this tool batch when every finalized tool result // in the batch also returns terminate: true. terminate: true, }; },
// Optional: Custom rendering renderCall(args, theme, context) { ... }, renderResult(result, options, theme, context) { ... },});エラーの通知: ツールの実行を失敗としてマークするには (結果に isError: true を設定し、LLM に報告します)、execute からエラーをスローします。戻りオブジェクトにどのようなプロパティを含めるかに関係なく、値を返すとエラー フラグが設定されることはありません。
早期終了: execute() から terminate: true を返し、現在のツール バッチの後に自動フォローアップ LLM 呼び出しをスキップする必要があることを示します。これは、すべての完成したツールの結果としてそのバッチが終了する場合にのみ有効になります。エージェントが最後の構造化出力ツール呼び出しで終了する最小限の例については、examples/extensions/structurd-output.ts を参照してください。
// Correct: throw to signal an errorasync execute(toolCallId, params) { if (!isValid(params.input)) { throw new Error(`Invalid input: ${params.input}`); } return { content: [{ type: "text", text: "OK" }], details: {} };}重要: 文字列列挙には @earendil-works/pi-ai から StringEnum を使用します。 Type.Union/Type.Literal は Google の API では動作しません。
引数の準備: prepareArguments(args) はオプションです。定義されている場合、スキーマ検証の前および execute() の前に実行されます。これを使用して、保存されているツール呼び出し引数が現在のスキーマと一致しなくなった古いセッションを pi が再開するときに、受け入れられた古い入力形状を模倣します。 parameters に対して検証するオブジェクトを返します。パブリックスキーマを厳密に保ちます。再開された古いセッションを動作し続けるためだけに、非推奨の互換性フィールドを parameters に追加しないでください。
例: 古いセッションには、トップレベル oldText および newText を使用した edit ツール呼び出しが含まれている可能性がありますが、現在のスキーマは edits: [{ oldText, newText }] のみを受け入れます。
pi.registerTool({ name: "edit", label: "Edit", description: "Edit a single file using exact text replacement", parameters: Type.Object({ path: Type.String(), edits: Type.Array( Type.Object({ oldText: Type.String(), newText: Type.String(), }), ), }), prepareArguments(args) { if (!args || typeof args !== "object") return args;
const input = args as { path?: string; edits?: Array<{ oldText: string; newText: string }>; oldText?: unknown; newText?: unknown; };
if (typeof input.oldText !== "string" || typeof input.newText !== "string") { return args; }
return { ...input, edits: [...(input.edits ?? []), { oldText: input.oldText, newText: input.newText }], }; }, async execute(toolCallId, params, signal, onUpdate, ctx) { // params now matches the current schema return { content: [{ type: "text", text: `Applying ${params.edits.length} edit block(s)` }], details: {}, }; },});組み込みツールのオーバーライド
Section titled “組み込みツールのオーバーライド”拡張機能は、同じ名前のツールを登録することで、組み込みツール (read、bash、edit、write、grep、find、ls) をオーバーライドできます。対話型モードでは、これが発生した場合に警告が表示されます。
# Extension's read tool replaces built-in readpi -e ./tool-override.tsまたは、--no-builtin-tools を使用して、拡張ツールを有効にしたまま組み込みツールなしで開始します。
# No built-in tools, only extension toolspi --no-builtin-tools -e ./my-extension.tsロギングとアクセス制御で read をオーバーライドする完全な例については、examples/extensions/tool-override.ts を参照してください。
レンダリング: 組み込みレンダラーの継承はスロットごとに解決されます。実行オーバーライドとレンダリング オーバーライドは独立しています。オーバーライドで renderCall を省略した場合は、組み込みの renderCall が使用されます。オーバーライドで renderResult を省略した場合は、組み込みの renderResult が使用されます。オーバーライドで両方を省略した場合、組み込みレンダラが自動的に使用されます (構文の強調表示、差分など)。これにより、UI を再実装することなく、ログ記録やアクセス制御用の組み込みツールをラップできます。
プロンプト メタデータ: promptSnippet および promptGuidelines は組み込みツールから継承されません。オーバーライドでこれらのプロンプト指示を維持する必要がある場合は、それらをオーバーライドで明示的に定義します。
実装は、details タイプを含む結果の形状と正確に一致する必要があります。 UI とセッション ロジックは、レンダリングと状態追跡のためにこれらの形状に依存します。
組み込みツールの実装:
- read.ts -
ReadToolDetails - bash.ts -
BashToolDetails - edit.ts
- write.ts
- grep.ts -
GrepToolDetails - find.ts -
FindToolDetails - ls.ts -
LsToolDetails
リモート実行
Section titled “リモート実行”組み込みツールは、リモート システム (SSH、コンテナなど) に委任するためのプラグ可能な操作をサポートします。
import { createReadTool, createBashTool, type ReadOperations } from "@earendil-works/pi-coding-agent";
// Create tool with custom operationsconst remoteRead = createReadTool(cwd, { operations: { readFile: (path) => sshExec(remote, `cat ${path}`), access: (path) => sshExec(remote, `test -r ${path}`).then(() => {}), }});
// Register, checking flag at execution timepi.registerTool({ ...remoteRead, async execute(id, params, signal, onUpdate, _ctx) { const ssh = getSshConfig(); if (ssh) { const tool = createReadTool(cwd, { operations: createRemoteOps(ssh) }); return tool.execute(id, params, signal, onUpdate); } return localRead.execute(id, params, signal, onUpdate); },});操作インターフェイス: ReadOperations、WriteOperations、EditOperations、BashOperations、LsOperations、GrepOperations、FindOperations
user_bash の場合、拡張機能はローカル プロセスの生成、シェル解決、プロセス ツリーの終了を再実装する代わりに、createLocalBashOperations() 経由で pi のローカル シェル バックエンドを再利用できます。
bash ツールは、実行前にコマンド、cwd、または env を調整するための spawn フックもサポートしています。
import { createBashTool } from "@earendil-works/pi-coding-agent";
const bashTool = createBashTool(cwd, { spawnHook: ({ command, cwd, env }) => ({ command: `source ~/.profile\n${command}`, cwd: `/mnt/sandbox${cwd}`, env: { ...env, CI: "1" }, }),});--ssh フラグを使用した完全な SSH の例については、examples/extensions/ssh.ts を参照してください。
出力の切り捨て
Section titled “出力の切り捨て”ツールは、LLM コンテキストの負荷を避けるために、出力を切り詰めなければなりません。出力が大きいと、次のような問題が発生する可能性があります。
- コンテキスト オーバーフロー エラー (プロンプトが長すぎます)
- 圧縮の失敗
- モデルのパフォーマンスの低下
組み込みの制限は 50KB (約 10,000 トークン) および 2000 行 のいずれか最初に到達した方です。エクスポートされた切り捨てユーティリティを使用します。
import { truncateHead, // Keep first N lines/bytes (good for file reads, search results) truncateTail, // Keep last N lines/bytes (good for logs, command output) truncateLine, // Truncate a single line to maxBytes with ellipsis formatSize, // Human-readable size (e.g., "50KB", "1.5MB") DEFAULT_MAX_BYTES, // 50KB DEFAULT_MAX_LINES, // 2000} from "@earendil-works/pi-coding-agent";
async execute(toolCallId, params, signal, onUpdate, ctx) { const output = await runCommand();
// Apply truncation const truncation = truncateHead(output, { maxLines: DEFAULT_MAX_LINES, maxBytes: DEFAULT_MAX_BYTES, });
let result = truncation.content;
if (truncation.truncated) { // Write full output to temp file const tempFile = writeTempFile(output);
// Inform the LLM where to find complete output result += `\n\n[Output truncated: ${truncation.outputLines} of ${truncation.totalLines} lines`; result += ` (${formatSize(truncation.outputBytes)} of ${formatSize(truncation.totalBytes)}).`; result += ` Full output saved to: ${tempFile}]`; }
return { content: [{ type: "text", text: result }] };}重要なポイント:
- 始まりが重要なコンテンツ (検索結果、ファイルの読み取り) には
truncateHeadを使用します。 - 終わりが重要なコンテンツ (ログ、コマンド出力) には
truncateTailを使用します。 - 出力が切り詰められた場合と、完全なバージョンの場所を常に LLM に通知します。
- ツールの説明に切り捨て制限を文書化します。
rg (ripgrep) を適切な切り捨てでラップする完全な例については、examples/extensions/truncated-tool.ts を参照してください。
複数のツール
Section titled “複数のツール”1 つの拡張機能で複数のツールを共有状態に登録できます。
export default function (pi: ExtensionAPI) { let connection = null;
pi.registerTool({ name: "db_connect", ... }); pi.registerTool({ name: "db_query", ... }); pi.registerTool({ name: "db_close", ... });
pi.on("session_shutdown", async () => { connection?.close(); });}カスタムレンダリング
Section titled “カスタムレンダリング”ツールは、カスタム TUI 表示用の renderCall および renderResult を提供できます。完全なコンポーネント API については tui.md を、ツール行の構成方法については tool-execution.ts を参照してください。
デフォルトでは、ツールの出力はパディングと背景を処理する Box でラップされます。定義された renderCall または renderResult は Component を返す必要があります。スロット レンダラーが定義されていない場合、tool-execution.ts はそのスロットに対してフォールバック レンダリングを使用します。
ツールがデフォルトの Box を使用する代わりに独自のシェルをレンダリングする必要がある場合は、renderShell: "self" を設定します。これは、フレームや背景の動作を完全に制御する必要があるツール、たとえば、ツールが安定した後も視覚的に安定した状態を維持する必要がある大きなプレビューなどに便利です。
pi.registerTool({ name: "my_tool", label: "My Tool", description: "Custom shell example", parameters: Type.Object({}), renderShell: "self", async execute() { return { content: [{ type: "text", text: "ok" }], details: undefined }; }, renderCall(args, theme, context) { return new Text(theme.fg("accent", "my custom shell"), 0, 0); },});renderCall と renderResult はそれぞれ、以下の context オブジェクトを受け取ります。
args- 現在のツール呼び出し引数state-renderCallとrenderResult間で行ローカル状態を共有lastComponent- そのスロットに対して以前に返されたコンポーネント (存在する場合)invalidate()- このツール行の再レンダリングを要求しますtoolCallId、cwd、executionStarted、argsComplete、isPartial、expanded、showImages、isError
クロススロット共有状態には context.state を使用します。レンダリング間で同じコンポーネントを再利用および変更する場合は、返されたコンポーネント インスタンスにスロット ローカル キャッシュを保持します。
renderCall
Section titled “renderCall”ツール呼び出しまたはヘッダーをレンダリングします。
import { Text } from "@earendil-works/pi-tui";
renderCall(args, theme, context) { const text = (context.lastComponent as Text | undefined) ?? new Text("", 0, 0); let content = theme.fg("toolTitle", theme.bold("my_tool ")); content += theme.fg("muted", args.action); if (args.text) { content += " " + theme.fg("dim", `"${args.text}"`); } text.setText(content); return text;}renderResult
Section titled “renderResult”ツールの結果または出力をレンダリングします。
renderResult(result, { expanded, isPartial }, theme, context) { if (isPartial) { return new Text(theme.fg("warning", "Processing..."), 0, 0); }
if (result.details?.error) { return new Text(theme.fg("error", `Error: ${result.details.error}`), 0, 0); }
let text = theme.fg("success", "✓ Done"); if (expanded && result.details?.items) { for (const item of result.details.items) { text += "\n " + theme.fg("dim", item); } } return new Text(text, 0, 0);}スロットに意図的に表示可能なコンテンツがない場合は、空の Container などの空の Component を返します。
キーバインドのヒント
Section titled “キーバインドのヒント”keyHint() を使用して、アクティブなキーバインド設定を考慮したキーバインドのヒントを表示します。
import { keyHint } from "@earendil-works/pi-coding-agent";
renderResult(result, { expanded }, theme, context) { let text = theme.fg("success", "✓ Done"); if (!expanded) { text += ` (${keyHint("app.tools.expand", "to expand")})`; } return new Text(text, 0, 0);}利用可能な機能:
keyHint(keybinding, description)-"app.tools.expand"や"tui.select.confirm"などの構成されたキーバインド ID をフォーマットしますkeyText(keybinding)- キーバインド ID に設定された生のキー テキストを返します。rawKeyHint(key, description)- 生のキー文字列をフォーマットする
名前空間付きキーバインド ID を使用します。
- コーディング エージェント ID は
app.*名前空間を使用します (例:app.tools.expand、app.editor.external、app.session.rename) - 共有 TUI ID は
tui.*名前空間を使用します (例:tui.select.confirm、tui.select.cancel、tui.input.tab)
キーバインド ID とデフォルトの完全なリストについては、keybindings.md を参照してください。 keybindings.json は、それらと同じ名前空間 ID を使用します。
カスタム エディターと ctx.ui.custom() コンポーネントは、挿入された引数として keybindings: KeybindingsManager を受け取ります。 getKeybindings() または setKeybindings() を呼び出す代わりに、挿入されたマネージャーを直接使用する必要があります。
ベスト プラクティス
Section titled “ベスト プラクティス”Textとパディング(0, 0)を使用します。デフォルトのボックスはパディングを処理します。- 複数行のコンテンツには
\nを使用します。 - ストリーミングの進行状況について
isPartialを処理します。 - オンデマンドで詳細を確認するには
expandedをサポートします。 - デフォルトのビューをコンパクトに保ちます。
- 引数を
context.stateにコピーする代わりに、renderResultのcontext.argsを読み取ります。 context.stateは、呼び出しスロットと結果スロット間で共有する必要があるデータにのみ使用します。- 同じコンポーネント インスタンスを適切な場所で更新できる場合は、
context.lastComponentを再利用します。 renderShell: "self"は、デフォルトのボックス化されたシェルが邪魔になる場合にのみ使用してください。セルフシェル モードでは、ツールは独自のフレーム、パディング、および背景を担当します。
フォールバック
Section titled “フォールバック”スロット レンダラーが定義されていない場合、またはスローされる場合:
renderCall: ツール名を表示します。renderResult:contentからの生のテキストを表示します
カスタム UI
Section titled “カスタム UI”拡張機能は、ctx.ui メソッドを介してユーザーと対話し、メッセージ/ツールのレンダリング方法をカスタマイズできます。
カスタム コンポーネントについては、tui.md を参照してください。これには次のコピー&ペースト パターンがあります。
- 選択ダイアログ (SelectList)
- キャンセル付きの非同期操作 (BorderLoader)
- 設定切り替え (SettingsList)
- ステータスインジケーター (setStatus)
- ストリーミング中の動作メッセージ、可視性、インジケーター (
setWorkingMessage、setWorkingVisible、setWorkingIndicator) - エディターの上/下のウィジェット (setWidget)
- 組み込みのスラッシュ/パス補完の上に重ねられたオートコンプリート プロバイダー (addAutocompleteProvider)
- カスタムフッター (setFooter)
// Select from optionsconst choice = await ctx.ui.select("Pick one:", ["A", "B", "C"]);
// Confirm dialogconst ok = await ctx.ui.confirm("Delete?", "This cannot be undone");
// Text inputconst name = await ctx.ui.input("Name:", "placeholder");
// Multi-line editorconst text = await ctx.ui.editor("Edit:", "prefilled text");
// Notification (non-blocking)ctx.ui.notify("Done!", "info"); // "info" | "warning" | "error"カウントダウン付きの時間指定ダイアログ
Section titled “カウントダウン付きの時間指定ダイアログ”ダイアログは、ライブ カウントダウン表示で自動終了する timeout オプションをサポートしています。
// Dialog shows "Title (5s)" → "Title (4s)" → ... → auto-dismisses at 0const confirmed = await ctx.ui.confirm( "Timed Confirmation", "This dialog will auto-cancel in 5 seconds. Confirm?", { timeout: 5000 });
if (confirmed) { // User confirmed} else { // User cancelled or timed out}タイムアウト時の戻り値:
select()はundefinedを返しますconfirm()はfalseを返しますinput()はundefinedを返します
AbortSignal による手動終了
Section titled “AbortSignal による手動終了”さらに制御するには (タイムアウトとユーザーのキャンセルを区別するなど)、AbortSignal を使用します。
const controller = new AbortController();const timeoutId = setTimeout(() => controller.abort(), 5000);
const confirmed = await ctx.ui.confirm( "Timed Confirmation", "This dialog will auto-cancel in 5 seconds. Confirm?", { signal: controller.signal });
clearTimeout(timeoutId);
if (confirmed) { // User confirmed} else if (controller.signal.aborted) { // Dialog timed out} else { // User cancelled (pressed Escape or selected "No")}完全な例については、examples/extensions/timed-confirm.ts を参照してください。
ウィジェット、ステータス、フッター
Section titled “ウィジェット、ステータス、フッター”// Status in footer (persistent until cleared)ctx.ui.setStatus("my-ext", "Processing...");ctx.ui.setStatus("my-ext", undefined); // Clear
// Working loader (shown during streaming)ctx.ui.setWorkingMessage("Thinking deeply...");ctx.ui.setWorkingMessage(); // Restore defaultctx.ui.setWorkingVisible(false); // Hide the built-in working loader row entirelyctx.ui.setWorkingVisible(true); // Show the built-in working loader row
// Working indicator (shown during streaming)ctx.ui.setWorkingIndicator({ frames: [ctx.ui.theme.fg("accent", "●")] }); // Static dotctx.ui.setWorkingIndicator({ frames: [ ctx.ui.theme.fg("dim", "·"), ctx.ui.theme.fg("muted", "•"), ctx.ui.theme.fg("accent", "●"), ctx.ui.theme.fg("muted", "•"), ], intervalMs: 120,});ctx.ui.setWorkingIndicator({ frames: [] }); // Hide indicatorctx.ui.setWorkingIndicator(); // Restore default spinner
// Widget above editor (default)ctx.ui.setWidget("my-widget", ["Line 1", "Line 2"]);// Widget below editorctx.ui.setWidget("my-widget", ["Line 1", "Line 2"], { placement: "belowEditor" });ctx.ui.setWidget("my-widget", (tui, theme) => new Text(theme.fg("accent", "Custom"), 0, 0));ctx.ui.setWidget("my-widget", undefined); // Clear
// Custom footer (replaces built-in footer entirely)ctx.ui.setFooter((tui, theme) => ({ render(width) { return [theme.fg("dim", "Custom footer")]; }, invalidate() {},}));ctx.ui.setFooter(undefined); // Restore built-in footer
// Terminal titlectx.ui.setTitle("pi - my-project");
// Editor textctx.ui.setEditorText("Prefill text");const current = ctx.ui.getEditorText();
// Paste into editor (triggers paste handling, including collapse for large content)ctx.ui.pasteToEditor("pasted content");
// Stack custom autocomplete behavior on top of the built-in providerctx.ui.addAutocompleteProvider((current) => ({ triggerCharacters: ["#"], async getSuggestions(lines, line, col, options) { const beforeCursor = (lines[line] ?? "").slice(0, col); const match = beforeCursor.match(/(?:^|[ \t])#([^\s#]*)$/); if (!match) { return current.getSuggestions(lines, line, col, options); }
return { prefix: `#${match[1] ?? ""}`, items: [{ value: "#2983", label: "#2983", description: "Extension API for autocomplete" }], }; }, applyCompletion(lines, line, col, item, prefix) { return current.applyCompletion(lines, line, col, item, prefix); }, shouldTriggerFileCompletion(lines, line, col) { return current.shouldTriggerFileCompletion?.(lines, line, col) ?? true; },}));
// Tool output expansionconst wasExpanded = ctx.ui.getToolsExpanded();ctx.ui.setToolsExpanded(true);ctx.ui.setToolsExpanded(wasExpanded);
// Custom editor (vim mode, emacs mode, etc.)ctx.ui.setEditorComponent((tui, theme, keybindings) => new VimEditor(tui, theme, keybindings));const currentEditor = ctx.ui.getEditorComponent();ctx.ui.setEditorComponent((tui, theme, keybindings) => new WrappedEditor(tui, theme, keybindings, currentEditor?.(tui, theme, keybindings)));ctx.ui.setEditorComponent(undefined); // Restore default editor
// Theme management (see themes.md for creating themes)const themes = ctx.ui.getAllThemes(); // [{ name: "dark", path: "/..." | undefined }, ...]const lightTheme = ctx.ui.getTheme("light"); // Load without switchingconst result = ctx.ui.setTheme("light"); // Switch by nameif (!result.success) { ctx.ui.notify(`Failed: ${result.error}`, "error");}ctx.ui.setTheme(lightTheme!); // Or switch by Theme objectctx.ui.theme.fg("accent", "styled text"); // Access current themeカスタムの作業インジケーター フレームはそのままレンダリングされます。色が必要な場合は、ctx.ui.theme.fg(...) などを使用して、自分でフレーム文字列に色を追加します。
オートコンプリートプロバイダー
Section titled “オートコンプリートプロバイダー”ctx.ui.addAutocompleteProvider() を使用して、組み込みのスラッシュ コマンドとパス プロバイダーの上にカスタム オートコンプリート ロジックをスタックします。 $ などのカスタム ナチュラル トリガーには triggerCharacters を設定します。
典型的なパターン:
- カーソルの前のテキストを検査します
- 拡張機能固有の構文が一致する場合に独自の提案を返します
- それ以外の場合は
current.getSuggestions(...)に委任します - カスタムの挿入動作が必要な場合を除き、
applyCompletion(...)をデリゲートします。
pi.on("session_start", (_event, ctx) => { ctx.ui.addAutocompleteProvider((current) => ({ triggerCharacters: ["#"], async getSuggestions(lines, cursorLine, cursorCol, options) { const line = lines[cursorLine] ?? ""; const beforeCursor = line.slice(0, cursorCol); const match = beforeCursor.match(/(?:^|[ \t])#([^\s#]*)$/); if (!match) { return current.getSuggestions(lines, cursorLine, cursorCol, options); }
return { prefix: `#${match[1] ?? ""}`, items: [ { value: "#2983", label: "#2983", description: "Extension API for registering custom @ autocomplete providers" }, { value: "#2753", label: "#2753", description: "Reload stale resource settings" }, ], }; },
applyCompletion(lines, cursorLine, cursorCol, item, prefix) { return current.applyCompletion(lines, cursorLine, cursorCol, item, prefix); },
shouldTriggerFileCompletion(lines, cursorLine, cursorCol) { return current.shouldTriggerFileCompletion?.(lines, cursorLine, cursorCol) ?? true; }, }));});gh issue list を使用して最新のオープンな GitHub 問題をプリロードし、高速に #... を完了できるようにローカルでフィルターする完全な例については、github-issue-autocomplete.ts を参照してください。 GitHub CLI (gh) と GitHub リポジトリのチェックアウトが必要です。
カスタムコンポーネント
Section titled “カスタムコンポーネント”複雑な UI の場合は、ctx.ui.custom() を使用します。これにより、done() が呼び出されるまで、エディターがコンポーネントに一時的に置き換えられます。
import { Text, Component } from "@earendil-works/pi-tui";
const result = await ctx.ui.custom<boolean>((tui, theme, keybindings, done) => { const text = new Text("Press Enter to confirm, Escape to cancel", 1, 1);
text.onKey = (key) => { if (key === "return") done(true); if (key === "escape") done(false); return true; };
return text;});
if (result) { // User pressed Enter}コールバックは以下を受け取ります:
tui- TUI インスタンス (画面サイズ、フォーカス管理用)theme- スタイリングの現在のテーマkeybindings- アプリのキーバインドマネージャー (ショートカットの確認用)done(value)- コンポーネントを閉じて値を返す呼び出し
完全なコンポーネント API については、tui.md を参照してください。
オーバーレイ モード (実験的)
Section titled “オーバーレイ モード (実験的)”{ overlay: true } を渡すと、画面をクリアせずにコンポーネントを既存のコンテンツの上にフローティング モーダルとしてレンダリングします。
const result = await ctx.ui.custom<string | null>( (tui, theme, keybindings, done) => new MyOverlayComponent({ onClose: done }), { overlay: true });高度な配置 (アンカー、マージン、パーセンテージ、応答性の可視性) の場合は、overlayOptions を渡します。 onHandle を使用して、プログラムでフォーカスまたは可視性を制御します。
const result = await ctx.ui.custom<string | null>( (tui, theme, keybindings, done) => new MyOverlayComponent({ onClose: done }), { overlay: true, overlayOptions: { anchor: "top-right", width: "50%", margin: 2 }, onHandle: (handle) => { handle.focus(); // focus this overlay and bring it to the visual front // handle.unfocus({ target: editorComponent }); // release input to a specific component // handle.setHidden(true/false); // toggle visibility // handle.hide(); // permanently remove } });フォーカスされた表示オーバーレイは、一時的な非オーバーレイ カスタム UI が閉じた後に入力を再利用できます。オーバーレイが表示されている間、別のコンポーネントが入力を維持するように意図的にしたい場合は、handle.unfocus({ target }) を呼び出します。 { target: null } を渡すと、別のコンポーネントにフォーカスすることなくオーバーレイが解放されます。
完全な OverlayOptions および OverlayHandle API については tui.md を、例については overlay-qa-tests.ts を参照してください。
カスタムエディター
Section titled “カスタムエディター”メインの入力エディタをカスタム実装 (vim モード、emacs モードなど) に置き換えます。
import { CustomEditor, type ExtensionAPI } from "@earendil-works/pi-coding-agent";import { matchesKey } from "@earendil-works/pi-tui";
class VimEditor extends CustomEditor { private mode: "normal" | "insert" = "insert";
handleInput(data: string): void { if (matchesKey(data, "escape") && this.mode === "insert") { this.mode = "normal"; return; } if (this.mode === "normal" && data === "i") { this.mode = "insert"; return; } super.handleInput(data); // App keybindings + text editing }}
export default function (pi: ExtensionAPI) { pi.on("session_start", (_event, ctx) => { ctx.ui.setEditorComponent((_tui, theme, keybindings) => new VimEditor(theme, keybindings) ); });}重要なポイント:
CustomEditor(ベースEditorではない) を拡張して、アプリのキーバインドを取得します (中止するエスケープ、ctrl+d、モデルの切り替え)- 扱っていないキーについては
super.handleInput(data)に電話してください - 工場はアプリから
themeとkeybindingsを受け取ります - 以前に構成したカスタム エディターをラップするには、
setEditorComponent()の前にctx.ui.getEditorComponent()を使用します。 undefinedを渡してデフォルトに戻します:ctx.ui.setEditorComponent(undefined)
すでにエディターを置き換えている別の拡張機能を使用して作成するには、自分のファクトリーを設定する前に、以前のファクトリーをキャプチャーします。
const previous = ctx.ui.getEditorComponent();ctx.ui.setEditorComponent((tui, theme, keybindings) => new MyEditor(tui, theme, keybindings, { base: previous?.(tui, theme, keybindings) }));モードインジケーターを使用した完全な例については、tui.md パターン 7 を参照してください。
メッセージのレンダリング
Section titled “メッセージのレンダリング”customType を使用してメッセージのカスタム レンダラーを登録します。
import { Text } from "@earendil-works/pi-tui";
pi.registerMessageRenderer("my-extension", (message, options, theme) => { const { expanded } = options; let text = theme.fg("accent", `[${message.customType}] `); text += message.content;
if (expanded && message.details) { text += "\n" + theme.fg("dim", JSON.stringify(message.details, null, 2)); }
return new Text(text, 0, 0);});メッセージは pi.sendMessage() 経由で送信されます。
pi.sendMessage({ customType: "my-extension", // Matches registerMessageRenderer content: "Status update", display: true, // Show in TUI details: { ... }, // Available in renderer});テーマカラー
Section titled “テーマカラー”すべてのレンダリング関数は theme オブジェクトを受け取ります。カスタム テーマとフルカラー パレットの作成については、themes.md を参照してください。
// Foreground colorstheme.fg("toolTitle", text) // Tool namestheme.fg("accent", text) // Highlightstheme.fg("success", text) // Success (green)theme.fg("error", text) // Errors (red)theme.fg("warning", text) // Warnings (yellow)theme.fg("muted", text) // Secondary texttheme.fg("dim", text) // Tertiary text
// Text stylestheme.bold(text)theme.italic(text)theme.strikethrough(text)カスタム ツール レンダラでの構文ハイライトの場合:
import { highlightCode, getLanguageFromPath } from "@earendil-works/pi-coding-agent";
// Highlight code with explicit languageconst highlighted = highlightCode("const x = 1;", "typescript", theme);
// Auto-detect language from file pathconst lang = getLanguageFromPath("/path/to/file.rs"); // "rust"const highlighted = highlightCode(code, lang, theme);- 内線エラーがログに記録され、エージェントは続行します
tool_callエラーがツールをブロックする (フェールセーフ)- ツール
executeエラーは、スローによって通知する必要があります。スローされたエラーが捕捉され、isError: trueを使用して LLM に報告され、実行が続行されます。
モードの動作
Section titled “モードの動作”| モード | ctx.mode | ctx.hasUI | メモ |
|---|---|---|---|
| インタラクティブ | "tui" | true | ターミナルレンダリングを備えた完全な TUI |
RPC (--mode rpc) | "rpc" | true | JSON プロトコルを介したダイアログと通知。custom() は undefined を返す。rpc.md を参照 |
JSON (--mode json) | "json" | false | イベントストリームを標準出力に送信。UI メソッドはノーオペレーション |
Print (-p) | "print" | false | 拡張機能は実行されるがプロンプト不可 |
TUI 固有の機能 (custom()、コンポーネント ファクトリ、端末入力) の前に ctx.mode === "tui" を使用します。 TUI モードと RPC モードの両方で機能するダイアログ メソッドと通知メソッドの前に ctx.hasUI を使用します。
サンプルリファレンス
Section titled “サンプルリファレンス”すべての例は examples/extensions/ にあります。
| 例 | 説明 | 主要な API |
|---|---|---|
| ツール | ||
hello.ts | 最小限のツール登録 | registerTool |
question.ts | ユーザーインタラクションを備えたツール | registerTool、ui.select |
questionnaire.ts | マルチステップウィザードツール | registerTool、ui.custom |
todo.ts | 永続性を備えたステートフルツール | registerTool、appendEntry、renderResult、セッションイベント |
dynamic-tools.ts | 起動後およびコマンド中にツールを登録 | registerTool、session_start、registerCommand |
structured-output.ts | terminate: true を使用した最終的な構造化出力ツール | registerTool、終了するツール結果 |
truncated-tool.ts | 出力の切り捨ての例 | registerTool、truncateHead |
tool-override.ts | 組み込み read ツールをオーバーライド | registerTool(組み込みと同じ名前) |
| コマンド | ||
pirate.ts | ターンごとにシステムプロンプトを変更 | registerCommand、before_agent_start |
summarize.ts | 会話要約コマンド | registerCommand、ui.custom |
handoff.ts | プロバイダー間モデルのハンドオフ | registerCommand、ui.editor、ui.custom |
qna.ts | カスタム UI に関する Q&A | registerCommand、ui.custom、setEditorText |
send-user-message.ts | ユーザーメッセージを挿入 | registerCommand、sendUserMessage |
reload-runtime.ts | リロードコマンドと LLM ツールのハンドオフ | registerCommand、ctx.reload()、sendUserMessage |
shutdown-command.ts | グレースフルシャットダウンコマンド | registerCommand、shutdown() |
| イベントとゲート | ||
permission-gate.ts | 危険なコマンドをブロック | on("tool_call")、ui.confirm |
project-trust.ts | ユーザー/グローバルまたは CLI 拡張からのプロジェクト信頼を決定または延期 | on("project_trust")、信頼 UI、必要な信頼結果 |
protected-paths.ts | 特定のパスへの書き込みをブロック | on("tool_call") |
confirm-destructive.ts | セッションの変更を確認 | on("session_before_switch")、on("session_before_fork") |
dirty-repo-guard.ts | ダーティな git リポジトリについて警告 | on("session_before_*")、exec |
input-transform.ts | ユーザー入力を変換 | on("input") |
input-transform-streaming.ts | ストリーミング対応の入力変換 | on("input")、streamingBehavior |
model-status.ts | モデル変更への対応 | on("model_select")、setStatus |
provider-payload.ts | ペイロードとプロバイダー応答ヘッダーを検査 | on("before_provider_request")、on("after_provider_response") |
system-prompt-header.ts | システムプロンプト情報を表示 | on("agent_start")、getSystemPrompt |
claude-rules.ts | ファイルからルールをロード | on("session_start")、on("before_agent_start") |
prompt-customizer.ts | systemPromptOptions を使用してコンテキスト認識ツールガイダンスを追加 | on("before_agent_start")、BuildSystemPromptOptions |
file-trigger.ts | ファイルウォッチャーがメッセージをトリガー | sendMessage |
| 圧縮とセッション | ||
custom-compaction.ts | カスタム圧縮の要約 | on("session_before_compact") |
trigger-compact.ts | 圧縮を手動でトリガー | compact() |
git-checkpoint.ts | ターン中の Git スタッシュ | on("turn_start")、on("session_before_fork")、exec |
git-merge-and-resolve.ts | 競合をフェッチ、マージ、解決 | on("agent_end")、exec、sendUserMessage |
auto-commit-on-exit.ts | シャットダウン時にコミット | on("session_shutdown")、exec |
| UI コンポーネント | ||
status-line.ts | フッターステータスインジケーター | setStatus、セッションイベント |
working-indicator.ts | ストリーミング動作インジケーターをカスタマイズ | setWorkingIndicator、registerCommand |
github-issue-autocomplete.ts | gh issue list から最近の未解決 issue をプリロードして組み込みオートコンプリートの上に #1234 issue 補完を追加 | addAutocompleteProvider、on("session_start")、exec |
custom-footer.ts | フッターを完全に置き換え | registerCommand、setFooter |
custom-header.ts | 起動ヘッダーを置き換え | on("session_start")、setHeader |
modal-editor.ts | Vim スタイルのモーダルエディター | setEditorComponent、CustomEditor |
rainbow-editor.ts | カスタムエディタースタイル | setEditorComponent |
widget-placement.ts | エディターの上/下のウィジェット | setWidget |
overlay-test.ts | オーバーレイコンポーネント | ui.custom(オーバーレイオプションあり) |
overlay-qa-tests.ts | 包括的なオーバーレイテスト | ui.custom、すべてのオーバーレイオプション |
notify.ts | シンプルな通知 | ui.notify |
timed-confirm.ts | タイムアウトのあるダイアログ | ui.confirm(タイムアウト/シグナル付き) |
mac-system-theme.ts | テーマの自動切り替え | setTheme、exec |
| 複雑な拡張機能 | ||
plan-mode/ | フルプランモードの実装 | すべてのイベントタイプ、registerCommand、registerShortcut、registerFlag、setStatus、setWidget、sendMessage、setActiveTools |
preset.ts | 保存可能なプリセット(モデル、ツール、思考) | registerCommand、registerShortcut、registerFlag、setModel、setActiveTools、setThinkingLevel、appendEntry |
tools.ts | ツールのオン/オフを切り替える UI | registerCommand、setActiveTools、SettingsList、セッションイベント |
| リモートとサンドボックス | ||
ssh.ts | SSH リモート実行 | registerFlag、on("user_bash")、on("before_agent_start")、ツール操作 |
interactive-shell.ts | 永続的なシェルセッション | on("user_bash") |
sandbox/ | サンドボックスツールの実行 | ツール操作 |
gondolin/ | 組み込みツールと ! コマンドを Gondolin マイクロ VM にルーティング | ツール操作、組み込みツールオーバーライド、on("user_bash") |
subagent/ | サブエージェントを起動 | registerTool、exec |
| ゲーム | ||
snake.ts | スネークゲーム | registerCommand、ui.custom、キーボード処理 |
space-invaders.ts | スペースインベーダーゲーム | registerCommand、ui.custom |
doom-overlay/ | オーバーレイでの Doom | ui.custom(オーバーレイ付き) |
| プロバイダー | ||
custom-provider-anthropic/ | カスタム Anthropic プロキシ | registerProvider |
custom-provider-gitlab-duo/ | GitLab Duo 統合 | OAuth を使用した registerProvider |
| メッセージと通信 | ||
message-renderer.ts | カスタムメッセージのレンダリング | registerMessageRenderer、sendMessage |
event-bus.ts | 拡張間イベント | pi.events |
| セッションメタデータ | ||
session-name.ts | セレクター用にセッションに名前を付ける | setSessionName、getSessionName |
bookmark.ts | /tree のブックマークエントリ | setLabel |
| その他 | ||
inline-bash.ts | ツール呼び出しのインライン bash | on("tool_call") |
bash-spawn-hook.ts | 実行前に bash コマンド、cwd、env を調整 | createBashTool、spawnHook |
with-deps/ | npm 依存関係を伴う拡張 | package.json を使ったパッケージ構造 |