diff --git a/apps/desktop/src-tauri/src/computer_control/presentation.rs b/apps/desktop/src-tauri/src/computer_control/presentation.rs index d77a8fd8f..42cc333bd 100644 --- a/apps/desktop/src-tauri/src/computer_control/presentation.rs +++ b/apps/desktop/src-tauri/src/computer_control/presentation.rs @@ -1,16 +1,20 @@ //! Native activity hints and local Escape detection never own execution authority. +#[cfg(windows)] use std::sync::atomic::AtomicBool; #[cfg(windows)] use std::sync::atomic::Ordering; +#[cfg(windows)] use tauri::{AppHandle, Manager}; #[derive(Default)] pub struct Presentation { + #[cfg(windows)] pub controlling: AtomicBool, #[cfg(windows)] pub shortcut_ready: AtomicBool, } +#[cfg(windows)] pub fn update_edge(app: &AppHandle, activity: bool) { let monitors = app.available_monitors().unwrap_or_default(); for (index, monitor) in monitors.iter().enumerate().take(16) { diff --git a/apps/desktop/src-tauri/src/state/app_state.rs b/apps/desktop/src-tauri/src/state/app_state.rs index 8e6c8e02a..36c9210fc 100644 --- a/apps/desktop/src-tauri/src/state/app_state.rs +++ b/apps/desktop/src-tauri/src/state/app_state.rs @@ -1,4 +1,5 @@ impl AppState { + #[cfg(test)] pub fn new(data_dir: PathBuf, resource_dir: PathBuf) -> DesktopResult { let desktop_data_dir = crate::desktop_data_dir::DesktopDataDir { effective: data_dir, @@ -391,6 +392,7 @@ impl AppState { self.shutdown_with_deadline(deadline).await; } + #[cfg(any(windows, test))] pub(crate) async fn shutdown_until(&self, deadline: Deadline) { self.shutdown_with_deadline(Some(deadline)).await; } diff --git a/apps/desktop/src-tauri/src/state/desktop_core.rs b/apps/desktop/src-tauri/src/state/desktop_core.rs index 0a46b8ce9..7d77a13a0 100644 --- a/apps/desktop/src-tauri/src/state/desktop_core.rs +++ b/apps/desktop/src-tauri/src/state/desktop_core.rs @@ -1007,255 +1007,6 @@ impl DesktopCore { self.autostart_connections(cancellation).await?; self.get_state().await } - pub async fn configure_remote_setup( - &mut self, - server_url: &str, - pairing_code: &str, - project_path: &str, - cancellation: &CancellationContext, - ) -> DesktopResult { - cancellation.check()?; - self.preflight_providers()?; - let server_url = crate::webcodex::validate_server_url(server_url)?; - let project = self.adapter.inspect_project(project_path).await?; - cancellation.check()?; - let binaries = self.adapter.ensure_binaries(cancellation).await?.clone(); - self.snapshot.binaries = Some(binaries.info()); - crate::runtime_selection::verify_resolved_files(&binaries).await?; - let exposure = if server_url.starts_with("https://") { - Exposure::ExistingHttps { - url: server_url.clone(), - } - } else { - Exposure::None - }; - let topology = RuntimeTopology { - experience: Experience::Full, - server: ServerTopology::Remote { - url: server_url.clone(), - }, - runner: RunnerTopology::Local, - exposure, - enrollment: Enrollment::ManagedPairing, - }; - self.snapshot.topology = Some(topology.clone()); - self.stage_project_scope(project.clone()); - self.config.topology = Some(topology.clone()); - self.config.runtime_autostart = Some(true); - self.config.preferred_connection = Some(RegularConnectionPreference::NoChatGpt); - self.snapshot.readiness = aggregate_readiness( - ServerReadiness::Starting, - RunnerReadiness::Connecting, - if server_url.starts_with("https://") { - ExposureReadiness::Starting - } else { - ExposureReadiness::Degraded - }, - ProjectReadiness::Configured, - ); - self.activity.push( - ActivityEventKind::RemoteConnecting, - "desktop", - ActivityLevel::Info, - "Connecting this computer to the existing WebCodex Server", - ); - self.publish_snapshot(); - - let reusable_identity = runner_identity_from_config(&self.config) - .filter(|identity| same_server(&identity.server_url, &server_url)); - let saved_runner_client_id = stored_runner_client_id(&self.config); - let reusable_observation = match reusable_identity.as_ref() { - Some(identity) => self - .adapter - .observe_runner_connection( - identity, - saved_runner_client_id.as_deref(), - cancellation, - ) - .await - .ok(), - None => None, - }; - let (identity, identity_replaced, runner_client_id) = match ( - reusable_identity, - reusable_observation, - ) { - (Some(identity), Some(observation)) => (identity, false, observation.client_id), - _ => { - if !pairing_code.starts_with("wc_pair_") { - return Err(DesktopError::new( - "pairing_code_invalid", - "The saved Runner identity is not reusable and no new WebCodex pairing code was provided", - "Refresh this Runner connection with a new wc_pair_… code.", - )); - } - let project_identity = self - .adapter - .login_with_pairing( - &server_url, - pairing_code, - &self.data_dir.join("connections"), - &project, - cancellation, - ) - .await?; - let observation = self - .adapter - .observe_runner_connection(&project_identity, None, cancellation) - .await?; - // A remote pairing code is one-shot. Publish the newly - // validated connection identity before Runner/project - // activation so a later readiness failure can retry this - // same credential instead of forcing another pairing. - self.config.topology = Some(topology.clone()); - self.store_identity( - &project, - &project_identity, - None, - Some(observation.client_id.clone()), - ) - .await?; - cancellation.check()?; - (project_identity.runner, true, observation.client_id) - } - }; - - let server_deadline = Deadline::after(SERVER_READY_TIMEOUT); - let server_status = match self - .adapter - .server_status_until( - Some(&server_url), - None, - Some(&identity.user_token_file), - cancellation, - server_deadline, - ) - .await - { - Ok(status) => status, - Err(error) => { - cancellation.check()?; - if server_deadline.is_elapsed() { - return Err(readiness_timeout_error( - "server_unreachable", - "The existing WebCodex Server did not respond before the readiness deadline", - "Check the Server URL and network path, then retry.", - )); - } - return Err(error); - } - }; - cancellation.check()?; - if !server_status.http_reachable { - return Err(DesktopError::new( - "server_unreachable", - "The existing WebCodex Server is not reachable", - "Check the Server URL and network path, then retry.", - )); - } - let runner_deadline = Deadline::after(RUNNER_READY_TIMEOUT); - let replacing_owned_runner = identity_replaced - && process_is_active(self.process_snapshot(ProcessKey::LocalRunner).await); - if replacing_owned_runner { - self.stop_process_until(ProcessKey::LocalRunner, runner_deadline) - .await; - cancellation.check()?; - } - let runner_ready = if identity_replaced { - false - } else { - self.adapter - .runner_ready_until(&identity, cancellation, runner_deadline) - .await - .unwrap_or(false) - }; - cancellation.check()?; - let runner_started = if !runner_ready { - if runner_deadline.is_elapsed() { - return Err(readiness_timeout_error( - "runner_offline", - "Runner did not become connected", - "Check Server reachability and Runner diagnostics, then retry.", - )); - } - self.spawn_configured_runner(&identity, cancellation) - .await?; - true - } else { - false - }; - self.wait_for_runner(&identity, cancellation, runner_deadline, runner_started) - .await?; - let identity = match self - .adapter - .activate_project(&identity, &runner_client_id, &project, cancellation) - .await - { - Ok(identity) => identity, - Err(error) - if matches!( - error.code.as_str(), - "project_activation_capability_unavailable" - | "project_activation_restart_required" - ) => - { - if !can_refresh_legacy_runner(self.process_snapshot(ProcessKey::LocalRunner).await) - { - return Err(DesktopError::new( - "project_activation_legacy_runner", - "This Runner needs to be refreshed before the new project can be activated", - "Refresh the Runner, then select this project again.", - )); - } - let legacy_identity = self - .adapter - .legacy_register_project(&identity, &runner_client_id, &project, cancellation) - .await?; - let legacy_deadline = Deadline::after(RUNNER_READY_TIMEOUT); - self.stop_process_until(ProcessKey::LocalRunner, legacy_deadline) - .await; - if legacy_deadline.is_elapsed() { - return Err(readiness_timeout_error( - "runner_offline", - "Desktop could not refresh its legacy Runner", - "Retry project setup after checking Runner diagnostics.", - )); - } - self.spawn_configured_runner(&legacy_identity, cancellation) - .await?; - self.wait_for_runner(&legacy_identity, cancellation, legacy_deadline, true) - .await?; - legacy_identity - } - Err(error) => return Err(error), - }; - self.wait_for_project(&identity, cancellation).await?; - cancellation.check()?; - self.config.topology = Some(topology); - self.store_identity(&project, &identity, None, Some(runner_client_id)) - .await?; - self.snapshot.readiness = aggregate_readiness( - ServerReadiness::Ready, - RunnerReadiness::Ready, - if server_url.starts_with("https://") { - // HTTPS proves transport shape, not that ChatGPT can reach and - // authenticate to the MCP endpoint. D1 has no canonical remote - // MCP/handoff probe, so keep this explicitly unverified. - ExposureReadiness::Unknown - } else { - ExposureReadiness::Degraded - }, - ProjectReadiness::Ready, - ); - self.activity.push( - ActivityEventKind::RemoteConnected, - "desktop", - ActivityLevel::Info, - "This computer is connected to the existing WebCodex Server", - ); - self.get_state().await - } - pub async fn start_quick_share( &mut self, project_path: &str, @@ -1704,29 +1455,6 @@ impl DesktopCore { } } - async fn store_identity( - &mut self, - project: &ProjectSelection, - identity: &ProjectRuntimeIdentity, - server_env_file: Option, - runner_client_id: Option, - ) -> DesktopResult<()> { - let mut project = project.clone(); - project.runtime_project_id = Some(identity.runtime_project_id.clone()); - self.config.project = Some(project.clone()); - self.config.runtime = Some(StoredRuntime { - server_url: identity.server_url.clone(), - server_env_file, - runner_config: Some(identity.runner_config.clone()), - user_token_file: Some(identity.user_token_file.clone()), - runner_client_id, - project_id: Some(identity.project_id.clone()), - runtime_project_id: Some(identity.runtime_project_id.clone()), - }); - self.snapshot.project = Some(project); - self.save_config().await - } - async fn save_config(&mut self) -> DesktopResult<()> { tokio::fs::create_dir_all(&self.data_dir) .await diff --git a/apps/desktop/src-tauri/src/state/tests.rs b/apps/desktop/src-tauri/src/state/tests.rs index 8a0e95fc8..d9536a2ca 100644 --- a/apps/desktop/src-tauri/src/state/tests.rs +++ b/apps/desktop/src-tauri/src/state/tests.rs @@ -1387,105 +1387,6 @@ mod tests { let _ = std::fs::remove_dir_all(&data_dir); } - #[tokio::test] - #[ignore = "requires current-source dogfood binaries and a temporary project"] - async fn windows_remote_full_dogfood_reuses_enrollment_without_local_server() { - if !cfg!(windows) { - return; - } - let project = std::env::var("WEBCODEX_DESKTOP_DOGFOOD_PROJECT") - .expect("WEBCODEX_DESKTOP_DOGFOOD_PROJECT must point to the temporary fixture"); - let suffix = std::process::id(); - let host_data = - std::env::temp_dir().join(format!("webcodex-desktop-remote-host-dogfood-{suffix}")); - let client_data = - std::env::temp_dir().join(format!("webcodex-desktop-remote-client-dogfood-{suffix}")); - let _ = std::fs::remove_dir_all(&host_data); - let _ = std::fs::remove_dir_all(&client_data); - - let mut host = DesktopCore::new(host_data.clone(), host_data.join("test-resources")) - .expect("create remote dogfood host state"); - let cancellation = CancellationContext::never(); - let host_runtime = host_data.join("runtime"); - let env_file = host_runtime.join("webcodex.env"); - let data_dir = host_runtime.join("data"); - tokio::fs::create_dir_all(&host_runtime) - .await - .expect("create remote dogfood host state"); - let listen = reserve_loopback_address().expect("reserve remote dogfood host port"); - let server_url = host - .adapter - .init_local_server(&listen, &data_dir, &env_file, &cancellation) - .await - .expect("initialize remote dogfood Server") - .probe_url; - let command = host - .adapter - .local_server_command(&env_file) - .expect("build remote dogfood Server command"); - host.spawn_owned(ProcessKey::LocalServer, command, false, &cancellation) - .await - .expect("start remote dogfood Server"); - - let mut client = DesktopCore::new(client_data.clone(), client_data.join("test-resources")) - .expect("create remote dogfood client state"); - let result: DesktopResult<(DesktopStateSnapshot, DesktopStateSnapshot, bool, bool)> = - async { - host.wait_for_server( - &server_url, - Some(&env_file), - None, - &cancellation, - Deadline::after(SERVER_READY_TIMEOUT), - true, - ) - .await?; - let pairing_code = host - .adapter - .create_local_pairing(&server_url, &env_file, &cancellation) - .await?; - let first = client - .configure_remote_setup(&server_url, &pairing_code, &project, &cancellation) - .await?; - let first_started_server = client - .process_snapshot(ProcessKey::LocalServer) - .await - .is_some(); - client.stop_local_runtime(&cancellation).await?; - - let second = client - .configure_remote_setup(&server_url, "", &project, &cancellation) - .await?; - let second_started_server = client - .process_snapshot(ProcessKey::LocalServer) - .await - .is_some(); - client.stop_local_runtime(&cancellation).await?; - Ok((first, second, first_started_server, second_started_server)) - } - .await; - - client.supervisor.lock().await.stop_all().await; - host.supervisor.lock().await.stop_all().await; - let _ = std::fs::remove_dir_all(&client_data); - let _ = std::fs::remove_dir_all(&host_data); - - let (first, second, first_started_server, second_started_server) = - result.expect("remote full dogfood should complete"); - for snapshot in [&first, &second] { - assert_eq!(snapshot.readiness.server, ServerReadiness::Ready); - assert_eq!(snapshot.readiness.runner, RunnerReadiness::Ready); - assert_eq!(snapshot.readiness.project, ProjectReadiness::Ready); - assert!(snapshot.readiness.runtime_ready); - assert!(matches!( - snapshot.topology.as_ref().map(|topology| &topology.server), - Some(ServerTopology::Remote { .. }) - )); - } - assert!(!first_started_server); - assert!(!second_started_server); - } - #[test] fn configured_https_exposure_stays_unverified_without_mcp_handoff_evidence() { let local = RuntimeTopology { diff --git a/apps/desktop/src-tauri/src/tray.rs b/apps/desktop/src-tauri/src/tray.rs index c2dac74bc..1331448b3 100644 --- a/apps/desktop/src-tauri/src/tray.rs +++ b/apps/desktop/src-tauri/src/tray.rs @@ -263,6 +263,7 @@ pub fn start_background_observer(app: &AppHandle) { }); } +#[cfg(windows)] pub fn start_computer_observer(app: &AppHandle) { let app = app.clone(); tauri::async_runtime::spawn(async move { @@ -613,6 +614,7 @@ pub(crate) fn handle_menu_event(app: &AppHandle, id: &str) { } } +#[cfg(windows)] pub(crate) fn pause_computer(app: &AppHandle) { let observed = app .state::() diff --git a/apps/desktop/src-tauri/src/webcodex/mod.rs b/apps/desktop/src-tauri/src/webcodex/mod.rs index a2e297a7b..ab547d709 100644 --- a/apps/desktop/src-tauri/src/webcodex/mod.rs +++ b/apps/desktop/src-tauri/src/webcodex/mod.rs @@ -4,8 +4,7 @@ mod models; pub mod settings; pub use adapter::{ - inspect_project_path, validate_server_url, ProjectRuntimeIdentity, RunnerRuntimeIdentity, - WebCodexAdapter, + inspect_project_path, ProjectRuntimeIdentity, RunnerRuntimeIdentity, WebCodexAdapter, }; #[cfg(all(test, target_os = "macos"))] pub(crate) use cli::run_test_bounded;