Render error state when agent binary exits unexpectedly (#35651)

Cole Miller and Agus created

This PR adds handling for the case where an agent binary exits
unexpectedly after successfully establishing a connection.

Release Notes:

- N/A

---------

Co-authored-by: Agus <agus@zed.dev>

Change summary

crates/acp_thread/src/acp_thread.rs    |  6 ++
crates/agent_servers/src/acp/v1.rs     | 23 +++++++--
crates/agent_servers/src/claude.rs     | 70 ++++++++++++++++------------
crates/agent_ui/src/acp/thread_view.rs | 59 ++++++++++++++++++++++-
crates/agent_ui/src/agent_diff.rs      |  3 
5 files changed, 121 insertions(+), 40 deletions(-)

Detailed changes

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<AcpThreadEvent> 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<Self>) {
+        cx.emit(AcpThreadEvent::ServerExited(status));
+    }
 }
 
 #[cfg(test)]

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 {

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 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?;
-
-                    log::trace!("Stopped (pid: {})", pid);
-
-                    drop(mcp_config_path);
-                    anyhow::Ok(())
-                }
+            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);
+
+            cx.background_spawn(async move {
+                let mut outgoing_rx = Some(outgoing_rx);
+
+                ClaudeAgentSession::handle_io(
+                    outgoing_rx.take().unwrap(),
+                    incoming_message_tx.clone(),
+                    stdin,
+                    stdout,
+                )
+                .await?;
+
+                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();
+                        }
+                    }
                 }
             });
 

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<dyn AgentConnection>,
     },
+    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<Self>) -> AnyElement {
+    fn render_server_exited(&self, status: ExitStatus, _cx: &Context<Self>) -> 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<Self>) -> 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();
 

crates/agent_ui/src/agent_diff.rs πŸ”—

@@ -1523,7 +1523,8 @@ impl AgentDiff {
             }
             AcpThreadEvent::Stopped
             | AcpThreadEvent::ToolAuthorizationRequired
-            | AcpThreadEvent::Error => {}
+            | AcpThreadEvent::Error
+            | AcpThreadEvent::ServerExited(_) => {}
         }
     }