Fix inconsistencies in "Transform" vs "Generate" tooltips for assistant v2 (#22160)

Richard Feldman , Agus , and Agus Zubiaga created

Also makes the inline assistant and inline terminal assistant share a
bunch more code.

Release Notes:

- N/A

---------

Co-authored-by: Agus <agus@zed.dev>
Co-authored-by: Agus Zubiaga <hi@aguz.me>

Change summary

crates/assistant2/src/assistant.rs                 |   1 
crates/assistant2/src/inline_assistant.rs          | 135 ++--------
crates/assistant2/src/inline_prompt_editor.rs      | 191 ++++++++++++++++
crates/assistant2/src/terminal_inline_assistant.rs | 123 ---------
4 files changed, 230 insertions(+), 220 deletions(-)

Detailed changes

crates/assistant2/src/assistant.rs πŸ”—

@@ -6,6 +6,7 @@ mod context_picker;
 mod context_store;
 mod context_strip;
 mod inline_assistant;
+mod inline_prompt_editor;
 mod message_editor;
 mod prompts;
 mod streaming_diff;

crates/assistant2/src/inline_assistant.rs πŸ”—

@@ -2,6 +2,9 @@ use crate::context::attach_context_to_message;
 use crate::context_picker::ContextPicker;
 use crate::context_store::ContextStore;
 use crate::context_strip::ContextStrip;
+use crate::inline_prompt_editor::{
+    render_cancel_button, CodegenStatus, PromptEditorEvent, PromptMode,
+};
 use crate::thread_store::ThreadStore;
 use crate::{
     assistant_settings::AssistantSettings,
@@ -652,7 +655,7 @@ impl InlineAssistant {
             PromptEditorEvent::StopRequested => {
                 self.stop_assist(assist_id, cx);
             }
-            PromptEditorEvent::ConfirmRequested => {
+            PromptEditorEvent::ConfirmRequested { execute: _ } => {
                 self.finish_assist(assist_id, false, cx);
             }
             PromptEditorEvent::CancelRequested => {
@@ -661,6 +664,9 @@ impl InlineAssistant {
             PromptEditorEvent::DismissRequested => {
                 self.dismiss_assist(assist_id, cx);
             }
+            PromptEditorEvent::Resized { .. } => {
+                // This only matters for the terminal inline
+            }
         }
     }
 
@@ -1475,14 +1481,6 @@ impl InlineAssistGroupId {
     }
 }
 
-enum PromptEditorEvent {
-    StartRequested,
-    StopRequested,
-    ConfirmRequested,
-    CancelRequested,
-    DismissRequested,
-}
-
 struct PromptEditor {
     id: InlineAssistId,
     editor: View<Editor>,
@@ -1510,93 +1508,20 @@ impl Render for PromptEditor {
         if codegen.alternative_count(cx) > 1 {
             buttons.push(self.render_cycle_controls(cx));
         }
-
-        let status = codegen.status(cx);
-        buttons.extend(match status {
-            CodegenStatus::Idle => {
-                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)),
-                        )
-                        .into_any_element(),
-                    IconButton::new("start", IconName::SparkleAlt)
-                        .icon_color(Color::Muted)
-                        .shape(IconButtonShape::Square)
-                        .tooltip(|cx| Tooltip::for_action("Transform", &menu::Confirm, cx))
-                        .on_click(
-                            cx.listener(|_, _, cx| cx.emit(PromptEditorEvent::StartRequested)),
-                        )
-                        .into_any_element(),
-                ]
+        let prompt_mode = if codegen.is_insertion {
+            PromptMode::Generate {
+                supports_execute: false,
             }
-            CodegenStatus::Pending => {
-                vec![
-                    IconButton::new("cancel", IconName::Close)
-                        .icon_color(Color::Muted)
-                        .shape(IconButtonShape::Square)
-                        .tooltip(|cx| Tooltip::text("Cancel Assist", cx))
-                        .on_click(
-                            cx.listener(|_, _, cx| cx.emit(PromptEditorEvent::CancelRequested)),
-                        )
-                        .into_any_element(),
-                    IconButton::new("stop", IconName::Stop)
-                        .icon_color(Color::Error)
-                        .shape(IconButtonShape::Square)
-                        .tooltip(|cx| {
-                            Tooltip::with_meta(
-                                "Interrupt Transformation",
-                                Some(&menu::Cancel),
-                                "Changes won't be discarded",
-                                cx,
-                            )
-                        })
-                        .on_click(cx.listener(|_, _, cx| cx.emit(PromptEditorEvent::StopRequested)))
-                        .into_any_element(),
-                ]
-            }
-            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)),
-                        )
-                        .into_any_element(),
-                    if self.edited_since_done || matches!(status, CodegenStatus::Error(_)) {
-                        IconButton::new("restart", IconName::RotateCw)
-                            .icon_color(Color::Info)
-                            .shape(IconButtonShape::Square)
-                            .tooltip(|cx| {
-                                Tooltip::with_meta(
-                                    "Restart Transformation",
-                                    Some(&menu::Confirm),
-                                    "Changes will be discarded",
-                                    cx,
-                                )
-                            })
-                            .on_click(cx.listener(|_, _, cx| {
-                                cx.emit(PromptEditorEvent::StartRequested);
-                            }))
-                            .into_any_element()
-                    } else {
-                        IconButton::new("confirm", IconName::Check)
-                            .icon_color(Color::Info)
-                            .shape(IconButtonShape::Square)
-                            .tooltip(|cx| Tooltip::for_action("Confirm Assist", &menu::Confirm, cx))
-                            .on_click(cx.listener(|_, _, cx| {
-                                cx.emit(PromptEditorEvent::ConfirmRequested);
-                            }))
-                            .into_any_element()
-                    },
-                ]
-            }
-        });
+        } else {
+            PromptMode::Transform
+        };
+
+        buttons.extend(render_cancel_button(
+            codegen.status(cx).into(),
+            self.edited_since_done,
+            prompt_mode,
+            cx,
+        ));
 
         v_flex()
             .border_y_1()
