agent: Add `use_modifier_to_send` setting (#34709)

Danilo Leal created

When `use_modifier_to_send` is turned on, holding `cmd`/`ctrl` is
necessary to send a message in the agent panel. Text threads already use
`cmd-enter` by default to submit a message, and it was done this way to
have the usual text editing bindings not taken over when writing a
prompt, sort of stimulating more thoughtful writing. While `enter` to
send is still somewhat a huge pattern in chat-like LLM UIs, it still
makes sense to allow this for the new agent panel... hence the existence
of this setting now!

Release Notes:

- agent: Added the `use_modifier_to_send` setting, which makes holding a
modifier (`cmd`/`ctrl`), together with `enter`, required to send a new
message.

Change summary

assets/keymaps/default-linux.json           | 13 +++++++++
assets/keymaps/default-macos.json           | 14 +++++++++
crates/agent_settings/src/agent_settings.rs | 13 +++++++++
crates/agent_ui/src/message_editor.rs       | 30 +++++++++++++++++++++-
4 files changed, 66 insertions(+), 4 deletions(-)

Detailed changes

assets/keymaps/default-linux.json 🔗

@@ -277,7 +277,7 @@
     }
   },
   {
-    "context": "MessageEditor > Editor",
+    "context": "MessageEditor > Editor && !use_modifier_to_send",
     "bindings": {
       "enter": "agent::Chat",
       "ctrl-enter": "agent::ChatWithFollow",
@@ -287,6 +287,17 @@
       "ctrl-shift-n": "agent::RejectAll"
     }
   },
+  {
+    "context": "MessageEditor > Editor && use_modifier_to_send",
+    "bindings": {
+      "ctrl-enter": "agent::Chat",
+      "enter": "editor::Newline",
+      "ctrl-i": "agent::ToggleProfileSelector",
+      "shift-ctrl-r": "agent::OpenAgentDiff",
+      "ctrl-shift-y": "agent::KeepAll",
+      "ctrl-shift-n": "agent::RejectAll"
+    }
+  },
   {
     "context": "EditMessageEditor > Editor",
     "bindings": {

assets/keymaps/default-macos.json 🔗

@@ -318,7 +318,7 @@
     }
   },
   {
-    "context": "MessageEditor > Editor",
+    "context": "MessageEditor > Editor && !use_modifier_to_send",
     "use_key_equivalents": true,
     "bindings": {
       "enter": "agent::Chat",
@@ -329,6 +329,18 @@
       "cmd-shift-n": "agent::RejectAll"
     }
   },
+  {
+    "context": "MessageEditor > Editor && use_modifier_to_send",
+    "use_key_equivalents": true,
+    "bindings": {
+      "cmd-enter": "agent::Chat",
+      "enter": "editor::Newline",
+      "cmd-i": "agent::ToggleProfileSelector",
+      "shift-ctrl-r": "agent::OpenAgentDiff",
+      "cmd-shift-y": "agent::KeepAll",
+      "cmd-shift-n": "agent::RejectAll"
+    }
+  },
   {
     "context": "EditMessageEditor > Editor",
     "use_key_equivalents": true,

crates/agent_settings/src/agent_settings.rs 🔗

@@ -69,6 +69,7 @@ pub struct AgentSettings {
     pub enable_feedback: bool,
     pub expand_edit_card: bool,
     pub expand_terminal_card: bool,
+    pub use_modifier_to_send: bool,
 }
 
 impl AgentSettings {
@@ -174,6 +175,10 @@ impl AgentSettingsContent {
         self.single_file_review = Some(allow);
     }
 
+    pub fn set_use_modifier_to_send(&mut self, always_use: bool) {
+        self.use_modifier_to_send = Some(always_use);
+    }
+
     pub fn set_profile(&mut self, profile_id: AgentProfileId) {
         self.default_profile = Some(profile_id);
     }
@@ -301,6 +306,10 @@ pub struct AgentSettingsContent {
     ///
     /// Default: true
     expand_terminal_card: Option<bool>,
+    /// Whether to always use cmd-enter (or ctrl-enter on Linux) to send messages in the agent panel.
+    ///
+    /// Default: false
+    use_modifier_to_send: Option<bool>,
 }
 
 #[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Default)]
@@ -456,6 +465,10 @@ impl Settings for AgentSettings {
                 &mut settings.expand_terminal_card,
                 value.expand_terminal_card,
             );
+            merge(
+                &mut settings.use_modifier_to_send,
+                value.use_modifier_to_send,
+            );
 
             settings
                 .model_parameters

crates/agent_ui/src/message_editor.rs 🔗

@@ -28,8 +28,8 @@ use fs::Fs;
 use futures::future::Shared;
 use futures::{FutureExt as _, future};
 use gpui::{
-    Animation, AnimationExt, App, Entity, EventEmitter, Focusable, Subscription, Task, TextStyle,
-    WeakEntity, linear_color_stop, linear_gradient, point, pulsating_between,
+    Animation, AnimationExt, App, Entity, EventEmitter, Focusable, KeyContext, Subscription, Task,
+    TextStyle, WeakEntity, linear_color_stop, linear_gradient, point, pulsating_between,
 };
 use language::{Buffer, Language, Point};
 use language_model::{
@@ -132,6 +132,7 @@ pub(crate) fn create_editor(
             placement: Some(ContextMenuPlacement::Above),
         });
         editor.register_addon(ContextCreasesAddon::new());
+        editor.register_addon(MessageEditorAddon::new());
         editor
     });
 
@@ -1494,6 +1495,31 @@ pub struct ContextCreasesAddon {
     _subscription: Option<Subscription>,
 }
 
+pub struct MessageEditorAddon {}
+
+impl MessageEditorAddon {
+    pub fn new() -> Self {
+        Self {}
+    }
+}
+
+impl Addon for MessageEditorAddon {
+    fn to_any(&self) -> &dyn std::any::Any {
+        self
+    }
+
+    fn to_any_mut(&mut self) -> Option<&mut dyn std::any::Any> {
+        Some(self)
+    }
+
+    fn extend_key_context(&self, key_context: &mut KeyContext, cx: &App) {
+        let settings = agent_settings::AgentSettings::get_global(cx);
+        if settings.use_modifier_to_send {
+            key_context.add("use_modifier_to_send");
+        }
+    }
+}
+
 impl Addon for ContextCreasesAddon {
     fn to_any(&self) -> &dyn std::any::Any {
         self