diff --git a/packages/app/e2e/acp-provider-catalog.spec.ts b/packages/app/e2e/acp-provider-catalog.spec.ts new file mode 100644 index 000000000..9332bf5cc --- /dev/null +++ b/packages/app/e2e/acp-provider-catalog.spec.ts @@ -0,0 +1,33 @@ +import { test } from "./fixtures"; +import { gotoAppShell, openSettings } from "./helpers/app"; +import { + expectProviderInstalledInSettings, + installAcpCatalogProvider, + openAddProviderModal, + openSettingsHost, +} from "./helpers/settings"; + +const ACP_PROVIDER = { + id: "hermes", + name: "Hermes", +}; + +function getSeededServerId(): string { + const serverId = process.env.E2E_SERVER_ID; + if (!serverId) { + throw new Error("E2E_SERVER_ID is not set (expected from Playwright globalSetup)."); + } + return serverId; +} + +test.describe("ACP provider catalog", () => { + test("adds a catalog provider from settings", async ({ page }) => { + await gotoAppShell(page); + await openSettings(page); + await openSettingsHost(page, getSeededServerId()); + await openAddProviderModal(page); + + await installAcpCatalogProvider(page, ACP_PROVIDER.name); + await expectProviderInstalledInSettings(page, ACP_PROVIDER.name); + }); +}); diff --git a/packages/app/e2e/helpers/settings.ts b/packages/app/e2e/helpers/settings.ts index 5ef01b068..aa16c1a25 100644 --- a/packages/app/e2e/helpers/settings.ts +++ b/packages/app/e2e/helpers/settings.ts @@ -204,6 +204,41 @@ export async function expectHostActionCards(page: Page): Promise { await expect(page.getByTestId("host-page-remove-host-button")).toBeVisible(); } +export async function serveJson(page: Page, url: string, body: unknown): Promise { + await page.route(url, async (route) => { + await route.fulfill({ + status: 200, + contentType: "application/json", + body: JSON.stringify(body), + }); + }); +} + +export async function openAddProviderModal(page: Page): Promise { + await page.getByRole("button", { name: "Add provider", exact: true }).click(); + await expect(page.getByRole("textbox", { name: "Search providers" })).toBeVisible(); +} + +export async function findAcpCatalogProvider(page: Page, providerName: string): Promise { + await page.getByRole("textbox", { name: "Search providers" }).fill(providerName); + await expect(page.getByText(providerName, { exact: true })).toBeVisible(); +} + +export async function installAcpCatalogProvider(page: Page, providerName: string): Promise { + await findAcpCatalogProvider(page, providerName); + await page.getByRole("button", { name: "Add", exact: true }).click(); + await expect(page.getByRole("textbox", { name: "Search providers" })).toHaveCount(0); +} + +export async function expectProviderInstalledInSettings( + page: Page, + providerName: string, +): Promise { + await expect( + page.getByRole("button", { name: `${providerName} provider details`, exact: true }), + ).toBeVisible(); +} + export async function expectHostNoLocalOnlyRows(page: Page): Promise { await expect(page.getByTestId("host-page-pair-device-row")).toHaveCount(0); await expect(page.getByTestId("host-page-daemon-lifecycle-card")).toHaveCount(0); diff --git a/packages/app/src/assets/acp-provider-icons.ts b/packages/app/src/assets/acp-provider-icons.ts new file mode 100644 index 000000000..527a78e45 --- /dev/null +++ b/packages/app/src/assets/acp-provider-icons.ts @@ -0,0 +1,71 @@ +// Vendored ACP provider SVG assets. + +export const ACP_PROVIDER_ICON_SVGS = { + "agoragentic-acp": + '\n \n \n \n\n', + "amp-acp": + '\n\n\n\n\n\n', + auggie: + '\n \n \n\n', + autohand: + '\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n', + "claude-acp": + '\n \n\n', + cline: + '\n \n \n \n \n\n', + "codebuddy-code": + '\n \n \n \n\n', + "codex-acp": + '\n \n\n', + "cortex-code": + '\n \n\n', + "corust-agent": + '\n \n \n \n \n \n \n \n \n\n', + "crow-cli": + '\n \n\n', + cursor: + '\n \n\n', + deepagents: + '\n \n \n \n \n\n', + dimcode: + '\n\n\n', + dirac: + '\n \u03b4\n \n \n \n\n', + "factory-droid": + '\n', + "fast-agent": + '\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n\n', + gemini: + '\n \n\n', + "github-copilot-cli": + '\n\n\n\n\n', + "glm-acp-agent": + 'Z.ai\n', + goose: + '\n \n\n', + junie: + '\n \n \n \n\n', + kilo: '\n \n\n', + kimi: '\n \n\n', + "minion-code": + '\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n\n', + "mistral-vibe": + '\n\n\n\n\n\n\n\n\n\n\n\n', + nova: '\n \n \n \n \n \n \n \n \n\n', + opencode: + '\n\n\n', + "pi-acp": + '\n\n\n\n', + poolside: + '\n\n\n', + qoder: + '\n \n\n', + "qwen-code": + '\n', + sigit: + '\n\n\n\n\n\n\n\n\n\n\n\n\n', + stakpak: + '\n \n\n', + vtcode: + '\n \n VT\n\n', +} as const; diff --git a/packages/app/src/assets/acp-provider-icons/agoragentic-acp.svg b/packages/app/src/assets/acp-provider-icons/agoragentic-acp.svg new file mode 100644 index 000000000..b1372e683 --- /dev/null +++ b/packages/app/src/assets/acp-provider-icons/agoragentic-acp.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/packages/app/src/assets/acp-provider-icons/amp-acp.svg b/packages/app/src/assets/acp-provider-icons/amp-acp.svg new file mode 100644 index 000000000..314881aff --- /dev/null +++ b/packages/app/src/assets/acp-provider-icons/amp-acp.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/packages/app/src/assets/acp-provider-icons/auggie.svg b/packages/app/src/assets/acp-provider-icons/auggie.svg new file mode 100644 index 000000000..215107744 --- /dev/null +++ b/packages/app/src/assets/acp-provider-icons/auggie.svg @@ -0,0 +1,4 @@ + + + + diff --git a/packages/app/src/assets/acp-provider-icons/autohand.svg b/packages/app/src/assets/acp-provider-icons/autohand.svg new file mode 100644 index 000000000..f3bc983c4 --- /dev/null +++ b/packages/app/src/assets/acp-provider-icons/autohand.svg @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/app/src/assets/acp-provider-icons/claude-acp.svg b/packages/app/src/assets/acp-provider-icons/claude-acp.svg new file mode 100644 index 000000000..98dd82db1 --- /dev/null +++ b/packages/app/src/assets/acp-provider-icons/claude-acp.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/app/src/assets/acp-provider-icons/cline.svg b/packages/app/src/assets/acp-provider-icons/cline.svg new file mode 100644 index 000000000..aeeafbc61 --- /dev/null +++ b/packages/app/src/assets/acp-provider-icons/cline.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/packages/app/src/assets/acp-provider-icons/codebuddy-code.svg b/packages/app/src/assets/acp-provider-icons/codebuddy-code.svg new file mode 100644 index 000000000..735fd352a --- /dev/null +++ b/packages/app/src/assets/acp-provider-icons/codebuddy-code.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/packages/app/src/assets/acp-provider-icons/codex-acp.svg b/packages/app/src/assets/acp-provider-icons/codex-acp.svg new file mode 100644 index 000000000..42c78a06c --- /dev/null +++ b/packages/app/src/assets/acp-provider-icons/codex-acp.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/app/src/assets/acp-provider-icons/cortex-code.svg b/packages/app/src/assets/acp-provider-icons/cortex-code.svg new file mode 100644 index 000000000..28f87a258 --- /dev/null +++ b/packages/app/src/assets/acp-provider-icons/cortex-code.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/app/src/assets/acp-provider-icons/corust-agent.svg b/packages/app/src/assets/acp-provider-icons/corust-agent.svg new file mode 100644 index 000000000..9f30636cb --- /dev/null +++ b/packages/app/src/assets/acp-provider-icons/corust-agent.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/packages/app/src/assets/acp-provider-icons/crow-cli.svg b/packages/app/src/assets/acp-provider-icons/crow-cli.svg new file mode 100644 index 000000000..1169116cd --- /dev/null +++ b/packages/app/src/assets/acp-provider-icons/crow-cli.svg @@ -0,0 +1,9 @@ + + + diff --git a/packages/app/src/assets/acp-provider-icons/cursor.svg b/packages/app/src/assets/acp-provider-icons/cursor.svg new file mode 100644 index 000000000..4ca0c2501 --- /dev/null +++ b/packages/app/src/assets/acp-provider-icons/cursor.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/app/src/assets/acp-provider-icons/deepagents.svg b/packages/app/src/assets/acp-provider-icons/deepagents.svg new file mode 100644 index 000000000..abd818ec4 --- /dev/null +++ b/packages/app/src/assets/acp-provider-icons/deepagents.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/packages/app/src/assets/acp-provider-icons/dimcode.svg b/packages/app/src/assets/acp-provider-icons/dimcode.svg new file mode 100644 index 000000000..1fa31ce88 --- /dev/null +++ b/packages/app/src/assets/acp-provider-icons/dimcode.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/app/src/assets/acp-provider-icons/dirac.svg b/packages/app/src/assets/acp-provider-icons/dirac.svg new file mode 100644 index 000000000..f7b0114b1 --- /dev/null +++ b/packages/app/src/assets/acp-provider-icons/dirac.svg @@ -0,0 +1,6 @@ + + δ + + + + diff --git a/packages/app/src/assets/acp-provider-icons/factory-droid.svg b/packages/app/src/assets/acp-provider-icons/factory-droid.svg new file mode 100644 index 000000000..5c6fb8d1f --- /dev/null +++ b/packages/app/src/assets/acp-provider-icons/factory-droid.svg @@ -0,0 +1 @@ + diff --git a/packages/app/src/assets/acp-provider-icons/fast-agent.svg b/packages/app/src/assets/acp-provider-icons/fast-agent.svg new file mode 100644 index 000000000..a07fab288 --- /dev/null +++ b/packages/app/src/assets/acp-provider-icons/fast-agent.svg @@ -0,0 +1,293 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/app/src/assets/acp-provider-icons/gemini.svg b/packages/app/src/assets/acp-provider-icons/gemini.svg new file mode 100644 index 000000000..588d89c52 --- /dev/null +++ b/packages/app/src/assets/acp-provider-icons/gemini.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/app/src/assets/acp-provider-icons/github-copilot-cli.svg b/packages/app/src/assets/acp-provider-icons/github-copilot-cli.svg new file mode 100644 index 000000000..626d33bad --- /dev/null +++ b/packages/app/src/assets/acp-provider-icons/github-copilot-cli.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/packages/app/src/assets/acp-provider-icons/glm-acp-agent.svg b/packages/app/src/assets/acp-provider-icons/glm-acp-agent.svg new file mode 100644 index 000000000..d552d2a3d --- /dev/null +++ b/packages/app/src/assets/acp-provider-icons/glm-acp-agent.svg @@ -0,0 +1 @@ +Z.ai diff --git a/packages/app/src/assets/acp-provider-icons/goose.svg b/packages/app/src/assets/acp-provider-icons/goose.svg new file mode 100644 index 000000000..c49288542 --- /dev/null +++ b/packages/app/src/assets/acp-provider-icons/goose.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/app/src/assets/acp-provider-icons/junie.svg b/packages/app/src/assets/acp-provider-icons/junie.svg new file mode 100644 index 000000000..54ce5ff9a --- /dev/null +++ b/packages/app/src/assets/acp-provider-icons/junie.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/packages/app/src/assets/acp-provider-icons/kilo.svg b/packages/app/src/assets/acp-provider-icons/kilo.svg new file mode 100644 index 000000000..8af6e96f3 --- /dev/null +++ b/packages/app/src/assets/acp-provider-icons/kilo.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/app/src/assets/acp-provider-icons/kimi.svg b/packages/app/src/assets/acp-provider-icons/kimi.svg new file mode 100644 index 000000000..4f7547cf7 --- /dev/null +++ b/packages/app/src/assets/acp-provider-icons/kimi.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/app/src/assets/acp-provider-icons/minion-code.svg b/packages/app/src/assets/acp-provider-icons/minion-code.svg new file mode 100644 index 000000000..eb3d8eb31 --- /dev/null +++ b/packages/app/src/assets/acp-provider-icons/minion-code.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/packages/app/src/assets/acp-provider-icons/mistral-vibe.svg b/packages/app/src/assets/acp-provider-icons/mistral-vibe.svg new file mode 100644 index 000000000..b13631b96 --- /dev/null +++ b/packages/app/src/assets/acp-provider-icons/mistral-vibe.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/packages/app/src/assets/acp-provider-icons/nova.svg b/packages/app/src/assets/acp-provider-icons/nova.svg new file mode 100644 index 000000000..5e19f5887 --- /dev/null +++ b/packages/app/src/assets/acp-provider-icons/nova.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/packages/app/src/assets/acp-provider-icons/opencode.svg b/packages/app/src/assets/acp-provider-icons/opencode.svg new file mode 100644 index 000000000..a38d4cf5a --- /dev/null +++ b/packages/app/src/assets/acp-provider-icons/opencode.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/app/src/assets/acp-provider-icons/pi-acp.svg b/packages/app/src/assets/acp-provider-icons/pi-acp.svg new file mode 100644 index 000000000..68ea8fd7f --- /dev/null +++ b/packages/app/src/assets/acp-provider-icons/pi-acp.svg @@ -0,0 +1,4 @@ + + + + diff --git a/packages/app/src/assets/acp-provider-icons/poolside.svg b/packages/app/src/assets/acp-provider-icons/poolside.svg new file mode 100644 index 000000000..91de4c46d --- /dev/null +++ b/packages/app/src/assets/acp-provider-icons/poolside.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/app/src/assets/acp-provider-icons/qoder.svg b/packages/app/src/assets/acp-provider-icons/qoder.svg new file mode 100644 index 000000000..417d83693 --- /dev/null +++ b/packages/app/src/assets/acp-provider-icons/qoder.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/app/src/assets/acp-provider-icons/qwen-code.svg b/packages/app/src/assets/acp-provider-icons/qwen-code.svg new file mode 100644 index 000000000..6a33da557 --- /dev/null +++ b/packages/app/src/assets/acp-provider-icons/qwen-code.svg @@ -0,0 +1 @@ + diff --git a/packages/app/src/assets/acp-provider-icons/sigit.svg b/packages/app/src/assets/acp-provider-icons/sigit.svg new file mode 100644 index 000000000..334fc95cb --- /dev/null +++ b/packages/app/src/assets/acp-provider-icons/sigit.svg @@ -0,0 +1,98 @@ + + + + + + + + + + + + + diff --git a/packages/app/src/assets/acp-provider-icons/stakpak.svg b/packages/app/src/assets/acp-provider-icons/stakpak.svg new file mode 100644 index 000000000..64425076e --- /dev/null +++ b/packages/app/src/assets/acp-provider-icons/stakpak.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/app/src/assets/acp-provider-icons/vtcode.svg b/packages/app/src/assets/acp-provider-icons/vtcode.svg new file mode 100644 index 000000000..5ae616428 --- /dev/null +++ b/packages/app/src/assets/acp-provider-icons/vtcode.svg @@ -0,0 +1,4 @@ + + + VT + diff --git a/packages/app/src/components/add-provider-modal.tsx b/packages/app/src/components/add-provider-modal.tsx new file mode 100644 index 000000000..b5fc8b647 --- /dev/null +++ b/packages/app/src/components/add-provider-modal.tsx @@ -0,0 +1,327 @@ +import { useCallback, useMemo, useState } from "react"; +import { Alert, Pressable, Text, View } from "react-native"; +import { SvgXml } from "react-native-svg"; +import { StyleSheet, withUnistyles } from "react-native-unistyles"; +import { ExternalLink, PackagePlus, Search } from "lucide-react-native"; +import { AdaptiveModalSheet, AdaptiveTextInput } from "@/components/adaptive-modal-sheet"; +import { Button } from "@/components/ui/button"; +import { + buildAcpProviderConfigPatch, + useAcpProviderCatalog, + type AcpProviderCatalogItem, +} from "@/hooks/use-acp-provider-catalog"; +import { useDaemonConfig } from "@/hooks/use-daemon-config"; +import { useProvidersSnapshot } from "@/hooks/use-providers-snapshot"; +import type { Theme } from "@/styles/theme"; +import { openExternalUrl } from "@/utils/open-external-url"; + +interface AddProviderModalProps { + serverId: string; + visible: boolean; + onClose: () => void; +} + +type InstallState = "installed" | "available"; + +const FLEX_ONE_STYLE = { flex: 1 } as const; +const ACTION_BUTTON_STYLE = { width: 92 } as const; +const MODAL_SNAP_POINTS = ["78%", "92%"]; +const SEARCH_ICON_SIZE = 16; +const PROVIDER_FALLBACK_ICON_SIZE = 20; +const PROVIDER_REMOTE_ICON_SIZE = 24; + +const ThemedPackagePlus = withUnistyles(PackagePlus); +const ThemedSvgXml = withUnistyles(SvgXml); +const ThemedSearch = withUnistyles(Search); +const ThemedExternalLink = withUnistyles(ExternalLink); + +const foregroundColorMapping = (theme: Theme) => ({ color: theme.colors.foreground }); +const foregroundMutedColorMapping = (theme: Theme) => ({ + color: theme.colors.foregroundMuted, +}); + +function getInstallState( + entry: AcpProviderCatalogItem, + installedProviderIds: Set, +): InstallState { + if (installedProviderIds.has(entry.id)) return "installed"; + return "available"; +} + +function matchesSearch(entry: AcpProviderCatalogItem, query: string): boolean { + const normalized = query.trim().toLowerCase(); + if (!normalized) return true; + return [entry.title, entry.id, entry.description].some((value) => + value.toLowerCase().includes(normalized), + ); +} + +interface ProviderCatalogRowProps { + entry: AcpProviderCatalogItem; + state: InstallState; + installing: boolean; + onInstall: (entry: AcpProviderCatalogItem) => void; +} + +function ProviderCatalogRow({ entry, state, installing, onInstall }: ProviderCatalogRowProps) { + const isAvailable = state === "available"; + let actionLabel = "Add"; + if (installing) { + actionLabel = "Adding"; + } else if (state === "installed") { + actionLabel = "Installed"; + } + + const handleInstall = useCallback(() => { + onInstall(entry); + }, [entry, onInstall]); + + const handleOpenInstallLink = useCallback(() => { + void openExternalUrl(entry.installLink); + }, [entry.installLink]); + + return ( + + + {entry.iconSvg ? ( + + ) : ( + + )} + + + + + {entry.title} + + + {entry.version} + + + + {entry.description || entry.id} + + + + Install instructions + + + + + + + ); +} + +export function AddProviderModal({ serverId, visible, onClose }: AddProviderModalProps) { + const { entries } = useAcpProviderCatalog(); + const { entries: providerEntries, refresh } = useProvidersSnapshot(serverId); + const { patchConfig } = useDaemonConfig(serverId); + const [search, setSearch] = useState(""); + const [installingProviderId, setInstallingProviderId] = useState(null); + + const installedProviderIds = useMemo( + () => new Set(providerEntries?.map((entry) => entry.provider) ?? []), + [providerEntries], + ); + const filteredEntries = useMemo( + () => entries.filter((entry) => matchesSearch(entry, search)), + [entries, search], + ); + + const handleInstall = useCallback( + async (entry: AcpProviderCatalogItem) => { + if (installingProviderId) return; + + setInstallingProviderId(entry.id); + try { + await patchConfig(buildAcpProviderConfigPatch(entry)); + await refresh([entry.id]); + onClose(); + } catch (installError) { + Alert.alert( + "Unable to install provider", + installError instanceof Error ? installError.message : String(installError), + ); + } finally { + setInstallingProviderId((current) => (current === entry.id ? null : current)); + } + }, + [installingProviderId, onClose, patchConfig, refresh], + ); + + return ( + + + + + + + + + {filteredEntries.length === 0 ? ( + + No providers found + + ) : null} + + {filteredEntries.length > 0 ? ( + + {filteredEntries.map((entry) => ( + + ))} + + ) : null} + + + + + + ); +} + +const styles = StyleSheet.create((theme) => ({ + searchField: { + flexDirection: "row", + alignItems: "center", + gap: theme.spacing[2], + backgroundColor: theme.colors.surface2, + borderRadius: theme.borderRadius.lg, + borderWidth: 1, + borderColor: theme.colors.border, + paddingHorizontal: theme.spacing[3], + }, + searchIcon: { + width: 18, + alignItems: "center", + }, + searchInput: { + flex: 1, + minWidth: 0, + paddingVertical: theme.spacing[3], + color: theme.colors.foreground, + fontSize: theme.fontSize.sm, + }, + list: { + borderRadius: theme.borderRadius.lg, + borderWidth: 1, + borderColor: theme.colors.border, + overflow: "hidden", + }, + row: { + flexDirection: "row", + alignItems: "center", + gap: theme.spacing[3], + padding: theme.spacing[3], + borderBottomWidth: 1, + borderBottomColor: theme.colors.border, + }, + iconFrame: { + width: 36, + height: 36, + borderRadius: theme.borderRadius.md, + backgroundColor: theme.colors.surface2, + alignItems: "center", + justifyContent: "center", + flexShrink: 0, + }, + textColumn: { + flex: 1, + minWidth: 0, + gap: theme.spacing[1], + }, + titleRow: { + flexDirection: "row", + alignItems: "center", + gap: theme.spacing[2], + minWidth: 0, + }, + name: { + color: theme.colors.foreground, + fontSize: theme.fontSize.sm, + fontWeight: theme.fontWeight.medium, + flexShrink: 1, + }, + version: { + color: theme.colors.foregroundMuted, + fontSize: theme.fontSize.xs, + flexShrink: 0, + }, + description: { + color: theme.colors.foregroundMuted, + fontSize: theme.fontSize.xs, + }, + installLink: { + alignSelf: "flex-start", + flexDirection: "row", + alignItems: "center", + gap: theme.spacing[1], + maxWidth: "100%", + }, + installLinkText: { + color: theme.colors.foregroundMuted, + fontSize: theme.fontSize.xs, + }, + stateBox: { + minHeight: 96, + borderRadius: theme.borderRadius.lg, + borderWidth: 1, + borderColor: theme.colors.border, + backgroundColor: theme.colors.surface1, + alignItems: "center", + justifyContent: "center", + gap: theme.spacing[3], + padding: theme.spacing[4], + }, + stateText: { + color: theme.colors.foregroundMuted, + fontSize: theme.fontSize.sm, + }, + actions: { + flexDirection: "row", + }, +})); diff --git a/packages/app/src/data/acp-provider-catalog.ts b/packages/app/src/data/acp-provider-catalog.ts new file mode 100644 index 000000000..74c819598 --- /dev/null +++ b/packages/app/src/data/acp-provider-catalog.ts @@ -0,0 +1,369 @@ +import { ACP_PROVIDER_ICON_SVGS } from "@/assets/acp-provider-icons"; + +export interface AcpProviderCatalogEntry { + id: string; + title: string; + description: string; + version: string; + iconSvg: string | null; + installLink: string; + command: readonly [string, ...string[]]; + env?: Readonly>; +} + +const CATALOG_DATA = [ + { + id: "agoragentic-acp", + title: "Agoragentic", + description: + "Agent marketplace with 174+ AI capabilities. Browse, invoke, and pay for agent services settled in USDC on Base L2.", + version: "1.3.0", + iconId: "agoragentic-acp", + installLink: "https://agoragentic.com", + command: ["npx", "-y", "agoragentic-mcp@1.3.0", "--acp"], + }, + { + id: "amp-acp", + title: "Amp", + description: "ACP wrapper for Amp - the frontier coding agent", + version: "0.7.0", + iconId: "amp-acp", + installLink: "https://github.com/tao12345666333/amp-acp", + command: ["amp-acp"], + }, + { + id: "auggie", + title: "Auggie CLI", + description: + "Augment Code's powerful software agent, backed by industry-leading context engine", + version: "0.26.0", + iconId: "auggie", + installLink: "https://www.augmentcode.com/", + command: ["npx", "-y", "@augmentcode/auggie@0.26.0", "--acp"], + env: { + AUGMENT_DISABLE_AUTO_UPDATE: "1", + }, + }, + { + id: "autohand", + title: "Autohand Code", + description: "Autohand Code - AI coding agent powered by Autohand AI", + version: "0.2.1", + iconId: "autohand", + installLink: "https://www.autohand.ai/cli/", + command: ["npx", "-y", "@autohandai/autohand-acp@0.2.1"], + }, + { + id: "claude-acp", + title: "Claude Agent", + description: "ACP wrapper for Anthropic's Claude", + version: "0.32.0", + iconId: "claude-acp", + installLink: "https://github.com/agentclientprotocol/claude-agent-acp", + command: ["npx", "-y", "@agentclientprotocol/claude-agent-acp@0.32.0"], + }, + { + id: "cline", + title: "Cline", + description: + "Autonomous coding agent CLI - capable of creating/editing files, running commands, using the browser, and more", + version: "2.18.0", + iconId: "cline", + installLink: "https://cline.bot/cli", + command: ["npx", "-y", "cline@2.18.0", "--acp"], + }, + { + id: "codebuddy-code", + title: "Codebuddy Code", + description: "Tencent Cloud's official intelligent coding tool", + version: "2.95.0", + iconId: "codebuddy-code", + installLink: "https://www.codebuddy.cn/cli/", + command: ["npx", "-y", "@tencent-ai/codebuddy-code@2.95.0", "--acp"], + }, + { + id: "codex-acp", + title: "Codex CLI", + description: "ACP adapter for OpenAI's coding assistant", + version: "0.13.0", + iconId: "codex-acp", + installLink: "https://github.com/zed-industries/codex-acp", + command: ["codex-acp"], + }, + { + id: "cortex-code", + title: "Cortex Code", + description: "Snowflake's Cortex Code coding agent", + version: "1.0.73", + iconId: "cortex-code", + installLink: "https://docs.snowflake.com/en/user-guide/cortex-code/cortex-code-cli", + command: ["cortex", "acp", "serve"], + }, + { + id: "corust-agent", + title: "Corust Agent", + description: "Co-building with a seasoned Rust partner.", + version: "0.5.1", + iconId: "corust-agent", + installLink: "https://github.com/Corust-ai/corust-agent-release/releases", + command: ["corust-agent-acp"], + }, + { + id: "crow-cli", + title: "crow-cli", + description: "Minimal ACP Native Coding Agent", + version: "0.1.23", + iconId: "crow-cli", + installLink: "https://crow-ai.dev/", + command: ["crow-cli", "acp"], + }, + { + id: "cursor", + title: "Cursor", + description: "Cursor's coding agent", + version: "2026.03.30", + iconId: "cursor", + installLink: "https://docs.cursor.com/en/cli/overview", + command: ["cursor-agent", "acp"], + }, + { + id: "deepagents", + title: "DeepAgents", + description: "Batteries-included AI coding and general purpose agent powered by LangChain.", + version: "0.1.7", + iconId: "deepagents", + installLink: "https://docs.langchain.com/oss/javascript/deepagents/overview", + command: ["npx", "-y", "deepagents-acp@0.1.7"], + }, + { + id: "dimcode", + title: "DimCode", + description: "A coding agent that puts leading models at your command.", + version: "0.0.66", + iconId: "dimcode", + installLink: "https://dimcode.dev/docs/acp.html", + command: ["npx", "-y", "dimcode@0.0.66", "acp"], + }, + { + id: "dirac", + title: "Dirac", + description: + "Reduces API costs by more than 50%, produces better and faster work. Uses Hash anchored parallel edits, AST manipulation and a whole lot of neat optimizations. Fully Open Source.", + version: "0.3.36", + iconId: "dirac", + installLink: "https://dirac.run", + command: ["npx", "-y", "dirac-cli@0.3.36", "--acp"], + }, + { + id: "factory-droid", + title: "Factory Droid", + description: "Factory Droid - AI coding agent powered by Factory AI", + version: "0.119.0", + iconId: "factory-droid", + installLink: "https://factory.ai/product/cli", + command: ["npx", "-y", "droid@0.119.0", "exec", "--output-format", "acp-daemon"], + env: { + DROID_DISABLE_AUTO_UPDATE: "true", + FACTORY_DROID_AUTO_UPDATE_ENABLED: "false", + }, + }, + { + id: "fast-agent", + title: "fast-agent", + description: "Code and build agents with comprehensive multi-provider support", + version: "0.7.0", + iconId: "fast-agent", + installLink: "https://fast-agent.ai/acp/", + command: ["uvx", "fast-agent-acp@latest", "-x"], + }, + { + id: "gemini", + title: "Gemini CLI", + description: "Google's official CLI for Gemini", + version: "0.41.1", + iconId: "gemini", + installLink: "https://geminicli.com", + command: ["npx", "-y", "@google/gemini-cli@0.41.1", "--acp"], + }, + { + id: "github-copilot-cli", + title: "GitHub Copilot", + description: "GitHub's AI pair programmer", + version: "1.0.41", + iconId: "github-copilot-cli", + installLink: "https://github.com/features/copilot/cli/", + command: ["npx", "-y", "@github/copilot@1.0.41", "--acp"], + }, + { + id: "glm-acp-agent", + title: "GLM Agent", + description: + "ACP agent powered by Zhipu AI's GLM Coding Plan models (glm-5.1, glm-5-turbo, glm-4.7, glm-4.5-air). Supports streaming, tool calls, mid-session model switching, image input via Z.AI Coding Plan Vision MCP, and session load/fork/resume with on-disk persistence.", + version: "1.1.2", + iconId: "glm-acp-agent", + installLink: "https://github.com/stefandevo/glm-acp-agent", + command: ["npx", "-y", "glm-acp-agent@1.1.2"], + }, + { + id: "goose", + title: "goose", + description: "A local, extensible, open source AI agent that automates engineering tasks", + version: "1.33.1", + iconId: "goose", + installLink: "https://block.github.io/goose/", + command: ["goose", "acp"], + }, + { + id: "hermes", + title: "Hermes", + description: "Nous Research self-improving AI agent", + version: "manual", + iconId: null, + installLink: "https://hermes-agent.nousresearch.com/docs/user-guide/features/acp", + command: ["hermes", "acp"], + }, + { + id: "junie", + title: "Junie", + description: "AI Coding Agent by JetBrains", + version: "1468.30.0", + iconId: "junie", + installLink: "https://junie.jetbrains.com/docs/junie-cli-acp.html", + command: ["junie", "--acp", "true"], + }, + { + id: "kilo", + title: "Kilo", + description: "The open source coding agent", + version: "7.2.40", + iconId: "kilo", + installLink: "https://kilo.ai/docs/code-with-ai/platforms/cli", + command: ["kilo", "acp"], + }, + { + id: "kimi", + title: "Kimi CLI", + description: "Moonshot AI's coding assistant", + version: "1.41.0", + iconId: "kimi", + installLink: "https://github.com/MoonshotAI/kimi-cli", + command: ["kimi", "acp"], + }, + { + id: "minion-code", + title: "Minion Code", + description: + "An enhanced AI code assistant built on the Minion framework with rich development tools", + version: "0.1.44", + iconId: "minion-code", + installLink: "https://github.com/femto/minion-code", + command: ["uvx", "minion-code@latest", "acp"], + }, + { + id: "mistral-vibe", + title: "Mistral Vibe", + description: "Mistral's open-source coding assistant", + version: "2.9.3", + iconId: "mistral-vibe", + installLink: "https://github.com/mistralai/mistral-vibe", + command: ["vibe-acp"], + }, + { + id: "nova", + title: "Nova", + description: "Nova by Compass AI - a fully-fledged software engineer at your command", + version: "1.1.4", + iconId: "nova", + installLink: "https://www.compassap.ai/portfolio/nova.html", + command: ["npx", "-y", "@compass-ai/nova@1.1.4", "acp"], + }, + { + id: "opencode", + title: "OpenCode", + description: "The open source coding agent", + version: "1.14.39", + iconId: "opencode", + installLink: "https://opencode.ai/docs/acp/", + command: ["opencode", "acp"], + }, + { + id: "pi-acp", + title: "pi ACP", + description: "ACP adapter for pi coding agent", + version: "0.0.26", + iconId: "pi-acp", + installLink: "https://github.com/svkozak/pi-acp", + command: ["npx", "-y", "pi-acp@0.0.26"], + }, + { + id: "poolside", + title: "Poolside", + description: "Poolside's coding agent", + version: "1.0.0", + iconId: "poolside", + installLink: "https://docs.poolside.ai/cli/pool", + command: ["pool", "acp"], + }, + { + id: "qoder", + title: "Qoder CLI", + description: "AI coding assistant with agentic capabilities", + version: "0.2.6", + iconId: "qoder", + installLink: "https://qoder.com", + command: ["npx", "-y", "@qoder-ai/qodercli@0.2.6", "--acp"], + }, + { + id: "qwen-code", + title: "Qwen Code", + description: "Alibaba's Qwen coding assistant", + version: "0.15.6", + iconId: "qwen-code", + installLink: "https://qwenlm.github.io/qwen-code-docs/en/users/overview", + command: ["npx", "-y", "@qwen-code/qwen-code@0.15.6", "--acp", "--experimental-skills"], + }, + { + id: "sigit", + title: "siGit Code", + description: + "Local-first coding agent. Runs entirely on your machine with optional on-device LLM inference via Onde.", + version: "1.0.3", + iconId: "sigit", + installLink: "https://github.com/getsigit/sigit", + command: ["sigit"], + }, + { + id: "stakpak", + title: "Stakpak", + description: "Open-source DevOps agent in Rust with enterprise-grade security", + version: "0.3.80", + iconId: "stakpak", + installLink: "https://stakpak.dev/", + command: ["stakpak", "acp"], + }, + { + id: "vtcode", + title: "VT Code", + description: + "An open-source coding agent with LLM-native code understanding and robust shell safety. Supports multiple LLM providers with automatic failover and efficient context management.", + version: "0.96.14", + iconId: "vtcode", + installLink: "https://github.com/vinhnx/VTCode/blob/main/docs/guides/zed-acp.md", + command: ["vtcode", "acp"], + env: { + VT_ACP_ENABLED: "1", + VT_ACP_ZED_ENABLED: "1", + }, + }, +] as const; + +export const ACP_PROVIDER_CATALOG: AcpProviderCatalogEntry[] = CATALOG_DATA.map((entry) => ({ + id: entry.id, + title: entry.title, + description: entry.description, + version: entry.version, + installLink: entry.installLink, + command: entry.command, + env: "env" in entry ? entry.env : undefined, + iconSvg: entry.iconId ? (ACP_PROVIDER_ICON_SVGS[entry.iconId] ?? null) : null, +})); diff --git a/packages/app/src/hooks/use-acp-provider-catalog.test.ts b/packages/app/src/hooks/use-acp-provider-catalog.test.ts new file mode 100644 index 000000000..d96fcc5bf --- /dev/null +++ b/packages/app/src/hooks/use-acp-provider-catalog.test.ts @@ -0,0 +1,66 @@ +import { describe, expect, it } from "vitest"; +import { ACP_PROVIDER_CATALOG } from "@/data/acp-provider-catalog"; +import { buildAcpProviderConfigPatch, getAcpProviderCatalog } from "./use-acp-provider-catalog"; + +function findProvider(id: string) { + const entry = getAcpProviderCatalog().find((provider) => provider.id === id); + if (!entry) { + throw new Error(`Missing ACP provider catalog entry: ${id}`); + } + return entry; +} + +describe("ACP provider catalog", () => { + it("vendors provider entries with unique ids and concrete commands", () => { + const ids = new Set(); + + for (const entry of ACP_PROVIDER_CATALOG) { + expect(ids.has(entry.id)).toBe(false); + ids.add(entry.id); + expect(entry.title).not.toBe(""); + expect(entry.description).not.toBe(""); + expect(entry.installLink).toMatch(/^https:\/\//); + expect(entry.command.length).toBeGreaterThan(0); + expect(entry.command[0]).not.toBe(""); + } + }); + + it("bundles SVG icons for catalog entries that declare an icon", () => { + const entriesWithIcons = ACP_PROVIDER_CATALOG.filter((entry) => entry.id !== "hermes"); + + expect(entriesWithIcons.length).toBeGreaterThan(0); + for (const entry of entriesWithIcons) { + expect(entry.iconSvg).toContain(" { + expect(findProvider("amp-acp").command).toEqual(["amp-acp"]); + expect(findProvider("cursor").command).toEqual(["cursor-agent", "acp"]); + expect(findProvider("goose").command).toEqual(["goose", "acp"]); + expect(findProvider("junie").command).toEqual(["junie", "--acp", "true"]); + expect(findProvider("poolside").command).toEqual(["pool", "acp"]); + }); + + it("maps a catalog entry to the daemon provider config patch", () => { + expect(buildAcpProviderConfigPatch(findProvider("amp-acp"))).toEqual({ + providers: { + "amp-acp": { + extends: "acp", + label: "Amp", + description: "ACP wrapper for Amp - the frontier coding agent", + command: ["amp-acp"], + env: {}, + }, + }, + }); + }); + + it("preserves provider env in the daemon config patch", () => { + const patch = buildAcpProviderConfigPatch(findProvider("auggie")); + + expect(patch.providers?.auggie?.env).toEqual({ + AUGMENT_DISABLE_AUTO_UPDATE: "1", + }); + }); +}); diff --git a/packages/app/src/hooks/use-acp-provider-catalog.ts b/packages/app/src/hooks/use-acp-provider-catalog.ts new file mode 100644 index 000000000..03ed02ea5 --- /dev/null +++ b/packages/app/src/hooks/use-acp-provider-catalog.ts @@ -0,0 +1,33 @@ +import { useCallback, useState } from "react"; +import type { MutableDaemonConfigPatch } from "@server/shared/messages"; +import { ACP_PROVIDER_CATALOG, type AcpProviderCatalogEntry } from "@/data/acp-provider-catalog"; + +export type AcpProviderCatalogItem = AcpProviderCatalogEntry; + +export function getAcpProviderCatalog(): AcpProviderCatalogItem[] { + return ACP_PROVIDER_CATALOG; +} + +export function buildAcpProviderConfigPatch( + entry: AcpProviderCatalogItem, +): MutableDaemonConfigPatch { + return { + providers: { + [entry.id]: { + extends: "acp", + label: entry.title, + description: entry.description, + command: [...entry.command], + env: entry.env ? { ...entry.env } : {}, + }, + }, + }; +} + +export function useAcpProviderCatalog() { + const [entries] = useState(ACP_PROVIDER_CATALOG); + + const refetch = useCallback(async () => entries, [entries]); + + return { entries, loading: false, error: null, refetch }; +} diff --git a/packages/app/src/screens/settings/providers-section.test.tsx b/packages/app/src/screens/settings/providers-section.test.tsx index 29b72979a..d6a696e95 100644 --- a/packages/app/src/screens/settings/providers-section.test.tsx +++ b/packages/app/src/screens/settings/providers-section.test.tsx @@ -95,6 +95,7 @@ vi.mock("lucide-react-native", () => { const icon = (name: string) => () => React.createElement("span", { "data-icon": name }); return { ChevronRight: icon("ChevronRight"), + Plus: icon("Plus"), RotateCw: icon("RotateCw"), }; }); @@ -144,6 +145,10 @@ vi.mock("@/components/provider-diagnostic-sheet", () => ({ }), })); +vi.mock("@/components/add-provider-modal", () => ({ + AddProviderModal: () => null, +})); + vi.mock("@/hooks/use-providers-snapshot", () => ({ useProvidersSnapshot: () => ({ entries: snapshotState.entries, @@ -323,7 +328,7 @@ describe("ProvidersSection", () => { expect(sheet?.getAttribute("data-provider")).toBe("codex"); }); - it("toggles the provider enabled flag through patchConfig when the switch is pressed", () => { + it("toggles the provider enabled flag through patchConfig when the switch is pressed", async () => { snapshotState.entries = [claudeEntry]; configState.config = makeConfig(); @@ -334,7 +339,7 @@ describe("ProvidersSection", () => { expect(switchEl).not.toBeNull(); expect(switchEl?.getAttribute("aria-checked")).toBe("true"); - act(() => { + await act(async () => { switchEl?.dispatchEvent(new window.MouseEvent("click", { bubbles: true })); }); diff --git a/packages/app/src/screens/settings/providers-section.tsx b/packages/app/src/screens/settings/providers-section.tsx index d4df7babb..bdf2f4551 100644 --- a/packages/app/src/screens/settings/providers-section.tsx +++ b/packages/app/src/screens/settings/providers-section.tsx @@ -6,12 +6,13 @@ import { useHostRuntimeIsConnected } from "@/runtime/host-runtime"; import { useProvidersSnapshot } from "@/hooks/use-providers-snapshot"; import { useDaemonConfig } from "@/hooks/use-daemon-config"; import { buildProviderDefinitions } from "@/utils/provider-definitions"; +import { AddProviderModal } from "@/components/add-provider-modal"; import { getProviderIcon } from "@/components/provider-icons"; import { ProviderDiagnosticSheet } from "@/components/provider-diagnostic-sheet"; import { LoadingSpinner } from "@/components/ui/loading-spinner"; import { Switch } from "@/components/ui/switch"; import { SettingsSection } from "@/screens/settings/settings-section"; -import { ChevronRight, RotateCw } from "lucide-react-native"; +import { ChevronRight, Plus, RotateCw } from "lucide-react-native"; type ProviderDefinition = ReturnType[number]; type ProviderEntry = NonNullable["entries"]>[number]; @@ -181,6 +182,7 @@ export function ProvidersSection({ serverId }: ProvidersSectionProps) { const { entries, isLoading, isRefreshing, refresh } = useProvidersSnapshot(serverId); const { patchConfig } = useDaemonConfig(serverId); const [diagnosticProvider, setDiagnosticProvider] = useState(null); + const [isAddProviderOpen, setIsAddProviderOpen] = useState(false); const [pendingProviderId, setPendingProviderId] = useState(null); const providerDefinitions = useMemo(() => buildProviderDefinitions(entries), [entries]); @@ -193,6 +195,8 @@ export function ProvidersSection({ serverId }: ProvidersSectionProps) { }, [refresh]); const handleCloseDiagnostic = useCallback(() => setDiagnosticProvider(null), []); + const handleOpenAddProvider = useCallback(() => setIsAddProviderOpen(true), []); + const handleCloseAddProvider = useCallback(() => setIsAddProviderOpen(false), []); const handleToggleEnabled = useCallback( async (providerId: string, enabled: boolean) => { setPendingProviderId(providerId); @@ -210,29 +214,43 @@ export function ProvidersSection({ serverId }: ProvidersSectionProps) { [patchConfig], ); - const refreshAction = useMemo( + const headerActions = useMemo( () => hasServer && isConnected ? ( - - {providerRefreshInFlight ? ( - - ) : ( - - )} - + + + + Add provider + + + {providerRefreshInFlight ? ( + + ) : ( + + )} + + ) : undefined, [ hasServer, isConnected, + handleOpenAddProvider, handleRefresh, providerRefreshInFlight, theme.iconSize.sm, @@ -244,7 +262,7 @@ export function ProvidersSection({ serverId }: ProvidersSectionProps) { <> @@ -288,6 +306,9 @@ export function ProvidersSection({ serverId }: ProvidersSectionProps) { serverId={serverId} /> ) : null} + {hasServer && isConnected && isAddProviderOpen ? ( + + ) : null} ); } @@ -304,6 +325,11 @@ const styles = StyleSheet.create((theme) => ({ color: theme.colors.foregroundMuted, fontSize: theme.fontSize.sm, }, + headerActions: { + flexDirection: "row", + alignItems: "center", + gap: theme.spacing[3], + }, row: { gap: theme.spacing[3], minHeight: 56, diff --git a/packages/server/src/server/daemon-config-store.test.ts b/packages/server/src/server/daemon-config-store.test.ts index bce53cf37..fe44969ae 100644 --- a/packages/server/src/server/daemon-config-store.test.ts +++ b/packages/server/src/server/daemon-config-store.test.ts @@ -18,7 +18,11 @@ describe("applyMutableProviderConfigToOverrides", () => { }, }, { - gemini: { enabled: false }, + gemini: { + enabled: false, + description: "Gemini ACP", + env: { GEMINI_AUTO_UPDATE: "0" }, + }, claude: { additionalModels: [ { @@ -33,7 +37,9 @@ describe("applyMutableProviderConfigToOverrides", () => { gemini: { extends: "acp", label: "Gemini", + description: "Gemini ACP", command: ["gemini", "--acp"], + env: { GEMINI_AUTO_UPDATE: "0" }, enabled: false, }, claude: { @@ -137,4 +143,39 @@ describe("DaemonConfigStore", () => { ], }); }); + + test("patch persists custom ACP provider overrides into config.json", () => { + const paseoHome = mkdtempSync(path.join(tmpdir(), "paseo-daemon-config-store-")); + tempDirs.push(paseoHome); + + const store = new DaemonConfigStore( + paseoHome, + { + mcp: { injectIntoAgents: false }, + providers: {}, + }, + undefined, + ); + + store.patch({ + providers: { + "paseo-e2e-acp": { + extends: "acp", + label: "Paseo E2E ACP", + description: "E2E ACP provider fixture", + command: ["npx", "-y", "--version"], + env: {}, + }, + }, + }); + + const persisted = loadPersistedConfig(paseoHome); + expect(persisted.agents?.providers?.["paseo-e2e-acp"]).toEqual({ + extends: "acp", + label: "Paseo E2E ACP", + description: "E2E ACP provider fixture", + command: ["npx", "-y", "--version"], + env: {}, + }); + }); }); diff --git a/packages/server/src/server/daemon-config-store.ts b/packages/server/src/server/daemon-config-store.ts index c5149c31d..fba3401df 100644 --- a/packages/server/src/server/daemon-config-store.ts +++ b/packages/server/src/server/daemon-config-store.ts @@ -3,6 +3,7 @@ import { savePersistedConfig, type PersistedConfig, } from "./persisted-config.js"; +import { ProviderOverrideSchema } from "./agent/provider-launch-config.js"; import { MutableDaemonConfigSchema, MutableDaemonConfigPatchSchema } from "../shared/messages.js"; export type { MutableDaemonConfig, MutableDaemonConfigPatch } from "../shared/messages.js"; @@ -70,13 +71,8 @@ export function applyMutableProviderConfigToOverrides( for (const [providerId, providerConfig] of Object.entries(mutableProviders ?? {})) { nextOverrides[providerId] = { ...nextOverrides[providerId], + ...ProviderOverrideSchema.strip().parse(providerConfig), }; - if (providerConfig.enabled !== undefined) { - nextOverrides[providerId].enabled = providerConfig.enabled; - } - if (providerConfig.additionalModels !== undefined) { - nextOverrides[providerId].additionalModels = providerConfig.additionalModels; - } } return nextOverrides;