diff --git a/crates/app-tauri/src/embedded_server.rs b/crates/app-tauri/src/embedded_server.rs index 56391d0..0684543 100644 --- a/crates/app-tauri/src/embedded_server.rs +++ b/crates/app-tauri/src/embedded_server.rs @@ -162,6 +162,7 @@ impl FsServerExposureSettingsStore { /// Desktop-owned embedded server manager. pub struct EmbeddedServerController { app_data_dir: PathBuf, + resource_dir: Option, store: FsServerExposureSettingsStore, inner: Mutex, } @@ -170,9 +171,16 @@ impl EmbeddedServerController { /// Creates the controller. #[must_use] pub fn new(app_data_dir: PathBuf) -> Self { + Self::with_resource_dir(app_data_dir, None) + } + + /// Creates the controller with the Tauri bundle resource directory. + #[must_use] + pub fn with_resource_dir(app_data_dir: PathBuf, resource_dir: Option) -> Self { Self { store: FsServerExposureSettingsStore::new(app_data_dir.clone()), app_data_dir, + resource_dir, inner: Mutex::new(EmbeddedServerInner::default()), } } @@ -237,7 +245,7 @@ impl EmbeddedServerController { let config = match server_config_from_settings( &settings, self.app_data_dir.clone(), - resolve_web_root()?, + resolve_web_root(self.resource_dir.as_deref())?, ) { Ok(config) => config, Err(err) => { @@ -487,11 +495,25 @@ fn parse_ip(value: &str, field: &str) -> Result { .map_err(|_| invalid_error(format!("{field} must be an IP address"))) } -fn resolve_web_root() -> Result { +fn resolve_web_root(resource_dir: Option<&Path>) -> Result { + let explicit_web_root = std::env::var_os("IDEA_WEB_ROOT").map(PathBuf::from); + let candidates = web_root_candidates(explicit_web_root, resource_dir); + first_web_root(candidates).ok_or_else(|| { + invalid_error("web assets not found: build frontend/dist or set IDEA_WEB_ROOT") + }) +} + +fn web_root_candidates( + explicit_web_root: Option, + resource_dir: Option<&Path>, +) -> Vec { let mut candidates = Vec::new(); - if let Some(path) = std::env::var_os("IDEA_WEB_ROOT").map(PathBuf::from) { + if let Some(path) = explicit_web_root { candidates.push(path); } + if let Some(resource_dir) = resource_dir { + candidates.push(resource_dir.join("web")); + } if let Ok(exe) = std::env::current_exe() { if let Some(dir) = exe.parent() { candidates.push(dir.join("web")); @@ -501,12 +523,13 @@ fn resolve_web_root() -> Result { if let Ok(cwd) = std::env::current_dir() { candidates.push(cwd.join("frontend").join("dist")); } + candidates +} + +fn first_web_root(candidates: impl IntoIterator) -> Option { candidates .into_iter() .find(|candidate| candidate.join("index.html").is_file()) - .ok_or_else(|| { - invalid_error("web assets not found: build frontend/dist or set IDEA_WEB_ROOT") - }) } fn write_atomic_user_only(path: &Path, bytes: &[u8]) -> std::io::Result<()> { @@ -588,6 +611,36 @@ mod tests { web_root } + #[test] + fn web_root_candidates_keep_packaged_resource_before_exe_fallbacks() { + let explicit = tmp_app_data().join("explicit-web-root"); + let resource_dir = tmp_app_data().join("resources"); + + let candidates = web_root_candidates(Some(explicit.clone()), Some(&resource_dir)); + + assert_eq!(candidates[0], explicit); + assert_eq!(candidates[1], resource_dir.join("web")); + } + + #[test] + fn first_web_root_uses_first_candidate_with_index_html() { + let missing = tmp_app_data().join("missing-web-root"); + let resource_dir = tmp_app_data().join("resources"); + let resource_web = resource_dir.join("web"); + std::fs::create_dir_all(&resource_web).unwrap(); + std::fs::write( + resource_web.join("index.html"), + "Packaged", + ) + .unwrap(); + let later = tmp_web_root(); + + let resolved = first_web_root([missing, resource_web.clone(), later]) + .expect("web root should resolve"); + + assert_eq!(resolved, resource_web); + } + #[test] fn non_loopback_remote_requires_trusted_proxy() { let settings = ServerExposureSettingsDto { diff --git a/crates/app-tauri/src/lib.rs b/crates/app-tauri/src/lib.rs index 5ad0a59..d1b160b 100644 --- a/crates/app-tauri/src/lib.rs +++ b/crates/app-tauri/src/lib.rs @@ -96,13 +96,14 @@ pub fn run() { .path() .app_data_dir() .expect("failed to resolve the app data directory"); + let resource_dir = app.path().resource_dir().ok(); // Point the orchestrator's best-effort diagnostics at a persistent file // (`/logs/idea.log`) so inter-agent rendezvous beacons survive a // click-launched AppImage (whose stderr is otherwise discarded). Best-effort: // if the file can't be opened the beacons simply stay on stderr. application::diag::set_log_path(app_data_dir.join("logs").join("idea.log")); application::diag!("[startup] IdeA launched; diagnostics log armed"); - let app_state = AppState::build(app_data_dir); + let app_state = AppState::build_with_resource_dir(app_data_dir, resource_dir); // Wire the domain event bus → Tauri events relay. events::spawn_relay(app.handle().clone(), &app_state.event_bus); diff --git a/crates/app-tauri/src/state.rs b/crates/app-tauri/src/state.rs index e3aaa86..06f7933 100644 --- a/crates/app-tauri/src/state.rs +++ b/crates/app-tauri/src/state.rs @@ -51,6 +51,13 @@ impl AppState { /// into the backend core. #[must_use] pub fn build(app_data_dir: PathBuf) -> Self { + Self::build_with_resource_dir(app_data_dir, None) + } + + /// Builds the shared backend core with the Tauri bundle resource directory + /// available to desktop-only adapters. + #[must_use] + pub fn build_with_resource_dir(app_data_dir: PathBuf, resource_dir: Option) -> Self { let core = Arc::new(BackendCore::build(app_data_dir.clone())); core.ticket_tool_binder .bind(Arc::new(AppTicketToolProvider { @@ -69,7 +76,10 @@ impl AppState { core, pty_bridge: Arc::new(PtyBridge::new()), chat_bridge: Arc::new(ChatBridge::new()), - embedded_server: Arc::new(EmbeddedServerController::new(app_data_dir)), + embedded_server: Arc::new(EmbeddedServerController::with_resource_dir( + app_data_dir, + resource_dir, + )), focused_project: Mutex::new(None), } } diff --git a/crates/app-tauri/tauri.conf.json b/crates/app-tauri/tauri.conf.json index 740f159..fd84eab 100644 --- a/crates/app-tauri/tauri.conf.json +++ b/crates/app-tauri/tauri.conf.json @@ -6,7 +6,8 @@ "build": { "frontendDist": "../../frontend/dist", "devUrl": "http://localhost:5173", - "beforeDevCommand": "npm --prefix ../frontend run dev" + "beforeDevCommand": "npm --prefix ../frontend run dev", + "beforeBuildCommand": "npm --prefix ../frontend run build" }, "app": { "windows": [ @@ -24,6 +25,9 @@ "bundle": { "active": true, "targets": ["appimage", "nsis"], - "icon": ["icons/icon.png"] + "icon": ["icons/icon.png"], + "resources": { + "../../frontend/dist": "web" + } } }