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()}
+ />
}
>