feat(desktop): show splash overlay during server switch

ServerKey's keyed <Show> 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.
This commit is contained in:
LukeParkerDev
2026-04-17 14:51:53 +10:00
parent da2e640029
commit bff9e576b7
3 changed files with 42 additions and 7 deletions

View File

@@ -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<BaseRouterProps>
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 (
<ServerProvider
defaultServer={props.defaultServer}
@@ -312,6 +319,11 @@ export function AppInterface(props: {
servers={props.servers}
>
<ConnectionGate disableHealthCheck={props.disableHealthCheck}>
<Show when={serverSwitching()}>
<div class="fixed inset-0 z-[2147483647] bg-background-base flex flex-col items-center justify-center pointer-events-auto">
<Splash class="w-16 h-20 opacity-50 animate-pulse" />
</div>
</Show>
<ServerKey>
<GlobalSDKProvider>
<GlobalSyncProvider>

View File

@@ -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<boolean> }) {
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)
}}
>
<ServerHealthIndicator health={health[key]} />

View File

@@ -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 <Show> 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)