assistant: Allow accepting terminal inline assist suggestion without executing command (#17299)

Bennet Bo Fenner and Danilo Leal created

This adds a new button that on click, accepts the suggestion but does
not run the generated command.


https://github.com/user-attachments/assets/426b0ff3-8e19-435a-aa7f-89e062aefd4c

@danilo-leal @iamnbutler Any ideas on how to make both options
discoverable without having an extra button?

Release Notes:

- Added a way to accept terminal inline assist suggestions without
executing them

---------

Co-authored-by: Danilo Leal <67129314+danilo-leal@users.noreply.github.com>

Change summary

assets/icons/play.svg                             |  4 
crates/assistant/src/terminal_inline_assistant.rs | 72 +++++++++++-----
2 files changed, 51 insertions(+), 25 deletions(-)

Detailed changes

assets/icons/play.svg 🔗

@@ -1 +1,3 @@
-<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M3.24182 2.32181C3.3919 2.23132 3.5784 2.22601 3.73338 2.30781L12.7334 7.05781C12.8974 7.14436 13 7.31457 13 7.5C13 7.68543 12.8974 7.85564 12.7334 7.94219L3.73338 12.6922C3.5784 12.774 3.3919 12.7687 3.24182 12.6782C3.09175 12.5877 3 12.4252 3 12.25V2.75C3 2.57476 3.09175 2.4123 3.24182 2.32181ZM4 3.57925V11.4207L11.4288 7.5L4 3.57925Z" fill="currentColor" fill-rule="evenodd" clip-rule="evenodd"></path></svg>
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+    <path d="M5 4L12 8L5 12V4Z" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+</svg>

crates/assistant/src/terminal_inline_assistant.rs 🔗

@@ -157,11 +157,11 @@ impl TerminalInlineAssistant {
             PromptEditorEvent::StopRequested => {
                 self.stop_assist(assist_id, cx);
             }
-            PromptEditorEvent::ConfirmRequested => {
-                self.finish_assist(assist_id, false, cx);
+            PromptEditorEvent::ConfirmRequested { execute } => {
+                self.finish_assist(assist_id, false, *execute, cx);
             }
             PromptEditorEvent::CancelRequested => {
-                self.finish_assist(assist_id, true, cx);
+                self.finish_assist(assist_id, true, false, cx);
             }
             PromptEditorEvent::DismissRequested => {
                 self.dismiss_assist(assist_id, cx);
@@ -292,6 +292,7 @@ impl TerminalInlineAssistant {
         &mut self,
         assist_id: TerminalInlineAssistId,
         undo: bool,
+        execute: bool,
         cx: &mut WindowContext,
     ) {
         self.dismiss_assist(assist_id, cx);
@@ -307,7 +308,7 @@ impl TerminalInlineAssistant {
             assist.codegen.update(cx, |codegen, cx| {
                 if undo {
                     codegen.undo(cx);
-                } else {
+                } else if execute {
                     codegen.complete(cx);
                 }
             });
@@ -423,7 +424,7 @@ impl TerminalInlineAssist {
                             }
 
                             if assist.prompt_editor.is_none() {
-                                this.finish_assist(assist_id, false, cx);
+                                this.finish_assist(assist_id, false, false, cx);
                             }
                         }
                     })
@@ -436,7 +437,7 @@ impl TerminalInlineAssist {
 enum PromptEditorEvent {
     StartRequested,
     StopRequested,
-    ConfirmRequested,
+    ConfirmRequested { execute: bool },
     CancelRequested,
     DismissRequested,
     Resized { height_in_lines: u8 },
@@ -509,15 +510,15 @@ impl Render for PromptEditor {
                 ]
             }
             CodegenStatus::Error(_) | CodegenStatus::Done => {
-                vec![
-                    IconButton::new("cancel", IconName::Close)
-                        .icon_color(Color::Muted)
-                        .shape(IconButtonShape::Square)
-                        .tooltip(|cx| Tooltip::for_action("Cancel Assist", &menu::Cancel, cx))
-                        .on_click(
-                            cx.listener(|_, _, cx| cx.emit(PromptEditorEvent::CancelRequested)),
-                        ),
-                    if self.edited_since_done {
+                let cancel = IconButton::new("cancel", IconName::Close)
+                    .icon_color(Color::Muted)
+                    .shape(IconButtonShape::Square)
+                    .tooltip(|cx| Tooltip::for_action("Cancel Assist", &menu::Cancel, cx))
+                    .on_click(cx.listener(|_, _, cx| cx.emit(PromptEditorEvent::CancelRequested)));
+
+                if self.edited_since_done {
+                    vec![
+                        cancel,
                         IconButton::new("restart", IconName::RotateCw)
                             .icon_color(Color::Info)
                             .shape(IconButtonShape::Square)
@@ -531,19 +532,35 @@ impl Render for PromptEditor {
                             })
                             .on_click(cx.listener(|_, _, cx| {
                                 cx.emit(PromptEditorEvent::StartRequested);
-                            }))
-                    } else {
+                            })),
+                    ]
+                } else {
+                    vec![
+                        cancel,
+                        IconButton::new("accept", IconName::Check)
+                            .icon_color(Color::Info)
+                            .shape(IconButtonShape::Square)
+                            .tooltip(|cx| {
+                                Tooltip::for_action("Accept Generated Command", &menu::Confirm, cx)
+                            })
+                            .on_click(cx.listener(|_, _, cx| {
+                                cx.emit(PromptEditorEvent::ConfirmRequested { execute: false });
+                            })),
                         IconButton::new("confirm", IconName::Play)
                             .icon_color(Color::Info)
                             .shape(IconButtonShape::Square)
                             .tooltip(|cx| {
-                                Tooltip::for_action("Execute generated command", &menu::Confirm, cx)
+                                Tooltip::for_action(
+                                    "Execute Generated Command",
+                                    &menu::SecondaryConfirm,
+                                    cx,
+                                )
                             })
                             .on_click(cx.listener(|_, _, cx| {
-                                cx.emit(PromptEditorEvent::ConfirmRequested);
-                            }))
-                    },
-                ]
+                                cx.emit(PromptEditorEvent::ConfirmRequested { execute: true });
+                            })),
+                    ]
+                }
             }
         };
 
