diff --git a/crates/assistant2/src/assistant.rs b/crates/assistant2/src/assistant.rs index 93f3c3bace397..65156691a5a73 100644 --- a/crates/assistant2/src/assistant.rs +++ b/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; diff --git a/crates/assistant2/src/inline_assistant.rs b/crates/assistant2/src/inline_assistant.rs index ad376e5e0a427..1c4938872a110 100644 --- a/crates/assistant2/src/inline_assistant.rs +++ b/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 assistant + } } } @@ -1475,14 +1481,6 @@ impl InlineAssistGroupId { } } -enum PromptEditorEvent { - StartRequested, - StopRequested, - ConfirmRequested, - CancelRequested, - DismissRequested, -} - struct PromptEditor { id: InlineAssistId, editor: View, @@ -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 assisant_panel_shortcut = ui::text_for_action(&crate::ToggleFocus, cx) + .map(|keybinding| format!("{keybinding} to chat ― ")) + .unwrap_or_default(); - format!("{action}… ↓↑ for history") + format!("{action}… ({assisant_panel_shortcut}↓↑ 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, } -enum CodegenStatus { - Idle, - Pending, - Done, - Error(anyhow::Error), -} - #[derive(Default)] struct Diff { deleted_row_ranges: Vec<(Anchor, RangeInclusive)>, diff --git a/crates/assistant2/src/inline_prompt_editor.rs b/crates/assistant2/src/inline_prompt_editor.rs new file mode 100644 index 0000000000000..7cd0bccd3ba25 --- /dev/null +++ b/crates/assistant2/src/inline_prompt_editor.rs @@ -0,0 +1,194 @@ +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 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 tooltip_start(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>( + cancel_button_state: CancelButtonState, + edited_since_done: bool, + mode: PromptMode, + cx: &mut ViewContext, +) -> Vec { + 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(), + IconButton::new("start", IconName::SparkleAlt) + .icon_color(Color::Muted) + .shape(IconButtonShape::Square) + .tooltip(move |cx| { + Tooltip::for_action(mode.tooltip_start(), &menu::Confirm, cx) + }) + .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 }, +} diff --git a/crates/assistant2/src/terminal_inline_assistant.rs b/crates/assistant2/src/terminal_inline_assistant.rs index 95b2cf7ddb99e..c72c7d607120b 100644 --- a/crates/assistant2/src/terminal_inline_assistant.rs +++ b/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 for PromptEditor {} impl Render for PromptEditor { fn render(&mut self, cx: &mut ViewContext) -> 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), -}