diff options
| author | amankumarsingh77 <amankumarsingh7702@gmal.com> | 2026-05-02 16:00:58 +0530 |
|---|---|---|
| committer | amankumarsingh77 <amankumarsingh7702@gmal.com> | 2026-05-02 16:00:58 +0530 |
| commit | 6a4bfa31a925af67c026cd68700fe7779fd7c4cb (patch) | |
| tree | 640a95ed6b17787bd4022a21f73bd4c0a83d8f40 /src | |
| parent | d3d86f9de1cd3465cd1ec1e9a34c36b2f4878694 (diff) | |
feat: add defineBrowserTool helper, truncation, schemas/common, empty registry
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Diffstat (limited to 'src')
| -rw-r--r-- | src/registry.ts | 11 | ||||
| -rw-r--r-- | src/schemas/common.ts | 11 | ||||
| -rw-r--r-- | src/util/tool.ts | 158 | ||||
| -rw-r--r-- | src/util/truncate.ts | 35 |
4 files changed, 215 insertions, 0 deletions
diff --git a/src/registry.ts b/src/registry.ts new file mode 100644 index 0000000..014ba51 --- /dev/null +++ b/src/registry.ts @@ -0,0 +1,11 @@ +import type { ExtensionAPI } from "@mariozechner/pi-coding-agent"; +import type { BrowserClient } from "./client"; +import { type AnyBrowserToolDefinition, registerBrowserTool } from "./util/tool"; + +const TOOLS: ReadonlyArray<AnyBrowserToolDefinition> = [ + // Domain tools registered as they're migrated, one per domain file. +]; + +export const registerAllTools = (pi: ExtensionAPI, client: BrowserClient): void => { + for (const t of TOOLS) registerBrowserTool(pi, client, t); +}; diff --git a/src/schemas/common.ts b/src/schemas/common.ts new file mode 100644 index 0000000..00323c2 --- /dev/null +++ b/src/schemas/common.ts @@ -0,0 +1,11 @@ +import { Type } from "typebox"; + +export const Coords = Type.Object({ + x: Type.Number({ description: "X coordinate in CSS pixels from left edge of viewport" }), + y: Type.Number({ description: "Y coordinate in CSS pixels from top edge of viewport" }), +}); + +export const MouseButton = Type.Union( + [Type.Literal("left"), Type.Literal("right"), Type.Literal("middle")], + { description: 'Mouse button: "left", "right", or "middle"' }, +); diff --git a/src/util/tool.ts b/src/util/tool.ts new file mode 100644 index 0000000..fc1c5e2 --- /dev/null +++ b/src/util/tool.ts @@ -0,0 +1,158 @@ +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<Record<string, unknown>>; +}; + +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<Record<string, unknown>>; +}; + +export type HandlerContext = { + readonly client: BrowserClient; + readonly signal: AbortSignal | undefined; + readonly onUpdate: (update: ToolOk) => void; + readonly extensionCtx: ExtensionContext; +}; + +export type ToolHandler<S extends TSchema> = ( + args: Static<S>, + ctx: HandlerContext, +) => Promise<Result<ToolOk, ToolErr>>; + +export type BrowserToolDefinition<S extends TSchema> = { + readonly name: string; + readonly label: string; + readonly description: string; + readonly promptSnippet: string; + readonly promptGuidelines: ReadonlyArray<string>; + readonly parameters: S; + readonly handler: ToolHandler<S>; + readonly renderCall?: (args: Static<S>, theme: Theme) => Component; + readonly renderResult?: (result: AgentToolResult<unknown>, 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<S> binding. +export type AnyBrowserToolDefinition = BrowserToolDefinition<TSchema>; + +export const defineBrowserTool = <S extends TSchema>( + def: BrowserToolDefinition<S>, +): BrowserToolDefinition<S> => def; + +type OkDetails = { readonly ok: true } & Readonly<Record<string, unknown>>; +type ErrDetails = { readonly ok: false; readonly kind: ToolErrKind; readonly message: string } & Readonly<Record<string, unknown>>; + +const toToolResult = ( + r: Result<ToolOk, ToolErr>, + toolName: string, +): AgentToolResult<OkDetails | ErrDetails> => { + 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<T> 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 = <S extends TSchema>( + pi: ExtensionAPI, + client: BrowserClient, + def: BrowserToolDefinition<S>, +): void => { + const td: ToolDefinition<S> = { + name: def.name, + label: def.label, + description: def.description, + promptSnippet: def.promptSnippet, + promptGuidelines: [...def.promptGuidelines], + parameters: def.parameters, + ...(def.renderCall + ? { + renderCall: (args: Static<S>, theme: Theme, _ctx: Parameters<NonNullable<ToolDefinition<S>["renderCall"]>>[2]) => + // Non-null assertion safe: this branch only runs when renderCall was provided. + def.renderCall!(args, theme), + } + : {}), + ...(def.renderResult + ? { + renderResult: ( + result: AgentToolResult<unknown>, + options: ToolRenderResultOptions, + theme: Theme, + _ctx: Parameters<NonNullable<ToolDefinition<S>["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<TDetails>; we use unknown for TDetails. + const update: AgentToolResult<OkDetails> = { + content: [{ type: "text", text: u.text }], + details: { ok: true, ...(u.details ?? {}) }, + }; + // Cast needed: onUpdate is AgentToolUpdateCallback<unknown> (generic TDetails). + (onUpdate as AgentToolUpdateCallback<OkDetails>)(update); + } + }, + extensionCtx, + }); + return toToolResult(result, def.name); + }, + }; + pi.registerTool(td); +}; diff --git a/src/util/truncate.ts b/src/util/truncate.ts new file mode 100644 index 0000000..2bf0600 --- /dev/null +++ b/src/util/truncate.ts @@ -0,0 +1,35 @@ +import { mkdtemp, rm, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { + DEFAULT_MAX_BYTES, + DEFAULT_MAX_LINES, + formatSize, + truncateHead, + withFileMutationQueue, +} from "@mariozechner/pi-coding-agent"; + +const tempDirs: string[] = []; + +export const cleanupTempDirs = async (): Promise<void> => { + const dirs = tempDirs.splice(0); + await Promise.all(dirs.map((d) => rm(d, { recursive: true, force: true }).catch(() => {}))); +}; + +export type TruncatedOutput = { + readonly text: string; + readonly fullOutputPath?: string; + readonly wasTruncated: boolean; +}; + +export const applyTruncation = async (output: string, prefix: string): Promise<TruncatedOutput> => { + const t = truncateHead(output, { maxLines: DEFAULT_MAX_LINES, maxBytes: DEFAULT_MAX_BYTES }); + if (!t.truncated) return { text: t.content, wasTruncated: false }; + const dir = await mkdtemp(join(tmpdir(), `pi-bh-${prefix}-`)); + tempDirs.push(dir); + const file = join(dir, "output.txt"); + await withFileMutationQueue(file, async () => { await writeFile(file, output, "utf8"); }); + const omitted = t.totalBytes - t.outputBytes; + const text = `${t.content}\n\n[Output truncated: ${t.outputLines} of ${t.totalLines} lines (${formatSize(t.outputBytes)} of ${formatSize(t.totalBytes)}). ${formatSize(omitted)} omitted. Full output: ${file}]`; + return { text, fullOutputPath: file, wasTruncated: true }; +}; |
