Skip to content

Commit 080c864

Browse files
committed
perf: show window immediately during desktop startup
Previously the window was only created after the sidecar server was ready (up to 7 seconds). Now the window is created immediately and shows a loading screen while the server starts in the background. Changes: - Create window synchronously in setup() instead of async spawn - Add serverReady flag and opencode:server-ready event - Add DesktopServerGate component to show loading screen - Dispatch event when server is ready to hide loading screen
1 parent f991fbb commit 080c864

File tree

2 files changed

+121
-71
lines changed

2 files changed

+121
-71
lines changed

packages/app/src/app.tsx

Lines changed: 83 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import "@/index.css"
2-
import { ErrorBoundary, Show, type ParentProps } from "solid-js"
2+
import { createSignal, ErrorBoundary, onCleanup, onMount, Show, type ParentProps } from "solid-js"
33
import { Router, Route, Navigate } from "@solidjs/router"
44
import { MetaProvider } from "@solidjs/meta"
55
import { Font } from "@opencode-ai/ui/font"
@@ -20,6 +20,7 @@ import { FileProvider } from "@/context/file"
2020
import { NotificationProvider } from "@/context/notification"
2121
import { DialogProvider } from "@opencode-ai/ui/context/dialog"
2222
import { CommandProvider } from "@/context/command"
23+
import { Logo } from "@opencode-ai/ui/logo"
2324
import Layout from "@/pages/layout"
2425
import Home from "@/pages/home"
2526
import DirectoryLayout from "@/pages/directory-layout"
@@ -29,7 +30,7 @@ import { iife } from "@opencode-ai/util/iife"
2930

3031
declare global {
3132
interface Window {
32-
__OPENCODE__?: { updaterEnabled?: boolean; port?: number }
33+
__OPENCODE__?: { updaterEnabled?: boolean; port?: number; serverReady?: boolean }
3334
}
3435
}
3536

@@ -54,6 +55,44 @@ function ServerKey(props: ParentProps) {
5455
)
5556
}
5657

