acp: Receive available commands over notifications (#37499)

Agus Zubiaga and Cole Miller created

See: https://github.com/zed-industries/agent-client-protocol/pull/62

Release Notes:

- Agent Panel: Fixes an issue where Claude Code would timeout waiting
for slash commands to be loaded

Co-authored-by: Cole Miller <cole@zed.dev>

Change summary

Cargo.lock                             |  4 +-
Cargo.toml                             |  2 
crates/acp_thread/src/acp_thread.rs    | 12 ++----
crates/acp_thread/src/connection.rs    |  1 
crates/agent2/src/agent.rs             |  1 
crates/agent_servers/src/acp.rs        |  1 
crates/agent_servers/src/claude.rs     |  2 
crates/agent_ui/src/acp/thread_view.rs | 47 ++++++++++++++-------------
crates/agent_ui/src/agent_diff.rs      |  1 
9 files changed, 34 insertions(+), 37 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -196,9 +196,9 @@ dependencies = [
 
 [[package]]
 name = "agent-client-protocol"
-version = "0.2.0-alpha.4"
+version = "0.2.0-alpha.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "603941db1d130ee275840c465b73a2312727d4acef97449550ccf033de71301f"
+checksum = "6d02292efd75080932b6466471d428c70e2ac06908ae24792fc7c36ecbaf67ca"
 dependencies = [
  "anyhow",
  "async-broadcast",

Cargo.toml 🔗

@@ -430,7 +430,7 @@ zlog_settings = { path = "crates/zlog_settings" }
 # External crates
 #
 
-agent-client-protocol = { version = "0.2.0-alpha.4", features = ["unstable"]}
+agent-client-protocol = { version = "0.2.0-alpha.6", features = ["unstable"]}
 aho-corasick = "1.1"
 alacritty_terminal = { git = "https://github.com/zed-industries/alacritty.git", branch = "add-hush-login-flag" }
 any_vec = "0.14"

crates/acp_thread/src/acp_thread.rs 🔗

@@ -785,7 +785,6 @@ pub struct AcpThread {
     session_id: acp::SessionId,
     token_usage: Option<TokenUsage>,
     prompt_capabilities: acp::PromptCapabilities,
-    available_commands: Vec<acp::AvailableCommand>,
     _observe_prompt_capabilities: Task<anyhow::Result<()>>,
     determine_shell: Shared<Task<String>>,
     terminals: HashMap<acp::TerminalId, Entity<Terminal>>,
@@ -805,6 +804,7 @@ pub enum AcpThreadEvent {
     LoadError(LoadError),
     PromptCapabilitiesUpdated,
     Refusal,
+    AvailableCommandsUpdated(Vec<acp::AvailableCommand>),
 }
 
 impl EventEmitter<AcpThreadEvent> for AcpThread {}
@@ -860,7 +860,6 @@ impl AcpThread {
         action_log: Entity<ActionLog>,
         session_id: acp::SessionId,
         mut prompt_capabilities_rx: watch::Receiver<acp::PromptCapabilities>,
-        available_commands: Vec<acp::AvailableCommand>,
         cx: &mut Context<Self>,
     ) -> Self {
         let prompt_capabilities = *prompt_capabilities_rx.borrow();
@@ -900,7 +899,6 @@ impl AcpThread {
             session_id,
             token_usage: None,
             prompt_capabilities,
-            available_commands,
             _observe_prompt_capabilities: task,
             terminals: HashMap::default(),
             determine_shell,
@@ -911,10 +909,6 @@ impl AcpThread {
         self.prompt_capabilities
     }
 
-    pub fn available_commands(&self) -> Vec<acp::AvailableCommand> {
-        self.available_commands.clone()
-    }
-
     pub fn connection(&self) -> &Rc<dyn AgentConnection> {
         &self.connection
     }
@@ -1010,6 +1004,9 @@ impl AcpThread {
             acp::SessionUpdate::Plan(plan) => {
                 self.update_plan(plan, cx);
             }
+            acp::SessionUpdate::AvailableCommandsUpdate { available_commands } => {
+                cx.emit(AcpThreadEvent::AvailableCommandsUpdated(available_commands))
+            }
         }
         Ok(())
     }
@@ -3080,7 +3077,6 @@ mod tests {
                         audio: true,
                         embedded_context: true,
                     }),
-                    vec![],
                     cx,
                 )
             });

crates/agent2/src/agent.rs 🔗

@@ -292,7 +292,6 @@ impl NativeAgent {
                 action_log.clone(),
                 session_id.clone(),
                 prompt_capabilities_rx,
-                vec![],
                 cx,
             )
         });

