From 9232e623e802bc788f4685ffec4081f055b34b23 Mon Sep 17 00:00:00 2001 From: Illia Polosukhin Date: Mon, 2 Feb 2026 23:59:28 -0800 Subject: [PATCH] Fix TUI shutdown: send /shutdown message and handle in agent loop When the TUI quits (Ctrl+D twice), it now: 1. Sends a "/shutdown" message through the channel before closing 2. Explicitly drops msg_tx to ensure channel closure The agent loop now: 1. Returns Option from handle_message (None = shutdown) 2. Handles /quit, /exit, /shutdown commands by returning None 3. Breaks out of the main loop on shutdown signal 4. Lists /quit in help menu Co-Authored-By: Claude Opus 4.5 --- src/agent/agent_loop.rs | 84 +++++++++++++++++++++++--------------- src/channels/cli/events.rs | 7 +++- 2 files changed, 58 insertions(+), 33 deletions(-) diff --git a/src/agent/agent_loop.rs b/src/agent/agent_loop.rs index 75f677ae..b459f1f1 100644 --- a/src/agent/agent_loop.rs +++ b/src/agent/agent_loop.rs @@ -84,14 +84,25 @@ impl Agent { tracing::info!("Agent {} ready and listening", self.config.name); while let Some(message) = message_stream.next().await { - if let Err(e) = self.handle_message(&message).await { - tracing::error!("Error handling message: {}", e); - - // Try to send error response - let _ = self - .channels - .respond(&message, OutgoingResponse::text(format!("Error: {}", e))) - .await; + match self.handle_message(&message).await { + Ok(Some(response)) => { + let _ = self + .channels + .respond(&message, OutgoingResponse::text(response)) + .await; + } + Ok(None) => { + // Shutdown signal received + tracing::info!("Shutdown signal received, exiting..."); + break; + } + Err(e) => { + tracing::error!("Error handling message: {}", e); + let _ = self + .channels + .respond(&message, OutgoingResponse::text(format!("Error: {}", e))) + .await; + } } } @@ -104,7 +115,7 @@ impl Agent { Ok(()) } - async fn handle_message(&self, message: &IncomingMessage) -> Result<(), Error> { + async fn handle_message(&self, message: &IncomingMessage) -> Result, Error> { tracing::debug!( "Received message from {} on {}: {}", message.user_id, @@ -122,33 +133,30 @@ impl Agent { title, description, category, - } => self.handle_create_job(title, description, category).await?, + } => Some(self.handle_create_job(title, description, category).await?), - MessageIntent::CheckJobStatus { job_id } => self.handle_check_status(job_id).await?, + MessageIntent::CheckJobStatus { job_id } => { + Some(self.handle_check_status(job_id).await?) + } - MessageIntent::CancelJob { job_id } => self.handle_cancel_job(&job_id).await?, + MessageIntent::CancelJob { job_id } => Some(self.handle_cancel_job(&job_id).await?), - MessageIntent::ListJobs { filter } => self.handle_list_jobs(filter).await?, + MessageIntent::ListJobs { filter } => Some(self.handle_list_jobs(filter).await?), - MessageIntent::HelpJob { job_id } => self.handle_help_job(&job_id).await?, + MessageIntent::HelpJob { job_id } => Some(self.handle_help_job(&job_id).await?), - MessageIntent::Chat { content } => self.handle_chat(message, &content).await?, + MessageIntent::Chat { content } => Some(self.handle_chat(message, &content).await?), MessageIntent::Command { command, args } => { self.handle_command(&command, &args).await? } - MessageIntent::Unknown => { - "I'm not sure what you're asking. Try '/help' for available commands.".to_string() - } + MessageIntent::Unknown => Some( + "I'm not sure what you're asking. Try '/help' for available commands.".to_string(), + ), }; - // Send response - self.channels - .respond(message, OutgoingResponse::text(response)) - .await?; - - Ok(()) + Ok(response) } async fn handle_create_job( @@ -300,32 +308,44 @@ impl Agent { Ok(response) } - async fn handle_command(&self, command: &str, _args: &[String]) -> Result { + async fn handle_command( + &self, + command: &str, + _args: &[String], + ) -> Result, Error> { match command { - "help" => Ok(r#"Available commands: + "help" => Ok(Some( + r#"Available commands: /job - Create a new job /status [job_id] - Check job status /cancel - Cancel a job /list - List all jobs /help - Help a stuck job + /quit - Exit the agent Or just chat naturally and I'll try to understand what you need!"# - .to_string()), + .to_string(), + )), - "ping" => Ok("pong!".to_string()), + "ping" => Ok(Some("pong!".to_string())), - "version" => Ok(format!( + "version" => Ok(Some(format!( "{} v{}", env!("CARGO_PKG_NAME"), env!("CARGO_PKG_VERSION") - )), + ))), "tools" => { let tools = self.tools.list().await; - Ok(format!("Available tools: {}", tools.join(", "))) + Ok(Some(format!("Available tools: {}", tools.join(", ")))) } - _ => Ok(format!("Unknown command: {}. Try /help", command)), + "quit" | "exit" | "shutdown" => { + // Signal shutdown - return None to indicate no response needed + Ok(None) + } + + _ => Ok(Some(format!("Unknown command: {}. Try /help", command))), } } } diff --git a/src/channels/cli/events.rs b/src/channels/cli/events.rs index 91d91fee..d140e7ce 100644 --- a/src/channels/cli/events.rs +++ b/src/channels/cli/events.rs @@ -26,8 +26,13 @@ pub fn run_event_loop( // Render terminal.draw(|f| render::render(f, app))?; - // Check for quit + // Check for quit - send shutdown signal and exit if app.should_quit { + // Send a shutdown message so the agent loop knows to exit + let shutdown_msg = IncomingMessage::new("tui", "system", "/shutdown"); + let _ = msg_tx.blocking_send(shutdown_msg); + // Explicitly drop to close the channel + drop(msg_tx); return Ok(()); }