From b5e95e59ab0361fc7df61d6b9f094ba670bfa7a7 Mon Sep 17 00:00:00 2001 From: Muthuvel Date: Sat, 3 Jan 2026 13:43:38 +0800 Subject: [PATCH 1/3] 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 --- packages/app/src/app.tsx | 131 +++++++++++++++++--------- packages/desktop/src-tauri/src/lib.rs | 67 +++++++------ 2 files changed, 127 insertions(+), 71 deletions(-) diff --git a/packages/app/src/app.tsx b/packages/app/src/app.tsx index e41575e7ad4..c377930b005 100644 --- a/packages/app/src/app.tsx +++ b/packages/app/src/app.tsx @@ -1,5 +1,5 @@ import "@/index.css" -import { ErrorBoundary, Show, type ParentProps } from "solid-js" +import { createSignal, ErrorBoundary, onCleanup, onMount, Show, type ParentProps } from "solid-js" import { Router, Route, Navigate } from "@solidjs/router" import { MetaProvider } from "@solidjs/meta" import { Font } from "@opencode-ai/ui/font" @@ -20,6 +20,7 @@ import { FileProvider } from "@/context/file" import { NotificationProvider } from "@/context/notification" import { DialogProvider } from "@opencode-ai/ui/context/dialog" import { CommandProvider } from "@/context/command" +import { Logo } from "@opencode-ai/ui/logo" import Layout from "@/pages/layout" import Home from "@/pages/home" import DirectoryLayout from "@/pages/directory-layout" @@ -29,7 +30,7 @@ import { iife } from "@opencode-ai/util/iife" declare global { interface Window { - __OPENCODE__?: { updaterEnabled?: boolean; port?: number } + __OPENCODE__?: { updaterEnabled?: boolean; port?: number; serverReady?: boolean } } } @@ -54,6 +55,50 @@ function ServerKey(props: ParentProps) { ) } +// Loading screen shown while desktop server is starting +function LoadingScreen() { + return ( +
+ +
Starting server...
+
+ ) +} + +// Gate component that waits for the desktop server to be ready +function DesktopServerGate(props: ParentProps) { + // Check if we're running in desktop mode with serverReady flag + const isDesktop = typeof window.__OPENCODE__ !== "undefined" && "serverReady" in (window.__OPENCODE__ ?? {}) + const [ready, setReady] = createSignal(window.__OPENCODE__?.serverReady ?? true) + + onMount(() => { + if (!isDesktop) return + + // If already ready, no need to wait + if (window.__OPENCODE__?.serverReady) { + setReady(true) + return + } + + // Listen for the server-ready event + const handler = () => setReady(true) + window.addEventListener("opencode:server-ready", handler) + + // Check again after adding listener to handle race condition + if (window.__OPENCODE__?.serverReady) { + setReady(true) + } + + onCleanup(() => window.removeEventListener("opencode:server-ready", handler)) + }) + + return ( + }> + {props.children} + + ) +} + export function App() { return ( @@ -64,46 +109,48 @@ export function App() { - - - - - ( - - - - - {props.children} - - - - - )} - > - - - } /> - ( - - - - - - - - - - )} - /> - - - - - - + + + + + + ( + + + + + {props.children} + + + + + )} + > + + + } /> + ( + + + + + + + + + + )} + /> + + + + + + + diff --git a/packages/desktop/src-tauri/src/lib.rs b/packages/desktop/src-tauri/src/lib.rs index 46c0ab256db..3234f181892 100644 --- a/packages/desktop/src-tauri/src/lib.rs +++ b/packages/desktop/src-tauri/src/lib.rs @@ -211,9 +211,42 @@ pub fn run() { // Initialize log state app.manage(LogState(Arc::new(Mutex::new(VecDeque::new())))); - tauri::async_runtime::spawn(async move { - let port = get_sidecar_port(); + // Get port and create window immediately for faster perceived startup + let port = get_sidecar_port(); + + let primary_monitor = app.primary_monitor().ok().flatten(); + let size = primary_monitor + .map(|m| m.size().to_logical(m.scale_factor())) + .unwrap_or(LogicalSize::new(1920, 1080)); + + // Create window immediately with serverReady = false + let mut window_builder = + WebviewWindow::builder(&app, "main", WebviewUrl::App("/".into())) + .title("OpenCode") + .inner_size(size.width as f64, size.height as f64) + .decorations(true) + .zoom_hotkeys_enabled(true) + .disable_drag_drop_handler() + .initialization_script(format!( + r#" + window.__OPENCODE__ ??= {{}}; + window.__OPENCODE__.updaterEnabled = {updater_enabled}; + window.__OPENCODE__.port = {port}; + window.__OPENCODE__.serverReady = false; + "# + )); + + #[cfg(target_os = "macos")] + { + window_builder = window_builder + .title_bar_style(tauri::TitleBarStyle::Overlay) + .hidden_title(true); + } + + let _window = window_builder.build().expect("Failed to create window"); + // Spawn server in background and notify when ready + tauri::async_runtime::spawn(async move { let should_spawn_sidecar = !is_server_running(port).await; let child = if should_spawn_sidecar { @@ -257,35 +290,11 @@ pub fn run() { None }; - let primary_monitor = app.primary_monitor().ok().flatten(); - let size = primary_monitor - .map(|m| m.size().to_logical(m.scale_factor())) - .unwrap_or(LogicalSize::new(1920, 1080)); - - let mut window_builder = - WebviewWindow::builder(&app, "main", WebviewUrl::App("/".into())) - .title("OpenCode") - .inner_size(size.width as f64, size.height as f64) - .decorations(true) - .zoom_hotkeys_enabled(true) - .disable_drag_drop_handler() - .initialization_script(format!( - r#" - window.__OPENCODE__ ??= {{}}; - window.__OPENCODE__.updaterEnabled = {updater_enabled}; - window.__OPENCODE__.port = {port}; - "# - )); - - #[cfg(target_os = "macos")] - { - window_builder = window_builder - .title_bar_style(tauri::TitleBarStyle::Overlay) - .hidden_title(true); + // Notify the frontend that the server is ready + if let Some(window) = app.get_webview_window("main") { + let _ = window.eval("window.__OPENCODE__.serverReady = true; window.dispatchEvent(new Event('opencode:server-ready'));"); } - window_builder.build().expect("Failed to create window"); - app.manage(ServerState(Arc::new(Mutex::new(child)))); }); From d73057b7ea63c77f0595320f8e9608a66b416a65 Mon Sep 17 00:00:00 2001 From: Brendan Allan Date: Wed, 7 Jan 2026 14:59:04 +0800 Subject: [PATCH 2/3] use single suspending query to get server state --- packages/app/src/app.tsx | 154 ++++++++++---------------- packages/app/src/index.ts | 2 +- packages/desktop/src-tauri/Cargo.lock | 18 +++ packages/desktop/src-tauri/Cargo.toml | 1 + packages/desktop/src-tauri/src/lib.rs | 104 ++++++++--------- packages/desktop/src/index.tsx | 43 +++++-- 6 files changed, 164 insertions(+), 158 deletions(-) diff --git a/packages/app/src/app.tsx b/packages/app/src/app.tsx index c377930b005..d512d1e58fe 100644 --- a/packages/app/src/app.tsx +++ b/packages/app/src/app.tsx @@ -46,60 +46,7 @@ const defaultServerUrl = iife(() => { return window.location.origin }) -function ServerKey(props: ParentProps) { - const server = useServer() - return ( - - {props.children} - - ) -} - -// Loading screen shown while desktop server is starting -function LoadingScreen() { - return ( -
- -
Starting server...
-
- ) -} - -// Gate component that waits for the desktop server to be ready -function DesktopServerGate(props: ParentProps) { - // Check if we're running in desktop mode with serverReady flag - const isDesktop = typeof window.__OPENCODE__ !== "undefined" && "serverReady" in (window.__OPENCODE__ ?? {}) - const [ready, setReady] = createSignal(window.__OPENCODE__?.serverReady ?? true) - - onMount(() => { - if (!isDesktop) return - - // If already ready, no need to wait - if (window.__OPENCODE__?.serverReady) { - setReady(true) - return - } - - // Listen for the server-ready event - const handler = () => setReady(true) - window.addEventListener("opencode:server-ready", handler) - - // Check again after adding listener to handle race condition - if (window.__OPENCODE__?.serverReady) { - setReady(true) - } - - onCleanup(() => window.removeEventListener("opencode:server-ready", handler)) - }) - - return ( - }> - {props.children} - - ) -} - -export function App() { +export function AppBaseProviders(props: ParentProps) { return ( @@ -108,50 +55,7 @@ export function App() { - - - - - - - ( - - - - - {props.children} - - - - - )} - > - - - } /> - ( - - - - - - - - - - )} - /> - - - - - - - - + {props.children} @@ -160,3 +64,57 @@ export function App() { ) } + +function ServerKey(props: ParentProps) { + const server = useServer() + return ( + + {props.children} + + ) +} + +export function AppInterface() { + return ( + + + + + ( + + + + + {props.children} + + + + + )} + > + + + } /> + ( + + + + + + + + + + )} + /> + + + + + + + ) +} diff --git a/packages/app/src/index.ts b/packages/app/src/index.ts index cf5be9f512f..df3181133e9 100644 --- a/packages/app/src/index.ts +++ b/packages/app/src/index.ts @@ -1,2 +1,2 @@ export { PlatformProvider, type Platform } from "./context/platform" -export { App } from "./app" +export { AppBaseProviders, AppInterface } from "./app" diff --git a/packages/desktop/src-tauri/Cargo.lock b/packages/desktop/src-tauri/Cargo.lock index 11afce91e25..f26f41cb2dc 100644 --- a/packages/desktop/src-tauri/Cargo.lock +++ b/packages/desktop/src-tauri/Cargo.lock @@ -1177,6 +1177,21 @@ dependencies = [ "new_debug_unreachable", ] +[[package]] +name = "futures" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + [[package]] name = "futures-channel" version = "0.3.31" @@ -1184,6 +1199,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" dependencies = [ "futures-core", + "futures-sink", ] [[package]] @@ -1251,6 +1267,7 @@ version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" dependencies = [ + "futures-channel", "futures-core", "futures-io", "futures-macro", @@ -2775,6 +2792,7 @@ dependencies = [ name = "opencode-desktop" version = "0.0.0" dependencies = [ + "futures", "gtk", "listeners", "serde", diff --git a/packages/desktop/src-tauri/Cargo.toml b/packages/desktop/src-tauri/Cargo.toml index b7c238f064f..941d528e6bc 100644 --- a/packages/desktop/src-tauri/Cargo.toml +++ b/packages/desktop/src-tauri/Cargo.toml @@ -35,6 +35,7 @@ serde_json = "1" tokio = "1.48.0" listeners = "0.3" tauri-plugin-os = "2" +futures = "0.3.31" [target.'cfg(target_os = "linux")'.dependencies] gtk = "0.18.2" diff --git a/packages/desktop/src-tauri/src/lib.rs b/packages/desktop/src-tauri/src/lib.rs index 3234f181892..3ace2929cd5 100644 --- a/packages/desktop/src-tauri/src/lib.rs +++ b/packages/desktop/src-tauri/src/lib.rs @@ -1,14 +1,16 @@ mod window_customizer; +use futures::FutureExt; use std::{ collections::VecDeque, net::{SocketAddr, TcpListener}, sync::{Arc, Mutex}, time::{Duration, Instant}, }; -use tauri::{AppHandle, LogicalSize, Manager, RunEvent, WebviewUrl, WebviewWindow, path::BaseDirectory}; -use tauri_plugin_clipboard_manager::ClipboardExt; -use tauri_plugin_dialog::{DialogExt, MessageDialogButtons, MessageDialogResult}; +use tauri::{ + path::BaseDirectory, AppHandle, LogicalSize, Manager, RunEvent, State, WebviewUrl, + WebviewWindow, +}; use tauri_plugin_shell::process::{CommandChild, CommandEvent}; use tauri_plugin_shell::ShellExt; use tokio::net::TcpSocket; @@ -16,7 +18,26 @@ use tokio::net::TcpSocket; use crate::window_customizer::PinchZoomDisablePlugin; #[derive(Clone)] -struct ServerState(Arc>>); +struct ServerState { + child: Arc>>, + status: futures::future::Shared>>, +} + +impl ServerState { + pub fn new( + child: Option, + status: tokio::sync::oneshot::Receiver>, + ) -> Self { + Self { + child: Arc::new(Mutex::new(child)), + status: status.shared(), + } + } + + pub fn set_child(&self, child: Option) { + *self.child.lock().unwrap() = child; + } +} #[derive(Clone)] struct LogState(Arc>>); @@ -31,7 +52,7 @@ fn kill_sidecar(app: AppHandle) { }; let Some(server_state) = server_state - .0 + .child .lock() .expect("Failed to acquire mutex lock") .take() @@ -45,8 +66,7 @@ fn kill_sidecar(app: AppHandle) { println!("Killed server"); } -#[tauri::command] -async fn copy_logs_to_clipboard(app: AppHandle) -> Result<(), String> { +async fn get_logs(app: AppHandle) -> Result { let log_state = app.try_state::().ok_or("Log state not found")?; let logs = log_state @@ -54,25 +74,16 @@ async fn copy_logs_to_clipboard(app: AppHandle) -> Result<(), String> { .lock() .map_err(|_| "Failed to acquire log lock")?; - let log_text = logs.iter().cloned().collect::>().join(""); - - app.clipboard() - .write_text(log_text) - .map_err(|e| format!("Failed to copy to clipboard: {}", e))?; - - Ok(()) + Ok(logs.iter().cloned().collect::>().join("")) } #[tauri::command] -async fn get_logs(app: AppHandle) -> Result { - let log_state = app.try_state::().ok_or("Log state not found")?; - - let logs = log_state - .0 - .lock() - .map_err(|_| "Failed to acquire log lock")?; - - Ok(logs.iter().cloned().collect::>().join("")) +async fn ensure_server_started(state: State<'_, ServerState>) -> Result<(), String> { + state + .status + .clone() + .await + .map_err(|_| "Failed to get server status".to_string())? } fn get_sidecar_port() -> u32 { @@ -202,8 +213,7 @@ pub fn run() { .plugin(PinchZoomDisablePlugin) .invoke_handler(tauri::generate_handler![ kill_sidecar, - copy_logs_to_clipboard, - get_logs + ensure_server_started ]) .setup(move |app| { let app = app.handle().clone(); @@ -232,7 +242,6 @@ pub fn run() { window.__OPENCODE__ ??= {{}}; window.__OPENCODE__.updaterEnabled = {updater_enabled}; window.__OPENCODE__.port = {port}; - window.__OPENCODE__.serverReady = false; "# )); @@ -245,32 +254,23 @@ pub fn run() { let _window = window_builder.build().expect("Failed to create window"); + let (tx, rx) = tokio::sync::oneshot::channel(); + app.manage(ServerState::new(None, rx)); + // Spawn server in background and notify when ready tauri::async_runtime::spawn(async move { let should_spawn_sidecar = !is_server_running(port).await; - let child = if should_spawn_sidecar { + let (child, res) = if should_spawn_sidecar { let child = spawn_sidecar(&app, port); let timestamp = Instant::now(); - loop { + let res = loop { if timestamp.elapsed() > Duration::from_secs(7) { - let res = app.dialog() - .message("Failed to spawn OpenCode Server. Copy logs using the button below and send them to the team for assistance.") - .title("Startup Failed") - .buttons(MessageDialogButtons::OkCancelCustom("Copy Logs And Exit".to_string(), "Exit".to_string())) - .blocking_show_with_result(); - - if matches!(&res, MessageDialogResult::Custom(name) if name == "Copy Logs And Exit") { - match copy_logs_to_clipboard(app.clone()).await { - Ok(()) => println!("Logs copied to clipboard successfully"), - Err(e) => println!("Failed to copy logs to clipboard: {}", e), - } - } - - app.exit(1); - - return; + break Err(format!( + "Failed to spawn OpenCode Server. Logs:\n{}", + get_logs(app.clone()).await.unwrap() + )); } tokio::time::sleep(Duration::from_millis(10)).await; @@ -279,23 +279,23 @@ pub fn run() { // give the server a little bit more time to warm up tokio::time::sleep(Duration::from_millis(10)).await; - break; + break Ok(()); } - } + }; println!("Server ready after {:?}", timestamp.elapsed()); - Some(child) + (Some(child), res) } else { - None + (None, Ok(())) }; - // Notify the frontend that the server is ready + app.state::().set_child(child); + let _ = tx.send(res); + if let Some(window) = app.get_webview_window("main") { - let _ = window.eval("window.__OPENCODE__.serverReady = true; window.dispatchEvent(new Event('opencode:server-ready'));"); + let _ = window.eval("window.__OPENCODE__.serverReady = true;"); } - - app.manage(ServerState(Arc::new(Mutex::new(child)))); }); Ok(()) diff --git a/packages/desktop/src/index.tsx b/packages/desktop/src/index.tsx index 1b822e26522..51339796bc0 100644 --- a/packages/desktop/src/index.tsx +++ b/packages/desktop/src/index.tsx @@ -1,20 +1,22 @@ // @refresh reload import { render } from "solid-js/web" -import { App, PlatformProvider, Platform } from "@opencode-ai/app" +import { AppBaseProviders, AppInterface, PlatformProvider, Platform } from "@opencode-ai/app" import { open, save } from "@tauri-apps/plugin-dialog" import { open as shellOpen } from "@tauri-apps/plugin-shell" import { type as ostype } from "@tauri-apps/plugin-os" +import { check, Update } from "@tauri-apps/plugin-updater" +import { invoke } from "@tauri-apps/api/core" +import { getCurrentWindow } from "@tauri-apps/api/window" +import { isPermissionGranted, requestPermission } from "@tauri-apps/plugin-notification" +import { relaunch } from "@tauri-apps/plugin-process" import { AsyncStorage } from "@solid-primitives/storage" import { fetch as tauriFetch } from "@tauri-apps/plugin-http" import { Store } from "@tauri-apps/plugin-store" +import { Logo } from "@opencode-ai/ui/logo" +import { Suspense, createResource, ParentProps } from "solid-js" import { UPDATER_ENABLED } from "./updater" import { createMenu } from "./menu" -import { check, Update } from "@tauri-apps/plugin-updater" -import { invoke } from "@tauri-apps/api/core" -import { getCurrentWindow } from "@tauri-apps/api/window" -import { isPermissionGranted, requestPermission } from "@tauri-apps/plugin-notification" -import { relaunch } from "@tauri-apps/plugin-process" import pkg from "../package.json" const root = document.getElementById("root") @@ -200,7 +202,34 @@ render(() => { {ostype() === "macos" && (
)} - + + + + + ) }, root!) + +// Gate component that waits for the server to be ready +function ServerGate(props: ParentProps) { + const [status] = createResource(async () => { + if(window.__OPENCODE__?.serverReady) return; + return await invoke("ensure_server_started") + }) + + return ( + + +
Starting server...
+
+ } + > + {/* Triggers suspense/error boundaries without rendering the returned value */} + {(status(), null)} + {props.children} + + ) +} From 18b3ba14a1f4c70bafb7b9da4081067ef58769fa Mon Sep 17 00:00:00 2001 From: Brendan Allan Date: Wed, 7 Jan 2026 15:16:30 +0800 Subject: [PATCH 3/3] cleanup + formatting --- packages/desktop/src-tauri/src/lib.rs | 9 ++++----- packages/desktop/src/index.tsx | 9 +++------ 2 files changed, 7 insertions(+), 11 deletions(-) diff --git a/packages/desktop/src-tauri/src/lib.rs b/packages/desktop/src-tauri/src/lib.rs index 7bc040c0290..c96abd9f452 100644 --- a/packages/desktop/src-tauri/src/lib.rs +++ b/packages/desktop/src-tauri/src/lib.rs @@ -258,15 +258,13 @@ pub fn run() { .hidden_title(true); } - let _window = window_builder.build().expect("Failed to create window"); + let window = window_builder.build().expect("Failed to create window"); let (tx, rx) = tokio::sync::oneshot::channel(); app.manage(ServerState::new(None, rx)); { let app = app.clone(); - - // Spawn server in background and notify when ready tauri::async_runtime::spawn(async move { let should_spawn_sidecar = !is_server_running(port).await; @@ -300,11 +298,12 @@ pub fn run() { }; app.state::().set_child(child); - let _ = tx.send(res); - if let Some(window) = app.get_webview_window("main") { + if res.is_ok() { let _ = window.eval("window.__OPENCODE__.serverReady = true;"); } + + let _ = tx.send(res); }); } diff --git a/packages/desktop/src/index.tsx b/packages/desktop/src/index.tsx index 30d9c37da05..94cb887f521 100644 --- a/packages/desktop/src/index.tsx +++ b/packages/desktop/src/index.tsx @@ -283,13 +283,12 @@ render(() => { // Gate component that waits for the server to be ready function ServerGate(props: ParentProps) { const [status] = createResource(async () => { - if(window.__OPENCODE__?.serverReady) return; + if (window.__OPENCODE__?.serverReady) return return await invoke("ensure_server_started") }) return ( - < - Suspense + @@ -299,9 +298,7 @@ function ServerGate(props: ParentProps) { > {/* Triggers suspense/error boundaries without rendering the returned value */} {(status(), null)} - - {props.children} - + {props.children} ) }