From ad271990946cea7e0f41fec6610099b7c58815b5 Mon Sep 17 00:00:00 2001 From: lihong Date: Thu, 24 Sep 2026 16:04:36 +0800 Subject: [PATCH 1/2] feat(desktop): load external web dir via CODEG_STATIC_DIR env var Setting CODEG_STATIC_DIR to a directory containing index.html makes the desktop window load from the built-in HTTP server (which serves that dir) instead of the embedded frontend assets. This lets `pnpm build` + restart pick up UI changes without recompiling Rust. The web server is force-started when the env var is set, even if auto_start is off in the persisted config. --- src-tauri/src/lib.rs | 38 ++++++++++++++++++++++++++++++++++++-- src-tauri/src/web/mod.rs | 30 ++++++++++++++++++++++++++++++ 2 files changed, 66 insertions(+), 2 deletions(-) diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 496b633187..d006e99477 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -1120,7 +1120,24 @@ mod tauri_app { notify_web_auto_start_failed(app.handle(), port, &err); } } - Ok(_) => {} + Ok(config) => { + // `CODEG_STATIC_DIR` forces the web server on so the + // desktop window can load from an external directory. + if web::external_static_dir().is_some() { + let ws = app.state::(); + if let Err(err) = + tauri::async_runtime::block_on(web::do_start_web_server_tauri( + app.handle().clone(), + &ws, + config.port, + None, + config.token, + )) + { + tracing::error!("[WEB] CODEG_STATIC_DIR force-start failed: {err}"); + } + } + } Err(err) => tracing::error!("[WEB] failed to load auto-start config: {err}"), } @@ -1248,7 +1265,24 @@ mod tauri_app { // restored by the frontend via `list_open_folder_details` / // `list_opened_tabs` inside the main window. if app.get_webview_window("main").is_none() { - let url = tauri::WebviewUrl::App(workspace_path.into()); + let url = if web::external_static_dir().is_some() { + let port = app + .state::() + .running_port(); + let path = if workspace_path.starts_with('/') { + workspace_path + } else { + format!("/{workspace_path}") + }; + let external = + format!("http://127.0.0.1:{port}{path}"); + tracing::info!( + "[WEB] Desktop window loading from external dir: {external}" + ); + tauri::WebviewUrl::External(external.parse().unwrap()) + } else { + tauri::WebviewUrl::App(workspace_path.into()) + }; let builder = tauri::WebviewWindowBuilder::new(app, "main", url) .title("Codeg") .inner_size(1260.0, 860.0) diff --git a/src-tauri/src/web/mod.rs b/src-tauri/src/web/mod.rs index 693ce4f8b9..e9897a702e 100644 --- a/src-tauri/src/web/mod.rs +++ b/src-tauri/src/web/mod.rs @@ -74,6 +74,10 @@ impl WebServerState { self.shutdown_signal.clone() } + pub fn running_port(&self) -> u16 { + self.port.load(Ordering::Relaxed) + } + /// Mark the server as running from outside the Tauri command path. /// `codeg-server` calls `axum::serve` directly without going through /// `start_web_server`, so without this the `running` flag stays @@ -314,9 +318,35 @@ fn classify_bind_error(err: std::io::Error) -> AppCommandError { AppCommandError::new(code, key).with_detail(err.to_string()) } +/// Returns the path set by `CODEG_STATIC_DIR` if it contains an `index.html`. +#[cfg(feature = "tauri-runtime")] +pub(crate) fn external_static_dir() -> Option { + let raw = std::env::var("CODEG_STATIC_DIR").ok()?; + let p = PathBuf::from(&raw); + if p.join("index.html").exists() { + Some(p) + } else { + tracing::warn!( + "[WEB] CODEG_STATIC_DIR set but no index.html in: {}", + raw + ); + None + } +} + #[cfg(feature = "tauri-runtime")] pub(crate) fn find_static_dir_tauri(app: &tauri::AppHandle) -> PathBuf { use tauri::Manager; + // 0. External override: `CODEG_STATIC_DIR` forces the desktop window to + // load from a local directory instead of the embedded assets, so a + // frontend rebuild (`pnpm build`) takes effect without recompiling Rust. + if let Some(dir) = external_static_dir() { + tracing::info!( + "[WEB] Serving static files from CODEG_STATIC_DIR: {}", + dir.display() + ); + return dir; + } // 1. Production: bundle.resources copies out/ → web/ inside the resource directory. let resource = app.path().resource_dir().ok(); if let Some(ref dir) = resource { From e0a0ef2d21f9ad40daa34fabd2334bb6f90e5d38 Mon Sep 17 00:00:00 2001 From: lihong Date: Mon, 19 Oct 2026 12:20:18 +0800 Subject: [PATCH 2/2] fix(desktop): gate external window URL on a live port and grant remote capability A failed port bind left WebServerState.running_port at 0, so the main window could open http://127.0.0.1:0 while CODEG_STATIC_DIR was set. The URL choice now lives in resolve_main_window_url: the external origin is used only with a bound port, otherwise the window falls back to the bundled assets with an error log; a force-start failure also raises a system notification. Both capability files grant http://127.0.0.1:* via remote.urls so Tauri IPC works on the externally-served page. Covers review findings on #829. --- src-tauri/capabilities/default.json | 3 + src-tauri/capabilities/desktop.json | 3 + src-tauri/src/lib.rs | 118 +++++++++++++++++++++++----- 3 files changed, 106 insertions(+), 18 deletions(-) diff --git a/src-tauri/capabilities/default.json b/src-tauri/capabilities/default.json index dff99c315d..944d9703da 100644 --- a/src-tauri/capabilities/default.json +++ b/src-tauri/capabilities/default.json @@ -22,6 +22,9 @@ "remote-stash-*", "remote-push-*" ], + "remote": { + "urls": ["http://127.0.0.1:*"] + }, "permissions": [ "core:default", "core:window:default", diff --git a/src-tauri/capabilities/desktop.json b/src-tauri/capabilities/desktop.json index 11969a69d6..d0284292a0 100644 --- a/src-tauri/capabilities/desktop.json +++ b/src-tauri/capabilities/desktop.json @@ -21,5 +21,8 @@ "remote-stash-*", "remote-push-*" ], + "remote": { + "urls": ["http://127.0.0.1:*"] + }, "permissions": ["window-state:default", "deep-link:default"] } diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index d006e99477..83cac978c4 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -258,6 +258,50 @@ mod tauri_app { }); } + /// URL the `main` window opens with. + /// + /// With `CODEG_STATIC_DIR` set, the built-in web server serves that + /// directory and the window loads it over HTTP, so a frontend rebuild + /// takes effect without recompiling Rust. But only once the server + /// actually holds a port: a failed bind (e.g. a port collision) leaves + /// `WebServerState::running_port` at 0, and `http://127.0.0.1:0` is not + /// a page. On that failure — or on a URL that will not parse — fall back + /// to the bundled assets and log an actionable error. + fn resolve_main_window_url( + workspace_path: String, + external_dir_set: bool, + running_port: u16, + ) -> tauri::WebviewUrl { + let mut fallback = workspace_path; + if external_dir_set && running_port > 0 { + let path = if fallback.starts_with('/') { + fallback.clone() + } else { + format!("/{fallback}") + }; + let external = format!("http://127.0.0.1:{running_port}{path}"); + match external.parse() { + Ok(url) => { + tracing::info!("[WEB] Desktop window loading from external dir: {external}"); + return tauri::WebviewUrl::External(url); + } + Err(err) => { + tracing::error!( + "[WEB] Invalid CODEG_STATIC_DIR window URL {external}: {err}; \ + opening the bundled UI instead." + ); + fallback = path; + } + } + } else if external_dir_set { + tracing::error!( + "[WEB] CODEG_STATIC_DIR is set but the web server is not listening \ + (port bind failed?); opening the bundled UI instead." + ); + } + tauri::WebviewUrl::App(fallback.into()) + } + /// Chromium command line WebView2 appends to its own when launching. #[cfg(target_os = "windows")] const WEBVIEW2_ARGS_ENV: &str = "WEBVIEW2_ADDITIONAL_BROWSER_ARGUMENTS"; @@ -1135,6 +1179,11 @@ mod tauri_app { )) { tracing::error!("[WEB] CODEG_STATIC_DIR force-start failed: {err}"); + notify_web_auto_start_failed( + app.handle(), + config.port.unwrap_or(web::DEFAULT_WEB_SERVICE_PORT), + &err, + ); } } } @@ -1265,24 +1314,11 @@ mod tauri_app { // restored by the frontend via `list_open_folder_details` / // `list_opened_tabs` inside the main window. if app.get_webview_window("main").is_none() { - let url = if web::external_static_dir().is_some() { - let port = app - .state::() - .running_port(); - let path = if workspace_path.starts_with('/') { - workspace_path - } else { - format!("/{workspace_path}") - }; - let external = - format!("http://127.0.0.1:{port}{path}"); - tracing::info!( - "[WEB] Desktop window loading from external dir: {external}" - ); - tauri::WebviewUrl::External(external.parse().unwrap()) - } else { - tauri::WebviewUrl::App(workspace_path.into()) - }; + let url = resolve_main_window_url( + workspace_path, + web::external_static_dir().is_some(), + app.state::().running_port(), + ); let builder = tauri::WebviewWindowBuilder::new(app, "main", url) .title("Codeg") .inner_size(1260.0, 860.0) @@ -2101,6 +2137,52 @@ mod tauri_app { _ => {} }); } + + #[cfg(test)] + mod main_window_url_tests { + use super::resolve_main_window_url; + use std::path::Path; + + fn external(url: tauri::WebviewUrl) -> Option { + match url { + tauri::WebviewUrl::External(u) => Some(u.to_string()), + tauri::WebviewUrl::App(_) => None, + _ => None, + } + } + + #[test] + fn external_dir_with_live_port_loads_the_served_url() { + let url = resolve_main_window_url("/?folder=1".into(), true, 3129); + assert_eq!( + external(url).as_deref(), + Some("http://127.0.0.1:3129/?folder=1") + ); + } + + #[test] + fn missing_leading_slash_is_normalized_for_the_external_url() { + let url = resolve_main_window_url("?folder=1".into(), true, 3129); + assert_eq!( + external(url).as_deref(), + Some("http://127.0.0.1:3129/?folder=1") + ); + } + + #[test] + fn port_bind_failure_falls_back_to_the_bundled_app_url() { + // A failed bind leaves `running_port` at 0; the window must open + // the embedded assets, not `http://127.0.0.1:0/`. + let url = resolve_main_window_url("/?folder=1".into(), true, 0); + assert!(matches!(url, tauri::WebviewUrl::App(p) if p == Path::new("/?folder=1"))); + } + + #[test] + fn no_external_dir_keeps_the_bundled_app_url_even_with_a_server() { + let url = resolve_main_window_url("/".into(), false, 3129); + assert!(matches!(url, tauri::WebviewUrl::App(p) if p == Path::new("/"))); + } + } } #[cfg(feature = "tauri-runtime")]