acp: Add a timeout when initializing an ACP agent so the user isn't waiting forever (#43663)

Ben Brandt created

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.

Change summary

crates/agent_ui/src/acp/thread_view.rs | 62 +++++++++++++++++++++++++++
1 file changed, 61 insertions(+), 1 deletion(-)

Detailed changes

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<gpui::Result<(Rc<dyn AgentConnection>, Option<task::SpawnInTerminal>)>>
+            {
+                cx.spawn(async |_| futures::future::pending().await)
+            }
+
+            fn into_any(self: Rc<Self>) -> Rc<dyn Any> {
+                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"),
+        }
+    }
 }