/** * Tauri adapter for {@link AgentGateway} (L6). * * NOTE: The Tauri commands wired here (`list_agents`, `create_agent`, …) are * defined in the backend `app-tauri` crate and will be registered in a * subsequent lot. This adapter is complete on the frontend side; the mock * gateway covers tests and offline dev today. The real mode will work * transparently once the commands are registered. * * Commands use snake_case (Tauri convention); payload keys are camelCase * (matching the backend DTO `#[serde(rename_all = "camelCase")]`), consistent * with the other adapters in this directory. */ import { Channel, invoke } from "@tauri-apps/api/core"; import type { Agent, TerminalSession } from "@/domain"; import type { AgentGateway, ConversationDetails, CreateAgentInput, LiveAgent, OpenTerminalOptions, ReattachResult, TerminalHandle, } from "@/ports"; import { makeTerminalHandle } from "./terminal"; /** Wire shape returned by the `launch_agent` command (mirrors `open_terminal`). */ interface LaunchAgentResponse { sessionId: string; cwd: string; rows: number; cols: number; /** Conversation id minted by this launch (omitted when nothing was assigned). */ assignedConversationId?: string; } export class TauriAgentGateway implements AgentGateway { listAgents(projectId: string): Promise { return invoke("list_agents", { projectId }); } listLiveAgents(projectId: string): Promise { return invoke("list_live_agents", { projectId }); } attachLiveAgent( projectId: string, agentId: string, nodeId: string, ): Promise { return invoke("attach_live_agent", { request: { projectId, agentId, nodeId }, }).then((list) => { const attached = list[0]; if (!attached) { throw new Error(`running session for agent ${agentId} was not returned`); } return attached; }); } createAgent(projectId: string, input: CreateAgentInput): Promise { // The `create_agent` command takes a single `request` DTO; `projectId` must // live *inside* it (camelCase), not at the top level. return invoke("create_agent", { request: { projectId, name: input.name, profileId: input.profileId, initialContent: input.initialContent ?? null, }, }); } changeAgentProfile( projectId: string, agentId: string, profileId: string, rows: number, cols: number, ): Promise<{ agent: Agent; relaunchedSession?: TerminalSession }> { // `change_agent_profile` takes a single `request` DTO; the camelCase keys // match the backend `ChangeAgentProfileRequestDto`. The response carries the // mutated agent and, when a live session was hot-swapped, the relaunched one // (omitted otherwise — `relaunchedSession` stays `undefined`). return invoke<{ agent: Agent; relaunchedSession?: TerminalSession }>( "change_agent_profile", { request: { projectId, agentId, profileId, rows, cols } }, ); } readContext(projectId: string, agentId: string): Promise { return invoke("read_agent_context", { projectId, agentId }); } async updateContext( projectId: string, agentId: string, content: string, ): Promise { // `update_agent_context` takes a single `request` DTO. await invoke("update_agent_context", { request: { projectId, agentId, content }, }); } async deleteAgent(projectId: string, agentId: string): Promise { await invoke("delete_agent", { projectId, agentId }); } async launchAgent( projectId: string, agentId: string, options: OpenTerminalOptions, onData: (bytes: Uint8Array) => void, ): Promise { // Per-session output channel. The backend serialises chunks as byte arrays. const channel = new Channel(); channel.onmessage = (chunk) => onData(Uint8Array.from(chunk)); const res = await invoke("launch_agent", { request: { projectId, agentId, rows: options.rows, cols: options.cols, // Resume id: the leaf's persisted conversation id, when any (T4b). The // backend resumes it; absent ⇒ a fresh cell that may get a new id. conversationId: options.conversationId ?? null, // Hosting cell: lets the backend enforce one-live-session-per-agent. nodeId: options.nodeId ?? null, }, onOutput: channel, }); const handle = makeTerminalHandle(res.sessionId, channel); // Surface the id assigned by this launch so the caller persists it on the // leaf (`setCellConversation`) and resumes next time. return res.assignedConversationId ? { ...handle, assignedConversationId: res.assignedConversationId } : handle; } async reattach( sessionId: string, onData: (bytes: Uint8Array) => void, ): Promise { // Agent sessions reattach through the same session-based `reattach_terminal` // command as plain terminals (the PTY is identified by its session id). const channel = new Channel(); channel.onmessage = (chunk) => onData(Uint8Array.from(chunk)); const res = await invoke<{ sessionId: string; scrollback: number[] }>( "reattach_terminal", { sessionId, onOutput: channel }, ); return { handle: makeTerminalHandle(res.sessionId, channel), scrollback: Uint8Array.from(res.scrollback), }; } inspectConversation( projectId: string, agentId: string, conversationId: string, ): Promise { // `inspect_conversation` takes a single `request` DTO; absent fields are // omitted from the response (best-effort), so an empty object is valid. return invoke("inspect_conversation", { request: { projectId, agentId, conversationId }, }); } }