use std::collections::HashMap;
use std::sync::Arc;
use tokio::sync::Mutex;
use tokio_util::sync::CancellationToken;
use tauri::Emitter;
pub struct ClaudeCliOutput {
pub code: i32,
pub stdout: String,
pub stderr: String,
}
pub struct CliBridgeState {
cancel: Arc<Mutex<Option<CancellationToken>>>,
}
impl CliBridgeState {
pub fn new() -> Self {
Self {
cancel: Arc::new(Mutex::new(None)),
}
}
}
#[tauri::command]
#[specta::specta]
pub async fn run_cli_agent(
app: tauri::AppHandle,
state: tauri::State<'_, CliBridgeState>,
config: String,
env: HashMap<String, String>,
) -> Result<ClaudeCliOutput, String> {
let mut opts: cli_agents::RunOptions = serde_json::from_str(&config)
.map_err(|e| format!("Invalid cli-agent config: {e}"))?;
if !env.is_empty() {
let existing = opts.env.get_or_insert_with(HashMap::new);
existing.extend(env);
}
log::info!(
"[cli-bridge] Running cli-agents: cli={:?}, task_len={}",
opts.cli,
opts.task.len()
);
let cancel = CancellationToken::new();
{
let mut guard = state.cancel.lock().await;
*guard = Some(cancel.clone());
}
let app_clone = app.clone();
let on_event: Arc<dyn Fn(cli_agents::StreamEvent) + Send + Sync> = Arc::new(move |event| {
if let Ok(json) = serde_json::to_string(&event) {
let _ = app_clone.emit("cli-agent-stdout", &json);
}
});
let handle = cli_agents::run(opts, Some(on_event));
let result = tokio::select! {
res = handle.result => {
match res {
Ok(Ok(result)) => result,
Ok(Err(e)) => {
return Err(format!("cli-agents error: {e}"));
}
Err(e) => {
return Err(format!("cli-agents task panicked: {e}"));
}
}
}
_ = cancel.cancelled() => {
handle.abort();
let _ = tokio::time::timeout(
std::time::Duration::from_secs(5),
handle.result,
).await;
cli_agents::RunResult {
success: false,
text: Some("Cancelled.".into()),
exit_code: Some(-1),
..Default::default()
}
}
};
{
let mut guard = state.cancel.lock().await;
*guard = None;
}
let code = result.exit_code.unwrap_or(if result.success { 0 } else { 1 });
let stderr_str = result.stderr.unwrap_or_default();
let _ = app.emit(
"cli-agent-done",
serde_json::json!({ "code": code, "stderr": &stderr_str }),
);
Ok(ClaudeCliOutput {
code,
stdout: result.text.unwrap_or_default(),
stderr: stderr_str,
})
}
#[tauri::command]
#[specta::specta]
pub async fn cancel_cli_agent(
state: tauri::State<'_, CliBridgeState>,
) -> Result<(), String> {
let guard = state.cancel.lock().await;
if let Some(cancel) = guard.as_ref() {
cancel.cancel();
log::info!("[cli-bridge] Cancellation requested");
}
Ok(())
}