From 7f9f0cd21e67160028da19f1b82d99267160096e Mon Sep 17 00:00:00 2001 From: Illia Polosukhin Date: Tue, 3 Feb 2026 00:12:48 -0800 Subject: [PATCH] Add status updates to show agent thinking/processing state - Add StatusUpdate enum with Thinking, ToolStarted, ToolCompleted, StreamChunk, Status variants - Add send_status method to Channel trait (default no-op) - Implement send_status in TuiChannel to show status in UI - Add send_status to ChannelManager for routing to specific channels - Update handle_message to send "Processing..." status for Chat/CreateJob - Update handle_chat to send "Generating response..." and show errors Now when a user sends a message, they see feedback that the agent is working. Co-Authored-By: Claude Opus 4.5 --- src/agent/agent_loop.rs | 53 ++++++++++++++++++++++++++++++++++------- src/channels/channel.rs | 22 +++++++++++++++++ src/channels/cli/mod.rs | 22 ++++++++++++++++- src/channels/manager.rs | 17 ++++++++++++- src/channels/mod.rs | 2 +- 5 files changed, 104 insertions(+), 12 deletions(-) diff --git a/src/agent/agent_loop.rs b/src/agent/agent_loop.rs index b459f1f1..f816fd3a 100644 --- a/src/agent/agent_loop.rs +++ b/src/agent/agent_loop.rs @@ -7,7 +7,7 @@ use uuid::Uuid; use crate::agent::self_repair::DefaultSelfRepair; use crate::agent::{MessageIntent, RepairTask, Router, Scheduler}; -use crate::channels::{ChannelManager, IncomingMessage, OutgoingResponse}; +use crate::channels::{ChannelManager, IncomingMessage, OutgoingResponse, StatusUpdate}; use crate::config::AgentConfig; use crate::context::ContextManager; use crate::error::Error; @@ -127,6 +127,20 @@ impl Agent { let intent = self.router.route(message); tracing::debug!("Routed to intent: {:?}", intent); + // Send thinking status for non-trivial operations + match &intent { + MessageIntent::Chat { .. } | MessageIntent::CreateJob { .. } => { + let _ = self + .channels + .send_status( + &message.channel, + StatusUpdate::Thinking("Processing...".into()), + ) + .await; + } + _ => {} + } + // Handle based on intent let response = match intent { MessageIntent::CreateJob { @@ -293,19 +307,40 @@ impl Agent { } } - async fn handle_chat( - &self, - _message: &IncomingMessage, - content: &str, - ) -> Result { + async fn handle_chat(&self, message: &IncomingMessage, content: &str) -> Result { + // Send thinking status + let _ = self + .channels + .send_status( + &message.channel, + StatusUpdate::Thinking("Generating response...".into()), + ) + .await; + // Use LLM for general chat let reasoning = Reasoning::new(self.llm.clone(), self.safety.clone()); let context = ReasoningContext::new().with_message(ChatMessage::user(content)); - let response = reasoning.respond(&context).await?; - - Ok(response) + match reasoning.respond(&context).await { + Ok(response) => { + let _ = self + .channels + .send_status(&message.channel, StatusUpdate::Status("Done".into())) + .await; + Ok(response) + } + Err(e) => { + let _ = self + .channels + .send_status( + &message.channel, + StatusUpdate::Status(format!("Error: {}", e)), + ) + .await; + Err(e.into()) + } + } } async fn handle_command( diff --git a/src/channels/channel.rs b/src/channels/channel.rs index 96a2279b..cd9e8527 100644 --- a/src/channels/channel.rs +++ b/src/channels/channel.rs @@ -99,6 +99,21 @@ impl OutgoingResponse { } } +/// Status update types for showing agent activity. +#[derive(Debug, Clone)] +pub enum StatusUpdate { + /// Agent is thinking/processing. + Thinking(String), + /// Tool execution started. + ToolStarted { name: String }, + /// Tool execution completed. + ToolCompleted { name: String, success: bool }, + /// Streaming text chunk. + StreamChunk(String), + /// General status message. + Status(String), +} + /// Trait for message channels. /// /// Channels receive messages from external sources and convert them to @@ -124,6 +139,13 @@ pub trait Channel: Send + Sync { response: OutgoingResponse, ) -> Result<(), ChannelError>; + /// Send a status update (thinking, tool execution, etc.). + /// + /// Default implementation does nothing (for channels that don't support status). + async fn send_status(&self, _status: StatusUpdate) -> Result<(), ChannelError> { + Ok(()) + } + /// Check if the channel is healthy. async fn health_check(&self) -> Result<(), ChannelError>; diff --git a/src/channels/cli/mod.rs b/src/channels/cli/mod.rs index cc5d85de..f99554e5 100644 --- a/src/channels/cli/mod.rs +++ b/src/channels/cli/mod.rs @@ -26,7 +26,7 @@ use ratatui::backend::CrosstermBackend; use tokio::sync::{Mutex, mpsc}; use tokio_stream::wrappers::ReceiverStream; -use crate::channels::{Channel, IncomingMessage, MessageStream, OutgoingResponse}; +use crate::channels::{Channel, IncomingMessage, MessageStream, OutgoingResponse, StatusUpdate}; use crate::error::ChannelError; pub use app::{AppEvent, AppState, InputMode}; @@ -109,6 +109,26 @@ impl Channel for TuiChannel { Ok(()) } + async fn send_status(&self, status: StatusUpdate) -> Result<(), ChannelError> { + let event = match status { + StatusUpdate::Thinking(msg) => AppEvent::LogMessage(format!("🤔 {}", msg)), + StatusUpdate::ToolStarted { name } => AppEvent::ToolStarted { name }, + StatusUpdate::ToolCompleted { name, success } => { + AppEvent::ToolCompleted { name, success } + } + StatusUpdate::StreamChunk(chunk) => AppEvent::StreamChunk(chunk), + StatusUpdate::Status(msg) => AppEvent::LogMessage(msg), + }; + self.event_tx + .send(event) + .await + .map_err(|e| ChannelError::SendFailed { + name: "tui".to_string(), + reason: e.to_string(), + })?; + Ok(()) + } + async fn health_check(&self) -> Result<(), ChannelError> { // Channel is healthy if we haven't been closed if self.event_tx.is_closed() { diff --git a/src/channels/manager.rs b/src/channels/manager.rs index 2902eaf7..35cf3d73 100644 --- a/src/channels/manager.rs +++ b/src/channels/manager.rs @@ -6,7 +6,7 @@ use std::sync::Arc; use futures::stream; use tokio::sync::RwLock; -use crate::channels::{Channel, IncomingMessage, MessageStream, OutgoingResponse}; +use crate::channels::{Channel, IncomingMessage, MessageStream, OutgoingResponse, StatusUpdate}; use crate::error::ChannelError; /// Manages multiple input channels and merges their message streams. @@ -82,6 +82,21 @@ impl ChannelManager { } } + /// Send a status update to a specific channel. + pub async fn send_status( + &self, + channel_name: &str, + status: StatusUpdate, + ) -> Result<(), ChannelError> { + let channels = self.channels.read().await; + if let Some(channel) = channels.get(channel_name) { + channel.send_status(status).await + } else { + // Silently ignore if channel not found (status is best-effort) + Ok(()) + } + } + /// Check health of all channels. pub async fn health_check_all(&self) -> HashMap> { let channels = self.channels.read().await; diff --git a/src/channels/mod.rs b/src/channels/mod.rs index 2e80fa73..afde6c63 100644 --- a/src/channels/mod.rs +++ b/src/channels/mod.rs @@ -10,7 +10,7 @@ mod manager; mod slack; mod telegram; -pub use channel::{Channel, IncomingMessage, MessageStream, OutgoingResponse}; +pub use channel::{Channel, IncomingMessage, MessageStream, OutgoingResponse, StatusUpdate}; pub use cli::TuiChannel; pub use http::HttpChannel; pub use manager::ChannelManager;