diff --git a/apps/web/src/components/settings/AcpSessionManagementSection.browser.test.tsx b/apps/web/src/components/settings/AcpSessionManagementSection.browser.test.tsx index dbca30bbdeb45..a61814ae3efa6 100644 --- a/apps/web/src/components/settings/AcpSessionManagementSection.browser.test.tsx +++ b/apps/web/src/components/settings/AcpSessionManagementSection.browser.test.tsx @@ -5,8 +5,10 @@ import { ProviderInstanceId, ThreadId, type ServerProvider, + type AcpRegistryConfigurableProvider, } from "@t3tools/contracts"; import { expect, it, vi } from "vite-plus/test"; +import { userEvent } from "vite-plus/test/browser"; import { act, render } from "../../../test/render"; @@ -169,3 +171,56 @@ it("rechecks task access after confirming deletion", async () => { await screen.click("Delete"); expect(commands.delete).not.toHaveBeenCalled(); }); + +it.each(["Save", "Disable"])("keeps other provider edits through a %s refetch", async (action) => { + setup(); + const providers: AcpRegistryConfigurableProvider[] = ["google", "other"].map((providerId) => ({ + providerId, + supported: ["openai"], + required: false, + current: { apiType: "openai", baseUrl: "https://api.example.test/v1" }, + })); + const response = { _tag: "Success" as const, value: { providers } }; + commands.listProviders.mockResolvedValue(response); + await using screen = await renderSection(); + await screen.click("List providers"); + const input = (label: string) => + screen.container.querySelector(`[aria-label="${label}"]`)!; + await userEvent.fill(input("other base URL"), "https://other.example.test/v1"); + await userEvent.fill(input("other write-only headers JSON"), '{"X-Test":"unsaved"}'); + await userEvent.fill(input("google write-only headers JSON"), '{"X-Test":"saved"}'); + + let resolveReload!: (value: typeof response) => void; + const reload = new Promise((resolve) => { + resolveReload = resolve; + }); + commands.listProviders.mockReturnValueOnce(reload); + let resolveMutation!: () => void; + const mutation = new Promise((resolve) => { + resolveMutation = resolve; + }); + const command = action === "Save" ? commands.setProvider : commands.disableProvider; + command.mockReturnValueOnce( + mutation.then(() => ({ + _tag: "Success", + value: action === "Save" ? { configured: true } : { disabled: true }, + })), + ); + await screen.click(action); + expect(screen.button("Refresh").disabled).toBe(true); + await act(async () => resolveMutation()); + for (const button of screen.container.querySelectorAll("button")) { + if (button.textContent === "Save" || button.textContent === "Disable") { + expect(button.disabled).toBe(true); + } + } + await userEvent.fill(input("other base URL"), "https://newer.example.test/v1"); + await act(async () => resolveReload(response)); + + expect(input("other base URL").value).toBe("https://newer.example.test/v1"); + expect(input("other write-only headers JSON").value).toBe('{"X-Test":"unsaved"}'); + expect(input("google write-only headers JSON").value).toBe(""); + await screen.click("Refresh"); + expect(input("other base URL").value).toBe("https://api.example.test/v1"); + expect(input("other write-only headers JSON").value).toBe(""); +}); diff --git a/apps/web/src/components/settings/AcpSessionManagementSection.tsx b/apps/web/src/components/settings/AcpSessionManagementSection.tsx index 6d7f298265367..675294497b118 100644 --- a/apps/web/src/components/settings/AcpSessionManagementSection.tsx +++ b/apps/web/src/components/settings/AcpSessionManagementSection.tsx @@ -211,7 +211,7 @@ export function AcpSessionManagementSection(props: { reportFailure("Could not delete ACP session", result); }; - const loadProviders = async () => { + const loadProviders = async (resetProviderId?: string) => { if (projectId === null || loadingProviders) return; setLoadingProviders(true); const result = await listProviders({ @@ -221,11 +221,13 @@ export function AcpSessionManagementSection(props: { setLoadingProviders(false); if (result._tag === "Success") { setProviders(result.value.providers); - setProviderDrafts( + setProviderDrafts((current) => Object.fromEntries( result.value.providers.map((provider) => [ provider.providerId, - { + (resetProviderId !== undefined && provider.providerId !== resetProviderId + ? current[provider.providerId] + : undefined) ?? { apiType: provider.current?.apiType ?? provider.supported[0] ?? "", baseUrl: provider.current?.baseUrl ?? "", headers: "", @@ -242,7 +244,8 @@ export function AcpSessionManagementSection(props: { if ( !readEnvironmentScope(props.environmentId, AuthProvidersManageScope) || projectId === null || - savingProviderId !== null + savingProviderId !== null || + loadingProviders ) return; const draft = providerDrafts[provider.providerId]; @@ -272,14 +275,15 @@ export function AcpSessionManagementSection(props: { setSavingProviderId(null); if (result._tag === "Success") { toastManager.add({ type: "success", title: "ACP provider configured" }); - await loadProviders(); + await loadProviders(provider.providerId); return; } reportFailure("Could not configure ACP provider", result); }; const disableConfiguredProvider = async (provider: AcpRegistryConfigurableProvider) => { - if (projectId === null || savingProviderId !== null || provider.required) return; + if (projectId === null || savingProviderId !== null || loadingProviders || provider.required) + return; const confirmed = await ensureLocalApi().dialogs.confirm( `Disable ACP provider "${provider.providerId}"?`, { variant: "destructive" }, @@ -293,7 +297,7 @@ export function AcpSessionManagementSection(props: { setSavingProviderId(null); if (result._tag === "Success") { toastManager.add({ type: "success", title: "ACP provider disabled" }); - await loadProviders(); + await loadProviders(provider.providerId); return; } reportFailure("Could not disable ACP provider", result); @@ -466,7 +470,9 @@ export function AcpSessionManagementSection(props: { type="button" size="xs" variant="outline" - disabled={!canRead || loadingProviders || projectId === null} + disabled={ + !canRead || loadingProviders || savingProviderId !== null || projectId === null + } onClick={() => void loadProviders()} > {loadingProviders ? "Loading" : providers.length === 0 ? "List providers" : "Refresh"} @@ -532,6 +538,7 @@ export function AcpSessionManagementSection(props: { disabled={ props.readOnly || savingProviderId !== null || + loadingProviders || draft.apiType.length === 0 || draft.baseUrl.length === 0 } @@ -544,7 +551,9 @@ export function AcpSessionManagementSection(props: { type="button" size="xs" variant="ghost-muted" - disabled={props.readOnly || savingProviderId !== null} + disabled={ + props.readOnly || savingProviderId !== null || loadingProviders + } onClick={() => void disableConfiguredProvider(provider)} > Disable