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| {