diff --git a/packages/desktop/scripts/dev-web-server.mjs b/packages/desktop/scripts/dev-web-server.mjs index 6a1630d4..cdc580da 100644 --- a/packages/desktop/scripts/dev-web-server.mjs +++ b/packages/desktop/scripts/dev-web-server.mjs @@ -35,6 +35,7 @@ const sidecarName = process.platform === 'win32' const sidecarPath = path.join(tauriDir, 'sidecars', sidecarName); const distDir = path.join(tauriDir, 'resources', 'web-dist'); +const webDir = path.join(repoRoot, 'packages', 'web'); const run = (cmd, args, cwd) => { const result = spawnSync(cmd, args, { cwd, stdio: 'inherit' }); @@ -47,9 +48,9 @@ const run = (cmd, args, cwd) => { console.log('[desktop] ensuring sidecar + web-dist...'); run('node', ['./scripts/build-sidecar.mjs'], desktopDir); -console.log('[desktop] starting dev server on http://127.0.0.1:3001 ...'); +console.log('[desktop] starting API server on http://127.0.0.1:3001 ...'); -const child = spawn(sidecarPath, ['--port', '3001'], { +const apiChild = spawn(sidecarPath, ['--port', '3001'], { cwd: repoRoot, stdio: 'inherit', env: { @@ -61,14 +62,62 @@ const child = spawn(sidecarPath, ['--port', '3001'], { }, }); +console.log('[desktop] starting Vite HMR server on http://127.0.0.1:5173 ...'); + +const webChild = spawn('bun', ['x', 'vite', '--host', '127.0.0.1', '--port', '5173', '--strictPort'], { + cwd: webDir, + stdio: 'inherit', + env: { + ...process.env, + OPENCHAMBER_PORT: process.env.OPENCHAMBER_PORT || '3001', + NO_PROXY: process.env.NO_PROXY || 'localhost,127.0.0.1', + no_proxy: process.env.no_proxy || 'localhost,127.0.0.1', + }, +}); + +let shuttingDown = false; + const shutdown = () => { + if (shuttingDown) return; + shuttingDown = true; + try { - child.kill('SIGTERM'); - } catch { - // ignore - } + apiChild.kill('SIGTERM'); + } catch {} + + try { + webChild.kill('SIGTERM'); + } catch {} }; +const handleExit = (label) => (code, signal) => { + if (shuttingDown) { + return; + } + + if (code !== 0 || signal) { + console.error(`[desktop] ${label} exited unexpectedly (code=${code ?? 'null'} signal=${signal ?? 'none'})`); + } + + shutdown(); + process.exit(typeof code === 'number' ? code : 1); +}; + +apiChild.on('exit', handleExit('API server')); +webChild.on('exit', handleExit('Vite server')); + +const handleError = (label) => (error) => { + if (shuttingDown) { + return; + } + console.error(`[desktop] failed to start ${label}:`, error); + shutdown(); + process.exit(1); +}; + +apiChild.on('error', handleError('API server')); +webChild.on('error', handleError('Vite server')); + process.on('SIGINT', shutdown); process.on('SIGTERM', shutdown); process.on('exit', shutdown); diff --git a/packages/desktop/src-tauri/src/main.rs b/packages/desktop/src-tauri/src/main.rs index bc00c4aa..998e7e86 100644 --- a/packages/desktop/src-tauri/src/main.rs +++ b/packages/desktop/src-tauri/src/main.rs @@ -1504,7 +1504,6 @@ fn main() { .setup(|app| { let handle = app.handle().clone(); tauri::async_runtime::spawn(async move { - // Always ensure local server is running for escape hatch. let local_url = if cfg!(debug_assertions) { let dev_url = "http://127.0.0.1:3001"; if wait_for_health(dev_url).await { @@ -1528,29 +1527,41 @@ fn main() { } }; + let local_ui_url = if cfg!(debug_assertions) { + let vite_url = "http://127.0.0.1:5173"; + if wait_for_health(vite_url).await { + vite_url.to_string() + } else { + log::warn!("[desktop] Vite dev server not ready, using local API UI at {local_url}"); + local_url.clone() + } + } else { + local_url.clone() + }; + // Ensure local URL is always available to desktop commands, // even when we are using the Vite dev server (no sidecar child). if let Some(state) = handle.try_state::() { *state.url.lock().expect("sidecar url mutex") = Some(local_url.clone()); } - let local_origin = url::Url::parse(&local_url) + let local_origin = url::Url::parse(&local_ui_url) .ok() .map(|u| u.origin().ascii_serialization()) - .unwrap_or_else(|| local_url.clone()); + .unwrap_or_else(|| local_ui_url.clone()); // Selected host: env override first, then desktop default host, else local. let env_target = std::env::var("OPENCHAMBER_SERVER_URL") .ok() .and_then(|raw| normalize_server_url(&raw)); - let mut initial_url = env_target.unwrap_or_else(|| local_url.clone()); + let mut initial_url = env_target.unwrap_or_else(|| local_ui_url.clone()); - if initial_url == local_url { + if initial_url == local_ui_url { let cfg = read_desktop_hosts_config_from_disk(); if let Some(default_id) = cfg.default_host_id { if default_id == LOCAL_HOST_ID { - initial_url = local_url.clone(); + initial_url = local_ui_url.clone(); } else if let Some(host) = cfg.hosts.into_iter().find(|h| h.id == default_id) { initial_url = host.url; } diff --git a/packages/web/vite.config.ts b/packages/web/vite.config.ts index a08fe2bb..a821879f 100644 --- a/packages/web/vite.config.ts +++ b/packages/web/vite.config.ts @@ -56,6 +56,14 @@ export default defineConfig({ server: { port: 5173, proxy: { + '/auth': { + target: `http://127.0.0.1:${process.env.OPENCHAMBER_PORT || 3001}`, + changeOrigin: true, + }, + '/health': { + target: `http://127.0.0.1:${process.env.OPENCHAMBER_PORT || 3001}`, + changeOrigin: true, + }, '/api': { target: `http://127.0.0.1:${process.env.OPENCHAMBER_PORT || 3001}`, changeOrigin: true,