diff --git a/crates/acp_thread/src/acp_thread.rs b/crates/acp_thread/src/acp_thread.rs index 44190a4860..892fd16655 100644 --- a/crates/acp_thread/src/acp_thread.rs +++ b/crates/acp_thread/src/acp_thread.rs @@ -18,6 +18,7 @@ use project::{AgentLocation, Project}; use std::collections::HashMap; use std::error::Error; use std::fmt::Formatter; +use std::process::ExitStatus; use std::rc::Rc; use std::{ fmt::Display, @@ -581,6 +582,7 @@ pub enum AcpThreadEvent { ToolAuthorizationRequired, Stopped, Error, + ServerExited(ExitStatus), } impl EventEmitter for AcpThread {} @@ -1229,6 +1231,10 @@ impl AcpThread { pub fn to_markdown(&self, cx: &App) -> String { self.entries.iter().map(|e| e.to_markdown(cx)).collect() } + + pub fn emit_server_exited(&mut self, status: ExitStatus, cx: &mut Context) { + cx.emit(AcpThreadEvent::ServerExited(status)); + } } #[cfg(test)] diff --git a/crates/agent_servers/src/acp/v1.rs b/crates/agent_servers/src/acp/v1.rs index 0b6fa1c48b..cea7d7c1da 100644 --- a/crates/agent_servers/src/acp/v1.rs +++ b/crates/agent_servers/src/acp/v1.rs @@ -61,11 +61,24 @@ impl AcpConnection { } }); - let io_task = cx.background_spawn(async move { - io_task.await?; - drop(child); - Ok(()) - }); + let io_task = cx.background_spawn(io_task); + + cx.spawn({ + let sessions = sessions.clone(); + async move |cx| { + let status = child.status().await?; + + for session in sessions.borrow().values() { + session + .thread + .update(cx, |thread, cx| thread.emit_server_exited(status, cx)) + .ok(); + } + + anyhow::Ok(()) + } + }) + .detach(); let response = connection .initialize(acp::InitializeRequest { diff --git a/crates/agent_servers/src/claude.rs b/crates/agent_servers/src/claude.rs index b097c0345c..216624a932 100644 --- a/crates/agent_servers/src/claude.rs +++ b/crates/agent_servers/src/claude.rs @@ -114,42 +114,42 @@ impl AgentConnection for ClaudeAgentConnection { log::trace!("Starting session with id: {}", session_id); - cx.background_spawn({ - let session_id = session_id.clone(); - async move { - let mut outgoing_rx = Some(outgoing_rx); + let mut child = spawn_claude( + &command, + ClaudeSessionMode::Start, + session_id.clone(), + &mcp_config_path, + &cwd, + )?; - let mut child = spawn_claude( - &command, - ClaudeSessionMode::Start, - session_id.clone(), - &mcp_config_path, - &cwd, - )?; + let stdin = child.stdin.take().unwrap(); + let stdout = child.stdout.take().unwrap(); - let pid = child.id(); - log::trace!("Spawned (pid: {})", pid); + let pid = child.id(); + log::trace!("Spawned (pid: {})", pid); - ClaudeAgentSession::handle_io( - outgoing_rx.take().unwrap(), - incoming_message_tx.clone(), - child.stdin.take().unwrap(), - child.stdout.take().unwrap(), - ) - .await?; + cx.background_spawn(async move { + let mut outgoing_rx = Some(outgoing_rx); - log::trace!("Stopped (pid: {})", pid); + ClaudeAgentSession::handle_io( + outgoing_rx.take().unwrap(), + incoming_message_tx.clone(), + stdin, + stdout, + ) + .await?; - drop(mcp_config_path); - anyhow::Ok(()) - } + log::trace!("Stopped (pid: {})", pid); + + drop(mcp_config_path); + anyhow::Ok(()) }) .detach(); let end_turn_tx = Rc::new(RefCell::new(None)); let handler_task = cx.spawn({ let end_turn_tx = end_turn_tx.clone(); - let thread_rx = thread_rx.clone(); + let mut thread_rx = thread_rx.clone(); async move |cx| { while let Some(message) = incoming_message_rx.next().await { ClaudeAgentSession::handle_message( @@ -160,6 +160,16 @@ impl AgentConnection for ClaudeAgentConnection { ) .await } + + if let Some(status) = child.status().await.log_err() { + if let Some(thread) = thread_rx.recv().await.ok() { + thread + .update(cx, |thread, cx| { + thread.emit_server_exited(status, cx); + }) + .ok(); + } + } } }); diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index 7c6f315fb6..6449643cac 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -5,6 +5,7 @@ use audio::{Audio, Sound}; use std::cell::RefCell; use std::collections::BTreeMap; use std::path::Path; +use std::process::ExitStatus; use std::rc::Rc; use std::sync::Arc; use std::time::Duration; @@ -90,6 +91,9 @@ enum ThreadState { Unauthenticated { connection: Rc, }, + ServerExited { + status: ExitStatus, + }, } impl AcpThreadView { @@ -229,7 +233,7 @@ impl AcpThreadView { let connect_task = agent.connect(&root_dir, &project, cx); let load_task = cx.spawn_in(window, async move |this, cx| { let connection = match connect_task.await { - Ok(thread) => thread, + Ok(connection) => connection, Err(err) => { this.update(cx, |this, cx| { this.handle_load_error(err, cx); @@ -240,6 +244,20 @@ impl AcpThreadView { } }; + // this.update_in(cx, |_this, _window, cx| { + // let status = connection.exit_status(cx); + // cx.spawn(async move |this, cx| { + // let status = status.await.ok(); + // this.update(cx, |this, cx| { + // this.thread_state = ThreadState::ServerExited { status }; + // cx.notify(); + // }) + // .ok(); + // }) + // .detach(); + // }) + // .ok(); + let result = match connection .clone() .new_thread(project.clone(), &root_dir, cx) @@ -308,7 +326,8 @@ impl AcpThreadView { ThreadState::Ready { thread, .. } => Some(thread), ThreadState::Unauthenticated { .. } | ThreadState::Loading { .. } - | ThreadState::LoadError(..) => None, + | ThreadState::LoadError(..) + | ThreadState::ServerExited { .. } => None, } } @@ -318,6 +337,7 @@ impl AcpThreadView { ThreadState::Loading { .. } => "Loading…".into(), ThreadState::LoadError(_) => "Failed to load".into(), ThreadState::Unauthenticated { .. } => "Not authenticated".into(), + ThreadState::ServerExited { .. } => "Server exited unexpectedly".into(), } } @@ -647,6 +667,9 @@ impl AcpThreadView { cx, ); } + AcpThreadEvent::ServerExited(status) => { + self.thread_state = ThreadState::ServerExited { status: *status }; + } } cx.notify(); } @@ -1383,7 +1406,29 @@ impl AcpThreadView { .into_any() } - fn render_error_state(&self, e: &LoadError, cx: &Context) -> AnyElement { + fn render_server_exited(&self, status: ExitStatus, _cx: &Context) -> AnyElement { + v_flex() + .items_center() + .justify_center() + .child(self.render_error_agent_logo()) + .child( + v_flex() + .mt_4() + .mb_2() + .gap_0p5() + .text_center() + .items_center() + .child(Headline::new("Server exited unexpectedly").size(HeadlineSize::Medium)) + .child( + Label::new(format!("Exit status: {}", status.code().unwrap_or(-127))) + .size(LabelSize::Small) + .color(Color::Muted), + ), + ) + .into_any_element() + } + + fn render_load_error(&self, e: &LoadError, cx: &Context) -> AnyElement { let mut container = v_flex() .items_center() .justify_center() @@ -2494,7 +2539,13 @@ impl Render for AcpThreadView { .flex_1() .items_center() .justify_center() - .child(self.render_error_state(e, cx)), + .child(self.render_load_error(e, cx)), + ThreadState::ServerExited { status } => v_flex() + .p_2() + .flex_1() + .items_center() + .justify_center() + .child(self.render_server_exited(*status, cx)), ThreadState::Ready { thread, .. } => { let thread_clone = thread.clone(); diff --git a/crates/agent_ui/src/agent_diff.rs b/crates/agent_ui/src/agent_diff.rs index c4dc359093..e1ceaf761d 100644 --- a/crates/agent_ui/src/agent_diff.rs +++ b/crates/agent_ui/src/agent_diff.rs @@ -1523,7 +1523,8 @@ impl AgentDiff { } AcpThreadEvent::Stopped | AcpThreadEvent::ToolAuthorizationRequired - | AcpThreadEvent::Error => {} + | AcpThreadEvent::Error + | AcpThreadEvent::ServerExited(_) => {} } }