diff --git a/packages/desktop/src-tauri/src/lib.rs b/packages/desktop/src-tauri/src/lib.rs index 92f20b19a..c04b0a1ca 100644 --- a/packages/desktop/src-tauri/src/lib.rs +++ b/packages/desktop/src-tauri/src/lib.rs @@ -215,23 +215,38 @@ fn maybe_run_managed_headless_command(app: &AppHandle) -> Result { Ok(true) } -fn resolve_login_shell() -> String { - std::env::var("SHELL") - .ok() - .map(|value| value.trim().to_string()) - .filter(|value| !value.is_empty()) - .unwrap_or_else(|| "/bin/zsh".to_string()) +fn shell_command(script: &str) -> Command { + #[cfg(unix)] + { + let shell = std::env::var("SHELL") + .ok() + .map(|value| value.trim().to_string()) + .filter(|value| !value.is_empty()) + .unwrap_or_else(|| "/bin/zsh".to_string()); + let mut cmd = Command::new(shell); + cmd.arg("-lc").arg(script); + cmd + } + #[cfg(windows)] + { + let mut cmd = Command::new("cmd.exe"); + cmd.arg("/C").arg(script); + cmd + } } -fn execute_local_daemon_version(shell: &str) -> LocalDaemonVersionResult { +fn execute_local_daemon_version() -> LocalDaemonVersionResult { + #[cfg(unix)] let script = r#"if command -v paseo >/dev/null 2>&1; then paseo --version else echo "paseo command not found in PATH" >&2 exit 127 fi"#; + #[cfg(windows)] + let script = r#"where paseo >nul 2>&1 && paseo --version || (echo paseo command not found in PATH >&2 & exit /b 127)"#; - match Command::new(shell).arg("-lc").arg(script).output() { + match shell_command(script).output() { Ok(output) => { if output.status.success() { let version = String::from_utf8_lossy(&output.stdout).trim().to_string(); @@ -268,15 +283,18 @@ fi"#; } } -fn execute_local_daemon_update(shell: &str) -> DaemonUpdateCommandResult { +fn execute_local_daemon_update() -> DaemonUpdateCommandResult { + #[cfg(unix)] let script = r#"if command -v paseo >/dev/null 2>&1; then paseo daemon update else echo "paseo command not found in PATH. Ensure Paseo CLI is installed for this user." >&2 exit 127 fi"#; + #[cfg(windows)] + let script = r#"where paseo >nul 2>&1 && paseo daemon update || (echo paseo command not found in PATH. Ensure Paseo CLI is installed for this user. >&2 & exit /b 127)"#; - match Command::new(shell).arg("-lc").arg(script).output() { + match shell_command(script).output() { Ok(output) => DaemonUpdateCommandResult { exit_code: output.status.code().unwrap_or(1), stdout: String::from_utf8_lossy(&output.stdout).into_owned(), @@ -292,8 +310,7 @@ fi"#; #[tauri::command] async fn get_local_daemon_version() -> LocalDaemonVersionResult { - let shell = resolve_login_shell(); - tauri::async_runtime::spawn_blocking(move || execute_local_daemon_version(&shell)) + tauri::async_runtime::spawn_blocking(execute_local_daemon_version) .await .unwrap_or_else(|error| LocalDaemonVersionResult { version: None, @@ -303,8 +320,7 @@ async fn get_local_daemon_version() -> LocalDaemonVersionResult { #[tauri::command] async fn run_local_daemon_update() -> DaemonUpdateCommandResult { - let shell = resolve_login_shell(); - tauri::async_runtime::spawn_blocking(move || execute_local_daemon_update(&shell)) + tauri::async_runtime::spawn_blocking(execute_local_daemon_update) .await .unwrap_or_else(|error| DaemonUpdateCommandResult { exit_code: -1, diff --git a/packages/desktop/src-tauri/src/runtime_manager.rs b/packages/desktop/src-tauri/src/runtime_manager.rs index 6920d6f6b..227a680b1 100644 --- a/packages/desktop/src-tauri/src/runtime_manager.rs +++ b/packages/desktop/src-tauri/src/runtime_manager.rs @@ -561,15 +561,25 @@ fn dev_resource_root() -> PathBuf { fn bundled_runtime_root(app: &AppHandle) -> Result { if let Ok(resource_dir) = app.path().resource_dir() { - let candidate = resource_dir.join("managed-runtime"); - if candidate.exists() { - log::info!("[runtime] found bundled runtime at {}", candidate.display()); - return Ok(candidate); + // Tauri's bundle.resources preserves the source directory structure, so + // "resources/**/*" in tauri.conf.json places files at + // $RESOURCE/resources/managed-runtime/. Try the nested path first (installed + // builds), then the flat path for any future config changes. + for candidate in [ + resource_dir.join("resources").join("managed-runtime"), + resource_dir.join("managed-runtime"), + ] { + if candidate.exists() { + log::info!("[runtime] found bundled runtime at {}", candidate.display()); + return Ok(candidate); + } + log::info!( + "[runtime] no bundled runtime at {}", + candidate.display() + ); } - log::info!( - "[runtime] no bundled runtime at {}, checking dev path", - candidate.display() - ); + } else { + log::info!("[runtime] resource_dir() unavailable, checking dev path"); } let dev = dev_resource_root().join("managed-runtime"); if dev.exists() { @@ -624,10 +634,19 @@ fn is_pid_running(pid: i32) -> bool { .status(); return output.map(|status| status.success()).unwrap_or(false); } - #[cfg(not(unix))] + #[cfg(windows)] { - let _ = pid; - false + let output = Command::new("tasklist") + .args(["/FI", &format!("PID eq {pid}"), "/NH", "/FO", "CSV"]) + .stdout(Stdio::piped()) + .stderr(Stdio::null()) + .output(); + return output + .map(|out| { + let stdout = String::from_utf8_lossy(&out.stdout); + stdout.contains(&pid.to_string()) + }) + .unwrap_or(false); } } diff --git a/packages/server/src/server/agent/providers/claude-agent.ts b/packages/server/src/server/agent/providers/claude-agent.ts index b5bf3d503..33d642ce2 100644 --- a/packages/server/src/server/agent/providers/claude-agent.ts +++ b/packages/server/src/server/agent/providers/claude-agent.ts @@ -3269,25 +3269,35 @@ class ClaudeAgentSession implements AgentSession { } try { this.logger.trace("interruptActiveTurn: calling query.interrupt()..."); - const t0 = Date.now(); - await queryToInterrupt.interrupt(); - this.logger.trace({ durationMs: Date.now() - t0 }, "interruptActiveTurn: query.interrupt() returned"); - // After interrupt(), the query iterator is done (returns done: true). - // Clear it so ensureQuery() creates a fresh query for the next turn. - // Also end the input stream and call return() to clean up the SDK process. + await this.awaitWithTimeout( + queryToInterrupt.interrupt(), + "interruptActiveTurn query.interrupt()" + ); this.input?.end(); this.logger.trace("interruptActiveTurn: calling query.return()..."); - const t1 = Date.now(); - await queryToInterrupt.return?.(); - this.logger.trace({ durationMs: Date.now() - t1 }, "interruptActiveTurn: query.return() returned"); + await this.awaitWithTimeout( + queryToInterrupt.return?.(), + "interruptActiveTurn query.return()" + ); this.query = null; this.input = null; this.queryRestartNeeded = false; } catch (error) { this.logger.warn({ err: error }, "Failed to interrupt active turn"); - // If interrupt fails, the SDK iterator may remain in an indeterminate state. - // Force a teardown/recreate path so the next turn cannot reuse stale query state. - this.queryRestartNeeded = true; + this.input?.end(); + if (this.query === queryToInterrupt) { + this.query = null; + this.input = null; + } + // Try to force-close the iterator to unblock the pump's q.next() call. + this.awaitWithTimeout( + queryToInterrupt.return?.(), + "interruptActiveTurn force return after failure" + ).catch(() => {}); + // Disown the current pump and start a fresh one immediately so + // autonomous wakes are not lost while waiting for the next user turn. + this.queryPumpPromise = null; + this.startQueryPump(); } }