import type { ContentBlock } from "@agentclientprotocol/sdk"; import * as directAcp from "./acpApi"; import * as sessionTracker from "./acpSessionTracker"; import { setActiveMessageId, clearActiveMessageId, } from "./acpNotificationHandler"; import { searchSessionsViaExports } from "./sessionSearch"; import { perfLog } from "@/shared/lib/perfLog"; export interface AcpProvider { id: string; label: string; } export interface AcpSendMessageOptions { systemPrompt?: string; personaId?: string; personaName?: string; /** Image attachments as [base64Data, mimeType] pairs. */ images?: [string, string][]; } export interface AcpPrepareSessionOptions { personaId?: string; } /** Discover ACP providers installed on the system. */ export async function discoverAcpProviders(): Promise { return directAcp.listProviders(); } /** Send a message to an ACP agent. Response streams via Tauri events. */ export async function acpSendMessage( sessionId: string, prompt: string, options: AcpSendMessageOptions = {}, ): Promise { const { systemPrompt, personaId, images } = options; const sid = sessionId.slice(0, 8); const tStart = performance.now(); const gooseSessionId = sessionTracker.getGooseSessionId(sessionId, personaId); if (!gooseSessionId) { throw new Error("Session not prepared. Call acpPrepareSession first."); } const hasSystem = systemPrompt && systemPrompt.trim().length > 0; const effectivePrompt = hasSystem ? `\n${systemPrompt}\n\n\n\n${prompt}\n` : prompt; const content: ContentBlock[] = [{ type: "text", text: effectivePrompt }]; if (images) { for (const [data, mimeType] of images) { content.push({ type: "image", data, mimeType } as ContentBlock); } } const messageId = crypto.randomUUID(); setActiveMessageId(gooseSessionId, messageId); perfLog( `[perf:send] ${sid} acpSendMessage → prompt(len=${prompt.length}, imgs=${images?.length ?? 0})`, ); const tPrompt = performance.now(); await directAcp.prompt(gooseSessionId, content); const tDone = performance.now(); perfLog( `[perf:send] ${sid} prompt() resolved in ${(tDone - tPrompt).toFixed(1)}ms (total acpSendMessage ${(tDone - tStart).toFixed(1)}ms)`, ); clearActiveMessageId(gooseSessionId); } /** Prepare or warm an ACP session ahead of the first prompt. */ export async function acpPrepareSession( sessionId: string, providerId: string, workingDir: string, options: AcpPrepareSessionOptions = {}, ): Promise { const sid = sessionId.slice(0, 8); const t0 = performance.now(); perfLog( `[perf:prepare] ${sid} acpPrepareSession start (provider=${providerId})`, ); await sessionTracker.prepareSession( sessionId, providerId, workingDir, options.personaId, ); perfLog( `[perf:prepare] ${sid} acpPrepareSession done in ${(performance.now() - t0).toFixed(1)}ms`, ); } export async function acpSetModel( sessionId: string, modelId: string, ): Promise { const gooseSessionId = sessionTracker.getGooseSessionId(sessionId); return directAcp.setModel(gooseSessionId ?? sessionId, modelId); } /** Session info returned by the goose binary's list_sessions. */ export interface AcpSessionInfo { sessionId: string; title: string | null; updatedAt: string | null; messageCount: number; } export interface AcpSessionSearchResult { sessionId: string; snippet: string; messageId: string; messageRole?: "user" | "assistant" | "system"; matchCount: number; } /** List all sessions known to the goose binary. */ export async function acpListSessions(): Promise { return directAcp.listSessions(); } export async function acpSearchSessions( query: string, sessionIds: string[], ): Promise { return searchSessionsViaExports(query, sessionIds); } /** * Load an existing session from the goose binary. * * This triggers message replay via SessionNotification events that the * notification handler picks up automatically. */ export async function acpLoadSession( sessionId: string, gooseSessionId: string, workingDir?: string, ): Promise { const effectiveWorkingDir = workingDir ?? "~/.goose/artifacts"; const sid = sessionId.slice(0, 8); const t0 = performance.now(); perfLog(`[perf:load] ${sid} acpLoadSession → client.loadSession`); await directAcp.loadSession(gooseSessionId, effectiveWorkingDir); perfLog( `[perf:load] ${sid} client.loadSession resolved in ${(performance.now() - t0).toFixed(1)}ms`, ); sessionTracker.registerSession( sessionId, gooseSessionId, "goose", effectiveWorkingDir, ); } /** Export a session as JSON via the goose binary. */ export async function acpExportSession(sessionId: string): Promise { return directAcp.exportSession(sessionId); } /** Import a session from JSON via the goose binary. Returns new session metadata. */ export async function acpImportSession(json: string): Promise { return directAcp.importSession(json); } /** Duplicate (fork) a session via the goose binary. Returns new session metadata. */ export async function acpDuplicateSession( sessionId: string, ): Promise { const gooseSessionId = sessionTracker.getGooseSessionId(sessionId) ?? sessionId; return directAcp.forkSession(gooseSessionId); } /** Cancel an in-progress ACP session so the backend stops streaming. */ export async function acpCancelSession( sessionId: string, personaId?: string, ): Promise { const gooseSessionId = sessionTracker.getGooseSessionId(sessionId, personaId); await directAcp.cancelSession(gooseSessionId ?? sessionId); return true; }