agent_ui: Queue prompts by default when sending them while generating (#46797)

Danilo Leal created

This PR makes queueing a prompt the default behavior when sending a new
one while there's an on-going generation. You can still send a prompt
that will immediately interrupt the agent with the `cmd-shift-enter`
keybinding, though, which preserves the current behavior.

The main motivation for this change is to make the queueing not only
more discoverable, but more useful as well, as we're parting from the
assumption that most of the time, what you want is to queue it as
opposed to interrupting it (even though it's still possible to do either
through the keybinding I mentioned above or simply by stopping the
generation and sending a new one).

Here's a quick video:


https://github.com/user-attachments/assets/37f92433-70ef-459f-98ff-41ed80e3e43f

In the video, I show sending one prompt and then sending two others that
fall straight into the queue. Then, in the middle of the generation of
my first prompt, I use the `cmd-shift-enter` keybinding to send a prompt
immediately, interrupting the agent, effectively being sent in front of
the queue.

Release Notes:

- Agent: Made queueing prompts the default behavior when sending them
while there's an on-going generation.

Change summary

assets/keymaps/default-linux.json         |  2 
assets/keymaps/default-macos.json         |  2 
assets/keymaps/default-windows.json       |  2 
crates/agent_ui/src/acp/message_editor.rs | 30 +++++++---------
crates/agent_ui/src/acp/thread_view.rs    | 43 ++++++++++++++++--------
crates/agent_ui/src/agent_ui.rs           |  4 +-
6 files changed, 46 insertions(+), 37 deletions(-)

Detailed changes

assets/keymaps/default-linux.json 🔗

@@ -308,7 +308,7 @@
       "shift-alt-y": "agent::KeepAll",
       "shift-alt-z": "agent::RejectAll",
       "ctrl-enter": "agent::ChatWithFollow",
-      "ctrl-shift-enter": "agent::QueueMessage",
+      "ctrl-shift-enter": "agent::SendImmediately",
       "ctrl-shift-alt-enter": "agent::SendNextQueuedMessage",
       "ctrl-shift-backspace": "agent::ClearMessageQueue",
       "ctrl-shift-v": "agent::PasteRaw",

assets/keymaps/default-macos.json 🔗

@@ -356,7 +356,7 @@
       "shift-alt-y": "agent::KeepAll",
       "shift-alt-z": "agent::RejectAll",
       "cmd-enter": "agent::ChatWithFollow",
-      "cmd-shift-enter": "agent::QueueMessage",
+      "cmd-shift-enter": "agent::SendImmediately",
       "cmd-shift-alt-enter": "agent::SendNextQueuedMessage",
       "cmd-shift-backspace": "agent::ClearMessageQueue",
       "cmd-shift-v": "agent::PasteRaw",

assets/keymaps/default-windows.json 🔗

@@ -310,7 +310,7 @@
       "shift-alt-y": "agent::KeepAll",
       "shift-alt-z": "agent::RejectAll",
       "ctrl-enter": "agent::ChatWithFollow",
-      "ctrl-shift-enter": "agent::QueueMessage",
+      "ctrl-shift-enter": "agent::SendImmediately",
       "ctrl-shift-alt-enter": "agent::SendNextQueuedMessage",
       "ctrl-shift-backspace": "agent::ClearMessageQueue",
       "ctrl-shift-v": "agent::PasteRaw",

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

@@ -1,4 +1,4 @@
-use crate::QueueMessage;
+use crate::SendImmediately;
 use crate::acp::AcpThreadHistory;
 use crate::{
     ChatWithFollow,
@@ -53,7 +53,7 @@ pub struct MessageEditor {
 #[derive(Clone, Copy, Debug)]
 pub enum MessageEditorEvent {
     Send,
-    Queue,
+    SendImmediately,
     Cancel,
     Focus,
     LostFocus,
@@ -508,18 +508,6 @@ impl MessageEditor {
         cx.emit(MessageEditorEvent::Send)
     }
 
-    pub fn queue(&mut self, cx: &mut Context<Self>) {
-        if self.is_empty(cx) {
-            return;
-        }
-
-        self.editor.update(cx, |editor, cx| {
-            editor.clear_inlay_hints(cx);
-        });
-
-        cx.emit(MessageEditorEvent::Queue)
-    }
-
     pub fn trigger_completion_menu(&mut self, window: &mut Window, cx: &mut Context<Self>) {
         let editor = self.editor.clone();
 
@@ -563,8 +551,16 @@ impl MessageEditor {
         self.send(cx);
     }
 
-    fn queue_message(&mut self, _: &QueueMessage, _: &mut Window, cx: &mut Context<Self>) {
-        self.queue(cx);
+    fn send_immediately(&mut self, _: &SendImmediately, _: &mut Window, cx: &mut Context<Self>) {
+        if self.is_empty(cx) {
+            return;
+        }
+
+        self.editor.update(cx, |editor, cx| {
+            editor.clear_inlay_hints(cx);
+        });
+
+        cx.emit(MessageEditorEvent::SendImmediately)
     }
 
     fn chat_with_follow(
@@ -1009,7 +1005,7 @@ impl Render for MessageEditor {
         div()
             .key_context("MessageEditor")
             .on_action(cx.listener(Self::chat))
-            .on_action(cx.listener(Self::queue_message))
+            .on_action(cx.listener(Self::send_immediately))
             .on_action(cx.listener(Self::chat_with_follow))
             .on_action(cx.listener(Self::cancel))
             .on_action(cx.listener(Self::paste_raw))

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

@@ -73,7 +73,7 @@ use crate::ui::{AgentNotification, AgentNotificationEvent, BurnModeTooltip, Usag
 use crate::{
     AgentDiffPane, AgentPanel, AllowAlways, AllowOnce, ClearMessageQueue, ContinueThread,
     ContinueWithBurnMode, CycleFavoriteModels, CycleModeSelector, ExpandMessageEditor, Follow,
-    KeepAll, NewThread, OpenAgentDiff, OpenHistory, QueueMessage, RejectAll, RejectOnce,
+    KeepAll, NewThread, OpenAgentDiff, OpenHistory, RejectAll, RejectOnce, SendImmediately,
     SendNextQueuedMessage, ToggleBurnMode, ToggleProfileSelector,
 };
 
@@ -1204,7 +1204,7 @@ impl AcpThreadView {
     ) {
         match event {
             MessageEditorEvent::Send => self.send(window, cx),
-            MessageEditorEvent::Queue => self.queue_message(window, cx),
+            MessageEditorEvent::SendImmediately => self.interrupt_and_send(window, cx),
             MessageEditorEvent::Cancel => self.cancel_generation(cx),
             MessageEditorEvent::Focus => {
                 self.cancel_editing(&Default::default(), window, cx);
@@ -1256,7 +1256,7 @@ impl AcpThreadView {
                     }
                 }
             }
-            ViewEvent::MessageEditorEvent(_editor, MessageEditorEvent::Queue) => {}
+            ViewEvent::MessageEditorEvent(_editor, MessageEditorEvent::SendImmediately) => {}
             ViewEvent::MessageEditorEvent(editor, MessageEditorEvent::Send) => {
                 self.regenerate(event.entry_index, editor.clone(), window, cx);
             }
@@ -1300,7 +1300,7 @@ impl AcpThreadView {
         }
 
         if thread.read(cx).status() != ThreadStatus::Idle {
-            self.stop_current_and_send_new_message(window, cx);
+            self.queue_message(window, cx);
             return;
         }
 
@@ -1344,6 +1344,23 @@ impl AcpThreadView {
         self.send_impl(self.message_editor.clone(), window, cx)
     }
 
+    fn interrupt_and_send(&mut self, window: &mut Window, cx: &mut Context<Self>) {
+        let Some(thread) = self.thread() else {
+            return;
+        };
+
+        if self.is_loading_contents {
+            return;
+        }
+
+        if thread.read(cx).status() == ThreadStatus::Idle {
+            self.send_impl(self.message_editor.clone(), window, cx);
+            return;
+        }
+
+        self.stop_current_and_send_new_message(window, cx);
+    }
+
     fn stop_current_and_send_new_message(&mut self, window: &mut Window, cx: &mut Context<Self>) {
         let Some(thread) = self.thread().cloned() else {
             return;
@@ -5617,13 +5634,7 @@ impl AcpThreadView {
                 .tooltip(move |_window, cx| {
                     if is_editor_empty && !is_generating {
                         Tooltip::for_action("Type to Send", &Chat, cx)
-                    } else {
-                        let title = if is_generating {
-                            "Stop and Send Message"
-                        } else {
-                            "Send"
-                        };
-
+                    } else if is_generating {
                         let focus_handle = focus_handle.clone();
 
                         Tooltip::element(move |_window, cx| {
@@ -5633,7 +5644,7 @@ impl AcpThreadView {
                                     h_flex()
                                         .gap_2()
                                         .justify_between()
-                                        .child(Label::new(title))
+                                        .child(Label::new("Queue and Send"))
                                         .child(KeyBinding::for_action_in(&Chat, &focus_handle, cx)),
                                 )
                                 .child(
@@ -5643,15 +5654,17 @@ impl AcpThreadView {
                                         .justify_between()
                                         .border_t_1()
                                         .border_color(cx.theme().colors().border_variant)
-                                        .child(Label::new("Queue Message"))
+                                        .child(Label::new("Send Immediately"))
                                         .child(KeyBinding::for_action_in(
-                                            &QueueMessage,
+                                            &SendImmediately,
                                             &focus_handle,
                                             cx,
                                         )),
                                 )
                                 .into_any_element()
                         })(_window, cx)
+                    } else {
+                        Tooltip::for_action("Send Message", &Chat, cx)
                     }
                 })
                 .on_click(cx.listener(|this, _, window, cx| {
@@ -8846,7 +8859,7 @@ pub(crate) mod tests {
             editor.set_text("Message 2", window, cx);
         });
         thread_view.update_in(cx, |thread_view, window, cx| {
-            thread_view.send(window, cx);
+            thread_view.interrupt_and_send(window, cx);
         });
 
         cx.update(|_, cx| {

crates/agent_ui/src/agent_ui.rs 🔗

@@ -123,8 +123,8 @@ actions!(
         ContinueWithBurnMode,
         /// Toggles burn mode for faster responses.
         ToggleBurnMode,
-        /// Queues a message to be sent when generation completes.
-        QueueMessage,
+        /// Interrupts the current generation and sends the message immediately.
+        SendImmediately,
         /// Sends the next queued message immediately.
         SendNextQueuedMessage,
         /// Clears all messages from the queue.