From 71c2a11bd9c1f7d2d358f2716f091fac915b290e Mon Sep 17 00:00:00 2001 From: Danilo Leal <67129314+danilo-leal@users.noreply.github.com> Date: Thu, 10 Apr 2025 21:53:52 -0300 Subject: [PATCH] agent: Make the message editor expandable (#28420) This PR allows expanding the message editor textarea to fit almost the total height of the Agent Panel. Stylistically, I'm also changing the font family we use in the textarea to use the buffer font; want to experiment with this for a bit. Release Notes: - agent: The Agent Panel textarea can now be expanded to fill almost the total height of the panel. --------- Co-authored-by: Bennet Bo Fenner Co-authored-by: Bennet Bo Fenner --- assets/keymaps/default-linux.json | 1 + assets/keymaps/default-macos.json | 1 + crates/agent/src/assistant.rs | 1 + crates/agent/src/assistant_panel.rs | 1 + crates/agent/src/context_picker.rs | 3 +- crates/agent/src/message_editor.rs | 130 +++++++++++++++++++++++----- crates/editor/src/editor.rs | 20 ++++- crates/editor/src/element.rs | 16 +++- 8 files changed, 142 insertions(+), 31 deletions(-) diff --git a/assets/keymaps/default-linux.json b/assets/keymaps/default-linux.json index dbb1d14c103075223bf75c98a18fccac52b5bd30..2347f95a8cdef9c9c9c9437c2be1f1a300db5ec3 100644 --- a/assets/keymaps/default-linux.json +++ b/assets/keymaps/default-linux.json @@ -631,6 +631,7 @@ "ctrl-i": "agent::ToggleProfileSelector", "ctrl-alt-/": "assistant::ToggleModelSelector", "ctrl-shift-a": "agent::ToggleContextPicker", + "shift-escape": "agent::ExpandMessageEditor", "ctrl-e": "agent::ChatMode", "ctrl-alt-e": "agent::RemoveAllContext" } diff --git a/assets/keymaps/default-macos.json b/assets/keymaps/default-macos.json index 7e091862f2c6aa66d346a3bce5b3d60cf90b56b8..7dfbca91946e32fd5dea35cc9ca3c62d5264473e 100644 --- a/assets/keymaps/default-macos.json +++ b/assets/keymaps/default-macos.json @@ -287,6 +287,7 @@ "cmd-i": "agent::ToggleProfileSelector", "cmd-alt-/": "assistant::ToggleModelSelector", "cmd-shift-a": "agent::ToggleContextPicker", + "shift-escape": "agent::ExpandMessageEditor", "cmd-e": "agent::ChatMode", "cmd-alt-e": "agent::RemoveAllContext" } diff --git a/crates/agent/src/assistant.rs b/crates/agent/src/assistant.rs index c9aba03288fb00a7d98c4f0d50dcd8ea6a42e878..b4712229566a2387687aa8dbad64c6c0424304d5 100644 --- a/crates/agent/src/assistant.rs +++ b/crates/agent/src/assistant.rs @@ -50,6 +50,7 @@ actions!( ToggleContextPicker, ToggleProfileSelector, RemoveAllContext, + ExpandMessageEditor, OpenHistory, AddContextServer, RemoveSelectedThread, diff --git a/crates/agent/src/assistant_panel.rs b/crates/agent/src/assistant_panel.rs index e257990d7dc9416ed6bca8195a0e3e4d8e635a4e..384377f7c6919b6376e6c5a10e3a61448447b1c3 100644 --- a/crates/agent/src/assistant_panel.rs +++ b/crates/agent/src/assistant_panel.rs @@ -1298,6 +1298,7 @@ impl AssistantPanel { let configuration_error_ref = &configuration_error; parent + .overflow_hidden() .p_1p5() .justify_end() .gap_1() diff --git a/crates/agent/src/context_picker.rs b/crates/agent/src/context_picker.rs index a370112d0d535982cfed080cfcad186c5502d44c..4d0b48db4d03dcb9b63b1be7ca351243a0ec7efd 100644 --- a/crates/agent/src/context_picker.rs +++ b/crates/agent/src/context_picker.rs @@ -606,12 +606,13 @@ fn render_fold_icon_button( .gap_1() .child( Icon::from_path(icon_path.clone()) - .size(IconSize::Small) + .size(IconSize::XSmall) .color(Color::Muted), ) .child( Label::new(label.clone()) .size(LabelSize::Small) + .buffer_font(cx) .single_line(), ), ) diff --git a/crates/agent/src/message_editor.rs b/crates/agent/src/message_editor.rs index 953c1f0f6834399231221191efd9c1c3c0f4aa7a..81adea894538b56a33a4ffca8d7d4c5379d2fc30 100644 --- a/crates/agent/src/message_editor.rs +++ b/crates/agent/src/message_editor.rs @@ -4,7 +4,8 @@ use crate::assistant_model_selector::ModelType; use collections::HashSet; use editor::actions::MoveUp; use editor::{ - ContextMenuOptions, ContextMenuPlacement, Editor, EditorElement, EditorStyle, MultiBuffer, + ContextMenuOptions, ContextMenuPlacement, Editor, EditorElement, EditorMode, EditorStyle, + MultiBuffer, }; use file_icons::FileIcons; use fs::Fs; @@ -32,8 +33,8 @@ use crate::profile_selector::ProfileSelector; use crate::thread::{RequestKind, Thread, TokenUsageRatio}; use crate::thread_store::ThreadStore; use crate::{ - AgentDiff, Chat, ChatMode, NewThread, OpenAgentDiff, RemoveAllContext, ToggleContextPicker, - ToggleProfileSelector, + AgentDiff, Chat, ChatMode, ExpandMessageEditor, NewThread, OpenAgentDiff, RemoveAllContext, + ToggleContextPicker, ToggleProfileSelector, }; pub struct MessageEditor { @@ -50,10 +51,13 @@ pub struct MessageEditor { model_selector: Entity, profile_selector: Entity, edits_expanded: bool, + editor_is_expanded: bool, waiting_for_summaries_to_send: bool, _subscriptions: Vec, } +const MAX_EDITOR_LINES: usize = 10; + impl MessageEditor { pub fn new( fs: Arc, @@ -80,7 +84,9 @@ impl MessageEditor { let buffer = cx.new(|cx| Buffer::local("", cx).with_language(Arc::new(language), cx)); let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx)); let mut editor = Editor::new( - editor::EditorMode::AutoHeight { max_lines: 10 }, + editor::EditorMode::AutoHeight { + max_lines: MAX_EDITOR_LINES, + }, buffer, None, window, @@ -159,6 +165,7 @@ impl MessageEditor { ) }), edits_expanded: false, + editor_is_expanded: false, waiting_for_summaries_to_send: false, profile_selector: cx .new(|cx| ProfileSelector::new(fs, thread_store, editor.focus_handle(cx), cx)), @@ -170,6 +177,32 @@ impl MessageEditor { cx.notify(); } + fn expand_message_editor( + &mut self, + _: &ExpandMessageEditor, + _window: &mut Window, + cx: &mut Context, + ) { + self.set_editor_is_expanded(!self.editor_is_expanded, cx); + } + + fn set_editor_is_expanded(&mut self, is_expanded: bool, cx: &mut Context) { + self.editor_is_expanded = is_expanded; + self.editor.update(cx, |editor, _| { + if self.editor_is_expanded { + editor.set_mode(EditorMode::Full { + scale_ui_elements_with_buffer_font_size: false, + show_active_line_background: false, + }) + } else { + editor.set_mode(EditorMode::AutoHeight { + max_lines: MAX_EDITOR_LINES, + }) + } + }); + cx.notify(); + } + fn toggle_context_picker( &mut self, _: &ToggleContextPicker, @@ -197,7 +230,10 @@ impl MessageEditor { return; } + self.set_editor_is_expanded(false, cx); self.send_to_model(RequestKind::Chat, window, cx); + + cx.notify(); } fn is_editor_empty(&self, cx: &App) -> bool { @@ -345,12 +381,20 @@ impl Focusable for MessageEditor { impl Render for MessageEditor { fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { - let font_size = TextSize::Default.rems(cx); + let font_size = TextSize::Small.rems(cx); let line_height = font_size.to_pixels(window.rem_size()) * 1.5; let focus_handle = self.editor.focus_handle(cx); + let focus_handle_clone = focus_handle.clone(); let inline_context_picker = self.inline_context_picker.clone(); + let is_editor_expanded = self.editor_is_expanded; + let expand_icon = if is_editor_expanded { + IconName::Minimize + } else { + IconName::Maximize + }; + let thread = self.thread.read(cx); let is_generating = thread.is_generating(); let total_token_usage = thread.total_token_usage(cx); @@ -657,24 +701,56 @@ impl Render for MessageEditor { .on_action(cx.listener(Self::remove_all_context)) .on_action(cx.listener(Self::move_up)) .on_action(cx.listener(Self::toggle_chat_mode)) + .on_action(cx.listener(Self::expand_message_editor)) .gap_2() .p_2() .bg(editor_bg_color) .border_t_1() .border_color(cx.theme().colors().border) - .child(h_flex().justify_between().child(self.context_strip.clone())) + .child( + h_flex() + .items_start() + .justify_between() + .child(self.context_strip.clone()) + .child( + IconButton::new("toggle-height", expand_icon) + .icon_size(IconSize::XSmall) + .icon_color(Color::Muted) + .tooltip(move |window, cx| { + let focus_handle = focus_handle.clone(); + let expand_label = if is_editor_expanded { + "Minimize Message Editor".to_string() + } else { + "Expand Message Editor".to_string() + }; + + Tooltip::for_action_in( + expand_label, + &ExpandMessageEditor, + &focus_handle, + window, + cx, + ) + }) + .on_click(cx.listener(|_, _, window, cx| { + window.dispatch_action(Box::new(ExpandMessageEditor), cx); + })) + ) + ) .child( v_flex() - .gap_5() - .child({ + .size_full() + .gap_4() + .when(is_editor_expanded, |this| this.h(vh(0.8, window)).justify_between()) + .child(div().when(is_editor_expanded, |this| this.h_full()).child({ let settings = ThemeSettings::get_global(cx); + let text_style = TextStyle { color: cx.theme().colors().text, - font_family: settings.ui_font.family.clone(), - font_fallbacks: settings.ui_font.fallbacks.clone(), - font_features: settings.ui_font.features.clone(), + font_family: settings.buffer_font.family.clone(), + font_fallbacks: settings.buffer_font.fallbacks.clone(), + font_features: settings.buffer_font.features.clone(), font_size: font_size.into(), - font_weight: settings.ui_font.weight, line_height: line_height.into(), ..Default::default() }; @@ -689,7 +765,7 @@ impl Render for MessageEditor { ..Default::default() }, ).into_any() - }) + })) .child( PopoverMenu::new("inline-context-picker") .menu(move |window, cx| { @@ -709,11 +785,13 @@ impl Render for MessageEditor { ) .child( h_flex() + .flex_none() .justify_between() .child(h_flex().gap_2().child(self.profile_selector.clone())) .child( - h_flex().gap_1().child(self.model_selector.clone()) - .map(|parent| { + h_flex().gap_1() + .child(self.model_selector.clone()) + .map(move |parent| { if is_generating { parent.child( IconButton::new("stop-generation", IconName::StopFilled) @@ -727,12 +805,15 @@ impl Render for MessageEditor { cx, ) }) - .on_click(move |_event, window, cx| { - focus_handle.dispatch_action( - &editor::actions::Cancel, - window, - cx, - ); + .on_click({ + let focus_handle = focus_handle_clone.clone(); + move |_event, window, cx| { + focus_handle.dispatch_action( + &editor::actions::Cancel, + window, + cx, + ); + } }) .with_animation( "pulsating-label", @@ -752,8 +833,11 @@ impl Render for MessageEditor { || !is_model_selected || self.waiting_for_summaries_to_send ) - .on_click(move |_event, window, cx| { - focus_handle.dispatch_action(&Chat, window, cx); + .on_click({ + let focus_handle = focus_handle_clone.clone(); + move |_event, window, cx| { + focus_handle.dispatch_action(&Chat, window, cx); + } }) .when(!is_editor_empty && is_model_selected, |button| { button.tooltip(move |window, cx| { diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 6c7d072a5945ee08e8ae89de8f85e9ca3652ec81..08829ee53a54ed94d1cea0806de2e41b2f30dab8 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -396,14 +396,26 @@ pub enum SelectMode { #[derive(Copy, Clone, PartialEq, Eq, Debug)] pub enum EditorMode { - SingleLine { auto_width: bool }, - AutoHeight { max_lines: usize }, - Full, + SingleLine { + auto_width: bool, + }, + AutoHeight { + max_lines: usize, + }, + Full { + /// When set to `true`, the editor will scale its UI elements with the buffer font size. + scale_ui_elements_with_buffer_font_size: bool, + /// When set to `true`, the editor will render a background for the active line. + show_active_line_background: bool, + }, } impl EditorMode { pub fn full() -> Self { - Self::Full + Self::Full { + scale_ui_elements_with_buffer_font_size: true, + show_active_line_background: true, + } } pub fn is_full(&self) -> bool { diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index aa01db17970685c27968a73b4f2d709575dc881d..91c84e4ca7c338a54a50ef97ad8dc96b7b439f95 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -4176,7 +4176,11 @@ impl EditorElement { self.style.background, )); - if let EditorMode::Full { .. } = layout.mode { + if let EditorMode::Full { + show_active_line_background, + .. + } = layout.mode + { let mut active_rows = layout.active_rows.iter().peekable(); while let Some((start_row, contains_non_empty_selection)) = active_rows.next() { let mut end_row = start_row.0; @@ -4191,7 +4195,7 @@ impl EditorElement { end_row += 1; } - if !contains_non_empty_selection.selection { + if show_active_line_background && !contains_non_empty_selection.selection { let highlight_h_range = match layout.position_map.snapshot.current_line_highlight { CurrentLineHighlight::Gutter => Some(Range { @@ -6414,7 +6418,13 @@ impl EditorElement { /// This allows UI elements to scale based on the `buffer_font_size`. fn rem_size(&self, cx: &mut App) -> Option { match self.editor.read(cx).mode { - EditorMode::Full { .. } => { + EditorMode::Full { + scale_ui_elements_with_buffer_font_size, + .. + } => { + if !scale_ui_elements_with_buffer_font_size { + return None; + } let buffer_font_size = self.style.text.font_size; match buffer_font_size { AbsoluteLength::Pixels(pixels) => {