import type { TSchema, Static } from "typebox"; import type { AgentToolResult, AgentToolUpdateCallback, ExtensionAPI, ExtensionContext, Theme, ToolDefinition, ToolRenderResultOptions, } from "@mariozechner/pi-coding-agent"; import type { Component } from "@mariozechner/pi-tui"; import type { BrowserClient } from "../client"; import type { Result } from "./result"; export type ToolOk = { readonly text: string; readonly details?: Readonly>; }; export type ToolErrKind = | "not_connected" | "cdp_error" | "timeout" | "invalid_state" | "io_error" | "internal"; export type ToolErr = { readonly kind: ToolErrKind; readonly message: string; readonly details?: Readonly>; }; export type HandlerContext = { readonly client: BrowserClient; readonly signal: AbortSignal | undefined; readonly onUpdate: (update: ToolOk) => void; readonly extensionCtx: ExtensionContext; }; export type ToolHandler = ( args: Static, ctx: HandlerContext, ) => Promise>; export type BrowserToolDefinition = { readonly name: string; readonly label: string; readonly description: string; readonly promptSnippet: string; readonly promptGuidelines: ReadonlyArray; readonly parameters: S; readonly handler: ToolHandler; readonly renderCall?: (args: Static, theme: Theme) => Component; readonly renderResult?: (result: AgentToolResult, expanded: boolean, theme: Theme) => Component; readonly ensureAlive?: boolean; }; // Type-erased entry for arrays. AnyBrowserToolDefinition lets the registry // hold a heterogeneous list without losing the per-tool Static binding. export type AnyBrowserToolDefinition = BrowserToolDefinition; export const defineBrowserTool = ( def: BrowserToolDefinition, ): BrowserToolDefinition => def; type OkDetails = { readonly ok: true } & Readonly>; type ErrDetails = { readonly ok: false; readonly kind: ToolErrKind; readonly message: string } & Readonly>; const toToolResult = ( r: Result, toolName: string, ): AgentToolResult => { if (r.success) { const details: OkDetails = { ok: true, ...(r.data.details ?? {}) }; return { content: [{ type: "text", text: r.data.text }], details, }; } const details: ErrDetails = { ok: false, kind: r.error.kind, message: r.error.message, ...(r.error.details ?? {}), }; return { // isError is an extension the pi-coding-agent runtime picks up from the result object; // AgentToolResult itself doesn't declare it but the runtime reads it via duck-typing. // We cast through unknown to avoid widening the return type. ...(({ isError: true }) as unknown as object), content: [{ type: "text", text: `${toolName} failed (${r.error.kind}): ${r.error.message}` }], details, }; }; export const registerBrowserTool = ( pi: ExtensionAPI, client: BrowserClient, def: BrowserToolDefinition, ): void => { const td: ToolDefinition = { name: def.name, label: def.label, description: def.description, promptSnippet: def.promptSnippet, promptGuidelines: [...def.promptGuidelines], parameters: def.parameters, ...(def.renderCall ? { renderCall: (args: Static, theme: Theme, _ctx: Parameters["renderCall"]>>[2]) => // Non-null assertion safe: this branch only runs when renderCall was provided. def.renderCall!(args, theme), } : {}), ...(def.renderResult ? { renderResult: ( result: AgentToolResult, options: ToolRenderResultOptions, theme: Theme, _ctx: Parameters["renderResult"]>>[3], ) => // Non-null assertion safe: this branch only runs when renderResult was provided. def.renderResult!(result, options.expanded ?? false, theme), } : {}), async execute(_toolCallId, args, signal, onUpdate, extensionCtx) { if (def.ensureAlive !== false) { const alive = await client.ensureAlive(); if (!alive.success) { return toToolResult( { success: false, error: { kind: "not_connected", message: alive.error.message } }, def.name, ); } } const result = await def.handler(args, { client, signal, onUpdate: (u) => { if (onUpdate) { // AgentToolUpdateCallback expects AgentToolResult; we use unknown for TDetails. const update: AgentToolResult = { content: [{ type: "text", text: u.text }], details: { ok: true, ...(u.details ?? {}) }, }; // Cast needed: onUpdate is AgentToolUpdateCallback (generic TDetails). (onUpdate as AgentToolUpdateCallback)(update); } }, extensionCtx, }); return toToolResult(result, def.name); }, }; pi.registerTool(td); };