@@ -1747,7 +1672,7 @@ impl PromptEditor {
             // always show the cursor (even when it isn't focused) because
             // typing in one will make what you typed appear in all of them.
             editor.set_show_cursor_when_unfocused(true, cx);
-            editor.set_placeholder_text(Self::placeholder_text(codegen.read(cx)), cx);
+            editor.set_placeholder_text(Self::placeholder_text(codegen.read(cx), cx), cx);
             editor
         });
         let context_picker_menu_handle = PopoverMenuHandle::default();
@@ -1815,7 +1740,7 @@ impl PromptEditor {
         self.editor = cx.new_view(|cx| {
             let mut editor = Editor::auto_height(Self::MAX_LINES as usize, cx);
             editor.set_soft_wrap_mode(language::language_settings::SoftWrap::EditorWidth, cx);
-            editor.set_placeholder_text(Self::placeholder_text(self.codegen.read(cx)), cx);
+            editor.set_placeholder_text(Self::placeholder_text(self.codegen.read(cx), cx), cx);
             editor.set_placeholder_text("Add a prompt…", cx);
             editor.set_text(prompt, cx);
             if focus {
@@ -1826,14 +1751,17 @@ impl PromptEditor {
         self.subscribe_to_editor(cx);
     }
 
-    fn placeholder_text(codegen: &Codegen) -> String {
+    fn placeholder_text(codegen: &Codegen, cx: &WindowContext) -> String {
         let action = if codegen.is_insertion {
             "Generate"
         } else {
             "Transform"
         };
+        let assistant_panel_keybinding = ui::text_for_action(&crate::ToggleFocus, cx)
+            .map(|keybinding| format!("{keybinding} to chat ― "))
+            .unwrap_or_default();
 
-        format!("{action}… ↓↑ for history")
+        format!("{action}… ({assistant_panel_keybinding}↓↑ for history)")
     }
 
     fn prompt(&self, cx: &AppContext) -> String {
@@ -1950,7 +1878,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(_) => {
@@ -2566,13 +2494,6 @@ pub struct CodegenAlternative {
     message_id: Option<String>,
 }
 
-enum CodegenStatus {
-    Idle,
-    Pending,
-    Done,
-    Error(anyhow::Error),
-}
-
 #[derive(Default)]
 struct Diff {
     deleted_row_ranges: Vec<(Anchor, RangeInclusive<u32>)>,

crates/assistant2/src/inline_prompt_editor.rs πŸ”—

@@ -0,0 +1,191 @@
+use gpui::{AnyElement, EventEmitter};
+use ui::{prelude::*, IconButtonShape, Tooltip};
+
+pub enum CodegenStatus {
+    Idle,
+    Pending,
+    Done,
+    Error(anyhow::Error),
+}
+
+/// This is just CodegenStatus without the anyhow::Error, which causes a lifetime issue for rendering the Cancel button.
+#[derive(Copy, Clone)]
+pub enum CancelButtonState {
+    Idle,
+    Pending,
+    Done,
+    Error,
+}
+
+impl Into<CancelButtonState> for &CodegenStatus {
+    fn into(self) -> CancelButtonState {
+        match self {
+            CodegenStatus::Idle => CancelButtonState::Idle,
+            CodegenStatus::Pending => CancelButtonState::Pending,
+            CodegenStatus::Done => CancelButtonState::Done,
+            CodegenStatus::Error(_) => CancelButtonState::Error,
+        }
+    }
+}
+
+#[derive(Copy, Clone)]
+pub enum PromptMode {
+    Generate { supports_execute: bool },
+    Transform,
+}
+
+impl PromptMode {
+    fn start_label(self) -> &'static str {
+        match self {
+            PromptMode::Generate { .. } => "Generate",
+            PromptMode::Transform => "Transform",
+        }
+    }
+    fn tooltip_interrupt(self) -> &'static str {
+        match self {
+            PromptMode::Generate { .. } => "Interrupt Generation",
+            PromptMode::Transform => "Interrupt Transform",
+        }
+    }
+
+    fn tooltip_restart(self) -> &'static str {
+        match self {
+            PromptMode::Generate { .. } => "Restart Generation",
+            PromptMode::Transform => "Restart Transform",
+        }
+    }
+
+    fn tooltip_accept(self) -> &'static str {
+        match self {
+            PromptMode::Generate { .. } => "Accept Generation",
+            PromptMode::Transform => "Accept Transform",
+        }
+    }
+}
+
+pub fn render_cancel_button<T: EventEmitter<PromptEditorEvent>>(
+    cancel_button_state: CancelButtonState,
+    edited_since_done: bool,
+    mode: PromptMode,
+    cx: &mut ViewContext<T>,
+) -> Vec<AnyElement> {
+    match cancel_button_state {
+        CancelButtonState::Idle => {
+            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)))
+                    .into_any_element(),
+                Button::new("start", mode.start_label())
+                    .icon(IconName::Return)
+                    .icon_color(Color::Muted)
+                    .on_click(cx.listener(|_, _, cx| cx.emit(PromptEditorEvent::StartRequested)))
+                    .into_any_element(),
+            ]
+        }
+        CancelButtonState::Pending => vec![
+            IconButton::new("cancel", IconName::Close)
+                .icon_color(Color::Muted)
+                .shape(IconButtonShape::Square)
+                .tooltip(|cx| Tooltip::text("Cancel Assist", cx))
+                .on_click(cx.listener(|_, _, cx| cx.emit(PromptEditorEvent::CancelRequested)))
+                .into_any_element(),
+            IconButton::new("stop", IconName::Stop)
+                .icon_color(Color::Error)
+                .shape(IconButtonShape::Square)
+                .tooltip(move |cx| {
+                    Tooltip::with_meta(
+                        mode.tooltip_interrupt(),
+                        Some(&menu::Cancel),
+                        "Changes won't be discarded",
+                        cx,
+                    )
+                })
+                .on_click(cx.listener(|_, _, cx| cx.emit(PromptEditorEvent::StopRequested)))
+                .into_any_element(),
+        ],
+        CancelButtonState::Done | CancelButtonState::Error => {
+            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)))
+                .into_any_element();
+
+            let has_error = matches!(cancel_button_state, CancelButtonState::Error);
+            if has_error || edited_since_done {
+                vec![
+                    cancel,
+                    IconButton::new("restart", IconName::RotateCw)
+                        .icon_color(Color::Info)
+                        .shape(IconButtonShape::Square)
+                        .tooltip(move |cx| {
+                            Tooltip::with_meta(
+                                mode.tooltip_restart(),
+                                Some(&menu::Confirm),
+                                "Changes will be discarded",
+                                cx,
+                            )
+                        })
+                        .on_click(cx.listener(|_, _, cx| {
+                            cx.emit(PromptEditorEvent::StartRequested);
+                        }))
+                        .into_any_element(),
+                ]
+            } else {
+                let mut buttons = vec![
+                    cancel,
+                    IconButton::new("accept", IconName::Check)
+                        .icon_color(Color::Info)
+                        .shape(IconButtonShape::Square)
+                        .tooltip(move |cx| {
+                            Tooltip::for_action(mode.tooltip_accept(), &menu::Confirm, cx)
+                        })
+                        .on_click(cx.listener(|_, _, cx| {
+                            cx.emit(PromptEditorEvent::ConfirmRequested { execute: false });
+                        }))
+                        .into_any_element(),
+                ];
+
+                match mode {
+                    PromptMode::Generate { supports_execute } => {
+                        if supports_execute {
+                            buttons.push(
+                                IconButton::new("confirm", IconName::Play)
+                                    .icon_color(Color::Info)
+                                    .shape(IconButtonShape::Square)
+                                    .tooltip(|cx| {
+                                        Tooltip::for_action(
+                                            "Execute Generated Command",
+                                            &menu::SecondaryConfirm,
+                                            cx,
+                                        )
+                                    })
+                                    .on_click(cx.listener(|_, _, cx| {
+                                        cx.emit(PromptEditorEvent::ConfirmRequested {
+                                            execute: true,
+                                        });
+                                    }))
+                                    .into_any_element(),
+                            )
+                        }
+                    }
+                    PromptMode::Transform => {}
+                }
+
+                buttons
+            }
+        }
+    }
+}
+
+pub enum PromptEditorEvent {
+    StartRequested,
+    StopRequested,
+    ConfirmRequested { execute: bool },
+    CancelRequested,
+    DismissRequested,
+    Resized { height_in_lines: u8 },
+}

