agent_ui: Ensure message editor placeholder text is accurate (#42375)

Danilo Leal created

This PR creates a dedicated function for the agent panel message
editor's placeholder text so that we can wait for the agent
initialization to capture whether they support slash commands or not. On
the one (nice) hand, this allow us to stop matching agents by name and
make this a bit more generic. On the other (bad) hand, the "/ for
commands" bit should take a little second to show up because we can only
know whether an agent supports it after it is initialized.

This is particularly relevant now that we have agents coming from
extensions and for them, we would obviously not be able to match by
name.

Release Notes:

- agent: Fixed agent panel message editor's placeholder text by making
it more accurate as to whether agents support slash commands,
particularly those coming from extensions.

Change summary

crates/agent_ui/src/acp/message_editor.rs | 11 ++++++++
crates/agent_ui/src/acp/thread_view.rs    | 34 +++++++++++++++---------
2 files changed, 32 insertions(+), 13 deletions(-)

Detailed changes

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

@@ -1195,6 +1195,17 @@ impl MessageEditor {
         self.editor.read(cx).text(cx)
     }
 
+    pub fn set_placeholder_text(
+        &mut self,
+        placeholder: &str,
+        window: &mut Window,
+        cx: &mut Context<Self>,
+    ) {
+        self.editor.update(cx, |editor, cx| {
+            editor.set_placeholder_text(placeholder, window, cx);
+        });
+    }
+
     #[cfg(test)]
     pub fn set_text(&mut self, text: &str, window: &mut Window, cx: &mut Context<Self>) {
         self.editor.update(cx, |editor, cx| {

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

@@ -337,19 +337,7 @@ impl AcpThreadView {
         let prompt_capabilities = Rc::new(RefCell::new(acp::PromptCapabilities::default()));
         let available_commands = Rc::new(RefCell::new(vec![]));
 
-        let placeholder = if agent.name() == "Zed Agent" {
-            format!("Message the {} — @ to include context", agent.name())
-        } else if agent.name() == "Claude Code"
-            || agent.name() == "Codex"
-            || !available_commands.borrow().is_empty()
-        {
-            format!(
-                "Message {} — @ to include context, / for commands",
-                agent.name()
-            )
-        } else {
-            format!("Message {} — @ to include context", agent.name())
-        };
+        let placeholder = placeholder_text(agent.name().as_ref(), false);
 
         let message_editor = cx.new(|cx| {
             let mut editor = MessageEditor::new(
@@ -1456,7 +1444,14 @@ impl AcpThreadView {
                     });
                 }
 
+                let has_commands = !available_commands.is_empty();
                 self.available_commands.replace(available_commands);
+
+                let new_placeholder = placeholder_text(self.agent.name().as_ref(), has_commands);
+
+                self.message_editor.update(cx, |editor, cx| {
+                    editor.set_placeholder_text(&new_placeholder, window, cx);
+                });
             }
             AcpThreadEvent::ModeUpdated(_mode) => {
                 // The connection keeps track of the mode
@@ -5708,6 +5703,19 @@ fn loading_contents_spinner(size: IconSize) -> AnyElement {
         .into_any_element()
 }
 
+fn placeholder_text(agent_name: &str, has_commands: bool) -> String {
+    if agent_name == "Zed Agent" {
+        format!("Message the {} — @ to include context", agent_name)
+    } else if has_commands {
+        format!(
+            "Message {} — @ to include context, / for commands",
+            agent_name
+        )
+    } else {
+        format!("Message {} — @ to include context", agent_name)
+    }
+}
+
 impl Focusable for AcpThreadView {
     fn focus_handle(&self, cx: &App) -> FocusHandle {
         match self.thread_state {