From d577ef52cb2a62f40063fba3f91a9b2e5d4b66a8 Mon Sep 17 00:00:00 2001 From: Danilo Leal <67129314+danilo-leal@users.noreply.github.com> Date: Mon, 4 Aug 2025 12:44:29 -0300 Subject: [PATCH] thread view: Scroll to the bottom when sending new messages + adjust controls display (#35586) Release Notes: - N/A --- crates/agent_ui/src/acp/thread_view.rs | 120 +++++++++++++++---------- 1 file changed, 71 insertions(+), 49 deletions(-) diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index 57d3257f4d..24d8b73396 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -424,11 +424,14 @@ 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) }); + self.scroll_to_bottom(cx); + self.message_history.borrow_mut().push(chunks); } @@ -2022,15 +2025,15 @@ impl AcpThreadView { .icon_color(Color::Accent) .style(ButtonStyle::Filled) .disabled(self.thread().is_none() || is_editor_empty) - .on_click(cx.listener(|this, _, window, cx| { - this.chat(&Chat, window, cx); - })) .when(!is_editor_empty, |button| { button.tooltip(move |window, cx| Tooltip::for_action("Send", &Chat, window, cx)) }) .when(is_editor_empty, |button| { button.tooltip(Tooltip::text("Type a message to submit")) }) + .on_click(cx.listener(|this, _, window, cx| { + this.chat(&Chat, window, cx); + })) .into_any_element() } else { IconButton::new("stop-generation", IconName::StopFilled) @@ -2245,6 +2248,14 @@ impl AcpThreadView { cx.notify(); } + pub fn scroll_to_bottom(&mut self, cx: &mut Context) { + if let Some(thread) = self.thread() { + let entry_count = thread.read(cx).entries().len(); + self.list_state.reset(entry_count); + cx.notify(); + } + } + fn notify_with_sound( &mut self, caption: impl Into, @@ -2392,17 +2403,9 @@ impl AcpThreadView { self.notification_subscriptions.remove(&window); } } -} -impl Focusable for AcpThreadView { - fn focus_handle(&self, cx: &App) -> FocusHandle { - self.message_editor.focus_handle(cx) - } -} - -impl Render for AcpThreadView { - fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { - let open_as_markdown = IconButton::new("open-as-markdown", IconName::DocumentText) + fn render_thread_controls(&mut self, cx: &mut Context) -> impl IntoElement { + let open_as_markdown = IconButton::new("open-as-markdown", IconName::FileText) .icon_size(IconSize::XSmall) .icon_color(Color::Ignored) .tooltip(Tooltip::text("Open Thread as Markdown")) @@ -2421,6 +2424,28 @@ impl Render for AcpThreadView { this.scroll_to_top(cx); })); + h_flex() + .mt_1() + .mr_1() + .py_2() + .px(RESPONSE_PADDING_X) + .opacity(0.4) + .hover(|style| style.opacity(1.)) + .flex_wrap() + .justify_end() + .child(open_as_markdown) + .child(scroll_to_top) + } +} + +impl Focusable for AcpThreadView { + fn focus_handle(&self, cx: &App) -> FocusHandle { + self.message_editor.focus_handle(cx) + } +} + +impl Render for AcpThreadView { + fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { v_flex() .size_full() .key_context("AcpThread") @@ -2456,42 +2481,39 @@ impl Render for AcpThreadView { .items_center() .justify_center() .child(self.render_error_state(e, cx)), - ThreadState::Ready { thread, .. } => v_flex().flex_1().map(|this| { - if self.list_state.item_count() > 0 { - this.child( - list(self.list_state.clone()) - .with_sizing_behavior(gpui::ListSizingBehavior::Auto) - .flex_grow() - .into_any(), - ) - .child( - h_flex() - .group("controls") - .mt_1() - .mr_1() - .py_2() - .px(RESPONSE_PADDING_X) - .opacity(0.4) - .hover(|style| style.opacity(1.)) - .flex_wrap() - .justify_end() - .child(open_as_markdown) - .child(scroll_to_top) - .into_any_element(), - ) - .children(match thread.read(cx).status() { - ThreadStatus::Idle | ThreadStatus::WaitingForToolConfirmation => None, - ThreadStatus::Generating => div() - .px_5() - .py_2() - .child(LoadingLabel::new("").size(LabelSize::Small)) - .into(), - }) - .children(self.render_activity_bar(&thread, window, cx)) - } else { - this.child(self.render_empty_state(cx)) - } - }), + ThreadState::Ready { thread, .. } => { + let thread_clone = thread.clone(); + + v_flex().flex_1().map(|this| { + if self.list_state.item_count() > 0 { + let is_generating = + matches!(thread_clone.read(cx).status(), ThreadStatus::Generating); + + this.child( + list(self.list_state.clone()) + .with_sizing_behavior(gpui::ListSizingBehavior::Auto) + .flex_grow() + .into_any(), + ) + .when(!is_generating, |this| { + this.child(self.render_thread_controls(cx)) + }) + .children(match thread_clone.read(cx).status() { + ThreadStatus::Idle | ThreadStatus::WaitingForToolConfirmation => { + None + } + ThreadStatus::Generating => div() + .px_5() + .py_2() + .child(LoadingLabel::new("").size(LabelSize::Small)) + .into(), + }) + .children(self.render_activity_bar(&thread_clone, window, cx)) + } else { + this.child(self.render_empty_state(cx)) + } + }) + } }) .when_some(self.last_error.clone(), |el, error| { el.child(