Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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(
<ProviderStatusRefreshButton refreshing={false} onRefresh={vi.fn()} />,
);

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(
<ProviderStatusRefreshButton refreshing onRefresh={vi.fn()} />,
);

expect(markup).toContain("Refreshing status");
expect(markup).toContain('aria-busy="true"');
expect(markup).toContain("animate-spin");
expect(markup).not.toContain("disabled=");
});
});
23 changes: 23 additions & 0 deletions apps/web/src/components/settings/ProviderStatusRefreshButton.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<Button size="sm" variant="outline" type="button" aria-busy={refreshing} onClick={onRefresh}>
<RefreshCwIcon
className={cn("size-3.5 transition-transform duration-500", refreshing && "animate-spin")}
/>
{refreshing ? "Refreshing status" : "Refresh status"}
</Button>
);
}
11 changes: 6 additions & 5 deletions apps/web/src/routes/_chat.settings.index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import {
ChevronDownIcon,
Loader2Icon,
PlusIcon,
RefreshCwIcon,
SkipForwardIcon,
XCircleIcon,
XIcon,
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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={
<Button size="sm" variant="outline" onClick={() => void refreshProviderStatuses()}>
<RefreshCwIcon className="size-3.5" />
Refresh status
</Button>
<ProviderStatusRefreshButton
refreshing={isRefreshingProviderStatuses}
onRefresh={() => void refreshProviderStatuses()}
/>
}
>
<SettingsRow
Expand Down
Loading