import type { TSchema } from "@sinclair/typebox"; export interface ToolResultText { type: "text"; text: string } export interface ToolResultImage { type: "image"; data: string; mimeType: string } export interface ToolResultResource { type: "resource"; uri: string; title?: string } export type ToolResultContent = ToolResultText | ToolResultImage | ToolResultResource; export interface ToolResult { content: ToolResultContent[]; details?: Record; isError?: boolean; } export interface ExtensionMessage { customType: string; content: string | ToolResultContent[]; display?: boolean; details?: Record; } export interface SendMessageOptions { triggerTurn?: boolean; deliverAs?: "steer" | "followUp" | "nextTurn"; } export interface ToolExecuteOptions { signal: AbortSignal; update: (partial: Partial) => void; ctx: ExtensionContext; } export type ToolUpdate = (partial: Partial) => void; export type ToolApprovalTier = "read" | "write" | "exec"; export type ToolApprovalDecision = ToolApprovalTier | { tier: ToolApprovalTier; override?: boolean; reason?: string }; export type ToolApproval = ToolApprovalDecision | ((args: unknown) => ToolApprovalDecision); export interface ToolDefinition { name: string; description?: string; parameters: TSchema; approval?: ToolApproval; formatApprovalDetails?: (args: unknown) => string | string[] | undefined; execute: (toolCallId: string, params: unknown, signal: AbortSignal, update: ToolUpdate, ctx: ExtensionContext) => Promise | ToolResult; /** * Optional/additive: a tool result renderer so the finished tool can redraw a * persistent transcript card from `result.details` instead of collapsing to a * one-line string. The real host (`@earendil-works/pi-coding-agent` * ToolDefinition) already exposes `renderResult`/`renderCall`; this models that * seam in the shim, loosely typed to the shim's `CustomUiComponent`/string[] * idiom rather than the host's `Component`/`Theme` so existing tools that omit * it are unaffected. */ renderResult?: (result: ToolResult, ctx?: ExtensionContext) => CustomUiComponent | string[]; } export interface CommandOptions { description?: string; args?: unknown[]; handler: (args: CommandArgs, ctx: ExtensionCommandContext) => Promise | void; } export type CommandArgs = string | { text?: string; args: Record; }; export type LifecycleEvent = | "session_start" | "session_end" | "session_compact" | "resources_discover" | "input" | "before_agent_start" | "context" | "tool_call" | "tool_execution_start" | "tool_result" | "turn_start" | "turn_end" | "agent_start" | "agent_end" | "stop"; export interface EventPayload { type: LifecycleEvent; sessionId?: string; toolName?: string; input?: unknown; toolArgs?: unknown; systemPrompt?: string; messages?: Array<{ role: string; content: string; hidden?: boolean }>; reason?: string; /** `tool_result` event fields (real Pi: ToolResultEvent). */ toolCallId?: string; content?: ToolResultContent[]; details?: unknown; isError?: boolean; [key: string]: unknown; } export interface EventResult { block?: boolean; reason?: string; modify?: Partial; systemPrompt?: string; /** * `tool_result` rewrite shape (real Pi: ToolResultEventResult). Returning * `content` from a `tool_result` handler rewrites BOTH the stored and the * sent tool result in one shot, because `tool_result` fires once before storage. */ content?: ToolResultContent[]; details?: unknown; isError?: boolean; message?: { customType: string; content: string; display?: boolean; }; /** * `injectContext` was used by earlier local-only harnesses. * Real Pi currently supports `systemPrompt` and `message` return shapes for this hook. */ injectContext?: Array<{ role: string; content: string; hidden?: boolean }>; } export type EventHandler = (event: EventPayload, ctx: ExtensionContext) => Promise | EventResult | void; export interface SelectOption { label: string; value: string } export type SelectChoice = SelectOption | string; export interface SelectResult { value: string; label?: string; cancelled?: boolean } export type SelectReturn = SelectResult | string | undefined; export interface InputResult { value: string; cancelled?: boolean } export interface EditorResult { value: string; cancelled?: boolean } export interface CustomUiComponent { render(width: number): string[]; handleInput?(data: string): void | Promise; invalidate(): void; dispose?(): void; } export interface CustomUiTui { requestRender(force?: boolean): void; terminal?: { rows: number; columns: number }; } export interface WidgetFactoryTui extends CustomUiTui { terminal?: { rows: number; columns: number }; } export type CustomUiFactory = (tui: CustomUiTui, theme: unknown, keybindings: unknown, done: (result: T) => void) => CustomUiComponent | Promise; export interface CustomUiOverlayOptions { width?: number | `${number}%`; maxHeight?: number | `${number}%`; row?: number | `${number}%`; col?: number | `${number}%`; margin?: number | { top?: number; right?: number; bottom?: number; left?: number }; } /** * Minimal slice of Pi's interactive `Theme` exposed to extensions via * `ctx.ui.theme`. Methods return ANSI-wrapped strings. The real Theme has many * more members; we type only what we style with. Undefined outside interactive * mode (and in the test harness unless injected). */ export interface ThemeLike { fg(color: string, text: string): string; bg(color: string, text: string): string; bold(text: string): string; } /** A border-colorizer: wraps a border string in ANSI color (real Pi: Editor.borderColor). */ export type EditorBorderColor = (str: string) => string; /** * Factory for a custom input editor component (real Pi: `EditorFactory`). * Pi calls it synchronously with the live TUI, a reduced EditorTheme (only * `borderColor`), and the keybindings manager, and expects an editor instance * back. Typed loosely because the editor base class lives in the host package, * not in this shim — see `setEditorComponent`. */ export type EditorFactory = (tui: unknown, theme: { borderColor: EditorBorderColor }, keybindings: unknown) => unknown; export type ThinkingLevel = "off" | "minimal" | "low" | "medium" | "high" | "xhigh"; export interface ModelLike { provider: string; id: string; name?: string; thinking?: boolean | string[]; [key: string]: unknown; } export interface ProviderConfigLike { api?: string; baseUrl?: string; apiKey?: string; headers?: Record; authHeader?: string | boolean; models?: ModelLike[]; oauth?: unknown; [key: string]: unknown; } export type ModelRequestAuthLike = | { ok: true; apiKey?: string; headers?: Record; env?: Record; } | { ok: false; error: string }; export interface ModelRegistryLike { refresh?(): void; getError?(): string | undefined; getAll?(): ModelLike[] | Promise; getAvailable?(): ModelLike[] | Promise; find?(provider: string, id: string): ModelLike | undefined | Promise; getApiKeyAndHeaders?(model: ModelLike): Promise; registerProvider?(name: string, config: ProviderConfigLike): void | Promise; unregisterProvider?(name: string): void | Promise; } export interface ReplacementSessionEntryLike { type?: string; customType?: string; role?: string; content?: string; text?: string; message?: unknown; data?: unknown; payload?: unknown; [key: string]: unknown; } export interface ReplacementSessionManagerLike { getEntries(opts?: { type?: string; limit?: number }): Promise | ReplacementSessionEntryLike[]; getBranch?(): Promise | ReplacementSessionEntryLike[]; getSessionId?(): string; getSessionFile?(): string | undefined; } /** Live context-budget signal (real Pi: ContextUsage). */ export interface ContextUsage { /** Estimated context tokens, or null right after compaction until the next LLM response. */ tokens: number | null; contextWindow: number; /** Usage as percentage of the window, or null when tokens is null. */ percent: number | null; } /** Compaction result handed to CompactOptions callbacks (real Pi: CompactionResult). */ export interface CompactionResultLike { summary: string; tokensBefore?: number; [key: string]: unknown; } /** Options for ctx.compact() (real Pi: CompactOptions). */ export interface CompactOptions { customInstructions?: string; onComplete?: (result: CompactionResultLike) => void; onError?: (error: Error) => void; } export interface ExtensionContext { cwd?: string; hasUI?: boolean; /** Real Pi run mode. Terminal component factories/custom UI are valid only in `tui`. */ mode?: "tui" | "rpc" | "json" | "print"; model?: ModelLike; modelRegistry?: ModelRegistryLike; /** Real Pi 0.80.3 ctx.isIdle(): false while the parent agent is streaming. */ isIdle(): boolean; /** Abort the active parent agent turn (real Pi: ctx.abort()). */ abort?(): void; /** Active parent-agent abort signal when a turn is running (real Pi: ctx.signal). */ signal?: AbortSignal; /** Current context usage for the active model (real Pi: ctx.getContextUsage()). */ getContextUsage?(): ContextUsage | undefined; /** Trigger compaction without awaiting completion (real Pi: ctx.compact()). */ compact?(options?: CompactOptions): void; setModel?(model: ModelLike): boolean | Promise; setThinkingLevel?(level: ThinkingLevel): void; ui: { select(title: string, options: SelectChoice[], opts?: { timeout?: number; signal?: AbortSignal }): Promise; input(title: string, opts?: { default?: string; placeholder?: string }): Promise; editor(title: string, content: string, opts?: { language?: string }): Promise; confirm(title: string, message: string): Promise; notify(message: string, level?: "info" | "warning" | "error"): void; /** Listen to raw terminal input in interactive mode. Returns an unsubscribe function. */ onTerminalInput?(handler: (data: string) => { consume?: boolean; data?: string } | undefined): () => void; /** Replace/read the current editor text (real Pi extension UI). */ setEditorText?(text: string): void; getEditorText?(): string; setStatus(key: string, text: string | undefined): void; setWidget(key: string, content: string[] | WidgetFactory | undefined, options?: { placement?: "aboveEditor" | "belowEditor" }): void; setTitle(title: string): void; setWorkingIndicator(active: boolean): void; custom?(factory: CustomUiFactory, options?: { overlay?: boolean; overlayOptions?: CustomUiOverlayOptions }): Promise; /** Live interactive theme for styling status/badges. Undefined outside interactive mode. */ readonly theme?: ThemeLike; /** Install a custom input-editor component, or undefined to restore the default. */ setEditorComponent?(factory: EditorFactory | undefined): void; /** The currently-installed custom editor factory, or undefined when using the default. */ getEditorComponent?(): EditorFactory | undefined; }; session?: { id: string; projectRoot: string; workingDirectory: string; }; sessionManager?: ReplacementSessionManagerLike; // settings is not part of Pi's ExtensionContext; the test harness provides it. // At runtime ctx.settings is undefined. Fall back to file-based config. settings?: { get(key: string): unknown; set(key: string, value: unknown): Promise; }; } export interface ReplacementSessionContext extends ExtensionContext { sendMessage?(message: string, opts?: Record): Promise | void; sendUserMessage?(message: string, opts?: Record): Promise | void; waitForIdle?(opts?: Record): Promise | void; } export interface NewSessionOptionsLike { parentSession?: string; setup?: (sessionManager: unknown) => Promise | void; withSession?: (ctx: ReplacementSessionContext) => Promise | void; } export interface NewSessionResultLike { cancelled: boolean; reason?: string; } export interface ExtensionCommandContext extends ExtensionContext { reload?(): Promise | void; newSession?(opts?: NewSessionOptionsLike): Promise | NewSessionResultLike; fork?(opts?: NewSessionOptionsLike): Promise | NewSessionResultLike; switchSession?(sessionId: string): Promise | void; sendMessage?(message: string, opts?: Record): Promise | void; sendUserMessage?(message: string, opts?: Record): Promise | void; /** Real Pi command primitive; optional in the shim so older/partial hosts fail closed at runtime. */ waitForIdle?(): Promise; } export type WidgetFactory = (...args: unknown[]) => unknown; export interface CustomEntry { type: string; data: unknown; timestamp?: string } export interface ShortcutOptions { description?: string; handler: (ctx: ExtensionContext) => Promise | void; } export interface ExtensionAPI { registerCommand(name: string, opts: CommandOptions): void; /** Current session command inventory; duplicate extension commands may be namespaced by Pi. */ getCommands?(): Array<{ name: string; description?: string; source?: string; sourceInfo?: unknown }>; registerTool(tool: ToolDefinition): void; /** * Register a keyboard shortcut (real Pi: ExtensionAPI.registerShortcut). * Optional because the test harness and minimal hosts may not implement it. * NOTE: Pi drops shortcuts whose chord is a reserved built-in (e.g. `shift+tab` * is reserved for `app.thinking.cycle`); free the chord via the user * keybindings.json before relying on it. */ registerShortcut?(shortcut: string, opts: ShortcutOptions): void; registerProvider?(name: string, config: ProviderConfigLike): void | Promise; unregisterProvider?(name: string): void | Promise; on(event: LifecycleEvent, handler: EventHandler): void; appendEntry(type: string, data: unknown): Promise; sendMessage?(message: ExtensionMessage, opts?: SendMessageOptions): Promise | void; sendUserMessage(message: string, opts?: Record): Promise; setActiveTools(toolNames: string[]): void; setModel?(model: ModelLike): boolean | Promise; setThinkingLevel?(level: ThinkingLevel): void; getThinkingLevel?(): ThinkingLevel; getSessionId(): string; } export type ExtensionFactory = (pi: ExtensionAPI) => void | Promise; export function textResult(text: string, details?: Record): ToolResult { return details === undefined ? { content: [{ type: "text", text }] } : { content: [{ type: "text", text }], details }; } export function errorResult(text: string, details?: Record): ToolResult { return details === undefined ? { isError: true, content: [{ type: "text", text }] } : { isError: true, content: [{ type: "text", text }], details }; } export function setTextWidget(ctx: ExtensionContext, key: string, content: string, options?: { placement?: "aboveEditor" | "belowEditor" }): void { ctx.ui.setWidget(key, content.split(/\r?\n/), options); } export function getProjectRoot(ctx: ExtensionContext): string { return ctx.session?.projectRoot ?? ctx.cwd ?? process.cwd(); } export function getWorkingDirectory(ctx: ExtensionContext): string { return ctx.session?.workingDirectory ?? ctx.cwd ?? process.cwd(); } export function getSessionId(ctx: ExtensionContext): string { return ctx.session?.id ?? ctx.sessionManager?.getSessionId?.() ?? "unknown-session"; } export function getCommandText(args: CommandArgs): string { return typeof args === "string" ? args : args.text ?? ""; }