import type { RpcClient } from "../rpc/rpc-client.js"; export type ClientEvent = | { type: "ui_request"; ui_request: any } | { type: "text_delta"; text: string } | { type: "activity_delta"; text: string } | { type: "info"; [k: string]: any } | { type: "system_event"; [k: string]: any }; export type TurnState = "idle" | "running" | "waiting" | "failed"; export class SessionBridge { private state: TurnState = "idle"; private pending: any = null; // Pi (rpc-mode createDialogPromise) assegna a ogni ctx.ui.input un id RPC PROPRIO // (crypto.randomUUID) e correla extension_ui_response su quell'id — NON sull'id interno // del descriptor (che viaggia opaco nel `title`). Va memorizzato e rimandato indietro, // altrimenti Pi scarta la risposta e ctx.ui.input non si risolve mai (stuck senza output). private pendingPiId: string | null = null; private cbs = new Set<(e: ClientEvent) => void>(); constructor(private rpc: RpcClient) { rpc.on("event", (m) => { if (m.type === "extension_ui_request" && m.method === "input") { let descriptor: unknown; try { descriptor = JSON.parse(m.title as string); } catch { return; } this.pending = descriptor; this.pendingPiId = m.id as string | null; this.state = "waiting"; this.fan({ type: "ui_request", ui_request: descriptor }); } else if ( m.type === "message_end" && m.message?.role === "assistant" && m.message?.stopReason === "error" ) { this.state = "failed"; this.fan({ type: "info", level: "error", text: "Model request failed. Check provider connectivity, then Resume the session.", }); } else if (m.type === "extension_ui_request" && m.method === "notify") { this.fan({ type: "info", level: m.notifyType ?? "info", text: m.message ?? "" }); } else if (m.type === "message_update" && m.assistantMessageEvent?.type === "text_delta") { this.fan({ type: "text_delta", text: m.assistantMessageEvent.delta ?? "" }); } else if (m.type === "message_update" && m.assistantMessageEvent?.type === "thinking_delta") { this.fan({ type: "activity_delta", text: m.assistantMessageEvent.delta ?? "" }); } else if (m.type === "text_delta") { this.fan({ type: "text_delta", text: m.text ?? "" }); // tool_execution_* events are intentionally NOT forwarded: they clutter // the UI with raw bash/read output the user did not ask for. } else if (m.type === "system_event") { this.fan(m as ClientEvent); } else if (m.type === "agent_end") { if (this.state !== "failed" && !this.pending) this.state = "idle"; this.fan({ type: "system_event", event: "agent_end" }); } else if (m.type === "agent_start") { this.state = "running"; this.fan({ type: "system_event", event: "agent_start" }); } else if (m.type === "turn_end") { this.fan({ type: "system_event", event: "turn_end" }); } }); } private fan(e: ClientEvent) { for (const cb of this.cbs) cb(e); } turnState(): TurnState { return this.state; } beginTurn(): void { this.state = "running"; } onClientEvent(cb: (e: ClientEvent) => void): void { this.cbs.add(cb); } /** Eventi generati dal backend stesso (es. exit inatteso del child Pi), non da Pi. */ emitClientEvent(e: ClientEvent): void { this.fan(e); } respond(uiResponse: object & { id: string }): void { // Correla sull'id RPC di Pi; `value` porta l'uiResponse (con l'id del descriptor) cosi' // il check interno del gate (resp.id === descriptor.id) regge. const piId = this.pendingPiId ?? uiResponse.id; this.state = "running"; this.rpc.send({ type: "extension_ui_response", id: piId, value: JSON.stringify(uiResponse) }); if (this.pending && uiResponse.id === this.pending.id) { this.pending = null; this.pendingPiId = null; } } steer(text: string): void { this.state = "running"; this.rpc.send({ type: "steer", message: text }); } pendingWidget(): object | null { return this.pending; } }