//! Approval commands: list and respond //! //! When approved, kernel's `respond_to_approval` internally spawns the Hand execution //! and emits `hand-execution-complete` events to the frontend. use serde::{Deserialize, Serialize}; use serde_json; use tauri::{AppHandle, Emitter, State}; use super::KernelState; // ============================================================ // Approval Commands // ============================================================ /// Approval response #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct ApprovalResponse { pub id: String, pub hand_id: String, pub status: String, pub created_at: String, pub input: serde_json::Value, } /// List pending approvals #[tauri::command] pub async fn approval_list( state: State<'_, KernelState>, ) -> Result, String> { let kernel_lock = state.lock().await; let kernel = kernel_lock.as_ref() .ok_or_else(|| "Kernel not initialized".to_string())?; let approvals = kernel.list_approvals().await; Ok(approvals.into_iter().map(|a| ApprovalResponse { id: a.id, hand_id: a.hand_id, status: a.status, created_at: a.created_at.to_rfc3339(), input: a.input, }).collect()) } /// Respond to an approval /// /// When approved, the kernel's `respond_to_approval` internally spawns the Hand /// execution. We additionally emit Tauri events so the frontend can track when /// the execution finishes, since the kernel layer has no access to the AppHandle. #[tauri::command] pub async fn approval_respond( app: AppHandle, state: State<'_, KernelState>, id: String, approved: bool, reason: Option, ) -> Result<(), String> { // Capture hand info before calling respond_to_approval (which mutates the approval) let hand_id = { let kernel_lock = state.lock().await; let kernel = kernel_lock.as_ref() .ok_or_else(|| "Kernel not initialized".to_string())?; let approvals = kernel.list_approvals().await; let entry = approvals.iter().find(|a| a.id == id && a.status == "pending") .ok_or_else(|| format!("Approval not found or already resolved: {}", id))?; entry.hand_id.clone() }; // Call kernel respond_to_approval (this updates status and spawns Hand execution) { let kernel_lock = state.lock().await; let kernel = kernel_lock.as_ref() .ok_or_else(|| "Kernel not initialized".to_string())?; kernel.respond_to_approval(&id, approved, reason).await .map_err(|e| format!("Failed to respond to approval: {}", e))?; } // When approved, monitor the Hand execution and emit events to the frontend. // The kernel's respond_to_approval changes status to "approved" immediately, // then the spawned task sets it to "completed" or "failed" when done. if approved { let approval_id = id.clone(); let kernel_state: KernelState = (*state).clone(); tokio::spawn(async move { let timeout = tokio::time::Duration::from_secs(300); let poll_interval = tokio::time::Duration::from_millis(500); let result = tokio::time::timeout(timeout, async { loop { tokio::time::sleep(poll_interval).await; let kernel_lock = kernel_state.lock().await; if let Some(kernel) = kernel_lock.as_ref() { // Use get_approval to check any status (not just "pending") if let Some(entry) = kernel.get_approval(&approval_id).await { match entry.status.as_str() { "completed" => { tracing::info!("[approval_respond] Hand '{}' completed for approval {}", hand_id, approval_id); return (true, None::); } "failed" => { let error_msg = entry.input.get("error") .and_then(|v| v.as_str()) .unwrap_or("Unknown error") .to_string(); tracing::warn!("[approval_respond] Hand '{}' failed for approval {}: {}", hand_id, approval_id, error_msg); return (false, Some(error_msg)); } _ => {} // "approved" = still running } } else { // Entry disappeared entirely — kernel was likely restarted return (false, Some("Approval entry disappeared".to_string())); } } else { return (false, Some("Kernel not available".to_string())); } } }).await; let (success, error) = match result { Ok((s, e)) => (s, e), Err(_) => (false, Some("Hand execution timed out (5 minutes)".to_string())), }; let _ = app.emit("hand-execution-complete", serde_json::json!({ "approvalId": approval_id, "handId": hand_id, "success": success, "error": error, })); }); } Ok(()) }