thread view: Add ability to expand message editor and fix scroll (#34766)

Danilo Leal created

Release Notes:

- N/A

Change summary

crates/agent_ui/src/acp/thread_view.rs | 173 ++++++++++++++++++++-------
crates/agent_ui/src/agent_panel.rs     |   3 
crates/agent_ui/src/message_editor.rs  |  10 
3 files changed, 135 insertions(+), 51 deletions(-)

Detailed changes

crates/agent_ui/src/acp/thread_view.rs 🔗

@@ -45,7 +45,8 @@ use ::acp_thread::{
 use crate::acp::completion_provider::{ContextPickerCompletionProvider, MentionSet};
 use crate::acp::message_history::MessageHistory;
 use crate::agent_diff::AgentDiff;
-use crate::{AgentDiffPane, Follow, KeepAll, OpenAgentDiff, RejectAll};
+use crate::message_editor::{MAX_EDITOR_LINES, MIN_EDITOR_LINES};
+use crate::{AgentDiffPane, ExpandMessageEditor, Follow, KeepAll, OpenAgentDiff, RejectAll};
 
 const RESPONSE_PADDING_X: Pixels = px(19.);
 
@@ -65,6 +66,7 @@ pub struct AcpThreadView {
     expanded_tool_calls: HashSet<ToolCallId>,
     expanded_thinking_blocks: HashSet<(usize, usize)>,
     edits_expanded: bool,
+    editor_is_expanded: bool,
     message_history: Rc<RefCell<MessageHistory<acp::SendUserMessageParams>>>,
 }
 
@@ -94,6 +96,8 @@ impl AcpThreadView {
         workspace: WeakEntity<Workspace>,
         project: Entity<Project>,
         message_history: Rc<RefCell<MessageHistory<acp::SendUserMessageParams>>>,
+        min_lines: usize,
+        max_lines: Option<usize>,
         window: &mut Window,
         cx: &mut Context<Self>,
     ) -> Self {
@@ -113,8 +117,8 @@ impl AcpThreadView {
 
             let mut editor = Editor::new(
                 editor::EditorMode::AutoHeight {
-                    min_lines: 4,
-                    max_lines: None,
+                    min_lines,
+                    max_lines: max_lines,
                 },
                 buffer,
                 None,
@@ -182,6 +186,7 @@ impl AcpThreadView {
             expanded_tool_calls: HashSet::default(),
             expanded_thinking_blocks: HashSet::default(),
             edits_expanded: false,
+            editor_is_expanded: false,
             message_history,
         }
     }
@@ -321,6 +326,35 @@ impl AcpThreadView {
         }
     }
 
+    pub fn expand_message_editor(
+        &mut self,
+        _: &ExpandMessageEditor,
+        _window: &mut Window,
+        cx: &mut Context<Self>,
+    ) {
+        self.set_editor_is_expanded(!self.editor_is_expanded, cx);
+        cx.notify();
+    }
+
+    fn set_editor_is_expanded(&mut self, is_expanded: bool, cx: &mut Context<Self>) {
+        self.editor_is_expanded = is_expanded;
+        self.message_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,
+                    sized_by_content: false,
+                })
+            } else {
+                editor.set_mode(EditorMode::AutoHeight {
+                    min_lines: MIN_EDITOR_LINES,
+                    max_lines: Some(MAX_EDITOR_LINES),
+                })
+            }
+        });
+        cx.notify();
+    }
+
     fn chat(&mut self, _: &Chat, window: &mut Window, cx: &mut Context<Self>) {
         self.last_error.take();
 
@@ -381,6 +415,7 @@ impl AcpThreadView {
 
         let mention_set = self.mention_set.clone();
 
+        self.set_editor_is_expanded(false, cx);
         self.message_editor.update(cx, |editor, cx| {
             editor.clear(window, cx);
             editor.remove_creases(mention_set.lock().drain(), cx)
@@ -1793,34 +1828,96 @@ impl AcpThreadView {
         ))
     }
 
-    fn render_message_editor(&mut self, cx: &mut Context<Self>) -> AnyElement {
-        let settings = ThemeSettings::get_global(cx);
-        let font_size = TextSize::Small
-            .rems(cx)
-            .to_pixels(settings.agent_font_size(cx));
-        let line_height = settings.buffer_line_height.value() * font_size;
-
-        let text_style = TextStyle {
-            color: cx.theme().colors().text,
-            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(),
-            line_height: line_height.into(),
-            ..Default::default()
+    fn render_message_editor(&mut self, window: &mut Window, cx: &mut Context<Self>) -> AnyElement {
+        let focus_handle = self.message_editor.focus_handle(cx);
+        let editor_bg_color = cx.theme().colors().editor_background;
+        let (expand_icon, expand_tooltip) = if self.editor_is_expanded {
+            (IconName::Minimize, "Minimize Message Editor")
+        } else {
+            (IconName::Maximize, "Expand Message Editor")
         };
 
-        EditorElement::new(
-            &self.message_editor,
-            EditorStyle {
-                background: cx.theme().colors().editor_background,
-                local_player: cx.theme().players().local(),
-                text: text_style,
-                syntax: cx.theme().syntax().clone(),
-                ..Default::default()
-            },
-        )
-        .into_any()
+        v_flex()
+            .on_action(cx.listener(Self::expand_message_editor))
+            .p_2()
+            .gap_2()
+            .border_t_1()
+            .border_color(cx.theme().colors().border)
+            .bg(editor_bg_color)
+            .when(self.editor_is_expanded, |this| {
+                this.h(vh(0.8, window)).size_full().justify_between()
+            })
+            .child(
+                v_flex()
+                    .relative()
+                    .size_full()
+                    .pt_1()
+                    .pr_2p5()
+                    .child(div().flex_1().child({
+                        let settings = ThemeSettings::get_global(cx);
+                        let font_size = TextSize::Small
+                            .rems(cx)
+                            .to_pixels(settings.agent_font_size(cx));
+                        let line_height = settings.buffer_line_height.value() * font_size;
+
+                        let text_style = TextStyle {
+                            color: cx.theme().colors().text,
+                            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(),
+                            line_height: line_height.into(),
+                            ..Default::default()
+                        };
+
+                        EditorElement::new(
+                            &self.message_editor,
+                            EditorStyle {
+                                background: editor_bg_color,
+                                local_player: cx.theme().players().local(),
+                                text: text_style,
+                                syntax: cx.theme().syntax().clone(),
+                                ..Default::default()
+                            },
+                        )
+                    }))
+                    .child(
+                        h_flex()
+                            .absolute()
+                            .top_0()
+                            .right_0()
+                            .opacity(0.5)
+                            .hover(|this| this.opacity(1.0))
+                            .child(
+                                IconButton::new("toggle-height", expand_icon)
+                                    .icon_size(IconSize::XSmall)
+                                    .icon_color(Color::Muted)
+                                    .tooltip({
+                                        let focus_handle = focus_handle.clone();
+                                        move |window, cx| {
+                                            Tooltip::for_action_in(
+                                                expand_tooltip,
+                                                &ExpandMessageEditor,
+                                                &focus_handle,
+                                                window,
+                                                cx,
+                                            )
+                                        }
+                                    })
+                                    .on_click(cx.listener(|_, _, window, cx| {
+                                        window.dispatch_action(Box::new(ExpandMessageEditor), cx);
+                                    })),
+                            ),
+                    ),
+            )
+            .child(
+                h_flex()
+                    .flex_none()
+                    .justify_between()
+                    .child(self.render_follow_toggle(cx))
+                    .child(self.render_send_button(cx)),
+            )
+            .into_any()
     }
 
     fn render_send_button(&self, cx: &mut Context<Self>) -> AnyElement {
@@ -2132,7 +2229,6 @@ impl Render for AcpThreadView {
                                 .px(RESPONSE_PADDING_X)
                                 .opacity(0.4)
                                 .hover(|style| style.opacity(1.))
-                                .gap_1()
                                 .flex_wrap()
                                 .justify_end()
                                 .child(open_as_markdown)
@@ -2166,22 +2262,7 @@ impl Render for AcpThreadView {
                         ),
                 )
             })
-            .child(
-                v_flex()
-                    .p_2()
-                    .pt_3()
-                    .gap_1()
-                    .bg(cx.theme().colors().editor_background)
-                    .border_t_1()
-                    .border_color(cx.theme().colors().border)
-                    .child(self.render_message_editor(cx))
-                    .child(
-                        h_flex()
-                            .justify_between()
-                            .child(self.render_follow_toggle(cx))
-                            .child(self.render_send_button(cx)),
-                    ),
-            )
+            .child(self.render_message_editor(window, cx))
     }
 }
 

crates/agent_ui/src/agent_panel.rs 🔗

@@ -11,6 +11,7 @@ use serde::{Deserialize, Serialize};
 
 use crate::NewExternalAgentThread;
 use crate::agent_diff::AgentDiffThread;
+use crate::message_editor::{MAX_EDITOR_LINES, MIN_EDITOR_LINES};
 use crate::{
     AddContextServer, AgentDiffPane, ContinueThread, ContinueWithBurnMode,
     DeleteRecentlyOpenThread, ExpandMessageEditor, Follow, InlineAssistant, NewTextThread,
@@ -960,6 +961,8 @@ impl AgentPanel {
                         workspace.clone(),
                         project,
                         message_history,
+                        MIN_EDITOR_LINES,
+                        Some(MAX_EDITOR_LINES),
                         window,
                         cx,
                     )

crates/agent_ui/src/message_editor.rs 🔗

@@ -65,6 +65,9 @@ use agent::{
     thread_store::{TextThreadStore, ThreadStore},
 };
 
+pub const MIN_EDITOR_LINES: usize = 4;
+pub const MAX_EDITOR_LINES: usize = 8;
+
 #[derive(RegisterComponent)]
 pub struct MessageEditor {
     thread: Entity<Thread>,
@@ -88,9 +91,6 @@ pub struct MessageEditor {
     _subscriptions: Vec<Subscription>,
 }
 
-const MIN_EDITOR_LINES: usize = 4;
-const MAX_EDITOR_LINES: usize = 8;
-
 pub(crate) fn create_editor(
     workspace: WeakEntity<Workspace>,
     context_store: WeakEntity<ContextStore>,
@@ -711,11 +711,11 @@ impl MessageEditor {
                 cx.listener(|this, _: &RejectAll, window, cx| this.handle_reject_all(window, cx)),
             )
             .capture_action(cx.listener(Self::paste))
-            .gap_2()
             .p_2()
-            .bg(editor_bg_color)
+            .gap_2()
             .border_t_1()
             .border_color(cx.theme().colors().border)
+            .bg(editor_bg_color)
             .child(
                 h_flex()
                     .justify_between()