From 4ed0e5160f13e5a49ab2eb30c6793150b8449351 Mon Sep 17 00:00:00 2001 From: Richard Feldman Date: Fri, 20 Dec 2024 17:00:52 -0500 Subject: [PATCH] Inline assistant v2 layout (#22305) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Makes the inline assistant look like @danilo-leal's prototype. Also fixes the bug with indent guides that @maxdeviant found! Screenshot 2024-12-20 at 4 24 56 PM Release Notes: - N/A --- crates/assistant2/src/assistant.rs | 1 + .../src/assistant_model_selector.rs | 85 +++++++ crates/assistant2/src/inline_prompt_editor.rs | 217 ++++++++---------- crates/assistant2/src/message_editor.rs | 104 ++------- 4 files changed, 201 insertions(+), 206 deletions(-) create mode 100644 crates/assistant2/src/assistant_model_selector.rs diff --git a/crates/assistant2/src/assistant.rs b/crates/assistant2/src/assistant.rs index 24c0793c9a515..84081528ebf41 100644 --- a/crates/assistant2/src/assistant.rs +++ b/crates/assistant2/src/assistant.rs @@ -1,4 +1,5 @@ mod active_thread; +mod assistant_model_selector; mod assistant_panel; mod assistant_settings; mod buffer_codegen; diff --git a/crates/assistant2/src/assistant_model_selector.rs b/crates/assistant2/src/assistant_model_selector.rs new file mode 100644 index 0000000000000..86f37a9a6cc4e --- /dev/null +++ b/crates/assistant2/src/assistant_model_selector.rs @@ -0,0 +1,85 @@ +use fs::Fs; +use gpui::View; +use language_model::LanguageModelRegistry; +use language_model_selector::{LanguageModelSelector, LanguageModelSelectorPopoverMenu}; +use settings::update_settings_file; +use std::sync::Arc; +use ui::{prelude::*, ButtonLike, PopoverMenuHandle, Tooltip}; + +use crate::{assistant_settings::AssistantSettings, ToggleModelSelector}; + +pub struct AssistantModelSelector { + selector: View, + menu_handle: PopoverMenuHandle, +} + +impl AssistantModelSelector { + pub(crate) fn new( + fs: Arc, + menu_handle: PopoverMenuHandle, + cx: &mut WindowContext, + ) -> Self { + Self { + selector: cx.new_view(|cx| { + let fs = fs.clone(); + LanguageModelSelector::new( + move |model, cx| { + update_settings_file::( + fs.clone(), + cx, + move |settings, _cx| settings.set_model(model.clone()), + ); + }, + cx, + ) + }), + menu_handle, + } + } +} + +impl Render for AssistantModelSelector { + fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { + let active_model = LanguageModelRegistry::read_global(cx).active_model(); + let focus_handle = self.selector.focus_handle(cx).clone(); + + LanguageModelSelectorPopoverMenu::new( + self.selector.clone(), + ButtonLike::new("active-model") + .style(ButtonStyle::Subtle) + .child( + h_flex() + .w_full() + .gap_0p5() + .child( + div() + .overflow_x_hidden() + .flex_grow() + .whitespace_nowrap() + .child(match active_model { + Some(model) => h_flex() + .child( + Label::new(model.name().0) + .size(LabelSize::Small) + .color(Color::Muted), + ) + .into_any_element(), + _ => Label::new("No model selected") + .size(LabelSize::Small) + .color(Color::Muted) + .into_any_element(), + }), + ) + .child( + Icon::new(IconName::ChevronDown) + .color(Color::Muted) + .size(IconSize::XSmall), + ), + ) + .tooltip(move |cx| { + Tooltip::for_action_in("Change Model", &ToggleModelSelector, &focus_handle, cx) + }), + ) + .with_handle(self.menu_handle.clone()) + } +} diff --git a/crates/assistant2/src/inline_prompt_editor.rs b/crates/assistant2/src/inline_prompt_editor.rs index c146256341109..7f009a3f7066e 100644 --- a/crates/assistant2/src/inline_prompt_editor.rs +++ b/crates/assistant2/src/inline_prompt_editor.rs @@ -1,13 +1,12 @@ +use crate::assistant_model_selector::AssistantModelSelector; use crate::buffer_codegen::BufferCodegen; use crate::context_picker::ContextPicker; use crate::context_store::ContextStore; use crate::context_strip::ContextStrip; use crate::terminal_codegen::TerminalCodegen; use crate::thread_store::ThreadStore; -use crate::ToggleContextPicker; -use crate::{ - assistant_settings::AssistantSettings, CycleNextInlineAssist, CyclePreviousInlineAssist, -}; +use crate::{CycleNextInlineAssist, CyclePreviousInlineAssist}; +use crate::{ToggleContextPicker, ToggleModelSelector}; use client::ErrorExt; use collections::VecDeque; use editor::{ @@ -22,9 +21,9 @@ use gpui::{ WeakModel, WeakView, WindowContext, }; use language_model::{LanguageModel, LanguageModelRegistry}; -use language_model_selector::{LanguageModelSelector, LanguageModelSelectorPopoverMenu}; +use language_model_selector::LanguageModelSelector; use parking_lot::Mutex; -use settings::{update_settings_file, Settings}; +use settings::Settings; use std::cmp; use std::sync::Arc; use theme::ThemeSettings; @@ -39,7 +38,8 @@ pub struct PromptEditor { mode: PromptEditorMode, context_strip: View, context_picker_menu_handle: PopoverMenuHandle, - language_model_selector: View, + model_selector: View, + model_selector_menu_handle: PopoverMenuHandle, edited_since_done: bool, prompt_history: VecDeque, prompt_history_ix: Option, @@ -72,23 +72,28 @@ impl Render for PromptEditor { gutter_dimensions.full_width() + (gutter_dimensions.margin / 2.0) } - PromptEditorMode::Terminal { .. } => Pixels::ZERO, + PromptEditorMode::Terminal { .. } => { + // Give the equivalent of the same left-padding that we're using on the right + Pixels::from(24.0) + } }; buttons.extend(self.render_buttons(cx)); v_flex() + .key_context("PromptEditor") + .bg(cx.theme().colors().editor_background) + .block_mouse_down() .border_y_1() .border_color(cx.theme().status().info_border) .size_full() - .py(cx.line_height() / 2.5) + .pt_1() + .pb_2() .child( h_flex() - .key_context("PromptEditor") - .bg(cx.theme().colors().editor_background) - .block_mouse_down() .cursor(CursorStyle::Arrow) .on_action(cx.listener(Self::toggle_context_picker)) + .on_action(cx.listener(Self::toggle_model_selector)) .on_action(cx.listener(Self::confirm)) .on_action(cx.listener(Self::cancel)) .on_action(cx.listener(Self::move_up)) @@ -100,27 +105,7 @@ impl Render for PromptEditor { .w(spacing) .justify_center() .gap_2() - .child(LanguageModelSelectorPopoverMenu::new( - self.language_model_selector.clone(), - IconButton::new("context", IconName::SettingsAlt) - .shape(IconButtonShape::Square) - .icon_size(IconSize::Small) - .icon_color(Color::Muted) - .tooltip(move |cx| { - Tooltip::with_meta( - format!( - "Using {}", - LanguageModelRegistry::read_global(cx) - .active_model() - .map(|model| model.name().0) - .unwrap_or_else(|| "No model selected".into()), - ), - None, - "Change Model", - cx, - ) - }), - )) + .child(self.render_close_button(cx)) .map(|el| { let CodegenStatus::Error(error) = self.codegen_status(cx) else { return el; @@ -172,13 +157,26 @@ impl Render for PromptEditor { } }), ) - .child(div().flex_1().child(self.render_editor(cx))) - .child(h_flex().gap_2().pr_6().children(buttons)), + .child( + h_flex() + .w_full() + .justify_between() + .child(div().flex_1().child(self.render_editor(cx))) + .child(h_flex().gap_2().pr_6().children(buttons)), + ), ) .child( h_flex() - .child(h_flex().w(spacing).justify_center().gap_2()) - .child(self.context_strip.clone()), + .child(h_flex().w(spacing).justify_between().gap_2()) + .child( + h_flex() + .w_full() + .pl_1() + .pr_6() + .justify_between() + .child(div().pl_1().child(self.context_strip.clone())) + .child(self.model_selector.clone()), + ), ) } } @@ -311,6 +309,10 @@ impl PromptEditor { self.context_picker_menu_handle.toggle(cx); } + fn toggle_model_selector(&mut self, _: &ToggleModelSelector, cx: &mut ViewContext) { + self.model_selector_menu_handle.toggle(cx); + } + fn cancel(&mut self, _: &editor::actions::Cancel, cx: &mut ViewContext) { match self.codegen_status(cx) { CodegenStatus::Idle | CodegenStatus::Done | CodegenStatus::Error(_) => { @@ -400,73 +402,44 @@ impl PromptEditor { match codegen_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(), - 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(), - ] - } - CodegenStatus::Pending => vec![ - IconButton::new("cancel", IconName::Close) + vec![Button::new("start", mode.start_label()) + .icon(IconName::Return) + .label_size(LabelSize::Small) .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(), - ], + .on_click(cx.listener(|_, _, cx| cx.emit(PromptEditorEvent::StartRequested))) + .into_any_element()] + } + CodegenStatus::Pending => vec![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()], CodegenStatus::Done | CodegenStatus::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!(codegen_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(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(), - ] + vec![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 accept = IconButton::new("accept", IconName::Check) .icon_color(Color::Info) @@ -482,7 +455,6 @@ impl PromptEditor { match &self.mode { PromptEditorMode::Terminal { .. } => vec![ accept, - cancel, IconButton::new("confirm", IconName::Play) .icon_color(Color::Info) .shape(IconButtonShape::Square) @@ -498,7 +470,7 @@ impl PromptEditor { })) .into_any_element(), ], - PromptEditorMode::Buffer { .. } => vec![accept, cancel], + PromptEditorMode::Buffer { .. } => vec![accept], } } } @@ -527,6 +499,15 @@ impl PromptEditor { } } + fn render_close_button(&self, cx: &ViewContext) -> AnyElement { + IconButton::new("cancel", IconName::Close) + .icon_color(Color::Muted) + .shape(IconButtonShape::Square) + .tooltip(|cx| Tooltip::text("Close Assistant", cx)) + .on_click(cx.listener(|_, _, cx| cx.emit(PromptEditorEvent::CancelRequested))) + .into_any_element() + } + fn render_cycle_controls(&self, codegen: &BufferCodegen, cx: &ViewContext) -> AnyElement { let disabled = matches!(codegen.status(cx), CodegenStatus::Idle); @@ -795,6 +776,7 @@ impl PromptEditor { editor }); let context_picker_menu_handle = PopoverMenuHandle::default(); + let model_selector_menu_handle = PopoverMenuHandle::default(); let mut this: PromptEditor = PromptEditor { editor: prompt_editor.clone(), @@ -809,19 +791,10 @@ impl PromptEditor { ) }), context_picker_menu_handle, - language_model_selector: cx.new_view(|cx| { - let fs = fs.clone(); - LanguageModelSelector::new( - move |model, cx| { - update_settings_file::( - fs.clone(), - cx, - move |settings, _| settings.set_model(model.clone()), - ); - }, - cx, - ) + model_selector: cx.new_view(|cx| { + AssistantModelSelector::new(fs, model_selector_menu_handle.clone(), cx) }), + model_selector_menu_handle, edited_since_done: false, prompt_history, prompt_history_ix: None, @@ -942,6 +915,7 @@ impl PromptEditor { editor }); let context_picker_menu_handle = PopoverMenuHandle::default(); + let model_selector_menu_handle = PopoverMenuHandle::default(); let mut this = Self { editor: prompt_editor.clone(), @@ -956,19 +930,10 @@ impl PromptEditor { ) }), context_picker_menu_handle, - language_model_selector: cx.new_view(|cx| { - let fs = fs.clone(); - LanguageModelSelector::new( - move |model, cx| { - update_settings_file::( - fs.clone(), - cx, - move |settings, _| settings.set_model(model.clone()), - ); - }, - cx, - ) + model_selector: cx.new_view(|cx| { + AssistantModelSelector::new(fs, model_selector_menu_handle.clone(), cx) }), + model_selector_menu_handle, edited_since_done: false, prompt_history, prompt_history_ix: None, diff --git a/crates/assistant2/src/message_editor.rs b/crates/assistant2/src/message_editor.rs index 912a22a362f2a..d5da3c2d6a0b7 100644 --- a/crates/assistant2/src/message_editor.rs +++ b/crates/assistant2/src/message_editor.rs @@ -7,17 +7,17 @@ use gpui::{ WeakView, }; use language_model::{LanguageModelRegistry, LanguageModelRequestTool}; -use language_model_selector::{LanguageModelSelector, LanguageModelSelectorPopoverMenu}; +use language_model_selector::LanguageModelSelector; use rope::Point; -use settings::{update_settings_file, Settings}; +use settings::Settings; use theme::ThemeSettings; use ui::{ prelude::*, ButtonLike, CheckboxWithLabel, ElevationIndex, KeyBinding, PopoverMenu, - PopoverMenuHandle, Tooltip, + PopoverMenuHandle, }; use workspace::Workspace; -use crate::assistant_settings::AssistantSettings; +use crate::assistant_model_selector::AssistantModelSelector; use crate::context_picker::{ConfirmBehavior, ContextPicker}; use crate::context_store::ContextStore; use crate::context_strip::ContextStrip; @@ -33,8 +33,8 @@ pub struct MessageEditor { context_picker_menu_handle: PopoverMenuHandle, inline_context_picker: View, inline_context_picker_menu_handle: PopoverMenuHandle, - language_model_selector: View, - language_model_selector_menu_handle: PopoverMenuHandle, + model_selector: View, + model_selector_menu_handle: PopoverMenuHandle, use_tools: bool, _subscriptions: Vec, } @@ -50,6 +50,7 @@ impl MessageEditor { let context_store = cx.new_model(|_cx| ContextStore::new()); let context_picker_menu_handle = PopoverMenuHandle::default(); let inline_context_picker_menu_handle = PopoverMenuHandle::default(); + let model_selector_menu_handle = PopoverMenuHandle::default(); let editor = cx.new_view(|cx| { let mut editor = Editor::auto_height(10, cx); @@ -92,27 +93,17 @@ impl MessageEditor { context_picker_menu_handle, inline_context_picker, inline_context_picker_menu_handle, - language_model_selector: cx.new_view(|cx| { - let fs = fs.clone(); - LanguageModelSelector::new( - move |model, cx| { - update_settings_file::( - fs.clone(), - cx, - move |settings, _cx| settings.set_model(model.clone()), - ); - }, - cx, - ) + model_selector: cx.new_view(|cx| { + AssistantModelSelector::new(fs, model_selector_menu_handle.clone(), cx) }), - language_model_selector_menu_handle: PopoverMenuHandle::default(), + model_selector_menu_handle, use_tools: false, _subscriptions: subscriptions, } } fn toggle_model_selector(&mut self, _: &ToggleModelSelector, cx: &mut ViewContext) { - self.language_model_selector_menu_handle.toggle(cx); + self.model_selector_menu_handle.toggle(cx) } fn toggle_context_picker(&mut self, _: &ToggleContextPicker, cx: &mut ViewContext) { @@ -203,50 +194,6 @@ impl MessageEditor { let editor_focus_handle = self.editor.focus_handle(cx); cx.focus(&editor_focus_handle); } - - fn render_language_model_selector(&self, cx: &mut ViewContext) -> impl IntoElement { - let active_model = LanguageModelRegistry::read_global(cx).active_model(); - let focus_handle = self.language_model_selector.focus_handle(cx).clone(); - - LanguageModelSelectorPopoverMenu::new( - self.language_model_selector.clone(), - ButtonLike::new("active-model") - .style(ButtonStyle::Subtle) - .child( - h_flex() - .w_full() - .gap_0p5() - .child( - div() - .overflow_x_hidden() - .flex_grow() - .whitespace_nowrap() - .child(match active_model { - Some(model) => h_flex() - .child( - Label::new(model.name().0) - .size(LabelSize::Small) - .color(Color::Muted), - ) - .into_any_element(), - _ => Label::new("No model selected") - .size(LabelSize::Small) - .color(Color::Muted) - .into_any_element(), - }), - ) - .child( - Icon::new(IconName::ChevronDown) - .color(Color::Muted) - .size(IconSize::XSmall), - ), - ) - .tooltip(move |cx| { - Tooltip::for_action_in("Change Model", &ToggleModelSelector, &focus_handle, cx) - }), - ) - .with_handle(self.language_model_selector_menu_handle.clone()) - } } impl FocusableView for MessageEditor { @@ -321,22 +268,19 @@ impl Render for MessageEditor { }), )) .child( - h_flex() - .gap_1() - .child(self.render_language_model_selector(cx)) - .child( - ButtonLike::new("chat") - .style(ButtonStyle::Filled) - .layer(ElevationIndex::ModalSurface) - .child(Label::new("Submit")) - .children( - KeyBinding::for_action_in(&Chat, &focus_handle, cx) - .map(|binding| binding.into_any_element()), - ) - .on_click(move |_event, cx| { - focus_handle.dispatch_action(&Chat, cx); - }), - ), + h_flex().gap_1().child(self.model_selector.clone()).child( + ButtonLike::new("chat") + .style(ButtonStyle::Filled) + .layer(ElevationIndex::ModalSurface) + .child(Label::new("Submit")) + .children( + KeyBinding::for_action_in(&Chat, &focus_handle, cx) + .map(|binding| binding.into_any_element()), + ) + .on_click(move |_event, cx| { + focus_handle.dispatch_action(&Chat, cx); + }), + ), ), ) }