diff --git a/src/agent/runner.rs b/src/agent/runner.rs index 9c90f4b..3292d3c 100644 --- a/src/agent/runner.rs +++ b/src/agent/runner.rs @@ -80,6 +80,8 @@ pub struct Agent { pub memory_scores: Option, /// Whether a /score task is currently running. pub scoring_in_flight: bool, + /// Shared active tools — Agent writes, TUI reads. + pub active_tools: crate::user::ui_channel::SharedActiveTools, /// Background tool calls that outlive the current turn. background_tasks: futures::stream::FuturesUnordered< std::pin::Pin + Send>> @@ -105,6 +107,7 @@ impl Agent { prompt_file: String, conversation_log: Option, shared_context: SharedContextState, + active_tools: crate::user::ui_channel::SharedActiveTools, ) -> Self { let tool_defs = tools::definitions(); let tokenizer = tiktoken_rs::cl100k_base() @@ -135,6 +138,7 @@ impl Agent { agent_cycles, memory_scores: None, scoring_in_flight: false, + active_tools, background_tasks: futures::stream::FuturesUnordered::new(), }; @@ -240,20 +244,15 @@ impl Agent { // Inject completed background task results { use futures::{StreamExt, FutureExt}; + let mut bg_ds = DispatchState { + yield_requested: false, had_tool_calls: false, + tool_errors: 0, model_switch: None, dmn_pause: false, + }; while let Some(Some((call, output))) = std::pin::Pin::new(&mut self.background_tasks).next().now_or_never() { - let preview = &output.text[..output.text.len().min(500)]; - let _ = ui_tx.send(UiMessage::Info(format!( - "[background] {} completed: {}", - call.function.name, - &preview[..preview.len().min(80)], - ))); - let notification = format!( - "\nTool: {}\nResult: {}\n", - call.function.name, preview, - ); - self.push_message(Message::user(notification)); + // Show result in TUI and inject into conversation + self.apply_tool_result(&call, output, ui_tx, &mut bg_ds); } } @@ -324,11 +323,14 @@ impl Agent { name: call.function.name.clone(), args_summary: args_summary.clone(), }); - let _ = ui_tx.send(UiMessage::ToolStarted { - id: call.id.clone(), - name: call.function.name.clone(), - detail: args_summary, - }); + self.active_tools.write().unwrap().push( + crate::user::ui_channel::ActiveTool { + id: call.id.clone(), + name: call.function.name.clone(), + detail: args_summary, + started: std::time::Instant::now(), + } + ); let tracker = self.process_tracker.clone(); let is_background = args.get("run_in_background") .and_then(|v| v.as_bool()) @@ -548,11 +550,14 @@ impl Agent { name: call.function.name.clone(), args_summary: args_summary.clone(), }); - let _ = ui_tx.send(UiMessage::ToolStarted { - id: call.id.clone(), - name: call.function.name.clone(), - detail: args_summary, - }); + self.active_tools.write().unwrap().push( + crate::user::ui_channel::ActiveTool { + id: call.id.clone(), + name: call.function.name.clone(), + detail: args_summary, + started: std::time::Instant::now(), + } + ); // Handle working_stack tool — needs &mut self for context state if call.function.name == "working_stack" { @@ -568,7 +573,7 @@ impl Agent { name: call.function.name.clone(), result: output.text.clone(), }); - let _ = ui_tx.send(UiMessage::ToolFinished { id: call.id.clone() }); + self.active_tools.write().unwrap().retain(|t| t.id != call.id); self.push_message(Message::tool_result(&call.id, &output.text)); ds.had_tool_calls = true; @@ -616,7 +621,7 @@ impl Agent { name: call.function.name.clone(), result: output.text.clone(), }); - let _ = ui_tx.send(UiMessage::ToolFinished { id: call.id.clone() }); + self.active_tools.write().unwrap().retain(|t| t.id != call.id); // Tag memory_render results for context deduplication if call.function.name == "memory_render" && !output.text.starts_with("Error:") { diff --git a/src/bin/consciousness.rs b/src/bin/consciousness.rs index 39a7324..b2be592 100644 --- a/src/bin/consciousness.rs +++ b/src/bin/consciousness.rs @@ -378,6 +378,7 @@ impl Session { .ok(); let mut agent_guard = self.agent.lock().await; let shared_ctx = agent_guard.shared_context.clone(); + let shared_tools = agent_guard.active_tools.clone(); *agent_guard = Agent::new( ApiClient::new( &self.config.api_base, @@ -390,6 +391,7 @@ impl Session { self.config.prompt_file.clone(), new_log, shared_ctx, + shared_tools, ); } self.dmn = dmn::State::Resting { @@ -826,12 +828,13 @@ async fn run(cli: cli::CliArgs) -> Result<()> { // Create UI channel let (ui_tx, mut ui_rx) = ui_channel::channel(); - // Shared context state — agent writes, TUI reads for debug screen + // Shared state — agent writes, TUI reads let shared_context = ui_channel::shared_context_state(); + let shared_active_tools = ui_channel::shared_active_tools(); // Initialize TUI let mut terminal = tui::init_terminal()?; - let mut app = tui::App::new(config.model.clone(), shared_context.clone()); + let mut app = tui::App::new(config.model.clone(), shared_context.clone(), shared_active_tools.clone()); // Show startup info let _ = ui_tx.send(UiMessage::Info("consciousness v0.3 (tui)".into())); @@ -868,6 +871,7 @@ async fn run(cli: cli::CliArgs) -> Result<()> { config.prompt_file.clone(), Some(conversation_log), shared_context, + shared_active_tools, ))); // Keep a reference to the process tracker outside the agent lock diff --git a/src/user/tui/context_screen.rs b/src/user/tui/context_screen.rs index 6e0257b..c6967d2 100644 --- a/src/user/tui/context_screen.rs +++ b/src/user/tui/context_screen.rs @@ -168,7 +168,7 @@ impl App { ))); lines.push(Line::raw(format!(" Reasoning: {}", self.reasoning_effort))); lines.push(Line::raw(format!(" Running processes: {}", self.running_processes))); - lines.push(Line::raw(format!(" Active tools: {}", self.active_tools.len()))); + lines.push(Line::raw(format!(" Active tools: {}", self.active_tools.read().unwrap().len()))); let block = Block::default() .title_top(Line::from(SCREEN_LEGEND).left_aligned()) diff --git a/src/user/tui/main_screen.rs b/src/user/tui/main_screen.rs index 2dfcb95..fa8ce75 100644 --- a/src/user/tui/main_screen.rs +++ b/src/user/tui/main_screen.rs @@ -17,7 +17,8 @@ impl App { /// Draw the main (F1) screen — four-pane layout with status bar. pub(crate) fn draw_main(&mut self, frame: &mut Frame, size: Rect) { // Main layout: content area + active tools overlay + status bar - let tool_lines = self.active_tools.len() as u16; + let active_tools = self.active_tools.read().unwrap(); + let tool_lines = active_tools.len() as u16; let main_chunks = Layout::default() .direction(Direction::Vertical) .constraints([ @@ -107,9 +108,9 @@ impl App { frame.render_widget(&self.textarea, input_chunks[1]); // Draw active tools overlay - if !self.active_tools.is_empty() { + if !active_tools.is_empty() { let tool_style = Style::default().fg(Color::Yellow).add_modifier(Modifier::DIM); - let tool_text: Vec = self.active_tools.iter().map(|t| { + let tool_text: Vec = active_tools.iter().map(|t| { let elapsed = t.started.elapsed().as_secs(); let line = if t.detail.is_empty() { format!(" [{}] ({}s)", t.name, elapsed) diff --git a/src/user/tui/mod.rs b/src/user/tui/mod.rs index 0c134c9..0273177 100644 --- a/src/user/tui/mod.rs +++ b/src/user/tui/mod.rs @@ -308,12 +308,8 @@ pub(crate) fn parse_markdown(md: &str) -> Vec> { } /// A tool call currently in flight — shown above the status bar. -pub(crate) struct ActiveTool { - pub(crate) id: String, - pub(crate) name: String, - pub(crate) detail: String, - pub(crate) started: std::time::Instant, -} +// ActiveTool moved to ui_channel — shared between Agent and TUI +pub(crate) use crate::user::ui_channel::ActiveTool; /// Main TUI application state. pub struct App { @@ -335,7 +331,7 @@ pub struct App { pub running_processes: u32, /// Current reasoning effort level (for status display). pub reasoning_effort: String, - pub(crate) active_tools: Vec, + pub(crate) active_tools: crate::user::ui_channel::SharedActiveTools, pub(crate) active_pane: ActivePane, /// User input editor (handles wrapping, cursor positioning). pub textarea: tui_textarea::TextArea<'static>, @@ -422,7 +418,7 @@ pub enum HotkeyAction { } impl App { - pub fn new(model: String, shared_context: SharedContextState) -> Self { + pub fn new(model: String, shared_context: SharedContextState, active_tools: crate::user::ui_channel::SharedActiveTools) -> Self { Self { autonomous: PaneState::new(true), // markdown conversation: PaneState::new(true), // markdown @@ -444,7 +440,7 @@ impl App { needs_assistant_marker: false, running_processes: 0, reasoning_effort: "none".to_string(), - active_tools: Vec::new(), + active_tools, active_pane: ActivePane::Conversation, textarea: new_textarea(vec![String::new()]), input_history: Vec::new(), @@ -548,17 +544,8 @@ impl App { self.autonomous.current_color = Color::DarkGray; self.autonomous.append_text(&text); } - UiMessage::ToolStarted { id, name, detail } => { - self.active_tools.push(ActiveTool { - id, - name, - detail, - started: std::time::Instant::now(), - }); - } - UiMessage::ToolFinished { id } => { - self.active_tools.retain(|t| t.id != id); - } + UiMessage::ToolStarted { .. } => {} // handled by shared active_tools + UiMessage::ToolFinished { .. } => {} UiMessage::Debug(text) => { self.tools.push_line(format!("[debug] {}", text), Color::DarkGray); } diff --git a/src/user/ui_channel.rs b/src/user/ui_channel.rs index bf0bec0..29512ef 100644 --- a/src/user/ui_channel.rs +++ b/src/user/ui_channel.rs @@ -22,6 +22,22 @@ pub fn shared_context_state() -> SharedContextState { Arc::new(RwLock::new(Vec::new())) } +/// Active tool info for TUI display. +#[derive(Debug, Clone)] +pub struct ActiveTool { + pub id: String, + pub name: String, + pub detail: String, + pub started: std::time::Instant, +} + +/// Shared active tools — agent writes, TUI reads. +pub type SharedActiveTools = Arc>>; + +pub fn shared_active_tools() -> SharedActiveTools { + Arc::new(RwLock::new(Vec::new())) +} + /// Which pane streaming text should go to. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum StreamTarget {