crates/agent_servers/src/acp.rs 🔗

@@ -224,7 +224,6 @@ impl AgentConnection for AcpConnection {
                     session_id.clone(),
                     // ACP doesn't currently support per-session prompt capabilities or changing capabilities dynamically.
                     watch::Receiver::constant(self.agent_capabilities.prompt_capabilities),
-                    response.available_commands,
                     cx,
                 )
             })?;

crates/agent_servers/src/claude.rs 🔗

@@ -40,7 +40,7 @@ impl ClaudeCode {
                         Self::PACKAGE_NAME.into(),
                         "node_modules/@anthropic-ai/claude-code/cli.js".into(),
                         true,
-                        None,
+                        Some("0.2.5".parse().unwrap()),
                         cx,
                     )
                 })?

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

@@ -430,6 +430,7 @@ impl AcpThreadView {
             window,
             cx,
         );
+        self.available_commands.replace(vec![]);
         self.new_server_version_available.take();
         cx.notify();
     }
@@ -535,26 +536,6 @@ impl AcpThreadView {
                     Ok(thread) => {
                         let action_log = thread.read(cx).action_log().clone();
 
-                        let mut available_commands = thread.read(cx).available_commands();
-
-                        if connection
-                            .auth_methods()
-                            .iter()
-                            .any(|method| method.id.0.as_ref() == "claude-login")
-                        {
-                            available_commands.push(acp::AvailableCommand {
-                                name: "login".to_owned(),
-                                description: "Authenticate".to_owned(),
-                                input: None,
-                            });
-                            available_commands.push(acp::AvailableCommand {
-                                name: "logout".to_owned(),
-                                description: "Authenticate".to_owned(),
-                                input: None,
-                            });
-                        }
-                        this.available_commands.replace(available_commands);
-
                         this.prompt_capabilities
                             .set(thread.read(cx).prompt_capabilities());
 
@@ -1343,6 +1324,30 @@ impl AcpThreadView {
                     .set(thread.read(cx).prompt_capabilities());
             }
             AcpThreadEvent::TokenUsageUpdated => {}
+            AcpThreadEvent::AvailableCommandsUpdated(available_commands) => {
+                let mut available_commands = available_commands.clone();
+
+                if thread
+                    .read(cx)
+                    .connection()
+                    .auth_methods()
+                    .iter()
+                    .any(|method| method.id.0.as_ref() == "claude-login")
+                {
+                    available_commands.push(acp::AvailableCommand {
+                        name: "login".to_owned(),
+                        description: "Authenticate".to_owned(),
+                        input: None,
+                    });
+                    available_commands.push(acp::AvailableCommand {
+                        name: "logout".to_owned(),
+                        description: "Authenticate".to_owned(),
+                        input: None,
+                    });
+                }
+
+                self.available_commands.replace(available_commands);
+            }
         }
         cx.notify();
     }
@@ -5745,7 +5750,6 @@ pub(crate) mod tests {
                         audio: true,
                         embedded_context: true,
                     }),
-                    vec![],
                     cx,
                 )
             })))
@@ -5805,7 +5809,6 @@ pub(crate) mod tests {
                         audio: true,
                         embedded_context: true,
                     }),
-                    Vec::new(),
                     cx,
                 )
             })))

crates/agent_ui/src/agent_diff.rs 🔗

@@ -1528,6 +1528,7 @@ impl AgentDiff {
             | AcpThreadEvent::EntriesRemoved(_)
             | AcpThreadEvent::ToolAuthorizationRequired
             | AcpThreadEvent::PromptCapabilitiesUpdated
+            | AcpThreadEvent::AvailableCommandsUpdated(_)
             | AcpThreadEvent::Retry(_) => {}
         }
     }