From ca7de0fc5815e26c6836b373bb0c2cc7137baa7e Mon Sep 17 00:00:00 2001 From: Danilo Leal <67129314+danilo-leal@users.noreply.github.com> Date: Tue, 31 Mar 2026 19:29:07 -0300 Subject: [PATCH] sidebar: Add design adjustments to the thread import feature (#52858) - Adds a new icon for thread import - Iterate on the design to access the import modal: it's now through a button in the sidebar's footer, which only appears when the archive view is toggled - Fixed an issue where clicking on checkboxes within the import modal's list items wouldn't do anything Release Notes: - N/A --- assets/icons/thread_import.svg | 5 ++ crates/agent_ui/src/thread_import.rs | 49 +++++--------- crates/agent_ui/src/threads_archive_view.rs | 75 +-------------------- crates/icons/src/icons.rs | 1 + crates/sidebar/src/sidebar.rs | 43 ++++++------ crates/ui/src/components/list/list_item.rs | 12 ++-- 6 files changed, 52 insertions(+), 133 deletions(-) create mode 100644 assets/icons/thread_import.svg diff --git a/assets/icons/thread_import.svg b/assets/icons/thread_import.svg new file mode 100644 index 0000000000000000000000000000000000000000..a56b5a7cccc09c5795bfadff06f06d15833232f3 --- /dev/null +++ b/assets/icons/thread_import.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/crates/agent_ui/src/thread_import.rs b/crates/agent_ui/src/thread_import.rs index 9dd6b5efa0ae1cd3bc19dc6ae6a287218de8c668..f5fc89d3df4991ff5186e2af6d73ad6a840c09a1 100644 --- a/crates/agent_ui/src/thread_import.rs +++ b/crates/agent_ui/src/thread_import.rs @@ -121,18 +121,6 @@ impl ThreadImportModal { .collect() } - fn set_agent_checked(&mut self, agent_id: AgentId, state: ToggleState, cx: &mut Context) { - match state { - ToggleState::Selected => { - self.unchecked_agents.remove(&agent_id); - } - ToggleState::Unselected | ToggleState::Indeterminate => { - self.unchecked_agents.insert(agent_id); - } - } - cx.notify(); - } - fn toggle_agent_checked(&mut self, agent_id: AgentId, cx: &mut Context) { if self.unchecked_agents.contains(&agent_id) { self.unchecked_agents.remove(&agent_id); @@ -283,6 +271,11 @@ impl ModalView for ThreadImportModal {} impl Render for ThreadImportModal { fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { + let has_agents = !self.agent_entries.is_empty(); + let disabled_import_thread = self.is_importing + || !has_agents + || self.unchecked_agents.len() == self.agent_entries.len(); + let agent_rows = self .agent_entries .iter() @@ -295,6 +288,7 @@ impl Render for ThreadImportModal { .rounded() .spacing(ListItemSpacing::Sparse) .focused(is_focused) + .disabled(self.is_importing) .child( h_flex() .w_full() @@ -311,22 +305,14 @@ impl Render for ThreadImportModal { }) .child(Label::new(entry.display_name.clone())), ) - .end_slot( - Checkbox::new( - ("thread-import-agent-checkbox", ix), - if is_checked { - ToggleState::Selected - } else { - ToggleState::Unselected - }, - ) - .on_click({ - let agent_id = entry.agent_id.clone(); - cx.listener(move |this, state: &ToggleState, _window, cx| { - this.set_agent_checked(agent_id.clone(), *state, cx); - }) - }), - ) + .end_slot(Checkbox::new( + ("thread-import-agent-checkbox", ix), + if is_checked { + ToggleState::Selected + } else { + ToggleState::Unselected + }, + )) .on_click({ let agent_id = entry.agent_id.clone(); cx.listener(move |this, _event, _window, cx| { @@ -336,11 +322,6 @@ impl Render for ThreadImportModal { }) .collect::>(); - let has_agents = !self.agent_entries.is_empty(); - let disabled_import_thread = self.is_importing - || !has_agents - || self.unchecked_agents.len() == self.agent_entries.len(); - v_flex() .id("thread-import-modal") .key_context("ThreadImportModal") @@ -373,7 +354,7 @@ impl Render for ThreadImportModal { v_flex() .id("thread-import-agent-list") .max_h(rems_from_px(320.)) - .pb_2() + .pb_1() .overflow_y_scroll() .when(has_agents, |this| this.children(agent_rows)) .when(!has_agents, |this| { diff --git a/crates/agent_ui/src/threads_archive_view.rs b/crates/agent_ui/src/threads_archive_view.rs index 445d86c9ad4e37fa8b2502a754a5264cd1d4dc45..74a93129d387e0aaac6e7092d9e086dd64e369f7 100644 --- a/crates/agent_ui/src/threads_archive_view.rs +++ b/crates/agent_ui/src/threads_archive_view.rs @@ -1,5 +1,5 @@ use crate::agent_connection_store::AgentConnectionStore; -use crate::thread_import::{AcpThreadImportOnboarding, ThreadImportModal}; + use crate::thread_metadata_store::{ThreadMetadata, ThreadMetadataStore}; use crate::{Agent, RemoveSelectedThread}; @@ -15,15 +15,13 @@ use gpui::{ }; use itertools::Itertools as _; use menu::{Confirm, SelectFirst, SelectLast, SelectNext, SelectPrevious}; -use project::{AgentId, AgentRegistryStore, AgentServerStore}; +use project::{AgentId, AgentServerStore}; use settings::Settings as _; use theme::ActiveTheme; use ui::ThreadItem; use ui::{ Divider, KeyBinding, Tooltip, WithScrollbar, prelude::*, utils::platform_title_bar_height, }; -use util::ResultExt; -use workspace::{MultiWorkspace, Workspace}; use zed_actions::agents_sidebar::FocusSidebarFilter; use zed_actions::editor::{MoveDown, MoveUp}; @@ -114,18 +112,12 @@ pub struct ThreadsArchiveView { _refresh_history_task: Task<()>, agent_connection_store: WeakEntity, agent_server_store: WeakEntity, - agent_registry_store: WeakEntity, - workspace: WeakEntity, - multi_workspace: WeakEntity, } impl ThreadsArchiveView { pub fn new( agent_connection_store: WeakEntity, agent_server_store: WeakEntity, - agent_registry_store: WeakEntity, - workspace: WeakEntity, - multi_workspace: WeakEntity, window: &mut Window, cx: &mut Context, ) -> Self { @@ -184,11 +176,8 @@ impl ThreadsArchiveView { thread_metadata_store_subscription, ], _refresh_history_task: Task::ready(()), - agent_registry_store, agent_connection_store, agent_server_store, - workspace, - multi_workspace, }; this.update_items(cx); @@ -550,43 +539,6 @@ impl ThreadsArchiveView { .detach_and_log_err(cx); } - fn should_render_acp_import_onboarding(&self, cx: &App) -> bool { - let has_external_agents = self - .agent_server_store - .upgrade() - .map(|store| store.read(cx).has_external_agents()) - .unwrap_or(false); - - has_external_agents && !AcpThreadImportOnboarding::dismissed(cx) - } - - fn show_thread_import_modal(&mut self, window: &mut Window, cx: &mut Context) { - let Some(agent_server_store) = self.agent_server_store.upgrade() else { - return; - }; - let Some(agent_registry_store) = self.agent_registry_store.upgrade() else { - return; - }; - - let workspace_handle = self.workspace.clone(); - let multi_workspace = self.multi_workspace.clone(); - - self.workspace - .update(cx, |workspace, cx| { - workspace.toggle_modal(window, cx, |window, cx| { - ThreadImportModal::new( - agent_server_store, - agent_registry_store, - workspace_handle.clone(), - multi_workspace.clone(), - window, - cx, - ) - }); - }) - .log_err(); - } - fn render_header(&self, window: &Window, cx: &mut Context) -> impl IntoElement { let has_query = !self.filter_editor.read(cx).text(cx).is_empty(); let sidebar_on_left = matches!( @@ -729,28 +681,5 @@ impl Render for ThreadsArchiveView { .size_full() .child(self.render_header(window, cx)) .child(content) - .when(!self.should_render_acp_import_onboarding(cx), |this| { - this.child( - div() - .w_full() - .p_1p5() - .border_t_1() - .border_color(cx.theme().colors().border) - .child( - Button::new("import-acp", "Import ACP Threads") - .full_width() - .style(ButtonStyle::OutlinedCustom(cx.theme().colors().border)) - .label_size(LabelSize::Small) - .start_icon( - Icon::new(IconName::ArrowDown) - .size(IconSize::XSmall) - .color(Color::Muted), - ) - .on_click(cx.listener(|this, _, window, cx| { - this.show_thread_import_modal(window, cx); - })), - ), - ) - }) } } diff --git a/crates/icons/src/icons.rs b/crates/icons/src/icons.rs index 400b2a22bc6071b62c6ce22a2b1bf1053c8cf871..ad7faa6664d1ddc618c8984781a244af7dda6c97 100644 --- a/crates/icons/src/icons.rs +++ b/crates/icons/src/icons.rs @@ -240,6 +240,7 @@ pub enum IconName { ThinkingModeOff, Thread, ThreadFromSummary, + ThreadImport, ThreadsSidebarLeftClosed, ThreadsSidebarLeftOpen, ThreadsSidebarRightClosed, diff --git a/crates/sidebar/src/sidebar.rs b/crates/sidebar/src/sidebar.rs index 450a2674e0062d917003758c41c445048ee603f7..b1257b4c79c2ef193ec4594139cd1f57b93a5666 100644 --- a/crates/sidebar/src/sidebar.rs +++ b/crates/sidebar/src/sidebar.rs @@ -3309,10 +3309,24 @@ impl Sidebar { } fn render_sidebar_bottom_bar(&mut self, cx: &mut Context) -> impl IntoElement { - let on_right = self.side(cx) == SidebarSide::Right; let is_archive = matches!(self.view, SidebarView::Archive(..)); + let show_import_button = is_archive && !self.should_render_acp_import_onboarding(cx); + let on_right = self.side(cx) == SidebarSide::Right; + let action_buttons = h_flex() .gap_1() + .when(on_right, |this| this.flex_row_reverse()) + .when(show_import_button, |this| { + this.child( + IconButton::new("thread-import", IconName::ThreadImport) + .icon_size(IconSize::Small) + .tooltip(Tooltip::text("Import ACP Threads")) + .on_click(cx.listener(|this, _, window, cx| { + this.show_archive(window, cx); + this.show_thread_import_modal(window, cx); + })), + ) + }) .child( IconButton::new("archive", IconName::Archive) .icon_size(IconSize::Small) @@ -3325,21 +3339,16 @@ impl Sidebar { })), ) .child(self.render_recent_projects_button(cx)); - let border_color = cx.theme().colors().border; - let toggle_button = self.render_sidebar_toggle_button(cx); - let bar = h_flex() + h_flex() .p_1() .gap_1() + .when(on_right, |this| this.flex_row_reverse()) .justify_between() .border_t_1() - .border_color(border_color); - - if on_right { - bar.child(action_buttons).child(toggle_button) - } else { - bar.child(toggle_button).child(action_buttons) - } + .border_color(cx.theme().colors().border) + .child(self.render_sidebar_toggle_button(cx)) + .child(action_buttons) } fn active_workspace(&self, cx: &App) -> Option> { @@ -3409,7 +3418,7 @@ impl Sidebar { v_flex() .min_w_0() .w_full() - .p_1p5() + .p_2() .border_t_1() .border_color(cx.theme().colors().border) .bg(linear_gradient( @@ -3437,8 +3446,8 @@ impl Sidebar { .style(ButtonStyle::OutlinedCustom(cx.theme().colors().border)) .label_size(LabelSize::Small) .start_icon( - Icon::new(IconName::ArrowDown) - .size(IconSize::XSmall) + Icon::new(IconName::ThreadImport) + .size(IconSize::Small) .color(Color::Muted), ) .on_click(cx.listener(|this, _, window, cx| { @@ -3467,9 +3476,6 @@ impl Sidebar { let Some(agent_panel) = active_workspace.read(cx).panel::(cx) else { return; }; - let Some(agent_registry_store) = AgentRegistryStore::try_global(cx) else { - return; - }; let agent_server_store = active_workspace .read(cx) @@ -3484,9 +3490,6 @@ impl Sidebar { ThreadsArchiveView::new( agent_connection_store.clone(), agent_server_store.clone(), - agent_registry_store.downgrade(), - active_workspace.downgrade(), - self.multi_workspace.clone(), window, cx, ) diff --git a/crates/ui/src/components/list/list_item.rs b/crates/ui/src/components/list/list_item.rs index 693cf3d52e34369d04db445d1ddac765691fb429..0d3efc024f1a202947fd0e7b0dab917c40ae8337 100644 --- a/crates/ui/src/components/list/list_item.rs +++ b/crates/ui/src/components/list/list_item.rs @@ -234,9 +234,9 @@ impl RenderOnce for ListItem { this.ml(self.indent_level as f32 * self.indent_step_size) .px(DynamicSpacing::Base04.rems(cx)) }) - .when(!self.inset && !self.disabled, |this| { + .when(!self.inset, |this| { this.when_some(self.focused, |this, focused| { - if focused { + if focused && !self.disabled { this.border_1() .when(self.docked_right, |this| this.border_r_2()) .border_color(cx.theme().colors().border_focused) @@ -244,7 +244,7 @@ impl RenderOnce for ListItem { this.border_1() } }) - .when(self.selectable, |this| { + .when(self.selectable && !self.disabled, |this| { this.hover(|style| style.bg(cx.theme().colors().ghost_element_hover)) .active(|style| style.bg(cx.theme().colors().ghost_element_active)) .when(self.outlined, |this| this.rounded_sm()) @@ -268,16 +268,16 @@ impl RenderOnce for ListItem { ListItemSpacing::ExtraDense => this.py_neg_px(), ListItemSpacing::Sparse => this.py_1(), }) - .when(self.inset && !self.disabled, |this| { + .when(self.inset, |this| { this.when_some(self.focused, |this, focused| { - if focused { + if focused && !self.disabled { this.border_1() .border_color(cx.theme().colors().border_focused) } else { this.border_1() } }) - .when(self.selectable, |this| { + .when(self.selectable && !self.disabled, |this| { this.hover(|style| style.bg(cx.theme().colors().ghost_element_hover)) .active(|style| style.bg(cx.theme().colors().ghost_element_active)) .when(self.selected, |this| {