diff --git a/apps/web/src/components/settings/ProviderStatusRefreshButton.test.tsx b/apps/web/src/components/settings/ProviderStatusRefreshButton.test.tsx new file mode 100644 index 00000000..2481cc93 --- /dev/null +++ b/apps/web/src/components/settings/ProviderStatusRefreshButton.test.tsx @@ -0,0 +1,28 @@ +import { renderToStaticMarkup } from "react-dom/server"; +import { describe, expect, it, vi } from "vitest"; + +import { ProviderStatusRefreshButton } from "./ProviderStatusRefreshButton"; + +describe("ProviderStatusRefreshButton", () => { + it("shows an idle refresh control without a busy indicator", () => { + const markup = renderToStaticMarkup( + , + ); + + expect(markup).toContain("Refresh status"); + expect(markup).toContain('aria-busy="false"'); + expect(markup).not.toContain("animate-spin"); + expect(markup).not.toContain("disabled="); + }); + + it("shows an animated busy state while refresh is in flight", () => { + const markup = renderToStaticMarkup( + , + ); + + expect(markup).toContain("Refreshing status"); + expect(markup).toContain('aria-busy="true"'); + expect(markup).toContain("animate-spin"); + expect(markup).not.toContain("disabled="); + }); +}); diff --git a/apps/web/src/components/settings/ProviderStatusRefreshButton.tsx b/apps/web/src/components/settings/ProviderStatusRefreshButton.tsx new file mode 100644 index 00000000..aae034f9 --- /dev/null +++ b/apps/web/src/components/settings/ProviderStatusRefreshButton.tsx @@ -0,0 +1,23 @@ +import { RefreshCwIcon } from "lucide-react"; + +import { cn } from "../../lib/utils"; +import { Button } from "../ui/button"; + +interface ProviderStatusRefreshButtonProps { + refreshing: boolean; + onRefresh: () => void; +} + +export function ProviderStatusRefreshButton({ + refreshing, + onRefresh, +}: ProviderStatusRefreshButtonProps) { + return ( + + ); +} diff --git a/apps/web/src/routes/_chat.settings.index.tsx b/apps/web/src/routes/_chat.settings.index.tsx index 657488a0..dbd3d6bc 100644 --- a/apps/web/src/routes/_chat.settings.index.tsx +++ b/apps/web/src/routes/_chat.settings.index.tsx @@ -5,7 +5,6 @@ import { ChevronDownIcon, Loader2Icon, PlusIcon, - RefreshCwIcon, SkipForwardIcon, XCircleIcon, XIcon, @@ -39,6 +38,7 @@ import { Button } from "../components/ui/button"; import { Collapsible, CollapsibleContent } from "../components/ui/collapsible"; import { EnvironmentVariablesEditor } from "../components/EnvironmentVariablesEditor"; import { HotkeysSettingsSection } from "../components/settings/HotkeysSettingsSection"; +import { ProviderStatusRefreshButton } from "../components/settings/ProviderStatusRefreshButton"; import { SettingsShell, type SettingsSectionId } from "../components/settings/SettingsShell"; import { useSettingsRouteContext } from "../components/settings/SettingsRouteContext"; import { @@ -471,6 +471,7 @@ function SettingsRouteView() { const keybindingsConfigPath = serverConfigQuery.data?.keybindingsConfigPath ?? null; const availableEditors = serverConfigQuery.data?.availableEditors; const providerStatuses = serverConfigQuery.data?.providers ?? []; + const isRefreshingProviderStatuses = serverConfigQuery.isFetching; const selectableProviders = getSelectableThreadProviders({ statuses: providerStatuses, openclawGatewayUrl: settings.openclawGatewayUrl, @@ -1330,10 +1331,10 @@ function SettingsRouteView() { title="Authentication" description="Only providers that are ready and authenticated enough to run will appear in the new-thread provider picker. Existing threads remain pinned to their current provider." actions={ - + void refreshProviderStatuses()} + /> } >