From bff9e576b7a2c75ad516ea88b24da67fd3f15bb0 Mon Sep 17 00:00:00 2001 From: LukeParkerDev <10430890+Hona@users.noreply.github.com> Date: Fri, 17 Apr 2026 14:51:53 +1000 Subject: [PATCH] feat(desktop): show splash overlay during server switch ServerKey's keyed remount is a multi-second synchronous cascade (dispose + rebuild of the whole app subtree) that used to leave the UI looking frozen. A tiny module-level serverSwitching signal now gates a fullscreen Splash rendered above the ServerKey boundary, and the status-popover click handler setTimeout-defers the batched navigate+setActive so the browser paints the splash before the freeze begins and dismisses it after the new subtree paints. --- packages/app/src/app.tsx | 12 ++++++++ .../src/components/status-popover-body.tsx | 28 ++++++++++++++----- packages/app/src/utils/server-switch.tsx | 9 ++++++ 3 files changed, 42 insertions(+), 7 deletions(-) create mode 100644 packages/app/src/utils/server-switch.tsx diff --git a/packages/app/src/app.tsx b/packages/app/src/app.tsx index 362ac271e9..2ff68b9dd5 100644 --- a/packages/app/src/app.tsx +++ b/packages/app/src/app.tsx @@ -28,6 +28,7 @@ import { Suspense, } from "solid-js" import { Dynamic } from "solid-js/web" +import { serverSwitching } from "@/utils/server-switch" import { CommandProvider } from "@/context/command" import { CommentsProvider } from "@/context/comments" import { FileProvider } from "@/context/file" @@ -305,6 +306,12 @@ export function AppInterface(props: { router?: Component disableHealthCheck?: boolean }) { + // ServerKey wraps the whole Router so that switching `server.key` throws + // away any session / pty state from the previous server. Preserving the + // route across servers doesn't work because session ids, pty ids, and + // most URL-addressable resources are server-scoped — you'd 404 on every + // fetch. The click handler that swaps servers also navigates back to "/" + // so the fresh MemoryRouter doesn't try to re-resolve a now-dead URL. return ( + +
+ +
+
diff --git a/packages/app/src/components/status-popover-body.tsx b/packages/app/src/components/status-popover-body.tsx index 0075f501de..cad0b0673a 100644 --- a/packages/app/src/components/status-popover-body.tsx +++ b/packages/app/src/components/status-popover-body.tsx @@ -15,6 +15,7 @@ import { useSDK } from "@/context/sdk" import { normalizeServerUrl, ServerConnection, useServer } from "@/context/server" import { useSync } from "@/context/sync" import { useCheckServerHealth, type ServerHealth } from "@/utils/server-health" +import { setServerSwitching } from "@/utils/server-switch" const pollMs = 10_000 @@ -292,13 +293,26 @@ export function StatusPopoverBody(props: { shown: Accessor }) { aria-disabled={blocked()} onClick={() => { if (blocked()) return - // Run navigate + setActive in the same tick so Solid - // disposes the old subtree once instead of cascading - // the route change disposal into the ServerKey remount. - batch(() => { - navigate("/") - server.setActive(key) - }) + // Paint a full-window splash BEFORE the heavy + // ServerKey remount so the user gets visual + // feedback during the multi-second synchronous + // dispose cascade (xterm + file-tree + providers). + // setTimeout(0) yields to the browser so the + // splash lands on screen before the cascade + // starts; a second setTimeout(0) after the batch + // waits for the new subtree to paint, then + // dismisses the splash. + setServerSwitching(true) + setTimeout(() => { + try { + batch(() => { + navigate("/") + server.setActive(key) + }) + } finally { + setTimeout(() => setServerSwitching(false), 0) + } + }, 0) }} > diff --git a/packages/app/src/utils/server-switch.tsx b/packages/app/src/utils/server-switch.tsx new file mode 100644 index 0000000000..480990b184 --- /dev/null +++ b/packages/app/src/utils/server-switch.tsx @@ -0,0 +1,9 @@ +import { createSignal } from "solid-js" + +// Global flag used to paint a full-window splash overlay while a server +// swap is in progress. ServerKey's keyed remount is a big +// synchronous cascade (dispose + remount of the entire app subtree) that +// can freeze the UI for several seconds; setting this true before the +// swap and false after lets us render an overlay above the ServerKey +// boundary so the freeze has visual feedback instead of looking stuck. +export const [serverSwitching, setServerSwitching] = createSignal(false)