Compare commits

...
Author SHA1 Message Date
LukeParkerDev 9abcab6643 perf(app): mount the shell before the servers are known
The desktop gated the whole interface on the local service, WSL and SSH
discovery. The shell (titlebar, tabs, layout) now mounts from local
state as soon as the quick lookups resolve, and the server list is
marked pending while connections are still being discovered: the tabs
store does not prune tabs of servers that have not appeared yet, the
layout does not fall back to the connect screen, and draft and session
routes hold their panel's place until their connection exists.
2026-09-19 13:58:11 +10:00
7 changed files with 80 additions and 40 deletions
+2
View File
@@ -102,6 +102,7 @@ export function AppInterface(props: {
defaultServer?: ServerConnection.Key
canonicalLocalServer?: ServerConnection.Key
servers?: Array<ServerConnection.Any>
serversPending?: boolean
router?: Component<BaseRouterProps>
}) {
// The visual layout lives in the router root so it remains mounted across
@@ -128,6 +129,7 @@ export function AppInterface(props: {
defaultServer={props.defaultServer}
canonicalLocalServer={props.canonicalLocalServer}
servers={props.servers}
pending={props.serversPending}
>
<SettingsProvider>
<Dynamic component={props.router ?? Router} root={Root}>
+14 -2
View File
@@ -7,7 +7,7 @@ import { LocationProvider } from "@/workspaces/location"
import { ModelsProvider } from "@/providers/models/models"
import { ComposerPersistenceProvider } from "@/composer/persistence"
import { ServerProvider, useServer } from "@/runtime/server/current"
import { ServerConnection } from "@/runtime/server/registry"
import { ServerConnection, useServers } from "@/runtime/server/registry"
import { useTabs, type DraftTab } from "@/shell/tabs/tabs"
import { SessionUIProvider } from "@/shell/routes/session-ui-provider"
import NewSession from "@/new-session/screen"
@@ -30,11 +30,23 @@ export function DraftRoute() {
function ResolvedDraftRoute(props: { draft: DraftTab }) {
const global = useGlobal()
const servers = useServers()
const conn = createMemo(() => global.servers.list().find((item) => ServerConnection.key(item) === props.draft.server))
return (
<Show when={`${props.draft.server}\0${props.draft.directory}`} keyed>
<Show when={conn()} keyed>
<Show
when={conn()}
keyed
fallback={
// The shell is up before the local service has connected; hold the panel's place.
<Show when={servers.pending}>
<SessionRouteFrame padded>
<SessionPanelFrame raised />
</SessionRouteFrame>
</Show>
}
>
{(conn) => (
<ServerProvider conn={conn}>
<ResolvedDraftContent draft={props.draft} />
@@ -199,6 +199,9 @@ export const { use: useServers, provider: ServersProvider } = createSimpleContex
defaultServer?: ServerConnection.Key
canonicalLocalServer?: ServerConnection.Key
servers?: Array<ServerConnection.Any>
// The host is still discovering connections (desktop: the local service, WSL, SSH). The shell
// renders meanwhile; nothing that depends on the list being complete may act on it yet.
pending?: boolean
}) => {
const [store, setStore, _] = persisted(
{
@@ -258,6 +261,9 @@ export const { use: useServers, provider: ServersProvider } = createSimpleContex
get visible() {
return visibleServers()
},
get pending() {
return props.pending ?? false
},
isHidden(key: ServerConnection.Key) {
return store.hidden[key] ?? false
},
+13 -2
View File
@@ -62,12 +62,23 @@ export function AppRoutes() {
function TargetServerRoute(props: ParentProps) {
const params = useParams<{ serverKey: string }>()
const global = useGlobal()
const servers = useServers()
const connection = createMemo(() =>
global.servers.list().find((item) => ServerConnection.key(item) === requireServerKey(params.serverKey)),
)
return (
<Show when={connection()} keyed>
<Show
when={connection()}
keyed
fallback={
<Show when={servers.pending}>
<div class="flex min-h-0 flex-1 px-2 pb-[var(--shell-bottom-inset,8px)] pt-[var(--shell-top-inset,8px)]">
<SessionPanelFrame raised />
</div>
</Show>
}
>
{(connection) => <ServerProvider conn={connection}>{props.children}</ServerProvider>}
</Show>
)
@@ -76,7 +87,7 @@ function TargetServerRoute(props: ParentProps) {
function AppLayout(props: ParentProps) {
const servers = useServers()
return (
<Show when={servers.list.length > 0} fallback={<ConnectServerScreen />}>
<Show when={servers.list.length > 0 || servers.pending} fallback={<ConnectServerScreen />}>
<LayoutProvider>
<SettingsSurfaceProvider>
<BrowserAttachmentsProvider>
+4 -2
View File
@@ -138,8 +138,10 @@ export const { use: useTabs, provider: TabsProvider } = createSimpleContext({
onCleanup(memory.dispose)
// Tabs of a server that is gone are dropped, but not while the host is still discovering its
// servers: the shell mounts before the local service has connected.
createEffect(() => {
if (!ready() || !recentReady()) return
if (!ready() || !recentReady() || servers.pending) return
const serversSet = new Set(servers.list.map(ServerConnection.key))
const next = store.filter((tab) => serversSet.has(tab.server))
if (next.length !== store.length) {
@@ -165,7 +167,7 @@ export const { use: useTabs, provider: TabsProvider } = createSimpleContext({
})
createEffect(() => {
if (!closedReady()) return
if (!closedReady() || servers.pending) return
const serversSet = new Set(servers.list.map(ServerConnection.key))
const next = closed.filter((entry) => serversSet.has(entry.tab.server))
if (next.length !== closed.length) setClosed(() => next)
+30 -33
View File
@@ -78,15 +78,11 @@ export function DesktopApp(props: { api: ElectronAPI; updater: UpdaterPlatform;
const ssh = useSsh()
const sshConnections = createSshConnections(props.api.sshServers)
const language = useLanguage()
const ready = createMemo(
() =>
!firstLaunch.loading &&
!defaultServer.loading &&
!sidecar.loading &&
!locale.loading &&
!wslServers.isLoading &&
!ssh.loading,
)
// The shell mounts from local state as soon as the quick lookups resolve. The local service,
// WSL and SSH connections are discovered meanwhile and appear in the server list when ready;
// until then the list is marked pending so nothing acts on it being empty.
const ready = createMemo(() => !firstLaunch.loading && !defaultServer.loading && !locale.loading)
const serversPending = createMemo(() => sidecar.loading || wslServers.isLoading || ssh.loading)
const servers = createMemo(() => {
const data = initializationData(sidecar)
const list: ServerConnection.Any[] = []
@@ -109,30 +105,31 @@ export function DesktopApp(props: { api: ElectronAPI; updater: UpdaterPlatform;
return (
<Show when={ready()}>
<Show when={effectiveDefaultServer()} keyed>
{(key) => (
<AppInterface defaultServer={key} servers={servers()} router={router}>
<DesktopStartupReady
routeReady={!initialRoute.loading && startup.onboardingReady}
onReady={() => setStartup("ready", true)}
onRoute={(route) => setStartup("route", route)}
/>
<DesktopFirstLaunchOnboarding
api={props.api}
initialUrl={initialUrl}
serverKey={key}
pending={firstLaunch() ?? false}
onReady={() => setStartup("onboardingReady", true)}
/>
<DesktopEffects api={props.api} />
<Suspense fallback={null}>
<Show when={initializationData(sidecar)} keyed>
{(server) => <MigrationStatus server={server} />}
</Show>
</Suspense>
</AppInterface>
)}
</Show>
<AppInterface
defaultServer={effectiveDefaultServer()}
servers={servers()}
serversPending={serversPending()}
router={router}
>
<DesktopStartupReady
routeReady={!initialRoute.loading && startup.onboardingReady}
onReady={() => setStartup("ready", true)}
onRoute={(route) => setStartup("route", route)}
/>
<DesktopFirstLaunchOnboarding
api={props.api}
initialUrl={initialUrl}
serverKey={effectiveDefaultServer()}
pending={firstLaunch() ?? false}
onReady={() => setStartup("onboardingReady", true)}
/>
<DesktopEffects api={props.api} />
<Suspense fallback={null}>
<Show when={initializationData(sidecar)} keyed>
{(server) => <MigrationStatus server={server} />}
</Show>
</Suspense>
</AppInterface>
</Show>
)
}
@@ -1,5 +1,5 @@
import { ServerConnection, useCurrentRoute, useGlobal, useServers, useTabs } from "@opencode/app/desktop"
import { createResource } from "solid-js"
import { createEffect, createResource, createRoot } from "solid-js"
import type { ElectronAPI } from "../api-types"
export function DesktopFirstLaunchOnboarding(props: {
@@ -24,6 +24,16 @@ export function DesktopFirstLaunchOnboarding(props: {
if (!props.pending) return
await Promise.all([tabs.ready.promise, tabs.recentReady.promise].map((p) => p ?? Promise.resolve()))
// The shell mounts before the local service has connected; the decision needs the full list.
await new Promise<void>((resolve) =>
createRoot((dispose) =>
createEffect(() => {
if (server.pending) return
dispose()
resolve()
}),
),
)
const shouldTrigger =
props.initialUrl === "/" &&