@@ -555,6 +572,7 @@ impl Render for PromptEditor {
             .h_full()
             .w_full()
             .on_action(cx.listener(Self::confirm))
+            .on_action(cx.listener(Self::secondary_confirm))
             .on_action(cx.listener(Self::cancel))
             .on_action(cx.listener(Self::move_up))
             .on_action(cx.listener(Self::move_down))
@@ -605,7 +623,7 @@ impl Render for PromptEditor {
             .child(div().flex_1().child(self.render_prompt_editor(cx)))
             .child(
                 h_flex()
-                    .gap_2()
+                    .gap_1()
                     .pr_4()
                     .children(self.render_token_count(cx))
                     .children(buttons),
@@ -809,7 +827,7 @@ impl PromptEditor {
                 if self.edited_since_done {
                     cx.emit(PromptEditorEvent::StartRequested);
                 } else {
-                    cx.emit(PromptEditorEvent::ConfirmRequested);
+                    cx.emit(PromptEditorEvent::ConfirmRequested { execute: false });
                 }
             }
             CodegenStatus::Error(_) => {
@@ -818,6 +836,12 @@ impl PromptEditor {
         }
     }
 
+    fn secondary_confirm(&mut self, _: &menu::SecondaryConfirm, cx: &mut ViewContext<Self>) {
+        if matches!(self.codegen.read(cx).status, CodegenStatus::Done) {
+            cx.emit(PromptEditorEvent::ConfirmRequested { execute: true });
+        }
+    }
+
     fn move_up(&mut self, _: &MoveUp, cx: &mut ViewContext<Self>) {
         if let Some(ix) = self.prompt_history_ix {
             if ix > 0 {