From aab8274b1a795b398c6324db38ee45e45a1f16a0 Mon Sep 17 00:00:00 2001 From: Zoltan Papp Date: Wed, 20 May 2026 19:44:02 +0200 Subject: [PATCH] clear connect-action latch when external disconnect cancels Connecting The main-window toggle stayed visually stuck on "Connecting" when the user clicked Connect in the UI and then clicked Disconnect in the tray (or the daemon was otherwise cancelled mid-Connecting). Repro: open the main window, click the toggle to Connect, then while the daemon is still in Connecting click Disconnect in the tray menu. The tray and daemon agree the session is Idle, but the React toggle keeps painting "Connecting" until the next manual interaction. Root cause is in ConnectionStatusSwitch.tsx. The component holds an `action` latch ("connect" | "logging-in" | "disconnect" | null) so the toggle can show an optimistic transitional state while the daemon catches up. The connState memo treats `action === "connect"` plus any non-Connected daemon state as Connecting: if ((action === "connect" || action === "logging-in") && daemonState !== "Connected") { return ConnectionState.Connecting; } The effect that releases the latch only cleared it on `Connected` or `DaemonUnavailable`. There was no branch for "the connect flow was cancelled externally and the daemon is back at Idle", so the latch remained set forever and the optimistic Connecting state never collapsed. Fix: add a `sawConnectingRef` that flips to true the first time the daemon reports Connecting during an active "connect" action, and resets when `action` returns to null. When `action === "connect"` and the daemon flips from a state we'd observed as Connecting back to Idle, clear the latch so connState falls through to Disconnected. Other paths are untouched: - Successful connect still clears on Connected. - NeedsLogin still hands off to driveLogin. - DaemonUnavailable still clears via the `unreachable` branch. - The `"logging-in"` action is intentionally not handled here; Login's internal Down flaps the daemon through Idle and driveLogin's .finally remains the sole clearer for that latch. - The `"disconnect"` action's Idle/Disconnected/unreachable clear is unchanged. --- .../src/layouts/ConnectionStatusSwitch.tsx | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/client/ui/frontend/src/layouts/ConnectionStatusSwitch.tsx b/client/ui/frontend/src/layouts/ConnectionStatusSwitch.tsx index 84e24d077..ec339ee39 100644 --- a/client/ui/frontend/src/layouts/ConnectionStatusSwitch.tsx +++ b/client/ui/frontend/src/layouts/ConnectionStatusSwitch.tsx @@ -230,6 +230,14 @@ export const ConnectionStatusSwitch = () => { // See connect() above — clear via the effect, not eagerly. }; + // Tracks whether the daemon has entered Connecting during the + // current "connect" action. Lets us distinguish "still waiting for + // the daemon to start" (Idle → Idle) from "the connect flow was + // cancelled externally" (Connecting → Idle, e.g. tray Disconnect + // while the UI was Connecting). Reset whenever action returns to + // null. + const sawConnectingRef = useRef(false); + // Release the action latch when the daemon settles on a terminal // state for the user's intent — and, in the connect → NeedsLogin // case, hand off to driveLogin so the user doesn't have to click @@ -237,6 +245,13 @@ export const ConnectionStatusSwitch = () => { // .finally, not here: Login's internal Down makes the daemon flap // through Idle, which would otherwise look like a terminal state. useEffect(() => { + if (action === null) { + sawConnectingRef.current = false; + return; + } + if (daemonState === "Connecting") { + sawConnectingRef.current = true; + } if (action === "connect") { if (needsLogin) { driveLogin(); @@ -244,6 +259,14 @@ export const ConnectionStatusSwitch = () => { } if (daemonState === "Connected" || unreachable) { setAction(null); + return; + } + // Cancelled externally (e.g. tray Disconnect during our + // Connecting): the daemon went back to Idle after we'd + // observed Connecting. Clear the latch so the UI stops + // showing Connecting forever. + if (sawConnectingRef.current && daemonState === "Idle") { + setAction(null); } return; }