From 871a3428351799b4467d1a6a40b667bd9ebe9fc2 Mon Sep 17 00:00:00 2001 From: Danilo Leal <67129314+danilo-leal@users.noreply.github.com> Date: Tue, 7 Apr 2026 23:58:50 -0300 Subject: [PATCH] agent_panel: Add some UI fixes post max-width introduction (#53352) Quick follow up to https://github.com/zed-industries/zed/pull/52730 with some adjustments. Ended up cleaning some unused UI elements already (i.e., the "Recent" history items in the agent panel's empty state). Release Notes: - N/A --- crates/agent_ui/src/conversation_view.rs | 11 +- .../src/conversation_view/thread_view.rs | 255 ++++++------------ crates/agent_ui/src/thread_history_view.rs | 143 +--------- 3 files changed, 93 insertions(+), 316 deletions(-) diff --git a/crates/agent_ui/src/conversation_view.rs b/crates/agent_ui/src/conversation_view.rs index de02fdc5d384f08c693df848b63f7ef20bdd28f8..cb42cb92ddcd0799acb4ccb6839a38cd5690d93c 100644 --- a/crates/agent_ui/src/conversation_view.rs +++ b/crates/agent_ui/src/conversation_view.rs @@ -84,12 +84,11 @@ use crate::{ Agent, AgentDiffPane, AgentInitialContent, AgentPanel, AllowAlways, AllowOnce, AuthorizeToolCall, ClearMessageQueue, CycleFavoriteModels, CycleModeSelector, CycleThinkingEffort, EditFirstQueuedMessage, ExpandMessageEditor, Follow, KeepAll, NewThread, - OpenAddContextMenu, OpenAgentDiff, OpenHistory, RejectAll, RejectOnce, - RemoveFirstQueuedMessage, ScrollOutputLineDown, ScrollOutputLineUp, ScrollOutputPageDown, - ScrollOutputPageUp, ScrollOutputToBottom, ScrollOutputToNextMessage, - ScrollOutputToPreviousMessage, ScrollOutputToTop, SendImmediately, SendNextQueuedMessage, - ToggleFastMode, ToggleProfileSelector, ToggleThinkingEffortMenu, ToggleThinkingMode, - UndoLastReject, + OpenAddContextMenu, OpenAgentDiff, RejectAll, RejectOnce, RemoveFirstQueuedMessage, + ScrollOutputLineDown, ScrollOutputLineUp, ScrollOutputPageDown, ScrollOutputPageUp, + ScrollOutputToBottom, ScrollOutputToNextMessage, ScrollOutputToPreviousMessage, + ScrollOutputToTop, SendImmediately, SendNextQueuedMessage, ToggleFastMode, + ToggleProfileSelector, ToggleThinkingEffortMenu, ToggleThinkingMode, UndoLastReject, }; const STOPWATCH_THRESHOLD: Duration = Duration::from_secs(30); diff --git a/crates/agent_ui/src/conversation_view/thread_view.rs b/crates/agent_ui/src/conversation_view/thread_view.rs index 8f9a8a1f8578143a790f876ca497d42e31dce2c0..f82149c8471cc08840c393138c92316fc69e0ee5 100644 --- a/crates/agent_ui/src/conversation_view/thread_view.rs +++ b/crates/agent_ui/src/conversation_view/thread_view.rs @@ -2194,65 +2194,73 @@ impl ThreadView { let edits_expanded = self.edits_expanded; let queue_expanded = self.queue_expanded; - v_flex() - .mx_2() - .bg(self.activity_bar_bg(cx)) - .border_1() - .border_b_0() - .border_color(cx.theme().colors().border) - .rounded_t_md() - .shadow(vec![gpui::BoxShadow { - color: gpui::black().opacity(0.12), - offset: point(px(1.), px(-1.)), - blur_radius: px(2.), - spread_radius: px(0.), - }]) - .when_some(subagents_awaiting_permission, |this, element| { - this.child(element) - }) - .when( - has_subagents_awaiting - && (!plan.is_empty() || !changed_buffers.is_empty() || !queue_is_empty), - |this| this.child(Divider::horizontal().color(DividerColor::Border)), - ) - .when(!plan.is_empty(), |this| { - this.child(self.render_plan_summary(plan, window, cx)) - .when(plan_expanded, |parent| { - parent.child(self.render_plan_entries(plan, window, cx)) + let max_content_width = AgentSettings::get_global(cx).max_content_width; + + div() + .w_full() + .max_w(max_content_width) + .mx_auto() + .child( + v_flex() + .mx_2() + .bg(self.activity_bar_bg(cx)) + .border_1() + .border_b_0() + .border_color(cx.theme().colors().border) + .rounded_t_md() + .shadow(vec![gpui::BoxShadow { + color: gpui::black().opacity(0.12), + offset: point(px(1.), px(-1.)), + blur_radius: px(2.), + spread_radius: px(0.), + }]) + .when_some(subagents_awaiting_permission, |this, element| { + this.child(element) }) - }) - .when(!plan.is_empty() && !changed_buffers.is_empty(), |this| { - this.child(Divider::horizontal().color(DividerColor::Border)) - }) - .when( - !changed_buffers.is_empty() && thread.parent_session_id().is_none(), - |this| { - this.child(self.render_edits_summary( - &changed_buffers, - edits_expanded, - pending_edits, - cx, - )) - .when(edits_expanded, |parent| { - parent.child(self.render_edited_files( - action_log, - telemetry.clone(), - &changed_buffers, - pending_edits, - cx, - )) + .when( + has_subagents_awaiting + && (!plan.is_empty() || !changed_buffers.is_empty() || !queue_is_empty), + |this| this.child(Divider::horizontal().color(DividerColor::Border)), + ) + .when(!plan.is_empty(), |this| { + this.child(self.render_plan_summary(plan, window, cx)) + .when(plan_expanded, |parent| { + parent.child(self.render_plan_entries(plan, window, cx)) + }) }) - }, + .when(!plan.is_empty() && !changed_buffers.is_empty(), |this| { + this.child(Divider::horizontal().color(DividerColor::Border)) + }) + .when( + !changed_buffers.is_empty() && thread.parent_session_id().is_none(), + |this| { + this.child(self.render_edits_summary( + &changed_buffers, + edits_expanded, + pending_edits, + cx, + )) + .when(edits_expanded, |parent| { + parent.child(self.render_edited_files( + action_log, + telemetry.clone(), + &changed_buffers, + pending_edits, + cx, + )) + }) + }, + ) + .when(!queue_is_empty, |this| { + this.when(!plan.is_empty() || !changed_buffers.is_empty(), |this| { + this.child(Divider::horizontal().color(DividerColor::Border)) + }) + .child(self.render_message_queue_summary(window, cx)) + .when(queue_expanded, |parent| { + parent.child(self.render_message_queue_entries(window, cx)) + }) + }), ) - .when(!queue_is_empty, |this| { - this.when(!plan.is_empty() || !changed_buffers.is_empty(), |this| { - this.child(Divider::horizontal().color(DividerColor::Border)) - }) - .child(self.render_message_queue_summary(window, cx)) - .when(queue_expanded, |parent| { - parent.child(self.render_message_queue_entries(window, cx)) - }) - }) .into_any() .into() } @@ -8257,18 +8265,12 @@ impl ThreadView { fn render_resume_notice(_cx: &Context) -> AnyElement { let description = "This agent does not support viewing previous messages. However, your session will still continue from where you last left off."; - div() - .px_2() - .pt_2() - .pb_3() - .w_full() - .child( - Callout::new() - .severity(Severity::Info) - .icon(IconName::Info) - .title("Resumed Session") - .description(description), - ) + Callout::new() + .border_position(ui::BorderPosition::Bottom) + .severity(Severity::Info) + .icon(IconName::Info) + .title("Resumed Session") + .description(description) .into_any_element() } @@ -8282,96 +8284,6 @@ impl ThreadView { cx.notify(); } - fn render_empty_state_section_header( - &self, - label: impl Into, - action_slot: Option, - cx: &mut Context, - ) -> impl IntoElement { - div().pl_1().pr_1p5().child( - h_flex() - .mt_2() - .pl_1p5() - .pb_1() - .w_full() - .justify_between() - .border_b_1() - .border_color(cx.theme().colors().border_variant) - .child( - Label::new(label.into()) - .size(LabelSize::Small) - .color(Color::Muted), - ) - .children(action_slot), - ) - } - - fn render_recent_history(&self, cx: &mut Context) -> AnyElement { - let render_history = !self.recent_history_entries.is_empty(); - - v_flex() - .size_full() - .when(render_history, |this| { - let recent_history = self.recent_history_entries.clone(); - this.justify_end().child( - v_flex() - .child( - self.render_empty_state_section_header( - "Recent", - Some( - Button::new("view-history", "View All") - .style(ButtonStyle::Subtle) - .label_size(LabelSize::Small) - .key_binding( - KeyBinding::for_action_in( - &OpenHistory, - &self.focus_handle(cx), - cx, - ) - .map(|kb| kb.size(rems_from_px(12.))), - ) - .on_click(move |_event, window, cx| { - window.dispatch_action(OpenHistory.boxed_clone(), cx); - }) - .into_any_element(), - ), - cx, - ), - ) - .child(v_flex().p_1().pr_1p5().gap_1().children({ - let supports_delete = self - .history - .as_ref() - .map_or(false, |h| h.read(cx).supports_delete()); - recent_history - .into_iter() - .enumerate() - .map(move |(index, entry)| { - // TODO: Add keyboard navigation. - let is_hovered = - self.hovered_recent_history_item == Some(index); - crate::thread_history_view::HistoryEntryElement::new( - entry, - self.server_view.clone(), - ) - .hovered(is_hovered) - .supports_delete(supports_delete) - .on_hover(cx.listener(move |this, is_hovered, _window, cx| { - if *is_hovered { - this.hovered_recent_history_item = Some(index); - } else if this.hovered_recent_history_item == Some(index) { - this.hovered_recent_history_item = None; - } - cx.notify(); - })) - .into_any_element() - }) - })), - ) - }) - .into_any() - } - fn render_codex_windows_warning(&self, cx: &mut Context) -> Callout { Callout::new() .icon(IconName::Warning) @@ -8625,27 +8537,28 @@ impl ThreadView { impl Render for ThreadView { fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { let has_messages = self.list_state.item_count() > 0; - let v2_empty_state = !has_messages; - let max_content_width = AgentSettings::get_global(cx).max_content_width; + let list_state = self.list_state.clone(); let conversation = v_flex() - .mx_auto() - .max_w(max_content_width) - .when(!v2_empty_state, |this| this.flex_1().size_full()) + .when(self.resumed_without_history, |this| { + this.child(Self::render_resume_notice(cx)) + }) .map(|this| { - let this = this.when(self.resumed_without_history, |this| { - this.child(Self::render_resume_notice(cx)) - }); if has_messages { - let list_state = self.list_state.clone(); - this.child(self.render_entries(cx)) + this.flex_1() + .size_full() + .child( + v_flex() + .mx_auto() + .max_w(max_content_width) + .size_full() + .child(self.render_entries(cx)), + ) .vertical_scrollbar_for(&list_state, window, cx) .into_any() - } else if v2_empty_state { - this.into_any() } else { - this.child(self.render_recent_history(cx)).into_any() + this.into_any() } }); diff --git a/crates/agent_ui/src/thread_history_view.rs b/crates/agent_ui/src/thread_history_view.rs index 2def2c69d1bebdc150f15c93baa5f7f41bc188e2..a4a00455be471c2a76fd8b2598402dc6e925ad86 100644 --- a/crates/agent_ui/src/thread_history_view.rs +++ b/crates/agent_ui/src/thread_history_view.rs @@ -1,21 +1,19 @@ use crate::thread_history::ThreadHistory; -use crate::{ - AgentPanel, ConversationView, DEFAULT_THREAD_TITLE, RemoveHistory, RemoveSelectedThread, -}; +use crate::{DEFAULT_THREAD_TITLE, RemoveHistory, RemoveSelectedThread}; use acp_thread::AgentSessionInfo; use chrono::{Datelike as _, Local, NaiveDate, TimeDelta, Utc}; use editor::{Editor, EditorEvent}; use fuzzy::StringMatchCandidate; use gpui::{ AnyElement, App, Entity, EventEmitter, FocusHandle, Focusable, ScrollStrategy, Task, - UniformListScrollHandle, WeakEntity, Window, uniform_list, + UniformListScrollHandle, Window, uniform_list, }; use std::{fmt::Display, ops::Range}; use text::Bias; use time::{OffsetDateTime, UtcOffset}; use ui::{ - ElementId, HighlightedLabel, IconButtonShape, ListItem, ListItemSpacing, Tab, Tooltip, - WithScrollbar, prelude::*, + HighlightedLabel, IconButtonShape, ListItem, ListItemSpacing, Tab, Tooltip, WithScrollbar, + prelude::*, }; pub(crate) fn thread_title(entry: &AgentSessionInfo) -> SharedString { @@ -637,139 +635,6 @@ impl Render for ThreadHistoryView { } } -#[derive(IntoElement)] -pub struct HistoryEntryElement { - entry: AgentSessionInfo, - conversation_view: WeakEntity, - selected: bool, - hovered: bool, - supports_delete: bool, - on_hover: Box, -} - -impl HistoryEntryElement { - pub fn new(entry: AgentSessionInfo, conversation_view: WeakEntity) -> Self { - Self { - entry, - conversation_view, - selected: false, - hovered: false, - supports_delete: false, - on_hover: Box::new(|_, _, _| {}), - } - } - - pub fn supports_delete(mut self, supports_delete: bool) -> Self { - self.supports_delete = supports_delete; - self - } - - pub fn hovered(mut self, hovered: bool) -> Self { - self.hovered = hovered; - self - } - - pub fn on_hover(mut self, on_hover: impl Fn(&bool, &mut Window, &mut App) + 'static) -> Self { - self.on_hover = Box::new(on_hover); - self - } -} - -impl RenderOnce for HistoryEntryElement { - fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement { - let id = ElementId::Name(self.entry.session_id.0.clone().into()); - let title = thread_title(&self.entry); - let formatted_time = self - .entry - .updated_at - .map(|timestamp| { - let now = chrono::Utc::now(); - let duration = now.signed_duration_since(timestamp); - - if duration.num_days() > 0 { - format!("{}d", duration.num_days()) - } else if duration.num_hours() > 0 { - format!("{}h ago", duration.num_hours()) - } else if duration.num_minutes() > 0 { - format!("{}m ago", duration.num_minutes()) - } else { - "Just now".to_string() - } - }) - .unwrap_or_else(|| "Unknown".to_string()); - - ListItem::new(id) - .rounded() - .toggle_state(self.selected) - .spacing(ListItemSpacing::Sparse) - .start_slot( - h_flex() - .w_full() - .gap_2() - .justify_between() - .child(Label::new(title).size(LabelSize::Small).truncate()) - .child( - Label::new(formatted_time) - .color(Color::Muted) - .size(LabelSize::XSmall), - ), - ) - .on_hover(self.on_hover) - .end_slot::(if (self.hovered || self.selected) && self.supports_delete { - Some( - IconButton::new("delete", IconName::Trash) - .shape(IconButtonShape::Square) - .icon_size(IconSize::XSmall) - .icon_color(Color::Muted) - .tooltip(move |_window, cx| { - Tooltip::for_action("Delete", &RemoveSelectedThread, cx) - }) - .on_click({ - let conversation_view = self.conversation_view.clone(); - let session_id = self.entry.session_id.clone(); - - move |_event, _window, cx| { - if let Some(conversation_view) = conversation_view.upgrade() { - conversation_view.update(cx, |conversation_view, cx| { - conversation_view.delete_history_entry(&session_id, cx); - }); - } - } - }), - ) - } else { - None - }) - .on_click({ - let conversation_view = self.conversation_view.clone(); - let entry = self.entry; - - move |_event, window, cx| { - if let Some(workspace) = conversation_view - .upgrade() - .and_then(|view| view.read(cx).workspace().upgrade()) - { - if let Some(panel) = workspace.read(cx).panel::(cx) { - panel.update(cx, |panel, cx| { - if let Some(agent) = panel.selected_agent() { - panel.load_agent_thread( - agent, - entry.session_id.clone(), - entry.work_dirs.clone(), - entry.title.clone(), - true, - window, - cx, - ); - } - }); - } - } - } - }) - } -} - #[derive(Clone, Copy)] pub enum EntryTimeFormat { DateAndTime,