assistant: Fix evaluating slash commands in slash command output (like `/default`) (#20864)

Marshall Bowers created

This PR fixes an issue where slash commands in the output of other slash
commands were not being evaluated when configured to do so.

Closes https://github.com/zed-industries/zed/issues/20820.

Release Notes:

- Fixed slash commands from other slash commands (like `/default`) not
being evaluated (Preview only).

Change summary

crates/assistant/src/assistant_panel.rs               | 55 +++++++-----
crates/assistant/src/context.rs                       | 16 ++-
crates/assistant/src/slash_command/default_command.rs |  4 
3 files changed, 45 insertions(+), 30 deletions(-)

Detailed changes

crates/assistant/src/assistant_panel.rs 🔗

@@ -2050,30 +2050,6 @@ impl ContextEditor {
             ContextEvent::SlashCommandOutputSectionAdded { section } => {
                 self.insert_slash_command_output_sections([section.clone()], false, cx);
             }
-            ContextEvent::SlashCommandFinished {
-                output_range: _output_range,
-                run_commands_in_ranges,
-            } => {
-                for range in run_commands_in_ranges {
-                    let commands = self.context.update(cx, |context, cx| {
-                        context.reparse(cx);
-                        context
-                            .pending_commands_for_range(range.clone(), cx)
-                            .to_vec()
-                    });
-
-                    for command in commands {
-                        self.run_command(
-                            command.source_range,
-                            &command.name,
-                            &command.arguments,
-                            false,
-                            self.workspace.clone(),
-                            cx,
-                        );
-                    }
-                }
-            }
             ContextEvent::UsePendingTools => {
                 let pending_tool_uses = self
                     .context
@@ -2152,6 +2128,37 @@ impl ContextEditor {
         command_id: InvokedSlashCommandId,
         cx: &mut ViewContext<Self>,
     ) {
+        if let Some(invoked_slash_command) =
+            self.context.read(cx).invoked_slash_command(&command_id)
+        {
+            if let InvokedSlashCommandStatus::Finished = invoked_slash_command.status {
+                let run_commands_in_ranges = invoked_slash_command
+                    .run_commands_in_ranges
+                    .iter()
+                    .cloned()
+                    .collect::<Vec<_>>();
+                for range in run_commands_in_ranges {
+                    let commands = self.context.update(cx, |context, cx| {
+                        context.reparse(cx);
+                        context
+                            .pending_commands_for_range(range.clone(), cx)
+                            .to_vec()
+                    });
+
+                    for command in commands {
+                        self.run_command(
+                            command.source_range,
+                            &command.name,
+                            &command.arguments,
+                            false,
+                            self.workspace.clone(),
+                            cx,
+                        );
+                    }
+                }
+            }
+        }
+
         self.editor.update(cx, |editor, cx| {
             if let Some(invoked_slash_command) =
                 self.context.read(cx).invoked_slash_command(&command_id)

crates/assistant/src/context.rs 🔗

@@ -381,10 +381,6 @@ pub enum ContextEvent {
     SlashCommandOutputSectionAdded {
         section: SlashCommandOutputSection<language::Anchor>,
     },
-    SlashCommandFinished {
-        output_range: Range<language::Anchor>,
-        run_commands_in_ranges: Vec<Range<language::Anchor>>,
-    },
     UsePendingTools,
     ToolFinished {
         tool_use_id: Arc<str>,
@@ -916,6 +912,7 @@ impl Context {
                         InvokedSlashCommand {
                             name: name.into(),
                             range: output_range,
+                            run_commands_in_ranges: Vec::new(),
                             status: InvokedSlashCommandStatus::Running(Task::ready(())),
                             transaction: None,
                             timestamp: id.0,
@@ -1914,7 +1911,6 @@ impl Context {
                 }
 
                 let mut pending_section_stack: Vec<PendingSection> = Vec::new();
-                let mut run_commands_in_ranges: Vec<Range<language::Anchor>> = Vec::new();
                 let mut last_role: Option<Role> = None;
                 let mut last_section_range = None;
 
@@ -1980,7 +1976,13 @@ impl Context {
 
                                 let end = this.buffer.read(cx).anchor_before(insert_position);
                                 if run_commands_in_text {
-                                    run_commands_in_ranges.push(start..end);
+                                    if let Some(invoked_slash_command) =
+                                        this.invoked_slash_commands.get_mut(&command_id)
+                                    {
+                                        invoked_slash_command
+                                            .run_commands_in_ranges
+                                            .push(start..end);
+                                    }
                                 }
                             }
                             SlashCommandEvent::EndSection => {
@@ -2100,6 +2102,7 @@ impl Context {
             InvokedSlashCommand {
                 name: name.to_string().into(),
                 range: command_range.clone(),
+                run_commands_in_ranges: Vec::new(),
                 status: InvokedSlashCommandStatus::Running(insert_output_task),
                 transaction: Some(first_transaction),
                 timestamp: command_id.0,
@@ -3176,6 +3179,7 @@ pub struct ParsedSlashCommand {
 pub struct InvokedSlashCommand {
     pub name: SharedString,
     pub range: Range<language::Anchor>,
+    pub run_commands_in_ranges: Vec<Range<language::Anchor>>,
     pub status: InvokedSlashCommandStatus,
     pub transaction: Option<language::TransactionId>,
     timestamp: clock::Lamport,

crates/assistant/src/slash_command/default_command.rs 🔗

@@ -69,6 +69,10 @@ impl SlashCommand for DefaultSlashCommand {
                 text.push('\n');
             }
 
+            if !text.ends_with('\n') {
+                text.push('\n');
+            }
+
             Ok(SlashCommandOutput {
                 sections: vec![SlashCommandOutputSection {
                     range: 0..text.len(),