acp: Fix gemini process being leaked (#37012)

Cole Miller , Agus Zubiaga , Bennet Bo Fenner , and Antonio Scandurra created

Release Notes:

- acp: Fixed a bug that caused external agent server subprocesses to be
leaked.

---------

Co-authored-by: Agus Zubiaga <agus@zed.dev>
Co-authored-by: Bennet Bo Fenner <bennetbo@gmx.de>
Co-authored-by: Antonio Scandurra <me@as-cii.com>

Change summary

crates/agent_servers/src/acp.rs         | 15 +++++++++------
crates/agent_ui/src/acp/thread_view.rs  |  3 ++-
crates/tab_switcher/src/tab_switcher.rs |  4 ++--
3 files changed, 13 insertions(+), 9 deletions(-)

Detailed changes

crates/agent_servers/src/acp.rs 🔗

@@ -30,6 +30,8 @@ pub struct AcpConnection {
     auth_methods: Vec<acp::AuthMethod>,
     prompt_capabilities: acp::PromptCapabilities,
     _io_task: Task<Result<()>>,
+    _wait_task: Task<Result<()>>,
+    _stderr_task: Task<Result<()>>,
 }
 
 pub struct AcpSession {
@@ -86,7 +88,7 @@ impl AcpConnection {
 
         let io_task = cx.background_spawn(io_task);
 
-        cx.background_spawn(async move {
+        let stderr_task = cx.background_spawn(async move {
             let mut stderr = BufReader::new(stderr);
             let mut line = String::new();
             while let Ok(n) = stderr.read_line(&mut line).await
@@ -95,10 +97,10 @@ impl AcpConnection {
                 log::warn!("agent stderr: {}", &line);
                 line.clear();
             }
-        })
-        .detach();
+            Ok(())
+        });
 
-        cx.spawn({
+        let wait_task = cx.spawn({
             let sessions = sessions.clone();
             async move |cx| {
                 let status = child.status().await?;
@@ -114,8 +116,7 @@ impl AcpConnection {
 
                 anyhow::Ok(())
             }
-        })
-        .detach();
+        });
 
         let connection = Rc::new(connection);
 
@@ -148,6 +149,8 @@ impl AcpConnection {
             sessions,
             prompt_capabilities: response.agent_capabilities.prompt_capabilities,
             _io_task: io_task,
+            _wait_task: wait_task,
+            _stderr_task: stderr_task,
         })
     }
 

crates/agent_ui/src/acp/thread_view.rs 🔗

@@ -905,9 +905,10 @@ impl AcpThreadView {
         self.editing_message.take();
         self.thread_feedback.clear();
 
-        let Some(thread) = self.thread().cloned() else {
+        let Some(thread) = self.thread() else {
             return;
         };
+        let thread = thread.downgrade();
         if self.should_be_following {
             self.workspace
                 .update(cx, |workspace, cx| {

crates/tab_switcher/src/tab_switcher.rs 🔗

@@ -268,7 +268,7 @@ impl TabMatch {
             .flatten();
         let colored_icon = icon.color(git_status_color.unwrap_or_default());
 
-        let most_sever_diagostic_level = if show_diagnostics == ShowDiagnostics::Off {
+        let most_severe_diagnostic_level = if show_diagnostics == ShowDiagnostics::Off {
             None
         } else {
             let buffer_store = project.read(cx).buffer_store().read(cx);
@@ -287,7 +287,7 @@ impl TabMatch {
         };
 
         let decorations =
-            entry_diagnostic_aware_icon_decoration_and_color(most_sever_diagostic_level)
+            entry_diagnostic_aware_icon_decoration_and_color(most_severe_diagnostic_level)
                 .filter(|(d, _)| {
                     *d != IconDecorationKind::Triangle
                         || show_diagnostics != ShowDiagnostics::Errors