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

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>
This commit is contained in:
Cole Miller 2025-08-05 18:52:08 -04:00 committed by GitHub
parent 142efbac0d
commit bc2108cbba
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 116 additions and 35 deletions

View file

@ -18,6 +18,7 @@ use project::{AgentLocation, Project};
use std::collections::HashMap; use std::collections::HashMap;
use std::error::Error; use std::error::Error;
use std::fmt::Formatter; use std::fmt::Formatter;
use std::process::ExitStatus;
use std::rc::Rc; use std::rc::Rc;
use std::{ use std::{
fmt::Display, fmt::Display,
@ -581,6 +582,7 @@ pub enum AcpThreadEvent {
ToolAuthorizationRequired, ToolAuthorizationRequired,
Stopped, Stopped,
Error, Error,
ServerExited(ExitStatus),
} }
impl EventEmitter<AcpThreadEvent> for AcpThread {} impl EventEmitter<AcpThreadEvent> for AcpThread {}
@ -1229,6 +1231,10 @@ impl AcpThread {
pub fn to_markdown(&self, cx: &App) -> String { pub fn to_markdown(&self, cx: &App) -> String {
self.entries.iter().map(|e| e.to_markdown(cx)).collect() 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)] #[cfg(test)]

View file

@ -61,11 +61,24 @@ impl AcpConnection {
} }
}); });
let io_task = cx.background_spawn(async move { let io_task = cx.background_spawn(io_task);
io_task.await?;
drop(child); cx.spawn({
Ok(()) 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 let response = connection
.initialize(acp::InitializeRequest { .initialize(acp::InitializeRequest {

View file

@ -114,11 +114,6 @@ impl AgentConnection for ClaudeAgentConnection {
log::trace!("Starting session with id: {}", session_id); 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( let mut child = spawn_claude(
&command, &command,
ClaudeSessionMode::Start, ClaudeSessionMode::Start,
@ -127,14 +122,20 @@ impl AgentConnection for ClaudeAgentConnection {
&cwd, &cwd,
)?; )?;
let stdin = child.stdin.take().unwrap();
let stdout = child.stdout.take().unwrap();
let pid = child.id(); let pid = child.id();
log::trace!("Spawned (pid: {})", pid); log::trace!("Spawned (pid: {})", pid);
cx.background_spawn(async move {
let mut outgoing_rx = Some(outgoing_rx);
ClaudeAgentSession::handle_io( ClaudeAgentSession::handle_io(
outgoing_rx.take().unwrap(), outgoing_rx.take().unwrap(),
incoming_message_tx.clone(), incoming_message_tx.clone(),
child.stdin.take().unwrap(), stdin,
child.stdout.take().unwrap(), stdout,
) )
.await?; .await?;
@ -142,14 +143,13 @@ impl AgentConnection for ClaudeAgentConnection {
drop(mcp_config_path); drop(mcp_config_path);
anyhow::Ok(()) anyhow::Ok(())
}
}) })
.detach(); .detach();
let end_turn_tx = Rc::new(RefCell::new(None)); let end_turn_tx = Rc::new(RefCell::new(None));
let handler_task = cx.spawn({ let handler_task = cx.spawn({
let end_turn_tx = end_turn_tx.clone(); let end_turn_tx = end_turn_tx.clone();
let thread_rx = thread_rx.clone(); let mut thread_rx = thread_rx.clone();
async move |cx| { async move |cx| {
while let Some(message) = incoming_message_rx.next().await { while let Some(message) = incoming_message_rx.next().await {
ClaudeAgentSession::handle_message( ClaudeAgentSession::handle_message(
@ -160,6 +160,16 @@ impl AgentConnection for ClaudeAgentConnection {
) )
.await .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();
}
}
} }
}); });

View file

@ -5,6 +5,7 @@ use audio::{Audio, Sound};
use std::cell::RefCell; use std::cell::RefCell;
use std::collections::BTreeMap; use std::collections::BTreeMap;
use std::path::Path; use std::path::Path;
use std::process::ExitStatus;
use std::rc::Rc; use std::rc::Rc;
use std::sync::Arc; use std::sync::Arc;
use std::time::Duration; use std::time::Duration;
@ -90,6 +91,9 @@ enum ThreadState {
Unauthenticated { Unauthenticated {
connection: Rc<dyn AgentConnection>, connection: Rc<dyn AgentConnection>,
}, },
ServerExited {
status: ExitStatus,
},
} }
impl AcpThreadView { impl AcpThreadView {
@ -229,7 +233,7 @@ impl AcpThreadView {
let connect_task = agent.connect(&root_dir, &project, cx); let connect_task = agent.connect(&root_dir, &project, cx);
let load_task = cx.spawn_in(window, async move |this, cx| { let load_task = cx.spawn_in(window, async move |this, cx| {
let connection = match connect_task.await { let connection = match connect_task.await {
Ok(thread) => thread, Ok(connection) => connection,
Err(err) => { Err(err) => {
this.update(cx, |this, cx| { this.update(cx, |this, cx| {
this.handle_load_error(err, 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 let result = match connection
.clone() .clone()
.new_thread(project.clone(), &root_dir, cx) .new_thread(project.clone(), &root_dir, cx)
@ -308,7 +326,8 @@ impl AcpThreadView {
ThreadState::Ready { thread, .. } => Some(thread), ThreadState::Ready { thread, .. } => Some(thread),
ThreadState::Unauthenticated { .. } ThreadState::Unauthenticated { .. }
| ThreadState::Loading { .. } | ThreadState::Loading { .. }
| ThreadState::LoadError(..) => None, | ThreadState::LoadError(..)
| ThreadState::ServerExited { .. } => None,
} }
} }
@ -318,6 +337,7 @@ impl AcpThreadView {
ThreadState::Loading { .. } => "Loading…".into(), ThreadState::Loading { .. } => "Loading…".into(),
ThreadState::LoadError(_) => "Failed to load".into(), ThreadState::LoadError(_) => "Failed to load".into(),
ThreadState::Unauthenticated { .. } => "Not authenticated".into(), ThreadState::Unauthenticated { .. } => "Not authenticated".into(),
ThreadState::ServerExited { .. } => "Server exited unexpectedly".into(),
} }
} }
@ -647,6 +667,9 @@ impl AcpThreadView {
cx, cx,
); );
} }
AcpThreadEvent::ServerExited(status) => {
self.thread_state = ThreadState::ServerExited { status: *status };
}
} }
cx.notify(); cx.notify();
} }
@ -1383,7 +1406,29 @@ impl AcpThreadView {
.into_any() .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() let mut container = v_flex()
.items_center() .items_center()
.justify_center() .justify_center()
@ -2494,7 +2539,13 @@ impl Render for AcpThreadView {
.flex_1() .flex_1()
.items_center() .items_center()
.justify_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, .. } => { ThreadState::Ready { thread, .. } => {
let thread_clone = thread.clone(); let thread_clone = thread.clone();

View file

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