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::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)]

View file

@ -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 {

View file

@ -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();
}
}
}
});

View file

@ -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();

View file

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