From 0bb87daefd3e8a789d9e82edab6f7bfde47dc058 Mon Sep 17 00:00:00 2001 From: Matt Toohey Date: Wed, 29 Apr 2026 15:49:50 +1000 Subject: [PATCH] perf: deduplicate _goose/providers/list RPC call at startup (#8873) Signed-off-by: Matt Toohey Co-authored-by: Claude Opus 4.6 (1M context) --- ui/goose2/src/app/hooks/useAppStartup.ts | 43 +++++++++++------------- ui/goose2/src/shared/api/acp.ts | 16 +++++++++ ui/goose2/src/shared/api/acpApi.ts | 36 ++++++++++++++------ 3 files changed, 60 insertions(+), 35 deletions(-) diff --git a/ui/goose2/src/app/hooks/useAppStartup.ts b/ui/goose2/src/app/hooks/useAppStartup.ts index e3e32fb774..00a435425f 100644 --- a/ui/goose2/src/app/hooks/useAppStartup.ts +++ b/ui/goose2/src/app/hooks/useAppStartup.ts @@ -2,6 +2,7 @@ import { useEffect } from "react"; import { useAgentStore } from "@/features/agents/stores/agentStore"; import { useChatSessionStore } from "@/features/chat/stores/chatSessionStore"; import { useProviderInventoryStore } from "@/features/providers/stores/providerInventoryStore"; +import { discoverAcpProvidersFromEntries } from "@/shared/api/acp"; import { setNotificationHandler, getClient } from "@/shared/api/acpConnection"; import notificationHandler from "@/shared/api/acpNotificationHandler"; import { perfLog } from "@/shared/lib/perfLog"; @@ -47,46 +48,41 @@ export function useAppStartup() { } }; - const loadProviders = async () => { + const loadProvidersAndInventory = async () => { const t0 = performance.now(); store.setProvidersLoading(true); - try { - const { discoverAcpProviders } = await import("@/shared/api/acp"); - const providers = await discoverAcpProviders(); - store.setProviders(providers); - perfLog( - `[perf:startup] loadProviders done in ${(performance.now() - t0).toFixed(1)}ms (n=${providers.length})`, - ); - } catch (err) { - console.error("Failed to load ACP providers on startup:", err); - } finally { - store.setProvidersLoading(false); - } - }; - - const loadProviderInventory = async () => { - const t0 = performance.now(); inventoryStore.setLoading(true); try { const { getProviderInventory } = await import( "@/features/providers/api/inventory" ); const entries = await getProviderInventory(); + + // Populate inventory store inventoryStore.setEntries(entries); + + // Derive ACP providers from the same response + const providers = discoverAcpProvidersFromEntries(entries); + store.setProviders(providers); + perfLog( - `[perf:startup] loadProviderInventory done in ${(performance.now() - t0).toFixed(1)}ms (n=${entries.length})`, + `[perf:startup] loadProvidersAndInventory done in ${(performance.now() - t0).toFixed(1)}ms (entries=${entries.length}, providers=${providers.length})`, ); return entries; } catch (err) { - console.error("Failed to load provider inventory on startup:", err); + console.error( + "Failed to load providers and inventory on startup:", + err, + ); return []; } finally { + store.setProvidersLoading(false); inventoryStore.setLoading(false); } }; const refreshConfiguredProviderInventory = async ( - initialEntries?: Awaited>, + initialEntries?: Awaited>, ) => { try { const entries = @@ -146,15 +142,14 @@ export function useAppStartup() { setActiveSession(null); }; - const inventoryLoad = loadProviderInventory(); + const providersAndInventoryLoad = loadProvidersAndInventory(); await Promise.allSettled([ loadPersonas(), - loadProviders(), - inventoryLoad, + providersAndInventoryLoad, loadSessionState(), ]); - void inventoryLoad.then((entries) => + void providersAndInventoryLoad.then((entries) => refreshConfiguredProviderInventory(entries), ); perfLog( diff --git a/ui/goose2/src/shared/api/acp.ts b/ui/goose2/src/shared/api/acp.ts index f43c8fad62..4576417821 100644 --- a/ui/goose2/src/shared/api/acp.ts +++ b/ui/goose2/src/shared/api/acp.ts @@ -39,6 +39,22 @@ export interface AcpCreateSessionOptions extends AcpPrepareSessionOptions { /** Discover ACP providers installed on the system. */ export async function discoverAcpProviders(): Promise { const providers = await directAcp.listProviders(); + return resolveProvidersCatalog(providers); +} + +/** + * Derive ACP providers from already-fetched inventory entries, + * avoiding a duplicate `_goose/providers/list` RPC. + */ +export function discoverAcpProvidersFromEntries( + entries: Array<{ providerId: string; providerName: string }>, +): AcpProvider[] { + return resolveProvidersCatalog( + directAcp.buildProviderListFromEntries(entries), + ); +} + +function resolveProvidersCatalog(providers: AcpProvider[]): AcpProvider[] { const seen = new Set(); return providers diff --git a/ui/goose2/src/shared/api/acpApi.ts b/ui/goose2/src/shared/api/acpApi.ts index 2df1cb8bd1..33e7d8480f 100644 --- a/ui/goose2/src/shared/api/acpApi.ts +++ b/ui/goose2/src/shared/api/acpApi.ts @@ -27,26 +27,40 @@ export interface AcpSessionInfo { personaId: string | null; } -const DEPRECATED_PROVIDER_IDS = new Set(["claude-code", "codex", "gemini-cli"]); -const DEFAULT_PROVIDER: AcpProvider = { +export const DEPRECATED_PROVIDER_IDS = new Set([ + "claude-code", + "codex", + "gemini-cli", +]); +export const DEFAULT_PROVIDER: AcpProvider = { id: "goose", label: "Goose (Default)", }; +/** + * Build the ACP provider list from raw inventory entries. + * + * Shared by both `listProviders` (which fetches entries via RPC) and + * `discoverAcpProvidersFromEntries` in acp.ts (which reuses + * already-fetched entries at startup). + */ +export function buildProviderListFromEntries( + entries: Array<{ providerId: string; providerName: string }>, +): AcpProvider[] { + return [ + DEFAULT_PROVIDER, + ...entries + .filter((entry) => !DEPRECATED_PROVIDER_IDS.has(entry.providerId)) + .map((entry) => ({ id: entry.providerId, label: entry.providerName })), + ]; +} + export async function listProviders(): Promise { const client = await getClient(); const result = await client.goose.GooseProvidersList({ providerIds: [], }); - - const providers = result.entries - .filter((entry) => !DEPRECATED_PROVIDER_IDS.has(entry.providerId)) - .map((entry) => ({ - id: entry.providerId, - label: entry.providerName, - })); - - return [DEFAULT_PROVIDER, ...providers]; + return buildProviderListFromEntries(result.entries); } export async function listSessions(): Promise {