fix(app): stabilize server switching

This commit is contained in:
LukeParkerDev
2026-05-04 23:34:03 +10:00
parent e5a4f6f603
commit de69c5af0a
8 changed files with 80 additions and 71 deletions

View File

@@ -286,11 +286,11 @@ function ConnectionError(props: { onRetry?: () => void; onServerSelected?: (key:
)
}
function ServerKey(props: ParentProps) {
function ServerKey(props: { children: (key: ServerConnection.Key) => JSX.Element }) {
const server = useServer()
return (
<Show when={server.key} keyed>
{props.children}
{(key) => props.children(key)}
</Show>
)
}
@@ -310,22 +310,24 @@ export function AppInterface(props: {
>
<ConnectionGate disableHealthCheck={props.disableHealthCheck}>
<ServerKey>
<QueryProvider>
<GlobalSDKProvider>
<GlobalSyncProvider>
<Dynamic
component={props.router ?? Router}
root={(routerProps) => <RouterRoot appChildren={props.children}>{routerProps.children}</RouterRoot>}
>
<Route path="/" component={HomeRoute} />
<Route path="/:dir" component={DirectoryLayout}>
<Route path="/" component={SessionIndexRoute} />
<Route path="/session/:id?" component={SessionRoute} />
</Route>
</Dynamic>
</GlobalSyncProvider>
</GlobalSDKProvider>
</QueryProvider>
{() => (
<QueryProvider>
<GlobalSDKProvider>
<GlobalSyncProvider>
<Dynamic
component={props.router ?? Router}
root={(routerProps) => <RouterRoot appChildren={props.children}>{routerProps.children}</RouterRoot>}
>
<Route path="/" component={HomeRoute} />
<Route path="/:dir" component={DirectoryLayout}>
<Route path="/" component={SessionIndexRoute} />
<Route path="/session/:id?" component={SessionRoute} />
</Route>
</Dynamic>
</GlobalSyncProvider>
</GlobalSDKProvider>
</QueryProvider>
)}
</ServerKey>
</ConnectionGate>
</ServerProvider>

View File

@@ -8,7 +8,7 @@ import { List } from "@opencode-ai/ui/list"
import { TextField } from "@opencode-ai/ui/text-field"
import { useMutation } from "@tanstack/solid-query"
import { showToast } from "@opencode-ai/ui/toast"
import { batch, createEffect, createMemo, createResource, For, onCleanup, Show, startTransition, untrack } from "solid-js"
import { batch, createEffect, createMemo, createResource, For, onCleanup, Show, untrack } from "solid-js"
import { createStore, reconcile } from "solid-js/store"
import { DialogWslServer } from "@/components/dialog-wsl-server"
import { ServerHealthIndicator, ServerRow } from "@/components/server/server-row"
@@ -386,28 +386,21 @@ export function DialogSelectServer(props: DialogSelectServerProps = {}) {
const nextKey = ServerConnection.key(conn)
const changed = server.key !== nextKey
const navigateHome = () => {
if (changed && typeof window !== "undefined" && window.history?.replaceState) {
window.history.replaceState(null, "", "/")
const navigateHome = () => props.onNavigateHome?.()
const apply = () => {
dialog.close()
if (persist && conn.type === "http") {
server.add(conn)
navigateHome()
return
}
props.onNavigateHome?.()
}
const apply = () =>
startTransition(() => {
dialog.close()
if (persist && conn.type === "http") {
server.add(conn)
navigateHome()
return
}
batch(() => {
navigateHome()
server.setActive(nextKey)
})
batch(() => {
navigateHome()
server.setActive(nextKey)
})
}
if (!changed) {
await apply()

View File

@@ -5,7 +5,7 @@ import { Switch } from "@opencode-ai/ui/switch"
import { Tabs } from "@opencode-ai/ui/tabs"
import { useMutation, useQueryClient } from "@tanstack/solid-query"
import { showToast } from "@opencode-ai/ui/toast"
import { useNavigate } from "@solidjs/router"
import { useLocation, useNavigate } from "@solidjs/router"
import { type Accessor, createEffect, createMemo, For, type JSXElement, onCleanup, Show } from "solid-js"
import { createStore, reconcile } from "solid-js/store"
import { ServerHealthIndicator, ServerRow } from "@/components/server/server-row"
@@ -156,13 +156,14 @@ const useMcpToggleMutation = () => {
}))
}
export function StatusPopoverBody(props: { shown: Accessor<boolean> }) {
export function StatusPopoverBody(props: { shown: Accessor<boolean>; close?: () => void }) {
const sync = useSync()
const server = useServer()
const platform = usePlatform()
const dialog = useDialog()
const language = useLanguage()
const navigate = useNavigate()
const location = useLocation()
const fail = (err: unknown) => {
showToast({
@@ -251,8 +252,16 @@ export function StatusPopoverBody(props: { shown: Accessor<boolean> }) {
aria-disabled={blocked()}
onClick={() => {
if (blocked()) return
props.close?.()
navigate("/")
queueMicrotask(() => server.setActive(key))
const activate = () => {
if (location.pathname !== "/") {
setTimeout(activate, 16)
return
}
setTimeout(() => server.setActive(key), 0)
}
setTimeout(activate, 0)
}}
>
<ServerHealthIndicator health={health[key]} />

View File

@@ -58,7 +58,7 @@ export function StatusPopover() {
<div class="w-[360px] h-14 rounded-xl bg-background-strong shadow-[var(--shadow-lg-border-base)]" />
}
>
<Body shown={shown} />
<Body shown={shown} close={() => setShown(false)} />
</Suspense>
</Show>
</Popover>

View File

@@ -371,11 +371,7 @@ function createGlobalSync() {
onCleanup(() => {
queue.dispose()
})
onCleanup(() => {
for (const directory of Object.keys(children.children)) {
children.disposeDirectory(directoryKey(directory))
}
})
onCleanup(children.disposeAll)
onMount(() => {
if (typeof requestAnimationFrame === "function") {

View File

@@ -92,6 +92,22 @@ export function createChildStoreManager(input: {
})
}
function disposeChild(key: DirectoryKey) {
const dispose = disposers.get(key)
if (!key || !children[key]) return false
vcsCache.delete(key)
metaCache.delete(key)
iconCache.delete(key)
lifecycle.delete(key)
disposers.delete(key)
delete children[key]
input.onDispose(key)
if (dispose) {
dispose()
}
return true
}
function disposeDirectory(directory: DirectoryKey) {
const key = directory
if (
@@ -106,18 +122,13 @@ export function createChildStoreManager(input: {
return false
}
vcsCache.delete(key)
metaCache.delete(key)
iconCache.delete(key)
lifecycle.delete(key)
const dispose = disposers.get(key)
if (dispose) {
dispose()
disposers.delete(key)
return disposeChild(key)
}
function disposeAll() {
for (const directory of Object.keys(children)) {
disposeChild(directoryKey(directory))
}
delete children[key]
input.onDispose(key)
return true
}
function runEviction(skip?: string) {
@@ -329,6 +340,7 @@ export function createChildStoreManager(input: {
unpin,
pinned,
disposeDirectory,
disposeAll,
runEviction,
vcsCache,
metaCache,

View File

@@ -1,6 +1,6 @@
import { useDragDropContext } from "@thisbeyond/solid-dnd"
import type { Transformer } from "@thisbeyond/solid-dnd"
import { createRoot, onCleanup, type JSXElement } from "solid-js"
import type { JSXElement } from "solid-js"
type DragEvent = { draggable?: { id?: unknown } }
@@ -27,20 +27,16 @@ const createAxisConstraint = (axis: "x" | "y", transformerId: string) => (): JSX
if (!context) return null
const [, { onDragStart, onDragEnd, addTransformer, removeTransformer }] = context
const transformer = createTransformer(transformerId, axis)
const dispose = createRoot((dispose) => {
onDragStart((event) => {
const id = getDraggableId(event)
if (!id) return
addTransformer("draggables", id, transformer)
})
onDragEnd((event) => {
const id = getDraggableId(event)
if (!id) return
removeTransformer("draggables", id, transformer.id)
})
return dispose
onDragStart((event) => {
const id = getDraggableId(event)
if (!id) return
addTransformer("draggables", id, transformer)
})
onDragEnd((event) => {
const id = getDraggableId(event)
if (!id) return
removeTransformer("draggables", id, transformer.id)
})
onCleanup(dispose)
return null
}

View File

@@ -87,6 +87,7 @@ setupApp()
function setupApp() {
ensureLoopbackNoProxy()
app.commandLine.appendSwitch("proxy-bypass-list", "<-loopback>")
if (!app.isPackaged) app.commandLine.appendSwitch("remote-debugging-port", "9222")
if (!app.requestSingleInstanceLock()) {
app.quit()