Pi 的配置、擴充、平台設定和 API 參考。

Extensions

pi 可以建立擴充。要求它為您的用例建立一個。

Extensions 是 TypeScript 擴充 pi 行為的模組。他們可以訂閱生命週期事件、註冊可由 LLM 呼叫的自訂工具、新增命令等。

/reload 的放置: 將擴充放入 ~/.pi/agent/extensions/(全域)或 .pi/extensions/(專案本地)以進行自動發現。僅使用 pi -e./path.ts 進行快速測試。自動發現位置中的Extensions可以使用/reload進行熱重載。

關鍵能力:

  • 自訂工具 - 註冊LLM可以透過pi.registerTool()呼叫的工具
  • 事件攔截 - 阻止或修改工具呼叫、注入上下文、自訂壓縮
  • 使用者互動 - 透過ctx.ui提示使用者(選擇、確認、輸入、通知)
  • 自訂 UI 元件 - 完整的 TUI 元件,透過 ctx.ui.custom() 進行鍵盤輸入以實現複雜的交互
  • 自訂指令 - 透過 pi.registerCommand() 註冊諸如 /mycommand 之類的指令
  • 會話持久性 - 儲存透過 pi.appendEntry() 重新啟動後仍然存在的狀態
  • 自訂渲染 - 控制工具呼叫/結果和訊息在 TUI 中的顯示方式

用例範例:

  • 權限門(rm -rfsudo等前確認)
  • Git 檢查點(每回合隱藏,在分支上恢復)
  • 路徑保護(阻止寫入.envnode_modules/
  • 自訂壓縮(以您的方式總結對話)
  • 對話摘要(參見summarize.ts範例)
  • 互動式工具(問題、精靈、自訂對話框)
  • 有狀態工具(待辦事項清單、連線池)
  • 外部整合(文件觀察器、webhooks、CI 觸發器)
  • 等待時玩遊戲(請參閱snake.ts範例)

請參閱examples/extensions/了解有效的實作。

目錄

快速入門

創作~/.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

擴展地點

安全性: Extensions 以完整的系統權限運行,可以執行任意程式碼。僅從您信任的來源安裝。

Extensions 是從受信任的位置自動發現的。項目本地.pi/extensions條目僅在項目受信任後載入。

地點 範圍
~/.pi/agent/extensions/*.ts 全球(所有項目)
~/.pi/agent/extensions/*/index.ts 全域(子目錄)
.pi/extensions/*.ts 專案本地化
.pi/extensions/*/index.ts 專案本地(子目錄)

通過 settings.json 的其他路徑:

{
  "packages": [
    "npm:@foo/bar@1.0.0",
    "git:github.com/user/repo@v1"
  ],
  "extensions": [
    "/path/to/local/extension.ts",
    "/path/to/local/extension/dir"
  ]
}

若要透過 npm 或 git 將擴充功能共用為 pi 套件,請參閱 packages.md

可用進口

包裹 目的
@earendil-works/pi-coding-agent 擴充類型(ExtensionAPIExtensionContext、事件)
typebox 工具參數的架構定義
@earendil-works/pi-ai AI 實用程式(StringEnum 適用於 Google 相容枚舉)
@earendil-works/pi-tui 用於自訂渲染的TUI元件

npm 依賴關係也有效。在擴充旁邊(或父目錄)新增 package.json,執行 npm install,然後從 node_modules/ 匯入會自動解析。

對於使用pi install(npm或git)安裝的分散式pi包,運行時依賴必須位於dependencies中。軟體包安裝預設使用生產安裝(npm install --omit=dev),因此devDependencies在運行時不可用;當配置 npmCommand 時,git 包使用普通的 install 來與包裝器相容。

另提供 Node.js 內建函數(node:fsnode:path 等)。

編寫擴展

擴展導出一個接收 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", { ... });
}

Extensions 透過jiti 加載,因此TypeScript 無需編譯即可工作。

如果工廠返回Promise,pi 會在繼續啟動之前等待它。這意味著非同步初始化在session_start之前、resources_discover之前以及透過pi.registerProvider()排隊的提供者註冊被刷新之前完成。

非同步工廠函數

使用非同步工廠進行一次性啟動工作,例如取得遠端配置或動態發現可用模型。

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

長期資源和關閉

擴展工廠可能在從不啟動會話的呼叫中運行。不要從工廠啟動後台資源,例如進程、套接字、檔案觀察程式或計時器。

延後後台資源啟動,直到 session_start 或需要資源的指令/工具/事件。註冊一個冪等 session_shutdown 處理程序來關閉您啟動的任何會話範圍的資源。

擴充樣式

單一檔案 - 最簡單,適用於小型擴充:

~/.pi/agent/extensions/
└── my-extension.ts

帶有index.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
// package.json
{
  "name": "my-extension",
  "dependencies": {
    "zod": "^3.0.0",
    "chalk": "^5.0.0"
  },
  "pi": {
    "extensions": ["./src/index.ts"]
  }
}

在擴充目錄中運行npm install,然​​後從node_modules/自動匯入。

活動

生命週期概述

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_headers (can mutate headers)     |
  │   ├─► 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                                            │
  └─► agent_settled (no retry/compaction/follow-up left)   │
                                                           │
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" }

/name or pi.setSessionName()
  └─► session_info_changed

/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

啟動活動

項目_信任

在 pi 決定是否信任具有動態配置(.pi.agents/skills)的項目之前觸發。它在啟動期間以及當會話替換(例如 /resume)進入當前進程中信任尚未解析的 cwd 時運行。僅使用者/全域分機和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。如果沒有處理程序回傳是/否,則繼續正常的信任解析:先套用已儲存的 trust.json 決策,然後defaultProjectTrust 控制 pi 預設是否詢問、信任或拒絕。

資源事件

資源發現

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"],
  };
});

會議活動

請參閱 Session Format 了解會話儲存內部結構和 SessionManager API。

會話開始

當會話啟動、載入或重新載入時觸發。

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");
});

會話資訊已更改

透過 /name、RPC 或 pi.setSessionName() 設定目前會話顯示名稱時觸發。

pi.on("session_info_changed", async (event, ctx) => {
  // event.name - current normalized name, or undefined if cleared
  ctx.ui.notify(`Session renamed: ${event.name ?? "(none)"}`, "info");
});

切換前的會話

在開始新會話 (/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,為新會話重新加載並重新綁定擴展,然後發出 session_start 以及 reason: "new" | "resume"previousSessionFile。 在 session_shutdown 中進行清理工作,然後在 session_start 中重新建立任何記憶體中狀態。

分叉前的會話

透過 /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,為新會話重新加載並重新綁定擴展,然後發出 session_start 以及 reason: "fork"previousSessionFile。 在 session_shutdown 中進行清理工作,然後在 session_start 中重新建立任何記憶體中狀態。

session_before_compact / session_compact

壓實時發射。詳情請參閱compaction.md

pi.on("session_before_compact", async (event, ctx) => {
  const { preparation, branchEntries, customInstructions, reason, willRetry, signal } = event;

  // reason - "manual" (/compact), "threshold", or "overflow"
  // willRetry - whether the aborted turn is retried after compaction (overflow recovery)

  // Cancel:
  return { cancel: true };

  // Custom summary:
  return {
    compaction: {
      summary: "...",
      firstKeptEntryId: preparation.firstKeptEntryId,
      tokensBefore: preparation.tokensBefore,
      // usage: summaryResponse.usage, // Optional; included in session totals
    }
  };
});

pi.on("session_compact", async (event, ctx) => {
  // event.compactionEntry - the saved compaction
  // event.fromExtension - whether extension provided it
  // event.reason - "manual" (/compact), "threshold", or "overflow"
  // event.willRetry - whether the aborted turn is retried after compaction (overflow recovery)
});

session_before_tree / session_tree

/tree 導航上觸發。有關樹導航概念,請參閱Sessions

pi.on("session_before_tree", async (event, ctx) => {
  const { preparation, signal } = event;
  return { cancel: true };
  // OR provide custom summary:
  return {
    summary: {
      summary: "...",
      // usage: summaryResponse.usage, // Optional; included in session totals
      details: {},
    },
  };
});

pi.on("session_tree", async (event, ctx) => {
  // event.newLeafId, oldLeafId, summaryEntry, fromExtension
});

會話關閉

在啟動的會話運行時被拆除之前觸發。使用它來清理從 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.
});

