Add Windows support and improve cross-platform shell execution

This commit is contained in:
Mohamed Boudra
2026-03-11 14:14:06 +07:00
parent eb5f011161
commit 5bf698ff84
3 changed files with 82 additions and 37 deletions
+30 -14
View File
@@ -215,23 +215,38 @@ fn maybe_run_managed_headless_command(app: &AppHandle) -> Result<bool, String> {
Ok(true) Ok(true)
} }
fn resolve_login_shell() -> String { fn shell_command(script: &str) -> Command {
std::env::var("SHELL") #[cfg(unix)]
.ok() {
.map(|value| value.trim().to_string()) let shell = std::env::var("SHELL")
.filter(|value| !value.is_empty()) .ok()
.unwrap_or_else(|| "/bin/zsh".to_string()) .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 let script = r#"if command -v paseo >/dev/null 2>&1; then
paseo --version paseo --version
else else
echo "paseo command not found in PATH" >&2 echo "paseo command not found in PATH" >&2
exit 127 exit 127
fi"#; 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) => { Ok(output) => {
if output.status.success() { if output.status.success() {
let version = String::from_utf8_lossy(&output.stdout).trim().to_string(); 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 let script = r#"if command -v paseo >/dev/null 2>&1; then
paseo daemon update paseo daemon update
else else
echo "paseo command not found in PATH. Ensure Paseo CLI is installed for this user." >&2 echo "paseo command not found in PATH. Ensure Paseo CLI is installed for this user." >&2
exit 127 exit 127
fi"#; 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 { Ok(output) => DaemonUpdateCommandResult {
exit_code: output.status.code().unwrap_or(1), exit_code: output.status.code().unwrap_or(1),
stdout: String::from_utf8_lossy(&output.stdout).into_owned(), stdout: String::from_utf8_lossy(&output.stdout).into_owned(),
@@ -292,8 +310,7 @@ fi"#;
#[tauri::command] #[tauri::command]
async fn get_local_daemon_version() -> LocalDaemonVersionResult { async fn get_local_daemon_version() -> LocalDaemonVersionResult {
let shell = resolve_login_shell(); tauri::async_runtime::spawn_blocking(execute_local_daemon_version)
tauri::async_runtime::spawn_blocking(move || execute_local_daemon_version(&shell))
.await .await
.unwrap_or_else(|error| LocalDaemonVersionResult { .unwrap_or_else(|error| LocalDaemonVersionResult {
version: None, version: None,
@@ -303,8 +320,7 @@ async fn get_local_daemon_version() -> LocalDaemonVersionResult {
#[tauri::command] #[tauri::command]
async fn run_local_daemon_update() -> DaemonUpdateCommandResult { async fn run_local_daemon_update() -> DaemonUpdateCommandResult {
let shell = resolve_login_shell(); tauri::async_runtime::spawn_blocking(execute_local_daemon_update)
tauri::async_runtime::spawn_blocking(move || execute_local_daemon_update(&shell))
.await .await
.unwrap_or_else(|error| DaemonUpdateCommandResult { .unwrap_or_else(|error| DaemonUpdateCommandResult {
exit_code: -1, exit_code: -1,
@@ -561,15 +561,25 @@ fn dev_resource_root() -> PathBuf {
fn bundled_runtime_root(app: &AppHandle) -> Result<PathBuf, String> { fn bundled_runtime_root(app: &AppHandle) -> Result<PathBuf, String> {
if let Ok(resource_dir) = app.path().resource_dir() { if let Ok(resource_dir) = app.path().resource_dir() {
let candidate = resource_dir.join("managed-runtime"); // Tauri's bundle.resources preserves the source directory structure, so
if candidate.exists() { // "resources/**/*" in tauri.conf.json places files at
log::info!("[runtime] found bundled runtime at {}", candidate.display()); // $RESOURCE/resources/managed-runtime/. Try the nested path first (installed
return Ok(candidate); // 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!( } else {
"[runtime] no bundled runtime at {}, checking dev path", log::info!("[runtime] resource_dir() unavailable, checking dev path");
candidate.display()
);
} }
let dev = dev_resource_root().join("managed-runtime"); let dev = dev_resource_root().join("managed-runtime");
if dev.exists() { if dev.exists() {
@@ -624,10 +634,19 @@ fn is_pid_running(pid: i32) -> bool {
.status(); .status();
return output.map(|status| status.success()).unwrap_or(false); return output.map(|status| status.success()).unwrap_or(false);
} }
#[cfg(not(unix))] #[cfg(windows)]
{ {
let _ = pid; let output = Command::new("tasklist")
false .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);
} }
} }
@@ -3269,25 +3269,35 @@ class ClaudeAgentSession implements AgentSession {
} }
try { try {
this.logger.trace("interruptActiveTurn: calling query.interrupt()..."); this.logger.trace("interruptActiveTurn: calling query.interrupt()...");
const t0 = Date.now(); await this.awaitWithTimeout(
await queryToInterrupt.interrupt(); queryToInterrupt.interrupt(),
this.logger.trace({ durationMs: Date.now() - t0 }, "interruptActiveTurn: query.interrupt() returned"); "interruptActiveTurn query.interrupt()"
// 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.
this.input?.end(); this.input?.end();
this.logger.trace("interruptActiveTurn: calling query.return()..."); this.logger.trace("interruptActiveTurn: calling query.return()...");
const t1 = Date.now(); await this.awaitWithTimeout(
await queryToInterrupt.return?.(); queryToInterrupt.return?.(),
this.logger.trace({ durationMs: Date.now() - t1 }, "interruptActiveTurn: query.return() returned"); "interruptActiveTurn query.return()"
);
this.query = null; this.query = null;
this.input = null; this.input = null;
this.queryRestartNeeded = false; this.queryRestartNeeded = false;
} catch (error) { } catch (error) {
this.logger.warn({ err: error }, "Failed to interrupt active turn"); this.logger.warn({ err: error }, "Failed to interrupt active turn");
// If interrupt fails, the SDK iterator may remain in an indeterminate state. this.input?.end();
// Force a teardown/recreate path so the next turn cannot reuse stale query state. if (this.query === queryToInterrupt) {
this.queryRestartNeeded = true; 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();
} }
} }