/// Bridge between agor-core (PtyManager, SidecarManager) and Dioxus signals. /// /// In the Tauri app, TauriEventSink implements EventSink by emitting Tauri events /// that the Svelte frontend listens to. Here we implement EventSink to push /// events into Dioxus signals, demonstrating native Rust -> UI reactivity /// without any IPC layer. use std::sync::{Arc, Mutex}; use agor_core::event::EventSink; use agor_core::pty::{PtyManager, PtyOptions}; use agor_core::sidecar::{AgentQueryOptions, SidecarConfig, SidecarManager}; /// Collects events emitted by PtyManager and SidecarManager. /// In a real app, these would drive Dioxus signal updates. /// /// Key advantage over Tauri: no serialization/deserialization overhead. /// Events are typed Rust values, not JSON blobs crossing an IPC bridge. #[derive(Debug, Clone)] pub struct DioxusEventSink { /// Buffered events — a real implementation would use channels or /// direct signal mutation via Dioxus's `schedule_update`. events: Arc>>, } /// Typed event enum — replaces the untyped (event_name, JSON) pattern /// used by the Tauri EventSink. #[derive(Debug, Clone)] pub enum AppEvent { PtyOutput { id: String, data: String }, PtyExit { id: String, code: Option }, AgentMessage { session_id: String, payload: serde_json::Value }, AgentReady { provider: String }, AgentError { session_id: String, error: String }, } impl DioxusEventSink { pub fn new() -> Self { Self { events: Arc::new(Mutex::new(Vec::new())), } } /// Drain all buffered events. Called from a Dioxus use_effect or timer. pub fn drain_events(&self) -> Vec { let mut events = self.events.lock().unwrap(); std::mem::take(&mut *events) } } impl EventSink for DioxusEventSink { fn emit(&self, event: &str, payload: serde_json::Value) { let app_event = match event { "pty-output" => { let id = payload["id"].as_str().unwrap_or("").to_string(); let data = payload["data"].as_str().unwrap_or("").to_string(); AppEvent::PtyOutput { id, data } } "pty-exit" => { let id = payload["id"].as_str().unwrap_or("").to_string(); let code = payload["code"].as_i64().map(|c| c as i32); AppEvent::PtyExit { id, code } } "agent-message" => { let session_id = payload["sessionId"].as_str().unwrap_or("").to_string(); AppEvent::AgentMessage { session_id, payload: payload.clone(), } } "sidecar-ready" => { let provider = payload["provider"].as_str().unwrap_or("claude").to_string(); AppEvent::AgentReady { provider } } "agent-error" => { let session_id = payload["sessionId"].as_str().unwrap_or("").to_string(); let error = payload["error"].as_str().unwrap_or("unknown error").to_string(); AppEvent::AgentError { session_id, error } } _ => { // Unknown event — log and ignore log::debug!("Unknown event from backend: {event}"); return; } }; if let Ok(mut events) = self.events.lock() { events.push(app_event); } } } /// Backend handle holding initialized managers. /// /// In the Tauri app, these live in AppState behind Arc> /// and are accessed via Tauri commands. Here they're directly /// available to Dioxus components via use_context. pub struct Backend { pub pty_manager: PtyManager, pub sidecar_manager: SidecarManager, pub event_sink: DioxusEventSink, } impl Backend { /// Initialize backend managers. /// /// # Panics /// Panics if sidecar search paths cannot be resolved (mirrors Tauri app behavior). pub fn new() -> Self { let event_sink = DioxusEventSink::new(); let sink: Arc = Arc::new(event_sink.clone()); let pty_manager = PtyManager::new(Arc::clone(&sink)); // Resolve sidecar search paths — same logic as src-tauri/src/lib.rs let mut search_paths = Vec::new(); if let Ok(exe_dir) = std::env::current_exe() { if let Some(parent) = exe_dir.parent() { search_paths.push(parent.join("sidecar")); search_paths.push(parent.join("../sidecar/dist")); } } // Also check the repo's sidecar/dist for dev mode search_paths.push(std::path::PathBuf::from("../sidecar/dist")); search_paths.push(std::path::PathBuf::from("./sidecar/dist")); let sidecar_config = SidecarConfig { search_paths, env_overrides: std::collections::HashMap::new(), sandbox: agor_core::sandbox::SandboxConfig::default(), }; let sidecar_manager = SidecarManager::new(Arc::clone(&sink), sidecar_config); Self { pty_manager, sidecar_manager, event_sink, } } /// Spawn a PTY with default options. pub fn spawn_pty(&self, cwd: Option<&str>) -> Result { self.pty_manager.spawn(PtyOptions { shell: None, cwd: cwd.map(|s| s.to_string()), args: None, cols: Some(80), rows: Some(24), }) } /// Start an agent query. pub fn query_agent(&self, session_id: &str, prompt: &str, cwd: &str) -> Result<(), String> { let options = AgentQueryOptions { provider: "claude".to_string(), session_id: session_id.to_string(), prompt: prompt.to_string(), cwd: Some(cwd.to_string()), max_turns: None, max_budget_usd: None, resume_session_id: None, permission_mode: Some("bypassPermissions".to_string()), setting_sources: Some(vec!["user".to_string(), "project".to_string()]), system_prompt: None, model: None, claude_config_dir: None, additional_directories: None, worktree_name: None, provider_config: serde_json::Value::Null, extra_env: std::collections::HashMap::new(), }; self.sidecar_manager.query(&options) } /// Stop an agent session. pub fn stop_agent(&self, session_id: &str) -> Result<(), String> { self.sidecar_manager.stop_session(session_id) } }