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 <[email protected]>
This commit is contained in:
Illia Polosukhin
2026-02-03 00:12:48 -08:00
co-authored by Claude Opus 4.5
parent d1cb748914
commit 7f9f0cd21e
5 changed files with 104 additions and 12 deletions
+43 -8
View File
@@ -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,20 +307,41 @@ impl Agent {
}
}
async fn handle_chat(
&self,
_message: &IncomingMessage,
content: &str,
) -> Result<String, Error> {
async fn handle_chat(&self, message: &IncomingMessage, content: &str) -> Result<String, Error> {
// 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?;
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(
&self,
+22
View File
@@ -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>;
+21 -1
View File
@@ -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() {
+16 -1
View File
@@ -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<String, Result<(), ChannelError>> {
let channels = self.channels.read().await;
+1 -1
View File
@@ -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;