58+
// Loading screen shown while desktop server is starting
59+
function LoadingScreen() {
60+
return (
61+
<div class="h-screen w-screen flex flex-col items-center justify-center bg-background-base">
62+
<Logo class="w-xl opacity-12 animate-pulse" />
63+
<div class="mt-8 text-14-regular text-text-weak">Starting server...</div>
64+
</div>
65+
)
66+
}
67+
68+
// Gate component that waits for the desktop server to be ready
69+
function DesktopServerGate(props: ParentProps) {
70+
// Check if we're running in desktop mode with serverReady flag
71+
const isDesktop = () => typeof window.__OPENCODE__ !== "undefined" && "serverReady" in (window.__OPENCODE__ ?? {})
72+
const [ready, setReady] = createSignal(window.__OPENCODE__?.serverReady ?? true)
73+
74+
onMount(() => {
75+
if (!isDesktop()) return
76+
77+
// If already ready, no need to wait
78+
if (window.__OPENCODE__?.serverReady) {
79+
setReady(true)
80+
return
81+
}
82+
83+
// Listen for the server-ready event
84+
const handler = () => setReady(true)
85+
window.addEventListener("opencode:server-ready", handler)
86+
onCleanup(() => window.removeEventListener("opencode:server-ready", handler))
87+
})
88+
89+
return (
90+
<Show when={ready()} fallback={<LoadingScreen />}>
91+
{props.children}
92+
</Show>
93+
)
94+
}
95+
5796
export function App() {
5897
return (
5998
<MetaProvider>
@@ -64,46 +103,48 @@ export function App() {
64103
<MarkedProvider>
65104
<DiffComponentProvider component={Diff}>
66105
<CodeComponentProvider component={Code}>
67-
<ServerProvider defaultUrl={defaultServerUrl}>
68-
<ServerKey>
69-
<GlobalSDKProvider>
70-
<GlobalSyncProvider>
71-
<Router
72-
root={(props) => (
73-
<PermissionProvider>
74-
<LayoutProvider>
75-
<NotificationProvider>
76-
<CommandProvider>
77-
<Layout>{props.children}</Layout>
78-
</CommandProvider>
79-
</NotificationProvider>
80-
</LayoutProvider>
81-
</PermissionProvider>
82-
)}
83-
>
84-
<Route path="/" component={Home} />
85-
<Route path="/:dir" component={DirectoryLayout}>
86-
<Route path="/" component={() => <Navigate href="session" />} />
87-
<Route
88-
path="/session/:id?"
89-
component={(p) => (
90-
<Show when={p.params.id ?? "new"} keyed>
91-
<TerminalProvider>
92-
<FileProvider>
93-
<PromptProvider>
94-
<Session />
95-
</PromptProvider>
96-
</FileProvider>
97-
</TerminalProvider>
98-
</Show>
99-
)}
100-
/>
101-
</Route>
102-
</Router>
103-
</GlobalSyncProvider>
104-
</GlobalSDKProvider>
105-
</ServerKey>
106-
</ServerProvider>
106+
<DesktopServerGate>
107+
<ServerProvider defaultUrl={defaultServerUrl}>
108+
<ServerKey>
109+
<GlobalSDKProvider>
110+
<GlobalSyncProvider>
111+
<Router
112+
root={(props) => (
113+
<PermissionProvider>
114+
<LayoutProvider>
115+
<NotificationProvider>
116+
<CommandProvider>
117+
<Layout>{props.children}</Layout>
118+
</CommandProvider>
119+
</NotificationProvider>
120+
</LayoutProvider>
121+
</PermissionProvider>
122+
)}
123+
>
124+
<Route path="/" component={Home} />
125+
<Route path="/:dir" component={DirectoryLayout}>
126+
<Route path="/" component={() => <Navigate href="session" />} />
127+
<Route
128+
path="/session/:id?"
129+
component={(p) => (
130+
<Show when={p.params.id ?? "new"} keyed>
131+
<TerminalProvider>
132+
<FileProvider>
133+
<PromptProvider>
134+
<Session />
135+
</PromptProvider>
136+
</FileProvider>
137+
</TerminalProvider>
138+
</Show>
139+
)}
140+
/>
141+
</Route>
142+
</Router>
143+
</GlobalSyncProvider>
144+
</GlobalSDKProvider>
145+
</ServerKey>
146+
</ServerProvider>
147+
</DesktopServerGate>
107148
</CodeComponentProvider>
108149
</DiffComponentProvider>
109150
</MarkedProvider>

packages/desktop/src-tauri/src/lib.rs

Lines changed: 38 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -211,9 +211,42 @@ pub fn run() {
211211
// Initialize log state
212212
app.manage(LogState(Arc::new(Mutex::new(VecDeque::new()))));
213213

214-
tauri::async_runtime::spawn(async move {
215-
let port = get_sidecar_port();
214+
// Get port and create window immediately for faster perceived startup
215+
let port = get_sidecar_port();
216+
217+
let primary_monitor = app.primary_monitor().ok().flatten();
218+
let size = primary_monitor
219+
.map(|m| m.size().to_logical(m.scale_factor()))
220+
.unwrap_or(LogicalSize::new(1920, 1080));
221+
222+
// Create window immediately with serverReady = false
223+
let mut window_builder =
224+
WebviewWindow::builder(&app, "main", WebviewUrl::App("/".into()))
225+
.title("OpenCode")
226+
.inner_size(size.width as f64, size.height as f64)
227+
.decorations(true)
228+
.zoom_hotkeys_enabled(true)
229+
.disable_drag_drop_handler()
230+
.initialization_script(format!(
231+
r#"
232+
window.__OPENCODE__ ??= {{}};
233+
window.__OPENCODE__.updaterEnabled = {updater_enabled};
234+
window.__OPENCODE__.port = {port};
235+
window.__OPENCODE__.serverReady = false;
236+
"#
237+
));
238+
239+
#[cfg(target_os = "macos")]
240+
{
241+
window_builder = window_builder
242+
.title_bar_style(tauri::TitleBarStyle::Overlay)
243+
.hidden_title(true);
244+
}
245+
246+
let window = window_builder.build().expect("Failed to create window");
216247

248+
// Spawn server in background and notify when ready
249+
tauri::async_runtime::spawn(async move {
217250
let should_spawn_sidecar = !is_server_running(port).await;
218251

219252
let child = if should_spawn_sidecar {
@@ -257,35 +290,11 @@ pub fn run() {
257290
None
258291
};
259292

260-
let primary_monitor = app.primary_monitor().ok().flatten();
261-
let size = primary_monitor
262-
.map(|m| m.size().to_logical(m.scale_factor()))
263-
.unwrap_or(LogicalSize::new(1920, 1080));
264-
265-
let mut window_builder =
266-
WebviewWindow::builder(&app, "main", WebviewUrl::App("/".into()))
267-
.title("OpenCode")
268-
.inner_size(size.width as f64, size.height as f64)
269-
.decorations(true)
270-
.zoom_hotkeys_enabled(true)
271-
.disable_drag_drop_handler()
272-
.initialization_script(format!(
273-
r#"
274-
window.__OPENCODE__ ??= {{}};
275-
window.__OPENCODE__.updaterEnabled = {updater_enabled};
276-
window.__OPENCODE__.port = {port};
277-
"#
278-
));
279-
280-
#[cfg(target_os = "macos")]
281-
{
282-
window_builder = window_builder
283-
.title_bar_style(tauri::TitleBarStyle::Overlay)
284-
.hidden_title(true);
293+
// Notify the frontend that the server is ready
294+
if let Some(window) = app.get_webview_window("main") {
295+
let _ = window.eval("window.__OPENCODE__.serverReady = true; window.dispatchEvent(new Event('opencode:server-ready'));");
285296
}
286297

287-
window_builder.build().expect("Failed to create window");
288-
289298
app.manage(ServerState(Arc::new(Mutex::new(child))));
290299
});
291300

0 commit comments

Comments
 (0)