From 678fd29bceaf8a78a4907cf111a4fdca67c5e10e 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:25:54 +0000 Subject: [PATCH] acp: Add a timeout when initializing an ACP agent so the user isn't waiting forever (#43663) (cherry-pick to preview) (#43668) Cherry-pick of #43663 to preview ---- 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 1c9e3f83e383658051f7799a7e3096f532addbe1..3dc99b781a336392e247975862d218ae6b8dea9a 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -498,7 +498,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)) => { @@ -7358,4 +7368,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"), + } + } }