// ui_channel.rs — Output routing for TUI panes // // All output from the agent (streaming text, tool calls, status updates) // goes through a UiMessage enum sent over an mpsc channel. The TUI // receives these messages and routes them to the appropriate pane. // // This replaces direct stdout/stderr printing throughout the codebase. // The agent and API client never touch the terminal directly — they // just send messages that the TUI renders where appropriate. // // The channel also fans out to a broadcast channel so the observation // socket (observe.rs) can subscribe without touching the main path. use std::sync::{Arc, RwLock}; use tokio::sync::{broadcast, mpsc}; /// Shared, live context state — agent writes, TUI reads for the debug screen. pub type SharedContextState = Arc>>; /// Create a new shared context state. pub fn shared_context_state() -> SharedContextState { Arc::new(RwLock::new(Vec::new())) } /// Which pane streaming text should go to. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum StreamTarget { /// User-initiated turn — text goes to conversation pane. Conversation, /// DMN-initiated turn — text goes to autonomous pane. Autonomous, } /// Status info for the bottom status bar. #[derive(Debug, Clone)] #[allow(dead_code)] pub struct StatusInfo { pub dmn_state: String, pub dmn_turns: u32, pub dmn_max_turns: u32, pub prompt_tokens: u32, pub completion_tokens: u32, pub model: String, /// Number of tool calls dispatched in the current turn. pub turn_tools: u32, /// Context window budget breakdown (e.g. "id:8% mem:25% jnl:30% conv:37%"). pub context_budget: String, } /// A section of the context window, possibly with children. #[derive(Debug, Clone)] pub struct ContextSection { pub name: String, pub tokens: usize, pub content: String, pub children: Vec, } /// Context loading details for the debug screen. #[derive(Debug, Clone)] pub struct ContextInfo { pub model: String, pub available_models: Vec, pub prompt_file: String, pub backend: String, #[allow(dead_code)] pub instruction_files: Vec<(String, usize)>, #[allow(dead_code)] pub memory_files: Vec<(String, usize)>, pub system_prompt_chars: usize, pub context_message_chars: usize, } /// Messages sent from agent/API to the TUI for rendering. #[derive(Debug, Clone)] #[allow(dead_code)] pub enum UiMessage { /// Streaming text delta — routed to conversation or autonomous pane /// based on the current StreamTarget. TextDelta(String, StreamTarget), /// User's input echoed to conversation pane. UserInput(String), /// Tool call header: [tool_name] with args summary. ToolCall { name: String, args_summary: String, }, /// Full tool result — goes to tools pane. ToolResult { name: String, result: String, }, /// DMN state annotation: [dmn: foraging (3/20)]. DmnAnnotation(String), /// Status bar update. StatusUpdate(StatusInfo), /// Live activity indicator for the status bar — shows what the /// agent is doing right now ("thinking...", "calling: bash", etc). /// Empty string clears the indicator. Activity(String), /// Reasoning/thinking tokens from the model (internal monologue). /// Routed to the autonomous pane so the user can peek at what /// the model is thinking about during long tool chains. Reasoning(String), /// A tool call started — shown as a live overlay above the status bar. ToolStarted { id: String, name: String, detail: String }, /// A tool call finished — removes it from the live overlay. ToolFinished { id: String }, /// Debug message (only shown when POC_DEBUG is set). Debug(String), /// Informational message — goes to conversation pane (command output, etc). Info(String), /// Context loading details — stored for the debug screen (Ctrl+D). ContextInfoUpdate(ContextInfo), /// Agent cycle state update — refreshes the F2 agents screen. AgentUpdate(Vec), } /// Sender that fans out to both the TUI (mpsc) and observers (broadcast). #[derive(Clone)] pub struct UiSender { tui: mpsc::UnboundedSender, observe: broadcast::Sender, } impl UiSender { pub fn send(&self, msg: UiMessage) -> Result<(), mpsc::error::SendError> { // Broadcast to observers (ignore errors — no subscribers is fine) let _ = self.observe.send(msg.clone()); self.tui.send(msg) } /// Subscribe to the broadcast side (for the observation socket). pub fn subscribe(&self) -> broadcast::Receiver { self.observe.subscribe() } } /// Convenience type for the receiving half. pub type UiReceiver = mpsc::UnboundedReceiver; /// Create a new UI channel pair. pub fn channel() -> (UiSender, UiReceiver) { let (tui_tx, tui_rx) = mpsc::unbounded_channel(); let (observe_tx, _) = broadcast::channel(1024); (UiSender { tui: tui_tx, observe: observe_tx }, tui_rx) }