//! Bridge to agor-core: PtyManager, SidecarManager, EventSink. //! //! Implements the `EventSink` trait from agor-core so that PTY and sidecar //! events flow into GPUI's entity system via channel-based async dispatch. use agor_core::event::EventSink; use agor_core::pty::{PtyManager, PtyOptions}; use agor_core::sandbox::SandboxConfig; use agor_core::sidecar::{AgentQueryOptions, SidecarConfig, SidecarManager}; use std::collections::HashMap; use std::path::PathBuf; use std::sync::{mpsc, Arc}; // ── GPUI EventSink ────────────────────────────────────────────────── /// Event payload sent from backend threads to the GPUI main thread. #[derive(Debug, Clone)] pub struct BackendEvent { pub event_name: String, pub payload: serde_json::Value, } /// An `EventSink` that queues events into an `mpsc` channel. /// The GPUI main loop drains this channel each frame to update entity state. pub struct GpuiEventSink { sender: mpsc::Sender, } impl GpuiEventSink { pub fn new(sender: mpsc::Sender) -> Self { Self { sender } } } impl EventSink for GpuiEventSink { fn emit(&self, event: &str, payload: serde_json::Value) { let _ = self.sender.send(BackendEvent { event_name: event.to_string(), payload, }); } } // ── Backend Manager ───────────────────────────────────────────────── /// Owns the agor-core managers and the event channel. /// Created once at app startup; shared via `Arc` or `Entity`. pub struct Backend { pub pty_manager: PtyManager, pub sidecar_manager: SidecarManager, pub event_rx: mpsc::Receiver, } impl Backend { /// Create backend with default sidecar search paths. pub fn new() -> Self { let (tx, rx) = mpsc::channel(); let sink: Arc = Arc::new(GpuiEventSink::new(tx)); let pty_manager = PtyManager::new(Arc::clone(&sink)); // Sidecar search paths: look next to the binary, then common install locations let mut search_paths = vec![ PathBuf::from("./sidecar/dist"), PathBuf::from("../sidecar/dist"), ]; if let Ok(exe) = std::env::current_exe() { if let Some(dir) = exe.parent() { search_paths.push(dir.join("sidecar")); } } let sidecar_config = SidecarConfig { search_paths, env_overrides: HashMap::new(), sandbox: SandboxConfig::default(), }; let sidecar_manager = SidecarManager::new(Arc::clone(&sink), sidecar_config); Self { pty_manager, sidecar_manager, event_rx: rx, } } /// Spawn a PTY for a project terminal. pub fn spawn_pty(&self, cwd: &str) -> Result { self.pty_manager.spawn(PtyOptions { shell: None, cwd: Some(cwd.to_string()), args: None, cols: Some(120), rows: Some(30), }) } /// Start an agent query (sends to sidecar, non-blocking). pub fn start_agent(&self, session_id: &str, prompt: &str, cwd: &str) { 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: HashMap::new(), }; let _ = self.sidecar_manager.query(&options); } /// Drain all pending backend events (call once per frame / tick). pub fn drain_events(&self) -> Vec { let mut events = Vec::new(); while let Ok(ev) = self.event_rx.try_recv() { events.push(ev); } events } }