代理活動

代理啟動之前

在使用者提交提示後、代理循環之前觸發。可以注入訊息和/或修改系統提示。

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 已載入的內容 - 自訂提示、指南、工具片段、context files、技能 - 無需重新發​​現資源或重新解析標誌。當您的擴充功能需要對系統提示進行深入、明智的更改,同時尊重使用者提供的配置時,請使用它。

before_agent_startevent.systemPromptctx.getSystemPrompt()內部都反映了目前處理程序的連結系統提示符號。以後before_agent_start處理程序仍然可以再修改它。

代理開始/代理結束/代理結算

當低階代理運行開始時,agent_start 會觸發。 agent_end 在運行結束時觸發,但 Pi 仍可能自動重試、自動壓縮並重試,或繼續處理排隊的後續訊息。使用 agent_settled 進行需要知道 Pi 不會繼續自動運行的狀態整合。

pi.on("agent_start", async (_event, ctx) => {});

pi.on("agent_end", async (event, ctx) => {
  // event.messages - messages from this low-level run
});

pi.on("agent_settled", async (_event, ctx) => {
  // ctx.isIdle() is true here unless another extension started a new run.
});

轉彎開始/轉彎結束

每回合觸發(一個 LLM 回應 + 工具呼叫)。

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_startmessage_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 依工具完成順序發出
  • 最終 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
});

情境

在每次 LLM 通話之前觸發。非破壞性地修改訊息。訊息類型請參閱Session Format

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_headers

組裝傳出 HTTP 標頭後觸發。使用它來新增、覆蓋或刪除請求標頭。

處理程序就地突變event.headers。將鍵設為字串以新增或覆寫它,或設為 null 來刪除它。

pi.on("before_provider_headers", (event, ctx) => {
  // Add or override — e.g. a session id for gateway tracing/attribution
  event.headers["x-session-id"] = ctx.sessionManager.getSessionId();

  // Drop a tracking header pi adds for this call
  event.headers["X-OpenRouter-Title"] = null;
});

每個提供者請求運行一次;重試重用相同的標頭而不是重新觸發鉤子。

before_provider_request 之前

在建構特定於提供者的有效負載之後、發送請求之前觸發。處理程序按擴展載入順序運行。返回 undefined 保持有效負載不變。傳回任何其他值都會取代後續處理程序和實際請求的有效負載。

此鉤子可以重寫提供者層級的系統指令或完全刪除它們。這些有效負載等級的變更不會由 ctx.getSystemPrompt() 反映,它報告 Pi 的系統提示字串,而不是最終的序列化提供者有效負載。

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

在收到 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"]);
  }
});

標頭可用性取決於提供者和傳輸。 Providers 抽象 HTTP 回應可能不會公開標頭。

模特兒活動

模型選擇

當模型透過 /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 元素(狀態列、頁腳)或在活動模型變更時執行特定於模型的初始化。

思考等級選擇

當思維層次發生變化時被解僱。這僅用於通知;處理程序回傳值將被忽略。

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。

工具事件

工具調用

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, terminate?: boolean }控制阻塞
  • terminate僅適用於阻塞呼叫;僅當批次中的每個最終結果都終止時,代理才會提前停止
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", terminate: true };
    }
  }

  if (isToolCallEventType("read", event)) {
    // event.input is { path: string; offset?: number; limit?: number }
    console.log(`Reading: ${event.input.path}`);
  }
});

鍵入自訂工具輸入

自訂工具應匯出其輸入類型:

// my-extension.ts
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_execution_end 加上最終工具結果訊息事件發出之前觸發。 **可以修改結果。 **

在並行工具模式下,tool_resulttool_execution_end可能會依照工具完成順序交錯,而最終的toolResult訊息事件仍會依照輔助源順序稍後發出。

tool_result 處理程序鍊式中介軟體:

  • 處理程序按擴展載入順序運行
  • 每個處理程序都會看到前一個處理程序變更後的最新結果
  • 處理程序可以傳回部分補丁(contentdetailsisErrorusage);省略的欄位保留其目前值

使用 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, event.usage

  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, usage: nestedModelUsage };
});

用戶狂歡事件

用戶_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 } };
});

輸入事件

輸入

在檢查擴展命令之後但在技能和模板擴展之前收到用戶輸入時觸發。該事件看到原始輸入文本,因此 /skill:foo/template 尚未展開。

