diff --git a/desktop/src/app/RelayConnectionOverlay.tsx b/desktop/src/app/RelayConnectionOverlay.tsx
index 07c6933d04..246e1a1191 100644
--- a/desktop/src/app/RelayConnectionOverlay.tsx
+++ b/desktop/src/app/RelayConnectionOverlay.tsx
@@ -72,6 +72,7 @@ export function RelayConnectionOverlay({
>
- ) : isReconnectPending ? (
+ ) : isBusy ? (
) : (
@@ -93,7 +107,9 @@ export function SidebarRelayConnectionCompactCard({
className={className}
onAction={onReconnect}
onDismiss={onDismiss}
- role={isConnected ? "status" : "alert"}
+ // Recovering on its own is a status, not an alarm — only escalate to
+ // `alert` once the connection needs the user.
+ role={isConnected || isRetryingWithoutUser ? "status" : "alert"}
surface={surface}
testId={testId}
title={
@@ -101,7 +117,9 @@ export function SidebarRelayConnectionCompactCard({
? "Connected"
: isReconnectPending
? reconnectTitle
- : "Can't reach the relay"
+ : isRetryingWithoutUser
+ ? "Reconnecting"
+ : "Can't reach the relay"
}
tone={isConnected ? "success" : "neutral"}
/>
diff --git a/desktop/src/features/sidebar/ui/useSidebarRelayConnectionCard.ts b/desktop/src/features/sidebar/ui/useSidebarRelayConnectionCard.ts
index 11f2bd1064..3f59f0979a 100644
--- a/desktop/src/features/sidebar/ui/useSidebarRelayConnectionCard.ts
+++ b/desktop/src/features/sidebar/ui/useSidebarRelayConnectionCard.ts
@@ -234,6 +234,9 @@ export function useSidebarRelayConnectionCard(
return {
hasRelayUnreachableError,
+ // The relay client's own backoff loop is mid-retry. The card uses this to
+ // say so instead of implying the user has to click something.
+ isRelayAutoReconnecting: relayConnectionState === "reconnecting",
isRelayConnectionSuccess,
isRelayReconnectPending,
isWaitingOnReconnectHook,
diff --git a/desktop/tests/e2e/relay-connectivity.spec.ts b/desktop/tests/e2e/relay-connectivity.spec.ts
index dbe2b5d77c..9972dfe3a6 100644
--- a/desktop/tests/e2e/relay-connectivity.spec.ts
+++ b/desktop/tests/e2e/relay-connectivity.spec.ts
@@ -1,5 +1,6 @@
import { expect, test } from "@playwright/test";
+import { waitForAnimations } from "../helpers/animations";
import { installMockBridge } from "../helpers/bridge";
const RELAY_UNREACHABLE = "relay unreachable: connection refused";
@@ -13,15 +14,12 @@ const MOCK_PUBKEY = "deadbeef".repeat(8);
const MOCK_RELAY_URL = "ws://localhost:3000";
const SELF_PROFILE_CACHE_KEY = `buzz-self-profile.v1:${MOCK_RELAY_URL}:${MOCK_PUBKEY}`;
-async function settle(page: import("@playwright/test").Page) {
- await page.evaluate(() =>
- // Tolerate cancelled animations: a SkeletonReveal animation cancelled
- // mid-flight (skeleton → live content swap) rejects `.finished` with an
- // AbortError. allSettled lets the animations that DO finish settle instead
- // of aborting the whole wait on the first cancel.
- Promise.allSettled(document.getAnimations().map((a) => a.finished)),
- );
-}
+// Defer to the shared bounded helper. The degraded relay card now renders a
+// looping spinner while the client auto-reconnects, and a looping animation's
+// `.finished` never resolves — an unbounded wait here hangs until Playwright
+// aborts the evaluate. `waitForAnimations` races the settle against a ceiling
+// and also tolerates animations cancelled mid-flight.
+const settle = waitForAnimations;
type ConnectionState =
| "idle"
@@ -103,8 +101,11 @@ test.describe("relay connectivity", () => {
await expect(relayCard).toBeVisible({
timeout: 5_000,
});
- await expect(relayCard).toContainText("Can't reach the relay");
- await expect(relayCard).toContainText("Click to connect");
+ // The backoff loop is retrying on its own — the card reports that rather
+ // than asking the user to click.
+ await expect(relayCard).toContainText("Reconnecting");
+ await expect(relayCard).toContainText("Trying to restore the connection");
+ await expect(relayCard).not.toContainText("Click to connect");
await expect(page.getByTestId("sidebar-reconnect")).toBeVisible();
await settle(page);
@@ -124,8 +125,9 @@ test.describe("relay connectivity", () => {
await expect(relayCard).toBeVisible({
timeout: 5_000,
});
- await expect(relayCard).toContainText("Can't reach the relay");
- await expect(relayCard).toContainText("Click to connect");
+ await expect(relayCard).toContainText("Reconnecting");
+ await expect(relayCard).toContainText("Trying to restore the connection");
+ await expect(relayCard).not.toContainText("Click to connect");
await expect(page.getByTestId("sidebar-reconnect")).toBeVisible();
await settle(page);
@@ -218,13 +220,15 @@ test.describe("relay connectivity", () => {
await expect(relayCard).toBeVisible({
timeout: 5_000,
});
- await expect(relayCard).toContainText("Can't reach the relay");
- await expect(relayCard).toContainText("Click to connect");
+ await expect(relayCard).toContainText("Reconnecting");
+ await expect(relayCard).toContainText("Trying to restore the connection");
await driveConnectionDegraded(page, "connected");
await expect(relayCard).toContainText("Connected");
- await expect(relayCard).not.toContainText("Click to connect");
+ await expect(relayCard).not.toContainText(
+ "Trying to restore the connection",
+ );
await page.waitForTimeout(3_000);
await expect(relayCard).toContainText("Connected");
await expect(relayCard).toBeHidden({