crates/assistant2/src/terminal_inline_assistant.rs πŸ”—

@@ -1,11 +1,12 @@
-use crate::assistant_settings::AssistantSettings;
 use crate::context::attach_context_to_message;
 use crate::context_picker::ContextPicker;
 use crate::context_store::ContextStore;
 use crate::context_strip::ContextStrip;
+use crate::inline_prompt_editor::{CodegenStatus, PromptEditorEvent, PromptMode};
 use crate::prompts::PromptBuilder;
 use crate::thread_store::ThreadStore;
 use crate::ToggleContextPicker;
+use crate::{assistant_settings::AssistantSettings, inline_prompt_editor::render_cancel_button};
 use anyhow::{Context as _, Result};
 use client::telemetry::Telemetry;
 use collections::{HashMap, VecDeque};
@@ -448,15 +449,6 @@ impl TerminalInlineAssist {
     }
 }
 
-enum PromptEditorEvent {
-    StartRequested,
-    StopRequested,
-    ConfirmRequested { execute: bool },
-    CancelRequested,
-    DismissRequested,
-    Resized { height_in_lines: u8 },
-}
-
 struct PromptEditor {
     id: TerminalInlineAssistId,
     height_in_lines: u8,
@@ -477,104 +469,16 @@ impl EventEmitter<PromptEditorEvent> for PromptEditor {}
 
 impl Render for PromptEditor {
     fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
-        let status = &self.codegen.read(cx).status;
         let mut buttons = Vec::new();
 
-        buttons.extend(match status {
-            CodegenStatus::Idle => 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)))
-                    .into_any_element(),
-                IconButton::new("start", IconName::SparkleAlt)
-                    .icon_color(Color::Muted)
-                    .shape(IconButtonShape::Square)
-                    .tooltip(|cx| Tooltip::for_action("Generate", &menu::Confirm, cx))
-                    .on_click(cx.listener(|_, _, cx| cx.emit(PromptEditorEvent::StartRequested)))
-                    .into_any_element(),
-            ],
-            CodegenStatus::Pending => vec![
-                IconButton::new("cancel", IconName::Close)
-                    .icon_color(Color::Muted)
-                    .shape(IconButtonShape::Square)
-                    .tooltip(|cx| Tooltip::text("Cancel Assist", cx))
-                    .on_click(cx.listener(|_, _, cx| cx.emit(PromptEditorEvent::CancelRequested)))
-                    .into_any_element(),
-                IconButton::new("stop", IconName::Stop)
-                    .icon_color(Color::Error)
-                    .shape(IconButtonShape::Square)
-                    .tooltip(|cx| {
-                        Tooltip::with_meta(
-                            "Interrupt Generation",
-                            Some(&menu::Cancel),
-                            "Changes won't be discarded",
-                            cx,
-                        )
-                    })
-                    .on_click(cx.listener(|_, _, cx| cx.emit(PromptEditorEvent::StopRequested)))
-                    .into_any_element(),
-            ],
-            CodegenStatus::Error(_) | CodegenStatus::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)))
-                    .into_any_element();
-
-                let has_error = matches!(status, CodegenStatus::Error(_));
-                if has_error || self.edited_since_done {
-                    vec![
-                        cancel,
-                        IconButton::new("restart", IconName::RotateCw)
-                            .icon_color(Color::Info)
-                            .shape(IconButtonShape::Square)
-                            .tooltip(|cx| {
-                                Tooltip::with_meta(
-                                    "Restart Generation",
-                                    Some(&menu::Confirm),
-                                    "Changes will be discarded",
-                                    cx,
-                                )
-                            })
-                            .on_click(cx.listener(|_, _, cx| {
-                                cx.emit(PromptEditorEvent::StartRequested);
-                            }))
-                            .into_any_element(),
-                    ]
-                } 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 });
-                            }))
-                            .into_any_element(),
-                        IconButton::new("confirm", IconName::Play)
-                            .icon_color(Color::Info)
-                            .shape(IconButtonShape::Square)
-                            .tooltip(|cx| {
-                                Tooltip::for_action(
-                                    "Execute Generated Command",
-                                    &menu::SecondaryConfirm,
-                                    cx,
-                                )
-                            })
-                            .on_click(cx.listener(|_, _, cx| {
-                                cx.emit(PromptEditorEvent::ConfirmRequested { execute: true });
-                            }))
-                            .into_any_element(),
-                    ]
-                }
-            }
-        });
+        buttons.extend(render_cancel_button(
+            (&self.codegen.read(cx).status).into(),
+            self.edited_since_done,
+            PromptMode::Generate {
+                supports_execute: true,
+            },
+            cx,
+        ));
 
         v_flex()
             .border_y_1()
@@ -1097,10 +1001,3 @@ impl Codegen {
         }
     }
 }
-
-enum CodegenStatus {
-    Idle,
-    Pending,
-    Done,
-    Error(anyhow::Error),
-}