Lifei/delete tauri backend acp (#8582)

Co-authored-by: Jack Amadeo <jackamadeo@squareup.com>
This commit is contained in:
Lifei Zhou
2026-04-17 02:46:37 +10:00
committed by GitHub
parent 67b90205ed
commit e7a91c7d4f
48 changed files with 289 additions and 12972 deletions
+49 -122
View File
@@ -1,5 +1,4 @@
import { invoke } from "@tauri-apps/api/core";
import { USE_DIRECT_ACP } from "./acpFeatureFlag";
import type { ContentBlock } from "@agentclientprotocol/sdk";
import * as directAcp from "./acpApi";
import * as sessionTracker from "./acpSessionTracker";
import {
@@ -15,7 +14,6 @@ export interface AcpProvider {
export interface AcpSendMessageOptions {
systemPrompt?: string;
workingDir?: string;
personaId?: string;
personaName?: string;
/** Image attachments as [base64Data, mimeType] pairs. */
@@ -29,63 +27,40 @@ export interface AcpPrepareSessionOptions {
/** Discover ACP providers installed on the system. */
export async function discoverAcpProviders(): Promise<AcpProvider[]> {
if (USE_DIRECT_ACP) {
return directAcp.listProviders();
}
return invoke("discover_acp_providers");
return directAcp.listProviders();
}
/** Send a message to an ACP agent. Response streams via Tauri events. */
export async function acpSendMessage(
sessionId: string,
providerId: string,
prompt: string,
options: AcpSendMessageOptions = {},
): Promise<void> {
if (USE_DIRECT_ACP) {
const { systemPrompt, personaId, images } = options;
const { systemPrompt, personaId, images } = options;
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
? `<persona-instructions>\n${systemPrompt}\n</persona-instructions>\n\n<user-message>\n${prompt}\n</user-message>`
: prompt;
const content: import("@agentclientprotocol/sdk").ContentBlock[] = [
{ type: "text", text: effectivePrompt },
];
if (images) {
for (const [data, mimeType] of images) {
content.push({ type: "image", data, mimeType } as any);
}
}
const messageId = crypto.randomUUID();
setActiveMessageId(gooseSessionId, messageId);
await directAcp.prompt(gooseSessionId, content);
clearActiveMessageId(gooseSessionId);
return;
const gooseSessionId = sessionTracker.getGooseSessionId(sessionId, personaId);
if (!gooseSessionId) {
throw new Error("Session not prepared. Call acpPrepareSession first.");
}
const { systemPrompt, workingDir, personaId, personaName, images } = options;
return invoke("acp_send_message", {
sessionId,
providerId,
prompt,
systemPrompt: systemPrompt ?? null,
workingDir: workingDir ?? null,
personaId: personaId ?? null,
personaName: personaName ?? null,
images: images ?? [],
});
const hasSystem = systemPrompt && systemPrompt.trim().length > 0;
const effectivePrompt = hasSystem
? `<persona-instructions>\n${systemPrompt}\n</persona-instructions>\n\n<user-message>\n${prompt}\n</user-message>`
: 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);
await directAcp.prompt(gooseSessionId, content);
clearActiveMessageId(gooseSessionId);
}
/** Prepare or warm an ACP session ahead of the first prompt. */
@@ -94,37 +69,21 @@ export async function acpPrepareSession(
providerId: string,
options: AcpPrepareSessionOptions = {},
): Promise<void> {
if (USE_DIRECT_ACP) {
const workingDir = options.workingDir ?? "~/.goose/artifacts";
await sessionTracker.prepareSession(
sessionId,
providerId,
workingDir,
options.personaId,
);
return;
}
const { workingDir, personaId } = options;
return invoke("acp_prepare_session", {
const workingDir = options.workingDir ?? "~/.goose/artifacts";
await sessionTracker.prepareSession(
sessionId,
providerId,
workingDir: workingDir ?? null,
personaId: personaId ?? null,
});
workingDir,
options.personaId,
);
}
export async function acpSetModel(
sessionId: string,
modelId: string,
): Promise<void> {
if (USE_DIRECT_ACP) {
const gooseSessionId = sessionTracker.getGooseSessionId(sessionId);
return directAcp.setModel(gooseSessionId ?? sessionId, modelId);
}
return invoke("acp_set_model", {
sessionId,
modelId,
});
const gooseSessionId = sessionTracker.getGooseSessionId(sessionId);
return directAcp.setModel(gooseSessionId ?? sessionId, modelId);
}
/** Session info returned by the goose binary's list_sessions. */
@@ -145,77 +104,54 @@ export interface AcpSessionSearchResult {
/** List all sessions known to the goose binary. */
export async function acpListSessions(): Promise<AcpSessionInfo[]> {
if (USE_DIRECT_ACP) {
return directAcp.listSessions();
}
return invoke("acp_list_sessions");
return directAcp.listSessions();
}
export async function acpSearchSessions(
query: string,
sessionIds: string[],
): Promise<AcpSessionSearchResult[]> {
if (USE_DIRECT_ACP) {
return searchSessionsViaExports(query, sessionIds);
}
return invoke("acp_search_sessions", { query, sessionIds });
return searchSessionsViaExports(query, sessionIds);
}
/**
* Load an existing session from the goose binary.
*
* This triggers message replay via SessionNotification events that the
* frontend's useAcpStream hook picks up automatically.
* notification handler picks up automatically.
*/
export async function acpLoadSession(
sessionId: string,
gooseSessionId: string,
workingDir?: string,
): Promise<void> {
if (USE_DIRECT_ACP) {
const effectiveWorkingDir = workingDir ?? "~/.goose/artifacts";
await directAcp.loadSession(gooseSessionId, effectiveWorkingDir);
sessionTracker.registerSession(
sessionId,
gooseSessionId,
"goose",
effectiveWorkingDir,
);
return;
}
return invoke("acp_load_session", {
const effectiveWorkingDir = workingDir ?? "~/.goose/artifacts";
await directAcp.loadSession(gooseSessionId, effectiveWorkingDir);
sessionTracker.registerSession(
sessionId,
gooseSessionId,
workingDir: workingDir ?? null,
});
"goose",
effectiveWorkingDir,
);
}
/** Export a session as JSON via the goose binary. */
export async function acpExportSession(sessionId: string): Promise<string> {
if (USE_DIRECT_ACP) {
return directAcp.exportSession(sessionId);
}
return invoke("acp_export_session", { sessionId });
return directAcp.exportSession(sessionId);
}
/** Import a session from JSON via the goose binary. Returns new session metadata. */
export async function acpImportSession(json: string): Promise<AcpSessionInfo> {
if (USE_DIRECT_ACP) {
return directAcp.importSession(json);
}
return invoke("acp_import_session", { json });
return directAcp.importSession(json);
}
/** Duplicate (fork) a session via the goose binary. Returns new session metadata. */
export async function acpDuplicateSession(
sessionId: string,
): Promise<AcpSessionInfo> {
if (USE_DIRECT_ACP) {
const gooseSessionId =
sessionTracker.getGooseSessionId(sessionId) ?? sessionId;
return directAcp.forkSession(gooseSessionId);
}
return invoke("acp_duplicate_session", { sessionId });
const gooseSessionId =
sessionTracker.getGooseSessionId(sessionId) ?? sessionId;
return directAcp.forkSession(gooseSessionId);
}
/** Cancel an in-progress ACP session so the backend stops streaming. */
@@ -223,16 +159,7 @@ export async function acpCancelSession(
sessionId: string,
personaId?: string,
): Promise<boolean> {
if (USE_DIRECT_ACP) {
const gooseSessionId = sessionTracker.getGooseSessionId(
sessionId,
personaId,
);
await directAcp.cancelSession(gooseSessionId ?? sessionId);
return true;
}
return invoke("acp_cancel_session", {
sessionId,
personaId: personaId ?? null,
});
const gooseSessionId = sessionTracker.getGooseSessionId(sessionId, personaId);
await directAcp.cancelSession(gooseSessionId ?? sessionId);
return true;
}
+20 -8
View File
@@ -23,28 +23,40 @@ const DEPRECATED_PROVIDER_IDS = new Set(["claude-code", "codex", "gemini-cli"]);
export async function listProviders(): Promise<AcpProvider[]> {
const client = await getClient();
const result = await client.goose.GooseProvidersList({});
// biome-ignore lint/suspicious/noExplicitAny: ACP SDK types don't expose providers field
return (result as any).providers
.filter((p: any) => !DEPRECATED_PROVIDER_IDS.has(p.id))
.map((p: any) => ({ id: p.id, label: p.label }));
.filter(
(p: { id: string; label: string }) => !DEPRECATED_PROVIDER_IDS.has(p.id),
)
.map((p: { id: string; label: string }) => ({ id: p.id, label: p.label }));
}
export async function listSessions(): Promise<AcpSessionInfo[]> {
const client = await getClient();
// GooseClient.unstable_listSessions doesn't work with SDK 0.19 (renamed to listSessions).
// Bypass GooseClient and call the connection directly. Fix when ui/acp is updated.
// biome-ignore lint/suspicious/noExplicitAny: SDK doesn't expose conn property
const conn = (client as any).conn;
const response = await conn.listSessions({});
return response.sessions.map((info: any) => ({
sessionId: info.sessionId,
title: info.title ?? null,
updatedAt: info.updatedAt ?? null,
messageCount: (info._meta?.messageCount as number) ?? 0,
}));
return response.sessions.map(
(info: {
sessionId: string;
title?: string;
updatedAt?: string;
_meta?: Record<string, unknown>;
}) => ({
sessionId: info.sessionId,
title: info.title ?? null,
updatedAt: info.updatedAt ?? null,
messageCount: (info._meta?.messageCount as number) ?? 0,
}),
);
}
export async function exportSession(sessionId: string): Promise<string> {
const client = await getClient();
const result = await client.goose.GooseSessionExport({ sessionId });
// biome-ignore lint/suspicious/noExplicitAny: SDK doesn't expose data field on export result
return (result as any).data;
}
+1 -1
View File
@@ -1,5 +1,5 @@
import { invoke } from "@tauri-apps/api/core";
import { GooseClient } from "@aaif/goose-acp";
import { GooseClient } from "@aaif/goose-sdk";
import {
PROTOCOL_VERSION,
type Client,
@@ -1 +0,0 @@
export const USE_DIRECT_ACP = true;
@@ -235,7 +235,7 @@ function handleLive(
...msg,
content: msg.content.map((c) =>
c.type === "toolRequest" && c.id === update.toolCallId
? { ...c, name: update.title! }
? { ...c, name: update.title ?? "" }
: c,
),
}));
@@ -310,7 +310,8 @@ function handleShared(sessionId: string, update: SessionUpdate): void {
};
if ("options" in configUpdate && Array.isArray(configUpdate.options)) {
const modelOption = configUpdate.options.find(
(opt: any) => opt.category === "model",
(opt: { category?: string; kind?: Record<string, unknown> }) =>
opt.category === "model",
);
if (modelOption?.kind?.type === "select") {
const select = modelOption.kind;
@@ -391,6 +392,7 @@ function findMessageWithToolCall(
}
function extractToolResultText(update: {
// biome-ignore lint/suspicious/noExplicitAny: ACP SDK ToolCallContent type is complex
content?: Array<any> | null;
rawOutput?: unknown;
}): string {
@@ -55,6 +55,7 @@ export function createWebSocketStream(wsUrl: string): Stream {
async pull(controller) {
await waitForMessage();
while (incoming.length > 0) {
// biome-ignore lint/style/noNonNullAssertion: length checked in while condition
controller.enqueue(incoming.shift()!);
}
if (closed && incoming.length === 0) {
+1 -1
View File
@@ -106,7 +106,7 @@ function searchSession(
};
}
function safeParse(json: string): Record<string, any> | null {
function safeParse(json: string): Record<string, unknown> | null {
try {
return JSON.parse(json);
} catch {
+3 -5
View File
@@ -1,8 +1,6 @@
// Provider types — these map to goose serve provider names.
// All sessions run through goose serve; the provider ID selects which
// backend provider goose uses for inference. The list is dynamic
// (fetched from the backend via discover_acp_providers) so this is a
// plain string rather than a narrow union.
// Provider types map to goose serve provider names.
// The provider list is dynamic, so this remains a plain string rather than
// a narrow union.
export type ProviderType = string;
export interface ProviderConfig {