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:
parent
142efbac0d
commit
bc2108cbba
5 changed files with 116 additions and 35 deletions
|
@ -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)]
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -114,42 +114,42 @@ impl AgentConnection for ClaudeAgentConnection {
|
||||||
|
|
||||||
log::trace!("Starting session with id: {}", session_id);
|
log::trace!("Starting session with id: {}", session_id);
|
||||||
|
|
||||||
cx.background_spawn({
|
let mut child = spawn_claude(
|
||||||
let session_id = session_id.clone();
|
&command,
|
||||||
async move {
|
ClaudeSessionMode::Start,
|
||||||
let mut outgoing_rx = Some(outgoing_rx);
|
session_id.clone(),
|
||||||
|
&mcp_config_path,
|
||||||
|
&cwd,
|
||||||
|
)?;
|
||||||
|
|
||||||
let mut child = spawn_claude(
|
let stdin = child.stdin.take().unwrap();
|
||||||
&command,
|
let stdout = child.stdout.take().unwrap();
|
||||||
ClaudeSessionMode::Start,
|
|
||||||
session_id.clone(),
|
|
||||||
&mcp_config_path,
|
|
||||||
&cwd,
|
|
||||||
)?;
|
|
||||||
|
|
||||||
let pid = child.id();
|
let pid = child.id();
|
||||||
log::trace!("Spawned (pid: {})", pid);
|
log::trace!("Spawned (pid: {})", pid);
|
||||||
|
|
||||||
ClaudeAgentSession::handle_io(
|
cx.background_spawn(async move {
|
||||||
outgoing_rx.take().unwrap(),
|
let mut outgoing_rx = Some(outgoing_rx);
|
||||||
incoming_message_tx.clone(),
|
|
||||||
child.stdin.take().unwrap(),
|
|
||||||
child.stdout.take().unwrap(),
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
log::trace!("Stopped (pid: {})", pid);
|
ClaudeAgentSession::handle_io(
|
||||||
|
outgoing_rx.take().unwrap(),
|
||||||
|
incoming_message_tx.clone(),
|
||||||
|
stdin,
|
||||||
|
stdout,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
drop(mcp_config_path);
|
log::trace!("Stopped (pid: {})", pid);
|
||||||
anyhow::Ok(())
|
|
||||||
}
|
drop(mcp_config_path);
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -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();
|
||||||
|
|
||||||
|
|
|
@ -1523,7 +1523,8 @@ impl AgentDiff {
|
||||||
}
|
}
|
||||||
AcpThreadEvent::Stopped
|
AcpThreadEvent::Stopped
|
||||||
| AcpThreadEvent::ToolAuthorizationRequired
|
| AcpThreadEvent::ToolAuthorizationRequired
|
||||||
| AcpThreadEvent::Error => {}
|
| AcpThreadEvent::Error
|
||||||
|
| AcpThreadEvent::ServerExited(_) => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue