From d78e6f47b43816bd506bed906db97ec44aa1aa78 Mon Sep 17 00:00:00 2001 From: "zed-zippy[bot]" <234243425+zed-zippy[bot]@users.noreply.github.com> Date: Thu, 27 Nov 2025 13:24:04 +0000 Subject: [PATCH] acp: Add a timeout when initializing an ACP agent so the user isn't waiting forever (#43663) (cherry-pick to stable) (#43667) Cherry-pick of #43663 to stable ---- Sometimes we are unable to receive messages at all from an agent. This puts on upper bound on the `initialize` call so we can at least give a message to the user that something is wrong here. 30s might feel like too long, but I wanted to avoid some false positives in case there was something an agent needed to do at startup. This will still communicate to the user at some point that something is wrong, rather than leave them waiting forever with no signal that something is going wrong. Release Notes: - agent: Show an error message to the user if we are unable to initialize an ACP agent in a reasonable amount of time. Co-authored-by: Ben Brandt --- crates/agent_ui/src/acp/thread_view.rs | 62 +++++++++++++++++++++++++- 1 file changed, 61 insertions(+), 1 deletion(-) diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index 57454c4feb0f5d5d8dfd27d9581fb80b3cc63625..d81d884dd1170f973b38548e9c699a1f7f6a9cd9 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -496,7 +496,17 @@ impl AcpThreadView { Some(new_version_available_tx), ); - let connect_task = agent.connect(root_dir.as_deref(), delegate, cx); + let agent_name = agent.name(); + let timeout = cx.background_executor().timer(Duration::from_secs(30)); + let connect_task = smol::future::or( + agent.connect(root_dir.as_deref(), delegate, cx), + async move { + timeout.await; + Err(anyhow::Error::new(LoadError::Other( + format!("{agent_name} is unable to initialize after 30 seconds.").into(), + ))) + }, + ); let load_task = cx.spawn_in(window, async move |this, cx| { let connection = match connect_task.await { Ok((connection, login)) => { @@ -7316,4 +7326,54 @@ pub(crate) mod tests { assert_eq!(text, expected_txt); }) } + + #[gpui::test] + async fn test_initialize_timeout(cx: &mut TestAppContext) { + init_test(cx); + + struct InfiniteInitialize; + + impl AgentServer for InfiniteInitialize { + fn telemetry_id(&self) -> &'static str { + "test" + } + + fn logo(&self) -> ui::IconName { + ui::IconName::Ai + } + + fn name(&self) -> SharedString { + "Test".into() + } + + fn connect( + &self, + _root_dir: Option<&Path>, + _delegate: AgentServerDelegate, + cx: &mut App, + ) -> Task, Option)>> + { + cx.spawn(async |_| futures::future::pending().await) + } + + fn into_any(self: Rc) -> Rc { + self + } + } + + let (thread_view, cx) = setup_thread_view(InfiniteInitialize, cx).await; + + cx.executor().advance_clock(Duration::from_secs(31)); + cx.run_until_parked(); + + let error = thread_view.read_with(cx, |thread_view, _| match &thread_view.thread_state { + ThreadState::LoadError(err) => err.clone(), + _ => panic!("Incorrect thread state"), + }); + + match error { + LoadError::Other(str) => assert!(str.contains("initialize")), + _ => panic!("Unexpected load error"), + } + } }