import type { AgentToolResult } from "@earendil-works/pi-agent-core"; import type { ExtensionContext, ToolDefinition } from "@earendil-works/pi-coding-agent"; import { proxyTeammateChildTool, type TeammateChildToolBrokerRequest, type TeammateChildToolResult, } from "pi-maestro-teammate/v1/child-extensions"; import { createBrowserTool, type BrowserToolDetails } from "../tools/browser-tool.ts"; import { BrowserManager, type BrowserManagerLike, type BrowserManagerStatus, type BrowserOpenOptions, type BrowserPickCaptureCallback, type BrowserPickResult, type BrowserRunOutput, type BrowserTabInfo, type BrowserVideoInfo, type BrowserVideoOptions, type BrowserVideoChapterOptions, type BrowserVideoActionOptions, } from "../tools/browser/manager.ts"; import type { PairingApproval } from "../tools/browser/bridge-server.ts"; class ScopedTeammateBrowserManager implements BrowserManagerLike { readonly #names = new Set(); constructor( readonly actorId: string, readonly manager: BrowserManagerLike, ) {} async open(options: BrowserOpenOptions): Promise { const physicalName = this.#physicalName(options.name); this.#names.add(physicalName); try { const info = await this.manager.open({ ...options, name: physicalName }); return { ...info, name: options.name }; } catch (error) { this.#names.delete(physicalName); throw error; } } run( name: string, code: string, cwd: string, signal: AbortSignal | undefined, timeoutMs: number, ): Promise { return this.manager.run(this.#physicalName(name), code, cwd, signal, timeoutMs); } pick( name: string, onCapture: BrowserPickCaptureCallback | undefined, signal: AbortSignal | undefined, timeoutMs: number, ): Promise { return this.manager.pick(this.#physicalName(name), onCapture, signal, timeoutMs); } startVideo(name: string, cwd: string, options: BrowserVideoOptions, signal: AbortSignal | undefined, timeoutMs: number): Promise { return this.manager.startVideo(this.#physicalName(name), cwd, options, signal, timeoutMs); } stopVideo(name: string, signal: AbortSignal | undefined, timeoutMs: number): Promise { return this.manager.stopVideo(this.#physicalName(name), signal, timeoutMs); } videoChapter(name: string, options: BrowserVideoChapterOptions, signal: AbortSignal | undefined, timeoutMs: number): Promise { return this.manager.videoChapter(this.#physicalName(name), options, signal, timeoutMs); } videoShowActions(name: string, options: BrowserVideoActionOptions, signal: AbortSignal | undefined, timeoutMs: number): Promise { return this.manager.videoShowActions(this.#physicalName(name), options, signal, timeoutMs); } videoHideActions(name: string, signal: AbortSignal | undefined, timeoutMs: number): Promise { return this.manager.videoHideActions(this.#physicalName(name), signal, timeoutMs); } async status(signal?: AbortSignal): Promise { const prefix = `teammate:${this.actorId}:`; const status = await this.manager.status(signal); return { ...status, namedTabs: status.namedTabs .filter((tab) => this.#names.has(tab.name)) .map((tab) => ({ ...tab, name: tab.name.slice(prefix.length) })), }; } pair(requestId: string, code: string, signal?: AbortSignal): Promise { return this.manager.pair(requestId, code, signal); } async close(name: string): Promise { const physicalName = this.#physicalName(name); if (!this.#names.delete(physicalName)) return false; return this.manager.close(physicalName); } async closeAll(): Promise { const names = [...this.#names]; this.#names.clear(); const closed = await Promise.all(names.map((name) => this.manager.close(name))); return closed.filter(Boolean).length; } #physicalName(name: string): string { return `teammate:${this.actorId}:${name}`; } } export class TeammateBrowserBroker { readonly #scopes = new Map(); constructor(readonly manager: BrowserManagerLike = new BrowserManager()) {} async execute( request: TeammateChildToolBrokerRequest, ctx: ExtensionContext, ): Promise { const actorId = request.actor.correlationId; if (!actorId || actorId === "unknown") { return { content: [{ type: "text", text: "Teammate browser request has no trusted correlation id." }], isError: true, }; } const scope = this.#scopes.get(actorId) ?? new ScopedTeammateBrowserManager(actorId, this.manager); this.#scopes.set(actorId, scope); const tool = createBrowserTool(scope); return await tool.execute( `teammate-browser:${actorId}`, request.input as never, request.signal ?? new AbortController().signal, undefined, ctx, ) as TeammateChildToolResult; } async closeActor(actorId: string): Promise { const scope = this.#scopes.get(actorId); if (!scope) return 0; this.#scopes.delete(actorId); return scope.closeAll(); } async closeAll(): Promise { const scopes = [...this.#scopes.values()]; this.#scopes.clear(); const counts = await Promise.all(scopes.map((scope) => scope.closeAll())); return counts.reduce((sum, count) => sum + count, 0); } } export function createTeammateChildBrowserTool(): ToolDefinition { const tool = createBrowserTool(); return { ...tool, async execute(_id, params, signal): Promise> { return proxyTeammateChildTool( "browser", params as unknown as Record, signal, ); }, } as ToolDefinition; }