加工訂單:

  1. 首先檢查擴展命令 (/cmd) - 如果找到,則運行處理程序並跳過輸入事件
  2. input 事件觸發 - 可以攔截、轉換或處理
  3. 如果不處理:技能指令(/skill:name)擴充為技能內容
  4. 如果不處理:prompt templates(/template)擴展到模板內容
  5. 代理處理開始(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 - 完全跳過代理(第一個返回該代理的處理程序獲勝)

跨處理程序轉換鏈。請參閱 input-transform.tsinput-transform-streaming.ts 了解 streamingBehavior 感知路由。

擴充上下文

所有處理程序都會收到 ctx: ExtensionContext

ctx.ui

使用者互動的 UI 方法。有關完整詳細信息,請參閱Custom UI

ctx模式

目前運行模式:"tui""rpc""json""print"。使用 ctx.mode === "tui" 保護僅限終端的功能,例如 custom()、組件工廠、終端輸入和直接 TUI 渲染。

ctx.hasUI

true in TUI and RPC modes. false in print mode (-p) and JSON mode. Use this to guard dialog methods (select, confirm, input, editor) and fire-and-forget methods (notify, setStatus, setWidget, setTitle) that work in TUI and RPC modes. In RPC mode, some TUI-specific methods are no-ops or return default values (see rpc.md).

ctx.cwd

目前工作目錄。

建構專案本地配置路徑時,使用 CONFIG_DIR_NAME 而不是硬編碼 .pi。重新命名的發行版可以使用不同的配置目錄名稱。

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()

傳回專案本機信任對於目前會話上下文是否處於活動狀態。這包括臨時信任決策和CLI信任覆蓋,而不僅僅是全局信任儲存中保存的決策。

在閱讀專案本機擴充配置之前使用此配置,此配置僅適用於受信任的專案。

ctx.sessionManager

對會話狀態的唯讀存取。請參閱 Session Format 以了解完整的 SessionManager API 和條目類型。

對於tool_call,該狀態在處理程序運行之前會透過目前輔助訊息進行同步。在平行工具執行模式下,仍不能保證包含來自相同輔助訊息的同級工具結果。

ctx.sessionManager.getEntries()             // All entries
ctx.sessionManager.getBranch()              // Current branch
ctx.sessionManager.buildContextEntries()    // Active branch entries with compaction applied
ctx.sessionManager.getLeafId()              // Current leaf entry ID

ctx.modelRegistry / ctx.model / ctx.thinkingLevel / ctx.scopedModels

存取模型、提供者和解析的身份驗證。 ctx.modelRegistry.getProvider(id) 傳回有效的 pi-ai 提供程序,而 getProviderAuth(id) 解析其目前的 API key、標頭、基本 URL 和提供者範圍的環境,而無需載入模型。 ctx.model是活躍模型,ctx.thinkingLevel是當前有效思維層次。

ctx.scopedModels 是目前會話範圍內模型的唯讀清單 - 與 /scoped-models 指令顯示的集合相同。它在會話開始時從 --models CLI 標誌和 enabledModels 設定進行解析(與 provider/modelId 上的 minimatch 或裸的 modelId 上的可用目錄進行匹配)。當未配置範圍時它為空,這意味著每個可用模型都可用。每個條目都是 { model, thinkingLevel? },其中 thinkingLevel 僅當模式固定它時才設定(例如 anthropic/*:high)。使用它來填充模型選擇器,該模型選擇器鏡像內建模型選擇器,而不是透過 ctx.modelRegistry.getAvailable() 枚舉整個目錄。

ctx訊號

目前代理中止訊號,或當沒有代理輪次處於活動狀態時為 undefined

將此用於由擴展處理程序啟動的中止感知嵌套工作,例如:

  • fetch(..., { signal: ctx.signal })
  • 接受 signal 的模型調用
  • 接受 AbortSignal 的檔案或進程助手

ctx.signal 通常在活動轉彎事件期間定義,例如 tool_calltool_resultmessage_updateturn_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()

控制流程助理。當 Pi 正在處理代理運行、自動重試、自動壓縮重試或排隊延續時,ctx.isIdle() 為 false。

ctx.shutdown()

請求正常關閉 pi。

  • 互動模式: 延遲到代理變得空閒(處理完所有排隊的轉向和後續訊息後)。
  • **RPC模式:**推遲到下一個空閒狀態(完成當前命令回應後,等待下一個命令時)。
  • 列印模式: 無操作。處理完所有提示後,流程將自動退出。

在退出之前向所有擴展發出 session_shutdown 事件。可用於所有上下文(事件處理程序、工具、命令、捷徑)。

pi.on("tool_call", (event, ctx) => {
  if (isFatal(event.input)) {
    ctx.shutdown();
  }
});

ctx.getContextUsage()

傳回活動模型的目前上下文使用情況。使用最後一次助理使用情況(如果可用),然後估計追蹤訊息的標記。

const usage = ctx.getContextUsage();
if (usage && usage.tokens > 100_000) {
  // ...
}

ctx.compact()

觸發壓縮而不等待完成。使用onCompleteonError進行後續操作。

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()

傳回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,它使用會話控制方法擴充ExtensionContext。這些僅在命令中可用,因為如果從事件處理程序呼叫它們可能會死鎖。

ctx.getSystemPromptOptions()

傳回目前用於建構系統提示的基本輸入 Pi。

const options = ctx.getSystemPromptOptions();
const contextPaths = options.contextFiles?.map((file) => file.path) ?? [];

它與before_agent_start``event.systemPromptOptions具有相同的形狀和可變性:自訂提示、活動工具、工具片段、提示指南、附加系統提示文字、cwd、載入context files和載入技能。它可能包含完整的上下文文件內容,因此將其視為敏感的擴展本地數據,並避免透過命令列表、日誌或自動完成元數據來公開它。

這會報告當前的基本提示輸入。它不包括每輪before_agent_start鍊式系統提示更改、後來的context事件訊息突變或before_provider_request有效負載重寫。

ctx.waitForIdle()

等待代理程式完全解決,包括自動重試、自動壓縮重試和排隊延續:

pi.registerCommand("my-cmd", {
  handler: async (args, ctx) => {
    await ctx.waitForIdle();
    // Agent is now idle, safe to modify session
  },
});

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;見Session replacement lifecycle and footguns

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;見Session replacement lifecycle and footguns

ctx.navigateTree(targetId, 選項?)

導航到 session tree 中的不同點:

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(會話路徑,選項?)

切換到不同的會話檔案:

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
}

選項:

若要發現可用會話,請使用靜態 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");
        },
      });
    }
  },
});

會話替換生命週期和腳槍

withSession 接收一個新的 ReplacedSessionContext,它使用綁定到替換會話的非同步 sendMessage()sendUserMessage() 幫助程式擴充 ExtensionCommandContext

生命週期和腳槍:

  • withSession僅在舊會話已發出session_shutdown、舊運行時已被拆除、替換會話已反彈並且新擴展實例已收到session_start之後運行。
  • 回調仍然在原始閉包中執行,而不是在新的擴充實例中執行。這表示您的舊擴充實例可能已經在 withSession 啟動之前運行了關閉清理。
  • 捕獲的舊 pi / 舊命令 ctx 會話綁定物件在替換後已過時,如果使用將拋出。僅使用傳遞給 withSessionctx 進行會話綁定工作。
  • 之前提取的原始物件仍然是您的責任。例如,如果您在替換之前捕獲 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()

運行與 /reload 相同的重新載入流程。

pi.registerCommand("reload-runtime", {
  description: "Reload extensions, skills, prompts, themes, and context files",
  handler: async (_args, ctx) => {
    await ctx.reload();
    return;
  },
});

重要行為:

  • await ctx.reload() 為當前擴充運行時發出 session_shutdown
  • 然後它重新加載資源並發出 session_startreason: "reload" 以及 resources_discover 和原因 "reload"
  • 目前運行的命令處理程序仍然在舊的呼叫框架中繼續
  • 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, themes, and context files",
    handler: async (_args, ctx) => {
      await ctx.reload();
      return;
    },
  });

  pi.registerTool({
    name: "reload_runtime",
    label: "Reload Runtime",
    description: "Reload extensions, skills, prompts, themes, and context files",
    parameters: Type.Object({}),
    async execute() {
      pi.sendUserMessage("/reload-runtime", { deliverAs: "followUp" });
      return {
        content: [{ type: "text", text: "Queued /reload-runtime as a follow-up command." }],
      };
    },
  });
}

擴展API方法

pi.on(事件,處理程序)

訂閱活動。事件類型和回傳值請參考Events

pi.registerTool(定義)

註冊一個可由法學碩士調用的自訂工具。有關完整詳細信息,請參閱Custom Tools

pi.registerTool() 在擴充載入期間和啟動後都有效。您可以在 session_start、命令處理程序或其他事件處理程序中呼叫它。新工具會在同一個會話中立即刷新,因此它們出現在pi.getAllTools()中,並且可以由法學碩士調用,無需/reload

使用pi.setActiveTools()在運行時啟用或停用工具(包括動態新增的工具)。

使用 promptSnippet 將自訂工具選取至 Available tools 中的單行條目中,並使用 promptGuidelines 在工具處於活動狀態時將特定於工具的項目符號附加到預設的 Guidelines 部分。

重要提示: promptGuidelines 項目符號平鋪到 Guidelines 部分,沒有工具名稱前綴。每個指南必須命名它所引用的工具——避免“在...時使用此工具”,因為法學碩士無法分辨“這”意味著哪個工具。寫入“當...時使用 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(訊息,選項?)

將自訂訊息注入會話中。自訂訊息參與 LLM 上下文。對於不應發送至 LLM 的持久性 TUI 內容,請將 pi.appendEntry()pi.registerEntryRenderer() 結合使用。

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(內容,選項?)

向代理程式發送用戶訊息。與發送自訂訊息的 sendMessage() 不同,它發送一條實際的用戶訊息,看起來就像是由用戶鍵入的。總是觸發轉彎。

// Simple text message
pi.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 mode
pi.sendUserMessage("Focus on error handling", { deliverAs: "steer" });
pi.sendUserMessage("And then summarize", { deliverAs: "followUp" });

選項:

  • deliverAs - 代理串流時需要:
    • "steer" - 在目前助手輪完成執行其工具呼叫後將訊息排隊等待傳遞
    • "followUp" - 等待代理完成所有工具

當不串流時,訊息會立即發送並觸發新一輪。當沒有 deliverAs 的情況下進行串流傳輸時,會拋出錯誤。

完整範例請參見send-user-message.ts

pi.appendEntry(自訂類型,資料?)

保留擴充資料。自訂條目不參與 LLM 上下文。在互動模式下,當與 pi.registerEntryRenderer() 配對時,它們也可以在聊天記錄中呈現。

pi.appendEntry("my-state", { count: 42 });
pi.appendEntry("status-card", { title: "Indexed files", count: 17 });

// Restore on reload
pi.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(名稱)

設定會話顯示名稱(顯示在會話選擇器中而不是第一條訊息中)。

pi.setSessionName("Refactor auth module");

pi.getSessionName()

取得目前會話名稱(如果已設定)。

const name = pi.getSessionName();
if (name) {
  console.log(`Session: ${name}`);
}

pi.setLabel(entryId, 標籤)

設定或清除條目上的標籤。標籤是使用者定義的書籤和導航標記(顯示在 /tree 選擇器中)。

// Set a label
pi.setLabel(entryId, "checkpoint-before-refactor");

// Clear a label
pi.setLabel(entryId, undefined);

// Read labels via sessionManager
const label = ctx.sessionManager.getLabel(entryId);

標籤在會話中保留並在重新啟動後繼續存在。使用它們來標記對話樹中的重要點(回合、檢查點)。

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()

在目前會話中透過prompt取得可呼叫的slash commands。包括擴充指令、prompt templates和技能指令。 此列表符合 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, 渲染器)

使用您的 customType 為自訂訊息註冊自訂 TUI 渲染器。自訂訊息使用 pi.sendMessage() 建立並參與 LLM 上下文。參見Custom UI

pi.registerMarkdownTransformer(變壓器)

為一般使用者文字、輔助文字和思考區塊中的Markdown註冊一個轉換器。變壓器按照擴展負載順序運行,每個變壓器接收前一個變壓器返回的Markdown。鏈完成後,Pi使用其內建渲染器渲染轉換後的內容。

轉換器接收 Markdown 字串和上下文:

  • messageType"user""assistant""assistant-thinking"
  • isStreamingtrue 用於部分助手更新; false 使用者、最終確定的助手和恢復的訊息
  • availableWidth — 可用於轉換後的 Markdown 內容的精確終端列

返回變換後的Markdown:

pi.registerMarkdownTransformer((markdown, { messageType, isStreaming }) => {
  if (isStreaming || messageType === "assistant-thinking") return markdown;
  return markdown.replaceAll("-->", "→");
});

如果變壓器拋出異常,Pi 會保留到目前為止產生的 Markdown 並繼續處理下一個變壓器。此掛鉤僅用於顯示:原始訊息在會話和模型上下文中保持不變。它運行新的用戶訊息、輔助流更新、恢復的會話訊息和終端寬度變化,因此變壓器應該保持同步且便宜。

pi.registerEntryRenderer(customType, 渲染器)

使用您的 customType 為自訂條目註冊自訂 TUI 渲染器。自訂條目是使用 pi.appendEntry() 建立的,不參與 LLM 上下文。

import { Box, Text } from "@earendil-works/pi-tui";

pi.registerEntryRenderer("status-card", (entry, { expanded }, theme) => {
  const data = entry.data as { title: string; count: number };
  const box = new Box(1, 1, (text) => theme.bg("customMessageBg", text));
  box.addChild(new Text(`${theme.bold(data.title)}: ${data.count}`));
  if (expanded) {
    box.addChild(new Text(theme.fg("dim", JSON.stringify(data, null, 2))));
  }
  return box;
});

pi.appendEntry("status-card", { title: "Indexed files", count: 17 });

pi.registerShortcut(快捷方式,選項)

註冊鍵盤快速鍵。請參閱 keybindings.md 以了解快捷方式格式和內建按鍵綁定。

pi.registerShortcut("ctrl+shift+p", {
  description: "Toggle plan mode",
  handler: async (ctx) => {
    ctx.ui.notify("Toggled!");
  },
});

pi.registerFlag(姓名, 選項)

註冊一個CLI標誌。

pi.registerFlag("plan", {
  description: "Start in plan mode",
  type: "boolean",
  default: false,
});

// Check value
if (pi.getFlag("plan")) {
  // Plan mode enabled
}

pi.exec(指令、參數、選項?)

執行外殼命令。

const result = await pi.exec("git", ["status"], { signal, timeout: 5000 });
// result.stdout, result.stderr, result.code, result.killed

pi.getActiveTools() / pi.getAllTools() / pi.setActiveTools(names)

管理活動工具。這適用於內建工具和動態註冊工具。 pi.getActiveTools() 傳回活動工具名稱為 string[]pi.getAllTools() 傳回所有已設定工具的元資料。

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_tool
pi.setActiveTools(["read", "bash"]); // Switch to read-only

pi.getAllTools() 返回 namedescriptionparameterspromptGuidelinessourceInfo

典型 sourceInfo.source 值:

  • builtin 用於內建工具
  • sdk 對於透過 createAgentSession({ customTools }) 傳遞的工具
  • 由擴展註冊的工具的擴展源元數據

pi.setModel(模型)

設定當前模型。如果模型沒有可用的 API key,則回傳 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(級別)

獲取或設定思維層次。等級受限於模型能力(非推理模型始終使用“off”)。變化發出thinking_level_select

const current = pi.getThinkingLevel();  // "off" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max"
pi.setThinkingLevel("high");

pi.事件

用於擴充之間通訊的共享事件匯流排:

pi.events.on("my:event", (data) => { ... });
pi.events.emit("my:event", { ... });

pi.registerProvider(name, 配置)

動態註冊或覆蓋模型提供者。對於代理、自訂端點或團隊範圍的模型配置很有用。

一旦運行程式初始化,擴展工廠函數期間進行的呼叫就會排隊並套用。此後進行的呼叫(例如,從使用者設定流程後的命令處理程序進行的呼叫)立即生效,無需 /reload

動態提供者可以實現refreshModels。 Pi 在模型刷新期間呼叫它,透過提供者同步發布傳回的列表,並傳遞規範憑證/儲存目錄/網路/訊號上下文。擴充功能透過產生檢查context.publish({ persist: entry })決定是否持久化目錄元資料;諸如 llama.cpp 之類的即時伺服器可以返回模型而不保留它們。

context.signal 始終是一個具體訊號,提供者回呼必須將其傳遞給阻塞 I/O。公共 ModelRuntime.refresh()ModelRegistry.refresh() 調用接受可選信號,並且在省略時不受限制;延期和申請選擇自己的截止日期。即使提供者忽略訊號,取消也會讓呼叫者停止等待,但仍需要合作停止底層工作。

需要本機提供者驗證、過濾、刷新或流行為的Extensions可以從@earendil-works/pi-ai註冊完整的Provider。提供者成為組合基礎,並且 models.json 覆蓋仍然適用於其之上。

import { createProvider, openAICompletionsApi } from "@earendil-works/pi-ai";

const provider = createProvider({
  id: "local-server",
  name: "Local Server",
  baseUrl: "http://localhost:8080/v1",
  auth: {
    apiKey: {
      name: "Local server setup",
      async login(interaction) {
        return {
          type: "api_key",
          key: await interaction.prompt({ type: "secret", message: "API key" }),
        };
      },
      async resolve({ credential }) {
        return credential?.key
          ? { auth: { apiKey: credential.key }, source: "stored API key" }
          : undefined;
      },
    },
  },
  models: [],
  api: openAICompletionsApi(),
});

pi.registerProvider(provider);

// Register a new provider with custom models
pi.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
    }
  ]
});

// Register a live llama.cpp catalog without persisting discovered models
pi.registerProvider("llama.cpp", {
  baseUrl: "http://localhost:8080/v1",
  apiKey: "local",
  api: "openai-completions",
  async refreshModels({ signal }) {
    const response = await fetch("http://localhost:8080/v1/models", { signal });
    const { data } = await response.json();
    return data.map(({ id }) => ({
      id,
      name: id,
      reasoning: false,
      input: ["text"],
      cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
      contextWindow: 128000,
      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 /login
pi.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, signal) {
      signal.throwIfAborted();
      // Refresh logic
      return credentials;
    },
    getApiKey(credentials) {
      return credentials.access;
    }
  }
});

物件形式接受完整的 pi-ai Provider,包括原生 authgetModelsrefreshModelsfilterModelsstreamstreamSimple 行為。

舊配置選項:

  • name - UI 中提供者的顯示名稱,例如 /login
  • baseUrl - API 端點 URL。定義模型時需要。
  • apiKey - API key 文字、環境內插($ENV_VAR${ENV_VAR})或前導 !command。定義模型時必需的(除非提供了 oauth)。 $轉義``apiKey - API key 文字、環境內插($ENV_VAR${ENV_VAR})或前導 !command。定義模型時必需的(除非提供了 oauth)。 $轉義,$!轉義文字!`而不觸發指令執行。
  • api - API 模式種:"anthropic-messages""openai-completions""openai-responses" 等。
  • headers - 要包含在請求中的自訂標頭。
  • authHeader - 如果為 true,則自動新增 Authorization: Bearer 標頭。
  • models - 模型定義數組。如果提供,則替換該提供者的所有現有模型。模型定義可以設定 baseUrl 來覆蓋該模型的提供者端點。
  • refreshModels - 非同步動態發現回呼。它返回的模型取代了擴展提供的模型。 context.stored 包含持久化提供者快照;僅當更新的目錄資料應持續存在時才使用生成檢查context.publish({ persist: entry })。使用 persist: null 刪除該快照。
  • oauth - OAuth 支援 /login 的提供程序配置。提供後,提供者會出現在登入選單中。
  • streamSimple - 非標準 API 的自訂流實作。

請參閱 custom-provider.md 以了解進階主題:自訂串流 API、OAuth 詳細資訊、模型定義參考。

pi.unregisterProvider(名稱)

刪除先前註冊的提供者及其模型。被提供者覆蓋的內建模型將被恢復。如果提供者未註冊,則無效。

registerProvider一樣,這在初始載入階段後呼叫時立即生效,因此不需要/reload

pi.registerCommand("my-setup-teardown", {
  description: "Remove the custom proxy provider",
  handler: async (_args, _ctx) => {
    pi.unregisterProvider("my-proxy");
  },
});

狀態管理

具有狀態的Extensions應將其儲存在工具結果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
      };
    },
  });
}

客製化工具

註冊LLM可以透過pi.registerTool()呼叫的工具。工具出現在系統提示字元中,並且可以進行自訂渲染。

在預設系統提示符的 Available tools 部分中使用 promptSnippet 進行簡短的一行輸入。如果省略,自訂工具將不包含在該部分中。

使用 promptGuidelines 將特定於工具的項目符號新增至預設系統提示Guidelines 部分。這些項目符號僅在工具處於活動狀態時包含(例如,在 pi.setActiveTools([...]) 之後)。

重要提示: promptGuidelines 項目符號平鋪到 Guidelines 部分,沒有工具名稱前綴或分組。每個指南必須命名它所引用的工具——避免“在...時使用此工具”,因為法學碩士無法分辨“這”意味著哪個工具。寫入“當...時使用 my_tool”。

注意:有些模型是白痴,在工具路徑參數中包含 @ 前綴。內建工具會在解析路徑之前去除前導@。如果您的自訂工具接受路徑,也請規範化前導@。

如果您的自訂工具會改變文件,請使用 withFileMutationQueue(),以便它參與與內建 editwrite 相同的每個文件佇列。這很重要,因為預設情況下工具呼叫是並行運行的。如果沒有佇列,兩個工具可以讀取相同的舊檔案內容,計算不同的更新,然後最後寫入的內容覆蓋另一個。

失敗案例範例:您的自訂工具編輯 foo.ts,而內建 edit 也在同一個助手回合中變更 foo.ts。如果您的工具不參與佇列,則兩者都可以讀取原始 foo.ts,應用單獨的更改,並且其中一個更改會遺失。

將真實的目標檔案路徑傳遞給 withFileMutationQueue(),而不是原始使用者參數。首先將其解析為相對於 ctx.cwd 或工具工作目錄的絕對路徑。對於現有文件,幫助器透過 realpath() 進行規範化,因此相同文件的符號連結別名共用一個佇列。對於新文件,它會回退到已解析的絕對路徑,因為 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: {},
    };
  });
}

工具定義

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
      // usage: nestedModelResponse.usage,          // Optional nested LLM usage
      // 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) { ... },
});

使用情況統計: 如果工具進行嵌套 LLM 調用,則將其組合 Usage 返回為 usage。 Pi 將其保留在工具結果中,並將其包含在頁腳、/session 和 RPC 會話總計中。 tool_result 處理程序可以檢查或替換該值。

發出錯誤訊號: 若要將工具執行標記為失敗(在結果上設定 isError: true 並將其報告給 LLM),請從 execute 拋出錯誤。無論傳回物件中包含哪些屬性,傳回值都不會設定錯誤標誌。

**提前終止:**從execute()返回terminate: true,以提示在當前工具批次之後應跳過自動後續LLM呼叫。只有當該批次中的每個最終工具結果都終止時,此操作才會生效。有關代理程式以最終結構化輸出工具呼叫結束的最小範例,請參閱 examples/extensions/structured-output.ts

// Correct: throw to signal an error
async 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

範例:舊會話可能包含具有頂級 oldTextnewTextedit 工具調用,而當前架構僅接受 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: {},
    };
  },
});

覆蓋內建工具

Extensions 可以透過註冊同名工具來涵蓋內建工具(readbasheditwritegrepfindls)。發生這種情況時,互動模式會顯示警告。

# Extension's read tool replaces built-in read
pi -e ./tool-override.ts

或者,使用 --no-builtin-tools 在不使用任何內建工具的情況下啟動,同時保持擴充工具啟用:

# No built-in tools, only extension tools
pi --no-builtin-tools -e ./my-extension.ts

有關使用日誌記錄和存取控制覆蓋 read 的完整範例,請參閱 examples/extensions/tool-override.ts

渲染: 內建渲染器繼承是按插槽解析的。執行覆蓋和渲染覆蓋是獨立的。如果您的覆蓋省略 renderCall,則使用內建 renderCall。如果您的覆蓋省略 renderResult,則使用內建 renderResult。如果您的覆蓋忽略兩者,則會自動使用內建渲染器(語法反白、差異等)。這使您可以封裝用於日誌記錄或存取控制的內建工具,而無需重新實作 UI。

提示元資料: promptSnippetpromptGuidelines不是從內建工具繼承的。如果您的覆蓋應保留這些提示說明,請在覆蓋上明確定義它們。

您的實作必須與確切的結果形狀相符,包括 details 類型。 UI 和會話邏輯依賴這些形狀來進行渲染和狀態追蹤。

內建工具實作:

遠端執行

內建工具支援可插拔操作以委託給遠端系統(SSH、容器等):

import { createReadTool, createBashTool, type ReadOperations } from "@earendil-works/pi-coding-agent";

// Create tool with custom operations
const remoteRead = createReadTool(cwd, {
  operations: {
    readFile: (path) => sshExec(remote, `cat ${path}`),
    access: (path) => sshExec(remote, `test -r ${path}`).then(() => {}),
  }
});

// Register, checking flag at execution time
pi.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);
  },
});

操作接口: ReadOperationsWriteOperationsEditOperationsBashOperationsLsOperationsGrepOperationsFindOperations

對於user_bash,擴充可以透過createLocalBashOperations()重用pi的本機shell後端,而不是重新實作本地進程產生、shell解析和進程樹終止。

bash工具也支援spawn hook,用於執行前調整指令、cwd或env:

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" },
  }),
});

createBashTool() 透過PI_SESSION_IDPI_SESSION_FILEPI_PROVIDERPI_MODELPI_REASONING_LEVEL 將目前會話公開給指令。注入發生在spawnHook之前,因此鉤子在env中接收這些值,並在如上所述傳播現有環境時保留它們。設定 exposeSessionEnvironment: false 禁用它們:

const bashTool = createBashTool(cwd, {
  exposeSessionEnvironment: false,
});

有關變數語義,請參閱Bash tool session environment。有關帶有 --ssh 標誌的完整 SSH 範例,請參閱 examples/extensions/ssh.ts

輸出截斷

工具必須截斷其輸出以避免壓垮 LLM 上下文。大輸出可能會導致:

  • 上下文溢出錯誤(提示太長)
  • 壓實失敗
  • 模型性能下降

內建限制為 50KB(約 10k 代幣)和 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
  • 當輸出被截斷時,請務必通知法學碩士以及在哪裡可以找到完整版本
  • 在工具描述中記錄截斷限制

請參閱 examples/extensions/truncated-tool.ts 以取得使用適當截斷包裝 rg (ripgrep) 的完整範例。

多種工具

一個擴充可以註冊多個具有共享狀態的工具:

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();
  });
}

自訂渲染

工具可以提供renderCallrenderResult用於自訂TUI顯示。請參閱 tui.md 以了解完整組件 API 和 tool-execution.ts 以了解工具行的組成方式。

預設情況下,工具輸出包裝在處理填充和背景的 Box 中。定義的 renderCallrenderResult 必須回傳 Component。如果未定義槽渲染器,則 tool-execution.ts 使用該槽的後備渲染。

當工具應該渲染自己的 shell 而不是使用預設的 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);
  },
});

renderCallrenderResult 各自接收一個 context 對象,其中:

  • args - 目前工具呼叫參數
  • state - 跨renderCallrenderResult共享行本地狀態
  • lastComponent - 該插槽之前回傳的組件(如果有)
  • invalidate() - 請求重新渲染此工具行
  • toolCallId, cwd, executionStarted, argsComplete, isPartial, expanded, showImages, isError

使用 context.state 來實現跨槽共享狀態。當您想要跨渲染重複使用和改變相同元件時,請在傳回的元件實例上保留插槽本機快取。

渲染調用

呈現工具調用或標頭:

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(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);
}

如果槽故意沒有可見內容,則傳回空的 Component,例如空的 Container

鍵綁定提示

使用 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) - 格式化配置的鍵綁定 ID,例如 "app.tools.expand""tui.select.confirm"
  • keyText(keybinding) - 返回按鍵綁定 id 的原始配置按鍵文本
  • rawKeyHint(key, description) - 格式化原始金鑰字串

使用命名空間鍵綁定 ID:

  • 編碼代理 ID 使用 app.* 命名空間,例如 app.tools.expandapp.editor.externalapp.session.rename
  • 共享 TUI id 使用 tui.* 命名空間,例如 tui.select.confirmtui.select.canceltui.input.tab

有關鍵綁定 ID 和預設值的詳盡列表,請參閱 keybindings.mdkeybindings.json 使用相同的命名空間 id。

自訂編輯器和 ctx.ui.custom() 組件接收 keybindings: KeybindingsManager 作為注入參數。他們應該直接使用注入的管理器,而不是呼叫 getKeybindings()setKeybindings()

最佳實踐

  • 使用 Text 和填充 (0, 0)。預設的 Box 處理填充。
  • 使用 \n 表示多行內容。
  • 處理 isPartial 以取得串流進度。
  • 支援expanded按需了解詳情。
  • 保持預設視圖緊湊。
  • 讀取renderResult中的context.args,而不是將參數複製到context.state
  • 僅對必須在呼叫和結果槽之間共享的資料使用context.state
  • 當相同的元件實例可以就地更新時,重複使用context.lastComponent
  • 僅當預設盒裝 shell 妨礙時才使用 renderShell: "self"。在自外殼模式下,該工具負責其自己的框架、填充和背景。

倒退

如果槽渲染器未定義或拋出:

  • renderCall:顯示工具名稱
  • renderResult:顯示來自content的原始文本

動態刀具載入

Extensions 可以註冊許多工具,同時僅保持一小部分初始集處於活動狀態。然後,工具可以在執行期間使用 pi.setActiveTools() 添加更多工具。 Pi 偵測純粹的附加更改,記錄該工具結果上新可用的工具名稱,並在下一個模型請求之前應用更新的活動集。

這適用於每個型號。 Models 具有本機延遲加載支持,保留穩定的提示前綴並在工具結果位置加載新定義。其他模型使用下面描述的後備。

生命週期是:

  1. 將每個工具註冊到pi.registerTool(),以便它出現在pi.getAllTools()中。
  2. 保持載入工具(例如 search_tools)處於活動狀態,並使可搜尋工具處於非活動狀態。
  3. 在載入器執行期間,呼叫pi.setActiveTools([...currentTools,...matchingTools])。更改必須是附加的:不要在同一呼叫中刪除目前活動的工具。
  4. Pi記錄載入器的工具結果上新增了哪些工具。
  5. 在下一個模型回應之前,Pi 使用本機延遲載入(如果支援)公開新增的定義,否則使用正常的活動工具清單。

您不需要傳回特定於提供者的工具引用或將載入程式標記為特殊搜尋工具。主動換刀就是訊號。傳遞給pi.setActiveTools()的名稱必須已經註冊;未知的名稱將被忽略。

Models 具有本機延遲加載

  • 人擇
    • Models: Sonnet、Opus、Fable 版本 4.5 或更高版本(不含俳句)
    • 原生表示: 延遲定義使用 defer_loading;載入點使用 tool_reference 內容。
  • 開放人工智慧
    • Models: gpt-5.4 及更新系列
    • 本機表示: Pi 在載入點新增已完成的客戶端 tool_search_calltool_search_output 專案。

對於經過驗證的自訂模型或代理,可以使用 anthropic-messagescompat.supportsToolReferences: trueopenai-responsesopenai-codex-responsescompat.supportsToolSearch: true 啟用本機處理。除非端點和模型接受相應的本機協議,否則將它們保持停用狀態。

回退行為

對於所有其他模型和提供程序,動態啟動仍然有效:Pi 通常在下一個請求時發送完整的當前活動工具清單。該模型可以呼叫新啟動的工具,但添加它們的定義可能會使提供者的快取提示前綴無效。

當活動集不是純粹的累加性時(例如用一組工具替換另一組工具),Pi 也會使用這種安全回退。因此,工具刪除可以工作,但它們不使用延遲載入。

為了獲得最佳快取行為,請在整個會話中保持載入程式工具處於活動狀態並新增工具而不是替換活動集。另請注意,使用promptSnippetpromptGuidelines啟動工具會重建系統提示符號;即使提供程式支援延遲模式,系統提示的變更也可能使前綴無效。延遲載入的工具通常應該依賴它們的工具 description 並省略僅活動的提示元資料。

搜尋工具範例

以下擴充功能註冊了兩個可搜尋工具,將它們從初始活動集中刪除,並僅保留 search_tools 作為它們的載入器。此範例使用簡單的關鍵字匹配,但搜尋實作可以使用 BM25、嵌入、遠端目錄或特定於項目的路由。

import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
import { Type } from "typebox";

const SEARCHABLE_TOOL_NAMES = new Set(["lookup_weather", "search_issues"]);

export default function (pi: ExtensionAPI) {
  pi.registerTool({
    name: "lookup_weather",
    label: "Lookup Weather",
    description: "Look up the current weather for a city",
    parameters: Type.Object({ city: Type.String() }),
    async execute(_toolCallId, params) {
      return {
        content: [{ type: "text", text: `Weather for ${params.city}: sunny` }],
        details: {},
      };
    },
  });

  pi.registerTool({
    name: "search_issues",
    label: "Search Issues",
    description: "Search project issues by keyword",
    parameters: Type.Object({ query: Type.String() }),
    async execute(_toolCallId, params) {
      return {
        content: [{ type: "text", text: `No open issues matching ${params.query}` }],
        details: {},
      };
    },
  });

  pi.registerTool({
    name: "search_tools",
    label: "Search Tools",
    description: "Search for and enable tools relevant to a task",
    promptSnippet: "Search for additional tools when the active tools cannot perform the task",
    promptGuidelines: [
      "Use search_tools when a task requires a capability that is not currently available.",
    ],
    parameters: Type.Object({
      query: Type.String({ description: "Capability or task to search for" }),
      limit: Type.Optional(Type.Integer({ minimum: 1, maximum: 10 })),
    }),
    async execute(_toolCallId, params) {
      const terms = params.query.toLowerCase().split(/[^a-z0-9]+/).filter(Boolean);
      const matches = pi.getAllTools()
        .filter((tool) => SEARCHABLE_TOOL_NAMES.has(tool.name))
        .map((tool) => ({
          tool,
          score: terms.reduce(
            (score, term) =>
              score + (`${tool.name} ${tool.description}`.toLowerCase().includes(term) ? 1 : 0),
            0,
          ),
        }))
        .filter((match) => match.score > 0)
        .sort((a, b) => b.score - a.score)
        .slice(0, params.limit ?? 3)
        .map((match) => match.tool.name);

      if (matches.length === 0) {
        return {
          content: [{ type: "text", text: `No tools found for: ${params.query}` }],
          details: { matches: [] },
        };
      }

      const active = pi.getActiveTools();
      const added = matches.filter((name) => !active.includes(name));
      pi.setActiveTools([...new Set([...active, ...added])]);

      return {
        content: [{
          type: "text",
          text: added.length > 0
            ? `Loaded tools: ${added.join(", ")}`
            : `Matching tools already active: ${matches.join(", ")}`,
        }],
        details: { matches, added },
      };
    },
  });

  pi.on("session_start", () => {
    // Keep searchable tools registered but initially inactive. Preserve built-ins
    // and tools owned by other extensions, and keep the loader itself active.
    const initialTools = pi.getActiveTools().filter(
      (name) => !SEARCHABLE_TOOL_NAMES.has(name),
    );
    pi.setActiveTools([...new Set([...initialTools, "search_tools"])]);
  });
}

search_tools 新增匹配項時,模型會在緊接其後的請求中收到該定義。在支援本機的模型上,定義錨定在搜尋結果之後,而不更改初始工具模式前綴。在其他型號上,它會根據相同的以下請求出現在正常工具清單中。

自訂使用者介面

Extensions可以透過ctx.ui方法與使用者互動並自訂訊息/工具的呈現方式。

對於自訂元件,請參閱 tui.md,它具有以下複製貼上模式:

  • 選擇對話框(SelectList)
  • 帶取消的非同步操作 (BorderedLoader)
  • 設定切換(設定清單)
  • 狀態指示器(setStatus)
  • 串流媒體期間的工作訊息、可見性和指示器(setWorkingMessagesetWorkingVisiblesetWorkingIndicator
  • 編輯器上方/下方的小工具 (setWidget)
  • 自動完成提供者位於內建斜線/路徑完成之上 (addAutocompleteProvider)
  • 自訂頁尾 (setFooter)

對話框

// Select from options
const choice = await ctx.ui.select("Pick one:", ["A", "B", "C"]);

// Confirm dialog
const ok = await ctx.ui.confirm("Delete?", "This cannot be undone");

// Text input
const name = await ctx.ui.input("Name:", "placeholder");

// Multi-line editor
const text = await ctx.ui.editor("Edit:", "prefilled text");

// Notification (non-blocking)
ctx.ui.notify("Done!", "info");  // "info" | "warning" | "error"

帶有倒數計時的定時對話框

對話方塊支援 timeout 選項,可透過即時倒數計時顯示自動關閉:

// Dialog shows "Title (5s)" → "Title (4s)" → ... → auto-dismisses at 0
const 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 手動解僱

若要進行更多控制(例如,區分逾時和使用者取消),請使用 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

小工具、狀態和頁腳

// 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 default
ctx.ui.setWorkingVisible(false);  // Hide the built-in working loader row entirely
ctx.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 dot
ctx.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 indicator
ctx.ui.setWorkingIndicator();  // Restore default spinner

// Widget above editor (default)
ctx.ui.setWidget("my-widget", ["Line 1", "Line 2"]);
// Widget below editor
ctx.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 title
ctx.ui.setTitle("pi - my-project");

// Editor text
ctx.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 provider
ctx.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 expansion
const 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 switching
const result = ctx.ui.setTheme("light");  // Switch by name
if (!result.success) {
  ctx.ui.notify(`Failed: ${result.error}`, "error");
}
ctx.ui.setTheme(lightTheme!);  // Or switch by Theme object
ctx.ui.theme.fg("accent", "styled text");  // Access current theme

自訂工作指示器幀逐字呈現。如果您想要顏色,請自行將它們新增至框架字串中,例如使用 ctx.ui.theme.fg(...)

自動完成Providers

使用 ctx.ui.addAutocompleteProvider() 將自訂自動完成邏輯堆疊在內建斜線指令和路徑提供者之上。為自訂自然觸發器設定 triggerCharacters,例如 使用 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;
    },
  }));
});

有關完整範例,請參閱 github-issue-autocomplete.ts,該範例使用 gh issue list 預先載入最新開放的 GitHub 問題,並在本地過濾它們以快速完成 #...。它需要 GitHub CLI (gh) 和 GitHub 儲存庫簽出。

客製化組件

對於複雜的 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) - 呼叫關閉元件並傳回值

請參閱 tui.md 了解完整組件 API。

疊加模式(實驗性)

傳遞 { 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 } 釋放覆蓋層而不聚焦另一個組件。

有關完整的 OverlayOptionsOverlayHandle API 和 overlay-qa-tests.ts 範例,請參閱 tui.md

自訂編輯器

將主輸入編輯器替換為自訂實作(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(tui, theme, keybindings)
    );
  });
}

重點:

  • 擴充 CustomEditor (不是基礎 Editor)以取得應用程式鍵綁定(轉義以中止、ctrl+d、模型切換)
  • 您不處理的鑰匙,請致電 super.handleInput(data)
  • Factory 從應用程式接收 tuithemekeybindings
  • 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。

訊息和條目渲染

使用您的 customType 為訊息註冊自訂渲染器。對應參與 LLM 上下文的內容使用訊息渲染器:

import { Text } from "@earendil-works/pi-tui";

pi.registerMessageRenderer("my-extension", (message, options, theme) => {
  const { expanded, outputPad } = 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, outputPad, 0);
});

訊息透過pi.sendMessage()發送:

pi.sendMessage({
  customType: "my-extension",  // Matches registerMessageRenderer
  content: "Status update",
  display: true,               // Show in TUI
  details: { ... },            // Available in renderer
});

對於不應發送至 LLM 的僅限 TUI 的內容,請改為呈現自訂條目:

pi.registerEntryRenderer("my-card", (entry, options, theme) => {
  return new Text(theme.fg("accent", JSON.stringify(entry.data)));
});

pi.appendEntry("my-card", { status: "done" });

主題顏色

所有渲染函數都會接收一個 theme 物件。請參閱 themes.md 建立自訂主題和完整調色板。

// Foreground colors
theme.fg("toolTitle", text)   // Tool names
theme.fg("accent", text)      // Highlights
theme.fg("success", text)     // Success (green)
theme.fg("error", text)       // Errors (red)
theme.fg("warning", text)     // Warnings (yellow)
theme.fg("muted", text)       // Secondary text
theme.fg("dim", text)         // Tertiary text

// Text styles
theme.bold(text)
theme.italic(text)
theme.strikethrough(text)

對於自訂工具渲染器中的語法突出顯示:

import { highlightCode, getLanguageFromPath } from "@earendil-works/pi-coding-agent";

// Highlight code with explicit language
const highlighted = highlightCode("const x = 1;", "typescript", theme);

// Auto-detect language from file path
const lang = getLanguageFromPath("/path/to/file.rs");  // "rust"
const highlighted = highlightCode(code, lang, theme);

錯誤處理

  • 記錄擴充錯誤,代理繼續
  • tool_call 錯誤阻止工具(故障安全)
  • 工具execute錯誤必須透過拋出來表示;拋出的錯誤被捕獲,並用isError: true報告給LLM,然後繼續執行

模式行為

模式 ctx.mode ctx.hasUI 筆記
互動的 "tui" true 具有終端渲染的完整TUI
RPC (--mode rpc) "rpc" true 透過JSON協議進行對話和通知;custom() 返回undefined。見rpc.md
JSON (--mode json) "json" false 事件流到stdout; UI 方法是無操作的
印刷 (-p) "print" false Extensions 運行但無法提示

在TUI特定功能(custom()、組件工廠、終端輸入)之前使用ctx.mode === "tui"。在同時適用於 TUI 和 RPC 模式的對話框和通知方法之前使用 ctx.hasUI

範例參考

所有範例都在examples/extensions/中。

例子 描述 鑰匙APIs
工具
hello.ts 最少的工具註冊 registerTool
question.ts 與使用者互動的工具 registerTool, ui.select
questionnaire.ts 多步驟精靈工具 registerTool, ui.custom
todo.ts 具有持久性的有狀態工具 registerToolappendEntryrenderResult、會話事件
dynamic-tools.ts 啟動後和命令期間註冊工具 registerTool, session_start, registerCommand
structured-output.ts 最終的結構化輸出工具,terminate: true registerTool,終止工具結果
truncated-tool.ts 輸出截斷範例 registerTool, truncateHead
tool-override.ts 覆蓋內建讀取工具 registerTool(與內建同名)
命令
pirate.ts 修改每回合系統提示 registerCommand, before_agent_start
summarize.ts 對話摘要命令 registerCommand, ui.custom
handoff.ts 跨提供者模型切換 registerCommand, ui.editor, ui.custom
qna.ts 帶有自訂 UI 的問答 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 React 模型變更 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
使用者介面組件
status-line.ts 頁腳狀態指示燈 setStatus,會話事件
working-indicator.ts 自訂串流媒體工作指示燈 setWorkingIndicator, registerCommand
github-issue-autocomplete.ts 透過預先載入 gh issue list 中最近未解決的問題,在內建自動完成之上新增 #1234 問題完成 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
複雜Extensions
plan-mode/ 全計劃模式實施 所有事件類型,registerCommandregisterShortcutregisterFlagsetStatussetWidgetsendMessagesetActiveTools
preset.ts 可保存的預設(模型、工具、思維) registerCommand, registerShortcut, registerFlag, setModel, setActiveTools, setThinkingLevel, appendEntry
tools.ts 開啟/關閉 UI 工具 registerCommandsetActiveToolsSettingsList、會話事件
遠端和沙箱
ssh.ts SSH遠端執行 registerFlagon("user_bash")on("before_agent_start")、工具操作
interactive-shell.ts 持久 shell 會話 on("user_bash")
sandbox/ 沙盒工具執行 工具操作
gondolin/ 將內建工具和 ! 指令路由到 Gondolin 微型虛擬機 工具操作、內建工具覆蓋、on("user_bash")
subagent/ 生成子代理 registerTool, exec
遊戲
snake.ts 貪吃蛇遊戲 registerCommandui.custom、鍵盤處理
space-invaders.ts 太空侵略者遊戲 registerCommand, ui.custom
doom-overlay/ 厄運疊加 ui.custom 帶覆蓋
Providers
custom-provider-anthropic/ 自訂人類代理 registerProvider
custom-provider-gitlab-duo/ GitLab Duo 集成 registerProvider 與 OAuth
訊息與通訊
message-renderer.ts 自訂訊息渲染 registerMessageRenderer, sendMessage
entry-renderer.ts TUI-僅自訂入口渲染 registerEntryRenderer, appendEntry
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 執行前調整bashcommand、cwd、env createBashTool, spawnHook
with-deps/ 具有 npm 依賴項的擴展 package.json的封裝結構