Extensiones
pi puede crear extensiones. Pídale que cree uno para su caso de uso.
Extensiones
Sección titulada «Extensiones»Las extensiones son módulos de TypeScript que amplían el comportamiento de pi. Pueden suscribirse a eventos del ciclo de vida, registrar herramientas personalizadas a las que puede llamar el LLM, agregar comandos y más.
Ubicación para /reload: Coloque extensiones en
~/.pi/agent/extensions/(global) o.pi/extensions/(proyecto-local) para el descubrimiento automático. Utilicepi -e ./path.tssólo para pruebas rápidas. Las extensiones en ubicaciones descubiertas automáticamente se pueden recargar en caliente con/reload.
Capacidades clave:
- Herramientas personalizadas - Registrar herramientas que el LLM puede llamar a través de
pi.registerTool() - Interceptación de eventos - Bloquear o modificar llamadas a herramientas, inyectar contexto, personalizar la compactación
- Interacción del usuario - Solicitar a los usuarios a través de
ctx.ui(seleccionar, confirmar, ingresar, notificar) - Componentes de UI personalizados - Componentes TUI completos con entrada de teclado a través de
ctx.ui.custom()para interacciones complejas - Comandos personalizados - Registra comandos como
/mycommanda través depi.registerCommand() - Persistencia de la sesión - Estado de la tienda que sobrevive a los reinicios mediante
pi.appendEntry() - Representación personalizada - Controle cómo aparecen las llamadas/resultados de herramientas y los mensajes en TUI
Casos de uso de ejemplo:
- Puertas de permiso (confirmar antes de
rm -rf,sudo, etc.) - Git checkpointing (guardar en cada turno, restaurar en la rama)
- Protección de ruta (bloque de escritura en
.env,node_modules/) - Compactación personalizada (resume la conversación a tu manera)
- Resúmenes de conversaciones (ver ejemplo
summarize.ts) - Herramientas interactivas (preguntas, asistentes, cuadros de diálogo personalizados)
- Herramientas con estado (listas de tareas pendientes, grupos de conexiones)
- Integraciones externas (observadores de archivos, webhooks, activadores de CI)
- Juegos mientras esperas (ver ejemplo
snake.ts)
Consulte examples/extensions/ para ver implementaciones funcionales.
Tabla de contenidos
Sección titulada «Tabla de contenidos»- Inicio rápido
- Ubicaciones de extensión
- Importaciones disponibles
- Escribir una extensión
- Eventos
- Contexto de extensión
- ExtensionCommandContext
- Métodos de ExtensionAPI
- Gestión del Estado
- Herramientas personalizadas
- IU personalizada
- Manejo de errores
- Comportamiento del modo
- Referencia de ejemplos
Inicio rápido
Sección titulada «Inicio rápido»Crear ~/.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"); }, });}Prueba con el indicador --extension (o -e):
pi -e ./my-extension.tsUbicaciones de extensión
Sección titulada «Ubicaciones de extensión»Seguridad: Las extensiones se ejecutan con todos los permisos del sistema y pueden ejecutar código arbitrario. Instálelo únicamente desde fuentes en las que confíe.
Las extensiones se descubren automáticamente desde ubicaciones confiables. Las entradas .pi/extensions locales del proyecto se cargan solo después de que el proyecto sea confiable.
| Ubicación | Alcance |
|---|---|
~/.pi/agent/extensions/*.ts | Global (todos los proyectos) |
~/.pi/agent/extensions/*/index.ts | Global (subdirectorio) |
.pi/extensions/*.ts | Proyecto-local |
.pi/extensions/*/index.ts | Proyecto local (subdirectorio) |
Rutas adicionales a través de settings.json:
{ "packages": [ "git:github.com/user/repo@v1" ], "extensions": [ "/path/to/local/extension.ts", "/path/to/local/extension/dir" ]}Para compartir extensiones a través de npm o git como paquetes pi, consulte packages.md.
Importaciones disponibles
Sección titulada «Importaciones disponibles»| Paquete | Propósito |
|---|---|
@earendil-works/pi-coding-agent | Tipos de extensión (ExtensionAPI, ExtensionContext, eventos) |
typebox | Definiciones de esquemas para parámetros de herramientas |
@earendil-works/pi-ai | Utilidades de IA (StringEnum para enumeraciones compatibles con Google) |
@earendil-works/pi-tui | Componentes TUI para renderizado personalizado |
Las dependencias de npm también funcionan. Agregue un package.json al lado de su extensión (o en un directorio principal), ejecute npm install y las importaciones desde node_modules/ se resolverán automáticamente.
Para paquetes pi distribuidos instalados con pi install (npm o git), los departamentos de tiempo de ejecución deben estar en dependencies. La instalación del paquete utiliza instalaciones de producción (npm install --omit=dev) de forma predeterminada, por lo que devDependencies no está disponible en tiempo de ejecución; cuando se configura npmCommand, los paquetes git usan install simple para compatibilidad con contenedores.
Las funciones integradas de Node.js (node:fs, node:path, etc.) también están disponibles.
Escribir una extensión
Sección titulada «Escribir una extensión»Una extensión exporta una función predeterminada de fábrica que recibe ExtensionAPI. La fábrica puede ser síncrona o asíncrona:
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", { ... });}Las extensiones se cargan a través de jiti, por lo que TypeScript funciona sin compilación.
Si la fábrica devuelve un Promise, pi lo espera antes de continuar con el inicio. Eso significa que la inicialización asíncrona se completa antes de session_start, antes de resources_discover y antes de que se vacíen los registros de proveedores en cola a través de pi.registerProvider().
Funciones de fábrica asíncronas
Sección titulada «Funciones de fábrica asíncronas»Utilice una fábrica asíncrona para trabajos de inicio únicos, como obtener una configuración remota o descubrir dinámicamente modelos disponibles.
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, })), });}Este patrón hace que los modelos recuperados estén disponibles durante el inicio normal y en pi --list-models.
Recursos de larga duración y cierre
Sección titulada «Recursos de larga duración y cierre»Las fábricas de extensiones pueden ejecutarse en invocaciones que nunca inician una sesión. No inicie recursos en segundo plano, como procesos, sockets, observadores de archivos o temporizadores, desde fábrica.
Posponga el inicio de recursos en segundo plano hasta session_start o el comando/herramienta/evento que necesita el recurso. Registre un controlador session_shutdown idempotente para cerrar cualquier recurso con ámbito de sesión que inicie.
Estilos de extensión
Sección titulada «Estilos de extensión»Archivo único - el más simple, para extensiones pequeñas:
~/.pi/agent/extensions/└── my-extension.tsDirectorio con index.ts - para extensiones de varios archivos:
~/.pi/agent/extensions/└── my-extension/ ├── index.ts # Entry point (exports default function) ├── tools.ts # Helper module └── utils.ts # Helper modulePaquete con dependencias - para extensiones que necesitan paquetes 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"] }}Ejecute npm install en el directorio de extensión, luego las importaciones desde node_modules/ funcionan automáticamente.
Eventos
Sección titulada «Eventos»Descripción general del ciclo de vida
Sección titulada «Descripción general del ciclo de vida»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_shutdownEventos de inicio
Sección titulada «Eventos de inicio»project_trust
Sección titulada «project_trust»Disparado antes de que pi decida si confiar en un proyecto con configuraciones dinámicas (.pi o .agents/skills). Se ejecuta durante el inicio y cuando el reemplazo de sesión (por ejemplo /resume) ingresa un cwd cuya confianza no se ha resuelto en el proceso actual. Solo participan extensiones de usuario/globales y extensiones CLI -e; Las extensiones locales del proyecto no se cargan hasta que se resuelve la confianza.
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" };});Un controlador project_trust debe devolver { trusted: "yes" | "no" | "undecided" }. Un usuario/extensión global o CLI que devuelve "yes" o "no" es propietario de la decisión; la primera decisión de sí/no gana y suprime el mensaje de confianza incorporado. Utilice remember: true para persistir en una decisión de sí/no; de lo contrario, se aplica sólo al proceso actual. Devuelva "undecided" para permitir que los controladores posteriores o el flujo de confianza integrado decidan. Marque ctx.hasUI antes de solicitarlo. Si ningún controlador devuelve sí/no, la resolución de confianza normal continúa: las decisiones trust.json guardadas se aplican primero, luego defaultProjectTrust controla si pi pregunta, confía o rechaza de forma predeterminada.
Eventos de recursos
Sección titulada «Eventos de recursos»resources_discover
Sección titulada «resources_discover»Se disparó después de session_start para que las extensiones puedan contribuir con habilidades, indicaciones y rutas de temas adicionales.
La ruta de inicio utiliza reason: "startup". La recarga utiliza 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"], };});Eventos de sesión
Sección titulada «Eventos de sesión»Consulte Formato de sesión para conocer los aspectos internos del almacenamiento de sesiones y la API SessionManager.
session_start
Sección titulada «session_start»Se activa cuando se inicia, carga o recarga una sesión.
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
Sección titulada «session_before_switch»Se dispara antes de iniciar una nueva sesión (/new) o cambiar de sesión (/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 }; }});Después de un cambio exitoso o una acción de nueva sesión, pi emite session_shutdown para la instancia de extensión anterior, recarga y vuelve a vincular las extensiones para la nueva sesión, luego emite session_start con reason: "new" | "resume" y previousSessionFile.
Realice el trabajo de limpieza en session_shutdown y luego restablezca cualquier estado en memoria en session_start.
session_before_fork
Sección titulada «session_before_fork»Se dispara al bifurcar mediante /fork o clonar mediante /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});Después de una bifurcación o clonación exitosa, pi emite session_shutdown para la instancia de extensión anterior, recarga y vuelve a vincular extensiones para la nueva sesión, luego emite session_start con reason: "fork" y previousSessionFile.
Realice el trabajo de limpieza en session_shutdown y luego restablezca cualquier estado en memoria en session_start.
session_before_compact / session_compact
Sección titulada «session_before_compact / session_compact»Cocido sobre compactación. Consulte compaction.md para obtener más detalles.
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});session_before_tree / session_tree
Sección titulada «session_before_tree / session_tree»Disparado en la navegación /tree. Consulte Sesiones para conocer los conceptos de navegación en árbol.
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});session_shutdown
Sección titulada «session_shutdown»Se activa antes de que se elimine el tiempo de ejecución de una sesión iniciada. Úselo para limpiar recursos abiertos desde session_start u otros enlaces con ámbito de sesión.
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.});Eventos del agente
Sección titulada «Eventos del agente»before_agent_start
Sección titulada «before_agent_start»Se activa después de que el usuario envía el mensaje, antes del ciclo del agente. Puede inyectar un mensaje y/o modificar el mensaje del sistema.
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...", };});El campo systemPromptOptions brinda a las extensiones acceso a los mismos datos estructurados que utiliza Pi para crear el indicador del sistema. Esto le permite inspeccionar lo que Pi ha cargado (indicaciones personalizadas, pautas, fragmentos de herramientas, archivos de contexto, habilidades) sin redescubrir recursos ni volver a analizar indicadores. Úselo cuando su extensión necesite realizar cambios profundos e informados en el mensaje del sistema respetando la configuración proporcionada por el usuario.
Dentro de before_agent_start, event.systemPrompt y ctx.getSystemPrompt() reflejan el mensaje del sistema encadenado a partir del controlador actual. Los controladores before_agent_start posteriores aún pueden modificarlo nuevamente.
agent_start / agent_end
Sección titulada «agent_start / agent_end»Se dispara una vez por solicitud del usuario.
pi.on("agent_start", async (_event, ctx) => {});
pi.on("agent_end", async (event, ctx) => { // event.messages - messages from this prompt});turn_start / turn_end
Sección titulada «turn_start / turn_end»Disparado por cada turno (una respuesta LLM + llamadas de herramientas).
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
Sección titulada «message_start / message_update / message_end»Activado por actualizaciones del ciclo de vida de los mensajes.
message_startymessage_endse activan para mensajes de usuario, asistente y resultado de herramienta.message_updatese activa para actualizaciones de transmisión del asistente.- Los controladores
message_endpueden devolver{ message }para reemplazar el mensaje finalizado. El reemplazo debe mantener el mismorole.
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
Sección titulada «tool_execution_start / tool_execution_update / tool_execution_end»Despedido por actualizaciones del ciclo de vida de ejecución de herramientas.
En modo de herramienta paralela:
tool_execution_startse emite en orden de fuente asistente durante la fase de verificación previa- Los eventos
tool_execution_updatepueden intercalarse entre herramientas. tool_execution_endse emite en el orden de finalización de la herramienta después de finalizar cada herramienta.- Los eventos de mensajes finales
toolResultaún se emiten más tarde en el orden de origen del asistente.
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
Sección titulada «context»Se activa antes de cada llamada al LLM. Modifica mensajes de forma no destructiva. Consulte Formato de sesión para conocer los tipos de mensajes.
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
Sección titulada «before_provider_request»Se activa después de crear la carga útil específica del proveedor, justo antes de enviar la solicitud. Los controladores se ejecutan en orden de carga de extensión. Devolver undefined mantiene la carga útil sin cambios. Devolver cualquier otro valor reemplaza la carga útil para controladores posteriores y para la solicitud real.
Este enlace puede reescribir las instrucciones del sistema a nivel de proveedor o eliminarlas por completo. Esos cambios a nivel de carga útil no se reflejan en ctx.getSystemPrompt(), que informa la cadena de aviso del sistema de Pi en lugar de la carga útil final serializada del proveedor.
pi.on("before_provider_request", (event, ctx) => { console.log(JSON.stringify(event.payload, null, 2));
// Optional: replace payload // return { ...event.payload, temperature: 0 };});Esto es principalmente útil para depurar la serialización del proveedor y el comportamiento de la caché.
after_provider_response
Sección titulada «after_provider_response»Se activa después de recibir una respuesta HTTP y antes de que se consuma el cuerpo de la transmisión. Los controladores se ejecutan en orden de carga de extensión.
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"]); }});La disponibilidad del encabezado depende del proveedor y del transporte. Los proveedores que abstraen respuestas HTTP no pueden exponer los encabezados.
Eventos modelo
Sección titulada «Eventos modelo»model_select
Sección titulada «model_select»Se activa cuando el modelo cambia mediante el comando /model, ciclo de modelo (Ctrl+P) o restauración de sesión.
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");});Utilícelo para actualizar elementos de la interfaz de usuario (barras de estado, pies de página) o realizar una inicialización específica del modelo cuando cambie el modelo activo.
thinking_level_select
Sección titulada «thinking_level_select»Despedido cuando cambia el nivel de pensamiento. Esto es sólo de notificación; Los valores de retorno del controlador se ignoran.
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}`);});Utilícelo para actualizar la interfaz de usuario de la extensión cuando pi.setThinkingLevel(), cambios de modelo o controles de nivel de pensamiento integrados cambien el nivel de pensamiento activo.
Eventos de herramientas
Sección titulada «Eventos de herramientas»tool_call
Sección titulada «tool_call»Se dispara después de tool_execution_start, antes de que se ejecute la herramienta. Puede bloquear. Utilice isToolCallEventType para restringir y obtener entradas escritas.
Antes de que se ejecute tool_call, pi espera a que los eventos del Agente emitidos previamente terminen de drenarse a través de AgentSession. Esto significa que ctx.sessionManager está actualizado a través del mensaje de llamada de herramienta asistente actual.
En el modo de ejecución de herramienta paralela predeterminado, las llamadas a herramientas hermanas desde el mismo mensaje del asistente se verifican previamente de forma secuencial y luego se ejecutan simultáneamente. No se garantiza que tool_call vea los resultados de las herramientas hermanas del mismo mensaje del asistente en ctx.sessionManager.
event.input es mutable. Mutéelo en su lugar para parchear los argumentos de la herramienta antes de la ejecución.
Garantías de comportamiento:
- Las mutaciones en
event.inputafectan la ejecución real de la herramienta. - Los manejadores posteriores de
tool_callven mutaciones realizadas por manejadores anteriores. - No se realiza ninguna revalidación después de su mutación.
- Los valores de retorno de
tool_callsolo controlan el bloqueo a través de{ 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}`); }});Escribir entrada de herramienta personalizada
Sección titulada «Escribir entrada de herramienta personalizada»Las herramientas personalizadas deben exportar su tipo de entrada:
export type MyToolInput = Static<typeof myToolSchema>;Utilice isToolCallEventType con parámetros de tipo explícitos:
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
Sección titulada «tool_result»Se activa después de que finaliza la ejecución de la herramienta y antes de que se emitan tool_execution_end más los eventos del mensaje de resultado final de la herramienta. Puede modificar el resultado.
En el modo de herramienta paralela, tool_result y tool_execution_end pueden intercalarse en el orden de finalización de la herramienta, mientras que los eventos de mensajes finales toolResult aún se emiten más tarde en el orden de origen del asistente.
tool_result maneja la cadena como middleware:
- Los controladores se ejecutan en orden de carga de extensión.
- Cada controlador ve el último resultado después de los cambios anteriores del controlador
- Los controladores pueden devolver parches parciales (
content,detailsoisError); Los campos omitidos mantienen sus valores actuales.
Utilice ctx.signal para trabajo asíncrono anidado dentro del controlador. Esto permite a Esc cancelar llamadas de modelo, fetch() y otras operaciones con detección de aborto iniciadas por la extensión.
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 };});Eventos de Bash de usuario
Sección titulada «Eventos de Bash de usuario»user_bash
Sección titulada «user_bash»Se activa cuando el usuario ejecuta los comandos ! o !!. Puede interceptar.
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 } };});Eventos de entrada
Sección titulada «Eventos de entrada»Se activa cuando se recibe la entrada del usuario, después de que se verifican los comandos de extensión pero antes de la expansión de habilidades y plantillas. El evento ve el texto de entrada sin formato, por lo que /skill:foo y /template aún no están expandidos.
Orden de procesamiento:
- Primero se verifican los comandos de extensión (
/cmd); si se encuentran, se ejecuta el controlador y se omite el evento de entrada. - Se activa el evento
input: puede interceptar, transformar o manejar - Si no se maneja: comandos de habilidad (
/skill:name) ampliados al contenido de la habilidad - Si no se maneja: plantillas de mensajes (
/template) expandidas al contenido de la plantilla - Comienza el procesamiento del agente (
before_agent_start, etc.)
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});Resultados:
continue- pasa sin cambios (predeterminado si el controlador no devuelve nada)transform- modificar texto/imágenes, luego continuar con la expansiónhandled: omite el agente por completo (el primer controlador que devuelva esto gana)
Transforma la cadena entre controladores. Consulte input-transform.ts y input-transform-streaming.ts para conocer el enrutamiento compatible con streamingBehavior.
Contexto de extensión
Sección titulada «Contexto de extensión»Todos los controladores reciben ctx: ExtensionContext.
Métodos de UI para la interacción del usuario. Consulte UI personalizada para obtener detalles completos.
ctx.mode
Sección titulada «ctx.mode»Modo de ejecución actual: "tui", "rpc", "json" o "print". Utilice ctx.mode === "tui" para proteger funciones exclusivas del terminal, como custom(), fábricas de componentes, entrada del terminal y representación TUI directa.
ctx.hasUI
Sección titulada «ctx.hasUI»true en modos TUI y RPC. false en modo impresión (-p) y modo JSON. Utilícelo para proteger los métodos de diálogo (select, confirm, input, editor) y los métodos de disparar y olvidar (notify, setStatus, setWidget, setTitle, setEditorText) que funcionan tanto en modo TUI como RPC. En el modo RPC, algunos métodos específicos de TUI no son operativos o devuelven valores predeterminados (consulte rpc.md).
ctx.cwd
Sección titulada «ctx.cwd»Directorio de trabajo actual.
Utilice CONFIG_DIR_NAME en lugar de codificar .pi al construir rutas de configuración locales del proyecto. Las distribuciones renombradas pueden usar un nombre de directorio de configuración diferente.
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()
Sección titulada «ctx.isProjectTrusted()»Devuelve si la confianza local del proyecto está activa para el contexto de la sesión actual. Esto incluye decisiones de confianza temporales y anulaciones de confianza de CLI, no solo decisiones guardadas en el almacén de confianza global.
Utilice esto antes de leer la configuración de la extensión local del proyecto que solo debe respetarse para proyectos confiables.
ctx.sessionManager
Sección titulada «ctx.sessionManager»Acceso de solo lectura al estado de la sesión. Consulte Formato de sesión para conocer la API de SessionManager completa y los tipos de entrada.
Para tool_call, este estado se sincroniza a través del mensaje del asistente actual antes de que se ejecuten los controladores. En el modo de ejecución de herramientas paralelas, todavía no se garantiza que se incluyan los resultados de herramientas hermanas del mismo mensaje del asistente.
ctx.sessionManager.getEntries() // All entriesctx.sessionManager.getBranch() // Current branchctx.sessionManager.getLeafId() // Current leaf entry IDctx.modelRegistry / ctx.model
Sección titulada «ctx.modelRegistry / ctx.model»Acceso a modelos y claves API.
ctx.signal
Sección titulada «ctx.signal»La señal de aborto del agente actual, o undefined cuando no hay ningún turno de agente activo.
Úselo para trabajos anidados con detección de abortos iniciados por controladores de extensiones, por ejemplo:
fetch(..., { signal: ctx.signal })- llamadas de modelo que aceptan
signal - archivos o asistentes de proceso que aceptan
AbortSignal
ctx.signal normalmente se define durante eventos de giro activos como tool_call, tool_result, message_update y turn_end.
Por lo general, es undefined en contextos inactivos o sin turnos, como eventos de sesión, comandos de extensión y atajos activados mientras pi está inactivo.
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()
Sección titulada «ctx.isIdle() / ctx.abort() / ctx.hasPendingMessages()»Controle los ayudantes de flujo.
ctx.shutdown()
Sección titulada «ctx.shutdown()»Solicite un cierre elegante de pi.
- Modo interactivo: Diferido hasta que el agente esté inactivo (después de procesar todos los mensajes de dirección y seguimiento en cola).
- Modo RPC: Diferido hasta el siguiente estado inactivo (después de completar la respuesta del comando actual, mientras se espera el siguiente comando).
- Modo de impresión: No operativo. El proceso sale automáticamente cuando se procesan todas las solicitudes.
Emite el evento session_shutdown a todas las extensiones antes de salir. Disponible en todos los contextos (controladores de eventos, herramientas, comandos, accesos directos).
pi.on("tool_call", (event, ctx) => { if (isFatal(event.input)) { ctx.shutdown(); }});ctx.getContextUsage()
Sección titulada «ctx.getContextUsage()»Devuelve el uso del contexto actual para el modelo activo. Utiliza el último uso del asistente cuando está disponible y luego estima los tokens para los mensajes de seguimiento.
const usage = ctx.getContextUsage();if (usage && usage.tokens > 100_000) { // ...}ctx.compact()
Sección titulada «ctx.compact()»Activar la compactación sin esperar a que finalice. Utilice onComplete y onError para acciones de seguimiento.
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()
Sección titulada «ctx.getSystemPrompt()»Devuelve la cadena de aviso del sistema actual de Pi.
- Durante
before_agent_start, esto refleja los cambios encadenados de mensajes del sistema realizados hasta el momento para el turno actual. - No incluye mutaciones posteriores del mensaje
context. - No incluye reescrituras de carga útil
before_provider_request. - Si las extensiones cargadas posteriormente se ejecutan después de la suya, aún pueden cambiar lo que se envía finalmente.
pi.on("before_agent_start", (event, ctx) => { const prompt = ctx.getSystemPrompt(); console.log(`System prompt length: ${prompt.length}`);});ExtensionCommandContext
Sección titulada «ExtensionCommandContext»Los controladores de comandos reciben ExtensionCommandContext, que amplía ExtensionContext con métodos de control de sesión. Estos sólo están disponibles en los comandos porque pueden bloquearse si se llaman desde los controladores de eventos.
ctx.getSystemPromptOptions()
Sección titulada «ctx.getSystemPromptOptions()»Devuelve las entradas base que Pi utiliza actualmente para crear el indicador del sistema.
const options = ctx.getSystemPromptOptions();const contextPaths = options.contextFiles?.map((file) => file.path) ?? [];Tiene la misma forma y mutabilidad que before_agent_start event.systemPromptOptions: aviso personalizado, herramientas activas, fragmentos de herramientas, pautas de aviso, texto de aviso del sistema agregado, cwd, archivos de contexto cargados y habilidades cargadas. Puede incluir contenidos de archivos de contexto completo, así que trátelos como datos sensibles de extensión local y evite exponerlos a través de listas de comandos, registros o metadatos de autocompletar.
Esto informa las entradas del mensaje base actual. No incluye cambios de avisos del sistema encadenados before_agent_start por turno, mutaciones posteriores de mensajes de eventos context ni reescrituras de carga útil before_provider_request.
ctx.waitForIdle()
Sección titulada «ctx.waitForIdle()»Espere a que el agente termine de transmitir:
pi.registerCommand("my-cmd", { handler: async (args, ctx) => { await ctx.waitForIdle(); // Agent is now idle, safe to modify session },});ctx.newSession(options?)
Sección titulada «ctx.newSession(options?)»Crea una nueva sesión:
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}Opciones:
parentSession: archivo de sesión principal para registrar en el encabezado de la nueva sesiónsetup: muta elSessionManagerde la nueva sesión antes de que se ejecutewithSession.withSession: ejecuta el trabajo posterior al cambio en un contexto de sesión de reemplazo nuevo. No utilice el antiguopi/ comandoctxcapturado; consulte Ciclo de vida del reemplazo de sesión y trampas comunes.
ctx.fork(entryId, options?)
Sección titulada «ctx.fork(entryId, options?)»Bifurca desde una entrada específica, creando un nuevo archivo de sesión:
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}Opciones:
position:"before"(predeterminado) se bifurca antes del mensaje del usuario seleccionado, restaurando ese mensaje en el editor.position:"at"duplica la ruta activa a través de la entrada seleccionada sin restaurar el texto del editor.withSession: ejecuta el trabajo posterior al cambio en un contexto de sesión de reemplazo nuevo. No utilice el antiguopi/ comandoctxcapturado; consulte Ciclo de vida del reemplazo de sesión y trampas comunes.
ctx.navigateTree(targetId, options?)
Sección titulada «ctx.navigateTree(targetId, options?)»Navegue a un punto diferente en el árbol de la sesión:
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",});Opciones:
summarize: Si se debe generar un resumen de la rama abandonadacustomInstructions: Instrucciones personalizadas para el resumidor.replaceInstructions: si es verdadero,customInstructionsreemplaza el mensaje predeterminado en lugar de agregarlolabel: Etiqueta para adjuntar a la entrada de resumen de la rama (o entrada de destino si no es un resumen)
ctx.switchSession(sessionPath, options?)
Sección titulada «ctx.switchSession(sessionPath, options?)»Cambie a un archivo de sesión diferente:
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}Opciones:
withSession: ejecuta el trabajo posterior al cambio en un contexto de sesión de reemplazo nuevo. No utilice el antiguopi/comandoctxcapturado; consulte Ciclo de vida del reemplazo de sesión y trampas comunes.
Para descubrir sesiones disponibles, utilice los métodos estáticos SessionManager.list() o 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"); }, }); } },});Ciclo de vida del reemplazo de sesión y trampas comunes
Sección titulada «Ciclo de vida del reemplazo de sesión y trampas comunes»withSession recibe un ReplacedSessionContext nuevo, que extiende ExtensionCommandContext con ayudantes asíncronos sendMessage() y sendUserMessage() vinculados a la sesión de reemplazo.
Ciclo de vida y trampas comunes:
withSessionse ejecuta solo después de que la sesión anterior haya emitidosession_shutdown, el tiempo de ejecución anterior se haya eliminado, la sesión de reemplazo se haya recuperado y la nueva instancia de extensión ya haya recibidosession_start.- La devolución de llamada aún se ejecuta en el cierre original, no dentro de la nueva instancia de extensión. Eso significa que es posible que su antigua instancia de extensión ya haya ejecutado su limpieza de apagado antes de que se inicie
withSession. - Los objetos capturados antiguos
pi/comando antiguoctxvinculados a la sesión quedan obsoletos después del reemplazo y se lanzarán si se usan. Utilice únicamente elctxpasado awithSessionpara trabajos vinculados a sesiones. - Los objetos en bruto previamente extraídos siguen siendo tu responsabilidad. Por ejemplo, si captura
const sm = ctx.sessionManagerantes del reemplazo,smsigue siendo el antiguo objetoSessionManager. No lo reutilice después del reemplazo. - El código en
withSessiondebe asumir que cualquier estado invalidado por su controladorsession_shutdownya desapareció. Capture únicamente datos simples que sobrevivan limpiamente al apagado, como cadenas, identificadores y configuraciones serializadas.
Patrón seguro:
pi.registerCommand("handoff", { handler: async (_args, ctx) => { const kickoff = "Continue from the replacement session"; await ctx.newSession({ withSession: async (ctx) => { await ctx.sendUserMessage(kickoff); }, }); },});Patrón inseguro:
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()
Sección titulada «ctx.reload()»Ejecute el mismo flujo de recarga que /reload.
pi.registerCommand("reload-runtime", { description: "Reload extensions, skills, prompts, and themes", handler: async (_args, ctx) => { await ctx.reload(); return; },});Comportamiento importante:
await ctx.reload()emitesession_shutdownpara el tiempo de ejecución de la extensión actual- Luego recarga recursos y emite
session_startconreason: "reload"yresources_discovercon motivo"reload" - El controlador de comandos actualmente en ejecución continúa en el marco de llamada anterior.
- El código posterior a
await ctx.reload()aún se ejecuta desde la versión previa a la recarga - El código posterior a
await ctx.reload()no debe asumir que el antiguo estado de extensión en memoria sigue siendo válido - Después de que el controlador regrese, los comandos/eventos/llamadas a herramientas futuras usarán la nueva versión de la extensión.
Para un comportamiento predecible, trate la recarga como terminal para ese controlador (await ctx.reload(); return;).
Las herramientas se ejecutan con ExtensionContext, por lo que no pueden llamar a ctx.reload() directamente. Utilice un comando como punto de entrada de recarga y luego exponga una herramienta que ponga en cola ese comando como un mensaje de seguimiento del usuario.
Herramienta de ejemplo que el LLM puede llamar para activar la recarga:
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." }], }; }, });}Métodos de ExtensionAPI
Sección titulada «Métodos de ExtensionAPI»pi.on(event, handler)
Sección titulada «pi.on(event, handler)»Suscríbete a eventos. Consulte Eventos para conocer los tipos de eventos y los valores de retorno.
pi.registerTool(definition)
Sección titulada «pi.registerTool(definition)»Registre una herramienta personalizada a la que pueda llamar el LLM. Consulte Herramientas personalizadas para obtener detalles completos.
pi.registerTool() funciona tanto durante la carga de la extensión como después del inicio. Puede llamarlo dentro de session_start, controladores de comandos u otros controladores de eventos. Las nuevas herramientas se actualizan inmediatamente en la misma sesión, por lo que aparecen en pi.getAllTools() y el LLM puede invocarlas sin /reload.
Utilice pi.setActiveTools() para habilitar o deshabilitar herramientas (incluidas las herramientas agregadas dinámicamente) en tiempo de ejecución.
Utilice promptSnippet para optar por una herramienta personalizada en una entrada de una línea en Available tools y promptGuidelines para agregar viñetas específicas de la herramienta a la sección predeterminada Guidelines cuando la herramienta esté activa.
Importante: Las viñetas promptGuidelines se agregan planas a la sección Guidelines sin prefijo de nombre de herramienta. Cada pauta debe nombrar la herramienta a la que se refiere; evite “Usar esta herramienta cuando…” porque el LLM no puede decir qué herramienta significa “esta”. En su lugar, escriba “Usar my_tool cuando…”.
Consulte dynamic-tools.ts para ver un ejemplo completo.
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(message, options?)
Sección titulada «pi.sendMessage(message, options?)»Inyecte un mensaje personalizado en la sesión.
pi.sendMessage({ customType: "my-extension", content: "Message text", display: true, details: { ... },}, { triggerTurn: true, deliverAs: "steer",});Opciones:
deliverAs- Modo de entrega:"steer"(predeterminado): pone en cola el mensaje durante la transmisión. Se entrega después de que el turno actual del asistente termina de ejecutar sus llamadas a herramientas, antes de la siguiente llamada de LLM."followUp": espera a que finalice el agente. Se entrega solo cuando el agente no tiene más llamadas de herramientas."nextTurn": en cola para el siguiente mensaje de usuario. No interrumpe ni desencadena nada.
triggerTurn: true: si el agente está inactivo, activa una respuesta LLM inmediatamente. Solo se aplica a los modos"steer"y"followUp"(ignorado para"nextTurn").
pi.sendUserMessage(content, options?)
Sección titulada «pi.sendUserMessage(content, options?)»Enviar un mensaje de usuario al agente. A diferencia de sendMessage(), que envía mensajes personalizados, este envía un mensaje de usuario real que aparece como si lo hubiera escrito el usuario. Siempre desencadena un turno.
// 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" });Opciones:
deliverAs- Requerido cuando el agente está transmitiendo:"steer": pone en cola el mensaje para su entrega después de que el turno actual del asistente termina de ejecutar sus llamadas de herramienta."followUp"- Espera a que el agente termine todas las herramientas
Cuando no se transmite, el mensaje se envía inmediatamente y desencadena un nuevo turno. Cuando se transmite sin deliverAs, se genera un error.
Consulte send-user-message.ts para obtener un ejemplo completo.
pi.appendEntry(customType, data?)
Sección titulada «pi.appendEntry(customType, data?)»Estado de extensión persistente (NO participa en el contexto 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(name)
Sección titulada «pi.setSessionName(name)»Establezca el nombre para mostrar de la sesión (que se muestra en el selector de sesión en lugar del primer mensaje).
pi.setSessionName("Refactor auth module");pi.getSessionName()
Sección titulada «pi.getSessionName()»Obtenga el nombre de la sesión actual, si está configurado.
const name = pi.getSessionName();if (name) { console.log(`Session: ${name}`);}pi.setLabel(entryId, label)
Sección titulada «pi.setLabel(entryId, label)»Establecer o borrar una etiqueta en una entrada. Las etiquetas son marcadores definidos por el usuario para marcadores y navegación (que se muestran en el selector /tree).
// Set a labelpi.setLabel(entryId, "checkpoint-before-refactor");
// Clear a labelpi.setLabel(entryId, undefined);
// Read labels via sessionManagerconst label = ctx.sessionManager.getLabel(entryId);Las etiquetas persisten en la sesión y sobreviven a los reinicios. Úsalos para marcar puntos importantes (giros, puntos de control) en el árbol de conversación.
pi.registerCommand(name, options)
Sección titulada «pi.registerCommand(name, options)»Registre un comando.
Si varias extensiones registran el mismo nombre de comando, pi las conserva todas y asigna sufijos de invocación numéricos en orden de carga, por ejemplo /review:1 y /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"); }});Opcional: agregue el autocompletado de argumentos para /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()
Sección titulada «pi.getCommands()»Obtenga los comandos de barra diagonal disponibles para invocarlos a través de prompt en la sesión actual. Incluye comandos de extensión, plantillas de mensajes y comandos de habilidad.
La lista coincide con el orden RPC get_commands: primero las extensiones, luego las plantillas y luego las habilidades.
const commands = pi.getCommands();const bySource = commands.filter((command) => command.source === "extension");const userScoped = commands.filter((command) => command.sourceInfo.scope === "user");Cada entrada tiene esta forma:
{ 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; };}Utilice sourceInfo como campo de procedencia canónica. No infiera la propiedad a partir de los nombres de los comandos ni del análisis de rutas ad hoc.
Los comandos interactivos integrados (como /model y /settings) no se incluyen aquí. Se manejan sólo en interactivo.
modo y no se ejecutaría si se enviara a través de prompt.
pi.registerMessageRenderer(customType, renderer)
Sección titulada «pi.registerMessageRenderer(customType, renderer)»Registre un procesador TUI personalizado para mensajes con su customType. Consulte UI personalizada.
pi.registerShortcut(shortcut, options)
Sección titulada «pi.registerShortcut(shortcut, options)»Registre un atajo de teclado. Consulte keybindings.md para conocer el formato de acceso directo y las combinaciones de teclas integradas.
pi.registerShortcut("ctrl+shift+p", { description: "Toggle plan mode", handler: async (ctx) => { ctx.ui.notify("Toggled!"); },});pi.registerFlag(name, options)
Sección titulada «pi.registerFlag(name, options)»Registre una bandera CLI.
pi.registerFlag("plan", { description: "Start in plan mode", type: "boolean", default: false,});
// Check valueif (pi.getFlag("plan")) { // Plan mode enabled}pi.exec(command, args, options?)
Sección titulada «pi.exec(command, args, options?)»Ejecute un comando de shell.
const result = await pi.exec("git", ["status"], { signal, timeout: 5000 });// result.stdout, result.stderr, result.code, result.killedpi.getActiveTools() / pi.getAllTools() / pi.setActiveTools(names)
Sección titulada «pi.getActiveTools() / pi.getAllTools() / pi.setActiveTools(names)»Administrar herramientas activas. Esto funciona tanto para herramientas integradas como para herramientas registradas dinámicamente. pi.getActiveTools() devuelve los nombres de las herramientas activas como string[]; pi.getAllTools() devuelve metadatos para todas las herramientas configuradas.
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() devuelve name, description, parameters, promptGuidelines y sourceInfo.
Valores típicos de sourceInfo.source:
builtinpara herramientas integradassdkpara herramientas pasadas mediantecreateAgentSession({ customTools })- metadatos de origen de extensión para herramientas registradas por extensiones
pi.setModel(model)
Sección titulada «pi.setModel(model)»Establecer el modelo actual. Devuelve false si no hay ninguna clave API disponible para el modelo. Consulte models.md para configurar modelos personalizados.
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(level)
Sección titulada «pi.getThinkingLevel() / pi.setThinkingLevel(level)»Obtenga o establezca el nivel de pensamiento. El nivel está sujeto a las capacidades del modelo (los modelos que no razonan siempre usan “apagado”). Los cambios emiten thinking_level_select.
const current = pi.getThinkingLevel(); // "off" | "minimal" | "low" | "medium" | "high" | "xhigh"pi.setThinkingLevel("high");pi.events
Sección titulada «pi.events»Bus de eventos compartido para comunicación entre extensiones:
pi.events.on("my:event", (data) => { ... });pi.events.emit("my:event", { ... });pi.registerProvider(name, config)
Sección titulada «pi.registerProvider(name, config)»Registre o anule un proveedor de modelo dinámicamente. Útil para servidores proxy, puntos finales personalizados o configuraciones de modelos para todo el equipo.
Las llamadas realizadas durante la función de fábrica de extensiones se ponen en cola y se aplican una vez que se inicializa el corredor. Las llamadas realizadas después de eso (por ejemplo, desde un controlador de comandos que sigue un flujo de configuración de usuario) entran en vigor inmediatamente sin requerir un /reload.
Si necesita descubrir modelos desde un punto final remoto, prefiera una fábrica de extensiones asíncronas en lugar de diferir la búsqueda a session_start. pi espera a que llegue la fábrica antes de continuar con el inicio, por lo que los modelos registrados están disponibles de inmediato, incluido 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; } }});Opciones de configuración:
name: nombre para mostrar del proveedor en la interfaz de usuario, como/login.baseUrl: URL del punto final de la API. Requerido al definir modelos.apiKey: literal de clave API, interpolación de entorno ($ENV_VARo${ENV_VAR}) o!commandinicial. Requerido al definir modelos (a menos que se proporcioneoauth).$$escapa de$y$!escapa de un!literal sin activar la ejecución del comando.api- Tipo de API:"anthropic-messages","openai-completions","openai-responses", etc.headers: encabezados personalizados para incluir en las solicitudes.authHeader: si es verdadero, agrega el encabezadoAuthorization: Bearerautomáticamente.models- Matriz de definiciones de modelos. Si se proporciona, reemplaza todos los modelos existentes de este proveedor. Las definiciones de modelo pueden configurarbaseUrlpara anular el punto final del proveedor para ese modelo.oauth: configuración del proveedor OAuth para compatibilidad con/login. Cuando se proporciona, el proveedor aparece en el menú de inicio de sesión.streamSimple: implementación de transmisión personalizada para API no estándar.
Consulte custom-provider.md para temas avanzados: API de streaming personalizadas, detalles de OAuth, referencia de definición de modelo.
pi.unregisterProvider(name)
Sección titulada «pi.unregisterProvider(name)»Eliminar un proveedor previamente registrado y sus modelos. Se restauran los modelos integrados que fueron anulados por el proveedor. No tiene efecto si el proveedor no estaba registrado.
Al igual que registerProvider, esto entra en vigor inmediatamente cuando se llama después de la fase de carga inicial, por lo que no se requiere un /reload.
pi.registerCommand("my-setup-teardown", { description: "Remove the custom proxy provider", handler: async (_args, _ctx) => { pi.unregisterProvider("my-proxy"); },});Gestión del Estado
Sección titulada «Gestión del Estado»Las extensiones con estado deben almacenarlo en el resultado de la herramienta details para un soporte de ramificación adecuado:
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 }; }, });}Herramientas personalizadas
Sección titulada «Herramientas personalizadas»Registre las herramientas que el LLM puede llamar a través de pi.registerTool(). Las herramientas aparecen en el indicador del sistema y pueden tener una representación personalizada.
Utilice promptSnippet para una entrada breve de una línea en la sección Available tools en el indicador predeterminado del sistema. Si se omiten, las herramientas personalizadas quedan fuera de esa sección.
Utilice promptGuidelines para agregar viñetas específicas de herramientas a la sección Guidelines del indicador predeterminado del sistema. Estas viñetas se incluyen solo mientras la herramienta está activa (por ejemplo, después de pi.setActiveTools([...])).
Importante: Las viñetas promptGuidelines se agregan planas a la sección Guidelines sin prefijo ni agrupación de nombre de herramienta. Cada pauta debe nombrar la herramienta a la que se refiere; evite “Usar esta herramienta cuando…” porque el LLM no puede decir qué herramienta significa “esta”. En su lugar, escriba “Usar my_tool cuando…”.
Nota: Algunos modelos incluyen el prefijo @ en los argumentos de ruta de herramienta. Las herramientas integradas eliminan una @ inicial antes de resolver las rutas. Si tu herramienta personalizada acepta una ruta, normaliza también una @ inicial.
Si su herramienta personalizada muta archivos, use withFileMutationQueue() para que participe en la misma cola por archivo que los edit y write integrados. Esto es importante porque las llamadas a herramientas se ejecutan en paralelo de forma predeterminada. Sin la cola, dos herramientas pueden leer el mismo contenido de archivo antiguo, calcular diferentes actualizaciones y luego, la última escritura que llegue sobrescribe a la otra.
Ejemplo de caso de error: su herramienta personalizada edita foo.ts mientras que el edit integrado también cambia foo.ts en el mismo turno del asistente. Si su herramienta no participa en la cola, ambas pueden leer el foo.ts original, aplicar cambios separados y uno de esos cambios se pierde.
Pase la ruta real del archivo de destino a withFileMutationQueue(), no el argumento de usuario sin formato. Resuélvalo primero en una ruta absoluta, relativa a ctx.cwd o al directorio de trabajo de su herramienta. Para los archivos existentes, el asistente canonicaliza a través de realpath(), por lo que los alias de enlaces simbólicos para el mismo archivo comparten una cola. Para archivos nuevos, recurre a la ruta absoluta resuelta porque todavía no hay nada para realpath().
Ponga en cola toda la ventana de mutación en esa ruta de destino. Eso incluye la lógica de lectura, modificación y escritura, no solo la escritura final.
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: {}, }; });}Definición de herramienta
Sección titulada «Definición de herramienta»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) { ... },});Errores de señalización: Para marcar la ejecución de una herramienta como fallida (establece isError: true en el resultado y lo informa al LLM), genera un error desde execute. Devolver un valor nunca establece el indicador de error independientemente de las propiedades que incluya en el objeto devuelto.
Terminación anticipada: Devuelve terminate: true de execute() para indicar que la llamada de LLM de seguimiento automático debe omitirse después del lote de herramientas actual. Esto solo tiene efecto cuando finaliza cada resultado de herramienta finalizada en ese lote. Consulte examples/extensions/structured-output.ts para ver un ejemplo mínimo en el que el agente finaliza con una llamada final a la herramienta de salida estructurada.
// 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: {} };}Importante: Utilice StringEnum de @earendil-works/pi-ai para enumeraciones de cadenas. Type.Union/Type.Literal no funciona con la API de Google.
Preparación de argumentos: prepareArguments(args) es opcional. Si está definido, se ejecuta antes de la validación del esquema y antes de execute(). Úselo para imitar una forma de entrada aceptada más antigua cuando pi reanude una sesión anterior cuyos argumentos de llamada de herramienta almacenados ya no coinciden con el esquema actual. Devuelve el objeto que deseas validar con parameters. Mantenga el esquema público estricto. No agregue campos de compatibilidad obsoletos a parameters solo para mantener funcionando las sesiones anteriores reanudadas.
Ejemplo: una sesión anterior puede contener una llamada a la herramienta edit con oldText y newText de nivel superior, mientras que el esquema actual solo acepta 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: {}, }; },});Anulación de herramientas integradas
Sección titulada «Anulación de herramientas integradas»Las extensiones pueden anular las herramientas integradas (read, bash, edit, write, grep, find, ls) registrando una herramienta con el mismo nombre. El modo interactivo muestra una advertencia cuando esto sucede.
# Extension's read tool replaces built-in readpi -e ./tool-override.tsAlternativamente, use --no-builtin-tools para comenzar sin herramientas integradas mientras mantiene habilitadas las herramientas de extensión:
# No built-in tools, only extension toolspi --no-builtin-tools -e ./my-extension.tsConsulte examples/extensions/tool-override.ts para obtener un ejemplo completo que anula read con registro y control de acceso.
Renderizado: La herencia del renderizador integrado se resuelve por ranura. La anulación de ejecución y la anulación de representación son independientes. Si su anulación omite renderCall, se utiliza el renderCall integrado. Si su anulación omite renderResult, se utiliza el renderResult integrado. Si su anulación omite ambos, el renderizador incorporado se usa automáticamente (resaltado de sintaxis, diferencias, etc.). Esto le permite empaquetar herramientas integradas para registro o control de acceso sin volver a implementar la interfaz de usuario.
Metadatos de solicitud: promptSnippet y promptGuidelines no se heredan de la herramienta integrada. Si su anulación debe conservar esas instrucciones rápidas, defínalas explícitamente en la anulación.
Su implementación debe coincidir exactamente con la forma del resultado, incluido el tipo details. La interfaz de usuario y la lógica de la sesión dependen de estas formas para la representación y el seguimiento del estado.
Implementaciones de herramientas integradas:
- read.ts -
ReadToolDetails - bash.ts -
BashToolDetails - edit.ts
- write.ts
- grep.ts -
GrepToolDetails - find.ts -
FindToolDetails - ls.ts -
LsToolDetails
Ejecución remota
Sección titulada «Ejecución remota»Las herramientas integradas admiten operaciones conectables para delegar a sistemas remotos (SSH, contenedores, etc.):
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); },});Interfaces de operaciones: ReadOperations, WriteOperations, EditOperations, BashOperations, LsOperations, GrepOperations, FindOperations
Para user_bash, las extensiones pueden reutilizar el backend del shell local de pi a través de createLocalBashOperations() en lugar de reimplementar la generación de procesos locales, la resolución del shell y la terminación del árbol de procesos.
La herramienta bash también admite un gancho de generación para ajustar el comando, cwd o env antes de la ejecución:
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" }, }),});Consulte examples/extensions/ssh.ts para obtener un ejemplo SSH completo con el indicador --ssh.
Truncamiento de salida
Sección titulada «Truncamiento de salida»Las herramientas DEBEN truncar su salida para evitar abrumar el contexto LLM. Grandes producciones pueden causar:
- Errores de desbordamiento de contexto (mensaje demasiado largo)
- Fallas de compactación
- Rendimiento del modelo degradado
El límite incorporado es 50 KB (~10 000 tokens) y 2000 líneas, lo que se alcance primero. Utilice las utilidades de truncamiento exportadas:
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 }] };}Puntos clave:
- Utilice
truncateHeadpara contenido donde el comienzo importa (resultados de búsqueda, lecturas de archivos) - Utilice
truncateTailpara contenido donde el final importa (registros, salida de comando) - Informe siempre al LLM cuando se trunque el resultado y dónde encontrar la versión completa.
- Documente los límites de truncamiento en la descripción de su herramienta.
Consulte examples/extensions/truncated-tool.ts para obtener un ejemplo completo que envuelve rg (ripgrep) con el truncamiento adecuado.
Múltiples herramientas
Sección titulada «Múltiples herramientas»Una extensión puede registrar múltiples herramientas con estado compartido:
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(); });}Representación personalizada
Sección titulada «Representación personalizada»Las herramientas pueden proporcionar renderCall y renderResult para una visualización TUI personalizada. Consulte tui.md para conocer la API completa del componente y tool-execution.ts para conocer cómo se componen las filas de herramientas.
De forma predeterminada, la salida de la herramienta está envuelta en un Box que maneja el relleno y el fondo. Un renderCall o renderResult definido debe devolver un Component. Si no se define un renderizador de ranura, tool-execution.ts utiliza el renderizado alternativo para esa ranura.
Establezca renderShell: "self" cuando la herramienta deba representar su propio shell en lugar de usar el Box predeterminado. Esto es útil para herramientas que necesitan un control total sobre el encuadre o el comportamiento del fondo, por ejemplo, vistas previas grandes que deben permanecer visualmente estables después de que la herramienta se estabilice.
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 y renderResult reciben cada uno un objeto context con:
args- los argumentos de llamada de la herramienta actualstate: estado local de fila compartido entrerenderCallyrenderResultlastComponent: el componente devuelto anteriormente para esa ranura, si correspondeinvalidate(): solicita una nueva representación de esta fila de herramientastoolCallId,cwd,executionStarted,argsComplete,isPartial,expanded,showImages,isError
Utilice context.state para el estado compartido entre ranuras. Mantenga los cachés locales de ranura en la instancia del componente devuelto cuando desee reutilizar y mutar el mismo componente en diferentes renderizados.
renderCall
Sección titulada «renderCall»Representa la llamada o encabezado de la herramienta:
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
Sección titulada «renderResult»Representa el resultado o salida de la herramienta:
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);}Si una ranura no tiene contenido visible intencionalmente, devuelve un Component vacío, como un Container vacío.
Sugerencias de combinación de teclas
Sección titulada «Sugerencias de combinación de teclas»Utilice keyHint() para mostrar sugerencias de combinación de teclas que respeten la configuración de combinación de teclas activa:
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);}Funciones disponibles:
keyHint(keybinding, description): formatea una identificación de combinación de teclas configurada, como"app.tools.expand"o"tui.select.confirm".keyText(keybinding): devuelve el texto clave configurado sin formato para una identificación de combinación de teclasrawKeyHint(key, description)- Formatear una cadena de clave sin formato
Utilice identificadores de combinación de teclas con espacios de nombres:
- Los identificadores de agente de codificación utilizan el espacio de nombres
app.*, por ejemploapp.tools.expand,app.editor.external,app.session.rename. - Los ID de TUI compartidos utilizan el espacio de nombres
tui.*, por ejemplotui.select.confirm,tui.select.cancel,tui.input.tab.
Para obtener una lista exhaustiva de identificadores de combinaciones de teclas y valores predeterminados, consulte keybindings.md. keybindings.json usa esos mismos identificadores de espacio de nombres.
Los editores personalizados y los componentes ctx.ui.custom() reciben keybindings: KeybindingsManager como argumento inyectado. Deberían usar ese administrador inyectado directamente en lugar de llamar a getKeybindings() o setKeybindings().
Mejores prácticas
Sección titulada «Mejores prácticas»- Utilice
Textcon relleno(0, 0). El cuadro predeterminado maneja el relleno. - Utilice
\npara contenido de varias líneas. - Manejar
isPartialpara el progreso de la transmisión. - Soporte
expandedpara detalles bajo demanda. - Mantenga compacta la vista predeterminada.
- Leer
context.argsenrenderResulten lugar de copiar argumentos encontext.state. - Utilice
context.statesolo para datos que deben compartirse entre espacios de llamadas y resultados. - Reutilice
context.lastComponentcuando la misma instancia de componente se pueda actualizar en su lugar. - Utilice
renderShell: "self"sólo cuando el shell en caja predeterminado se interponga en su camino. En el modo self-shell, la herramienta es responsable de su propio marco, relleno y fondo.
Reserva
Sección titulada «Reserva»Si un renderizador de ranura no está definido o arroja:
renderCall: Muestra el nombre de la herramienta.renderResult: muestra texto sin formato decontent
IU personalizada
Sección titulada «IU personalizada»Las extensiones pueden interactuar con los usuarios a través de métodos ctx.ui y personalizar la forma en que se representan los mensajes/herramientas.
Para componentes personalizados, consulte tui.md que tiene patrones de copiar y pegar para:
- Cuadros de diálogo de selección (SelectList)
- Operaciones asíncronas con cancelación (BorderedLoader)
- La configuración alterna (Lista de configuración)
- Indicadores de estado (setStatus)
- Mensaje de trabajo, visibilidad e indicador durante la transmisión (
setWorkingMessage,setWorkingVisible,setWorkingIndicator) - Widgets arriba/abajo del editor (setWidget)
- Proveedores de autocompletar superpuestos a la finalización de ruta/barra incorporada (addAutocompleteProvider)
- Pies de página personalizados (setFooter)
Diálogos
Sección titulada «Diálogos»// 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"Diálogos cronometrados con cuenta regresiva
Sección titulada «Diálogos cronometrados con cuenta regresiva»Los cuadros de diálogo admiten una opción timeout que se cierra automáticamente con una visualización de cuenta regresiva en vivo:
// 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}Valores devueltos en el tiempo de espera:
select()devuelveundefinedconfirm()devuelvefalseinput()devuelveundefined
Despido manual con AbortSignal
Sección titulada «Despido manual con AbortSignal»Para obtener más control (por ejemplo, para distinguir el tiempo de espera de la cancelación del usuario), utilice 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")}Consulte examples/extensions/timed-confirm.ts para ver ejemplos completos.
Widgets, estado y pie de página
Sección titulada «Widgets, estado y pie de página»// 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 themeLos marcos de indicadores de trabajo personalizados se representan palabra por palabra. Si desea colores, agréguelos usted mismo a las cadenas del marco, por ejemplo con ctx.ui.theme.fg(...).
Proveedores de autocompletar
Sección titulada «Proveedores de autocompletar»Utilice ctx.ui.addAutocompleteProvider() para apilar la lógica de autocompletar personalizada sobre el comando de barra diagonal integrado y el proveedor de ruta. Configure triggerCharacters para activadores naturales personalizados como $.
Patrón típico:
- inspeccionar el texto antes del cursor
- devolver sus propias sugerencias cuando la sintaxis específica de su extensión coincida
- de lo contrario delega a
current.getSuggestions(...) - delega
applyCompletion(...)a menos que necesites un comportamiento de inserción personalizado
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; }, }));});Consulte github-issue-autocomplete.ts para obtener un ejemplo completo que carga previamente los últimos problemas abiertos de GitHub con gh issue list y los filtra localmente para una rápida finalización de #.... Requiere GitHub CLI (gh) y un repositorio de GitHub.
Componentes personalizados
Sección titulada «Componentes personalizados»Para una interfaz de usuario compleja, utilice ctx.ui.custom(). Esto reemplaza temporalmente el editor con su componente hasta que se llame a 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}La devolución de llamada recibe:
tui- Instancia TUI (para dimensiones de pantalla, gestión de enfoque)theme- Tema actual para estilizarkeybindings- Administrador de combinación de teclas de la aplicación (para comprobar los accesos directos)done(value)- Llamada para cerrar componente y devolver valor
Consulte tui.md para conocer la API completa del componente.
Modo de superposición (experimental)
Sección titulada «Modo de superposición (experimental)»Pase { overlay: true } para representar el componente como un modal flotante sobre el contenido existente, sin borrar la pantalla:
const result = await ctx.ui.custom<string | null>( (tui, theme, keybindings, done) => new MyOverlayComponent({ onClose: done }), { overlay: true });Para posicionamiento avanzado (anclajes, márgenes, porcentajes, visibilidad receptiva), pase overlayOptions. Utilice onHandle para controlar el foco o la visibilidad mediante programación:
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 } });Una superposición visible enfocada puede recuperar la entrada después de que se cierra la interfaz de usuario personalizada temporal sin superposición. Si intencionalmente desea que otro componente mantenga la entrada mientras la superposición permanece visible, llame a handle.unfocus({ target }). Al pasar { target: null } se libera la superposición sin enfocar otro componente.
Consulte tui.md para obtener la API OverlayOptions y OverlayHandle completa y overlay-qa-tests.ts para ver ejemplos.
Editor personalizado
Sección titulada «Editor personalizado»Reemplace el editor de entrada principal con una implementación personalizada (modo vim, modo emacs, etc.):
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) ); });}Puntos clave:
- Extienda
CustomEditor(noEditorbase) para obtener combinaciones de teclas de aplicaciones (escape para cancelar, Ctrl+d, cambio de modelo) - Llame a
super.handleInput(data)para llaves que no maneja - La fábrica recibe
themeykeybindingsdesde la aplicación - Utilice
ctx.ui.getEditorComponent()antes desetEditorComponent()para ajustar el editor personalizado configurado previamente. - Pase
undefinedpara restaurar el valor predeterminado:ctx.ui.setEditorComponent(undefined)
Para componer con otra extensión que ya reemplazó al editor, captura la fábrica anterior antes de configurar la tuya:
const previous = ctx.ui.getEditorComponent();ctx.ui.setEditorComponent((tui, theme, keybindings) => new MyEditor(tui, theme, keybindings, { base: previous?.(tui, theme, keybindings) }));Consulte tui.md Patrón 7 para ver un ejemplo completo con indicador de modo.
Representación de mensajes
Sección titulada «Representación de mensajes»Registre un procesador personalizado para mensajes con su 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);});Los mensajes se envían a través de pi.sendMessage():
pi.sendMessage({ customType: "my-extension", // Matches registerMessageRenderer content: "Status update", display: true, // Show in TUI details: { ... }, // Available in renderer});Colores del tema
Sección titulada «Colores del tema»Todas las funciones de renderizado reciben un objeto theme. Consulte themes.md para crear temas personalizados y la paleta de colores completa.
// 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)Para resaltar la sintaxis en los renderizadores de herramientas personalizadas:
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);Manejo de errores
Sección titulada «Manejo de errores»- Se registran errores de extensión, el agente continúa
- Los errores
tool_callbloquean la herramienta (a prueba de fallos) - Los errores de la herramienta
executedeben señalarse lanzando; el error arrojado se detecta, se informa al LLM conisError: truey la ejecución continúa
Comportamiento del modo
Sección titulada «Comportamiento del modo»| Modo | ctx.mode | ctx.hasUI | Notas |
|---|---|---|---|
| Interactivo | "tui" | true | TUI completo con renderizado de terminal |
RPC (--mode rpc) | "rpc" | true | Diálogos y notificaciones vía protocolo JSON; custom() devuelve undefined. Consulte rpc.md |
JSON (--mode json) | "json" | false | Flujo de eventos a salida estándar; Los métodos de UI no son operativos |
Imprimir (-p) | "print" | false | Las extensiones se ejecutan pero no pueden avisar |
Utilice ctx.mode === "tui" antes de las funciones específicas de TUI (custom(), fábricas de componentes, entrada de terminal). Utilice ctx.hasUI antes de los métodos de diálogo y notificación que funcionan en los modos TUI y RPC.
Referencia de ejemplos
Sección titulada «Referencia de ejemplos»Todos los ejemplos en examples/extensions/.
| Ejemplo | Descripción | API clave |
|---|---|---|
| Herramientas | ||
hello.ts | Registro mínimo de herramienta | registerTool |
question.ts | Herramienta con interacción del usuario | registerTool, ui.select |
questionnaire.ts | Herramienta asistente de varios pasos | registerTool, ui.custom |
todo.ts | Herramienta con estado y persistencia | registerTool, appendEntry, renderResult, eventos de sesión |
dynamic-tools.ts | Registrar herramientas después del inicio y durante los comandos | registerTool, session_start, registerCommand |
structured-output.ts | Herramienta final de salida estructurada con terminate: true | registerTool, resultados de la herramienta de terminación |
truncated-tool.ts | Ejemplo de truncamiento de salida | registerTool, truncateHead |
tool-override.ts | Anular la herramienta de lectura incorporada | registerTool (mismo nombre que el integrado) |
| Comandos | ||
pirate.ts | Modificar el mensaje del sistema por turno | registerCommand, before_agent_start |
summarize.ts | Comando de resumen de conversación | registerCommand, ui.custom |
handoff.ts | Traspaso del modelo entre proveedores | registerCommand, ui.editor, ui.custom |
qna.ts | Preguntas y respuestas con interfaz de usuario personalizada | registerCommand, ui.custom, setEditorText |
send-user-message.ts | Inyectar mensajes de usuario | registerCommand, sendUserMessage |
reload-runtime.ts | Recargar comando y transferencia de herramientas LLM | registerCommand, ctx.reload(), sendUserMessage |
shutdown-command.ts | Comando de apagado elegante | registerCommand, shutdown() |
| Eventos y puertas | ||
permission-gate.ts | Bloquear comandos peligrosos | on("tool_call"), ui.confirm |
project-trust.ts | Decidir o diferir la confianza del proyecto desde un usuario/global o extensión CLI | on("project_trust"), interfaz de usuario de confianza, resultado de confianza requerido |
protected-paths.ts | Bloquear escrituras en rutas específicas | on("tool_call") |
confirm-destructive.ts | Confirmar cambios de sesión | on("session_before_switch"), on("session_before_fork") |
dirty-repo-guard.ts | Advertir sobre repositorio de git sucio | on("session_before_*"), exec |
input-transform.ts | Transformar la entrada del usuario | on("input") |
input-transform-streaming.ts | Transformación de entrada compatible con streaming | on("input"), streamingBehavior |
model-status.ts | Reaccionar a los cambios de modelo | on("model_select"), setStatus |
provider-payload.ts | Inspeccionar cargas útiles y encabezados de respuesta del proveedor | on("before_provider_request"), on("after_provider_response") |
system-prompt-header.ts | Mostrar información de aviso del sistema | on("agent_start"), getSystemPrompt |
claude-rules.ts | Cargar reglas desde archivos | on("session_start"), on("before_agent_start") |
prompt-customizer.ts | Agregue guía de herramientas sensible al contexto usando systemPromptOptions | on("before_agent_start"), BuildSystemPromptOptions |
file-trigger.ts | El observador de archivos activa mensajes | sendMessage |
| Compactación y Sesiones | ||
custom-compaction.ts | Resumen de compactación personalizado | on("session_before_compact") |
trigger-compact.ts | Activar la compactación manualmente | compact() |
git-checkpoint.ts | Git stash en turnos | on("turn_start"), on("session_before_fork"), exec |
git-merge-and-resolve.ts | Recuperar, fusionar y resolver conflictos | on("agent_end"), exec, sendUserMessage |
auto-commit-on-exit.ts | Comprometerse a cerrar | on("session_shutdown"), exec |
| Componentes de la interfaz de usuario | ||
status-line.ts | Indicador de estado de pie de página | setStatus, eventos de sesión |
working-indicator.ts | Personaliza el indicador de funcionamiento del streaming | setWorkingIndicator, registerCommand |
github-issue-autocomplete.ts | Agregue la finalización de problemas #1234 además del autocompletado integrado precargando problemas abiertos recientes desde gh issue list | addAutocompleteProvider, on("session_start"), exec |
custom-footer.ts | Reemplazar el pie de página por completo | registerCommand, setFooter |
custom-header.ts | Reemplazar encabezado de inicio | on("session_start"), setHeader |
modal-editor.ts | Editor modal estilo Vim | setEditorComponent, CustomEditor |
rainbow-editor.ts | Estilo de editor personalizado | setEditorComponent |
widget-placement.ts | Widget arriba/abajo del editor | setWidget |
overlay-test.ts | Componentes de superposición | ui.custom con opciones de superposición |
overlay-qa-tests.ts | Pruebas completas de superposición | ui.custom, todas las opciones de superposición |
notify.ts | Notificaciones sencillas | ui.notify |
timed-confirm.ts | Diálogos con tiempo de espera | ui.confirm con tiempo de espera/señal |
mac-system-theme.ts | Tema de cambio automático | setTheme, exec |
| Extensiones complejas | ||
plan-mode/ | Implementación del modo de plan completo | Todos los tipos de eventos, registerCommand, registerShortcut, registerFlag, setStatus, setWidget, sendMessage, setActiveTools |
preset.ts | Ajustes preestablecidos guardables (modelo, herramientas, pensamiento) | registerCommand, registerShortcut, registerFlag, setModel, setActiveTools, setThinkingLevel, appendEntry |
tools.ts | Activar/desactivar herramientas UI | registerCommand, setActiveTools, SettingsList, eventos de sesión |
| Remoto y zona de pruebas | ||
ssh.ts | Ejecución remota SSH | registerFlag, on("user_bash"), on("before_agent_start"), operaciones de herramientas |
interactive-shell.ts | Sesión de shell persistente | on("user_bash") |
sandbox/ | Ejecución de herramientas en espacio aislado | Operaciones con herramientas |
gondolin/ | Enrutar herramientas integradas y comandos ! a una micro-VM Gondolin | Operaciones de herramientas, anulaciones de herramientas integradas, on("user_bash") |
subagent/ | Generar subagentes | registerTool, exec |
| Juegos | ||
snake.ts | Juego de serpientes | registerCommand, ui.custom, manejo de teclado |
space-invaders.ts | Juego de invasores espaciales | registerCommand, ui.custom |
doom-overlay/ | Doom en superposición | ui.custom con superposición |
| Proveedores | ||
custom-provider-anthropic/ | Proxy antrópico personalizado | registerProvider |
custom-provider-gitlab-duo/ | Integración de GitLab Duo | registerProvider con OAuth |
| Mensajes y comunicación | ||
message-renderer.ts | Representación de mensajes personalizados | registerMessageRenderer, sendMessage |
event-bus.ts | Eventos entre extensiones | pi.events |
| Metadatos de la sesión | ||
session-name.ts | Nombre de sesiones para selector | setSessionName, getSessionName |
bookmark.ts | Entradas de marcadores para /tree | setLabel |
| Varios | ||
inline-bash.ts | Bash en línea en llamadas a herramientas | on("tool_call") |
bash-spawn-hook.ts | Ajuste el comando bash, cwd y env antes de la ejecución | createBashTool, spawnHook |
with-deps/ | Extensión con dependencias npm | Estructura del paquete con package.json |