diff --git a/assets/icons/clock.svg b/assets/icons/clock.svg
new file mode 100644
index 0000000000000000000000000000000000000000..fb8c6f851fff966732d287e296d29cf6383942b3
--- /dev/null
+++ b/assets/icons/clock.svg
@@ -0,0 +1,4 @@
+
diff --git a/assets/icons/history.svg b/assets/icons/history.svg
deleted file mode 100644
index f9b803f2bd64be8b287838e398b99032bc643f57..0000000000000000000000000000000000000000
--- a/assets/icons/history.svg
+++ /dev/null
@@ -1,4 +0,0 @@
-
diff --git a/assets/icons/thread_import.svg b/assets/icons/thread_import.svg
deleted file mode 100644
index a56b5a7cccc09c5795bfadff06f06d15833232f3..0000000000000000000000000000000000000000
--- a/assets/icons/thread_import.svg
+++ /dev/null
@@ -1,5 +0,0 @@
-
diff --git a/assets/keymaps/default-linux.json b/assets/keymaps/default-linux.json
index 15a4a6f64d5e1a12686d43ddc01dee14df80a7f4..97a84303b4f5d6bd0ef7b4b807902fc6da437dfb 100644
--- a/assets/keymaps/default-linux.json
+++ b/assets/keymaps/default-linux.json
@@ -384,6 +384,12 @@
"backspace": "agent::RemoveSelectedThread",
},
},
+ {
+ "context": "ThreadsArchiveView",
+ "bindings": {
+ "shift-backspace": "agent::ArchiveSelectedThread",
+ },
+ },
{
"context": "RulesLibrary",
"bindings": {
@@ -720,7 +726,7 @@
"right": "menu::SelectChild",
"enter": "menu::Confirm",
"ctrl-f": "agents_sidebar::FocusSidebarFilter",
- "ctrl-g": "agents_sidebar::ViewAllThreads",
+ "ctrl-g": "agents_sidebar::ToggleThreadHistory",
"shift-backspace": "agent::RemoveSelectedThread",
"ctrl-tab": "agents_sidebar::ToggleThreadSwitcher",
"ctrl-shift-tab": ["agents_sidebar::ToggleThreadSwitcher", { "select_last": true }],
diff --git a/assets/keymaps/default-macos.json b/assets/keymaps/default-macos.json
index 302225c87e369203b7593977695d6f606a76019f..38207365ef36df62afc228bbb7905ee37939f207 100644
--- a/assets/keymaps/default-macos.json
+++ b/assets/keymaps/default-macos.json
@@ -431,6 +431,12 @@
"shift-backspace": "agent::RemoveSelectedThread",
},
},
+ {
+ "context": "ThreadsArchiveView",
+ "bindings": {
+ "backspace": "agent::ArchiveSelectedThread",
+ },
+ },
{
"context": "RulesLibrary",
"use_key_equivalents": true,
@@ -776,7 +782,7 @@
"right": "menu::SelectChild",
"enter": "menu::Confirm",
"cmd-f": "agents_sidebar::FocusSidebarFilter",
- "cmd-g": "agents_sidebar::ViewAllThreads",
+ "cmd-g": "agents_sidebar::ToggleThreadHistory",
"shift-backspace": "agent::RemoveSelectedThread",
"ctrl-tab": "agents_sidebar::ToggleThreadSwitcher",
"ctrl-shift-tab": ["agents_sidebar::ToggleThreadSwitcher", { "select_last": true }],
diff --git a/assets/keymaps/default-windows.json b/assets/keymaps/default-windows.json
index 2098f18ff0f2e264b6891f412a29540972629a92..03225bb253fbe745c4c55862e7518a8b603c49cd 100644
--- a/assets/keymaps/default-windows.json
+++ b/assets/keymaps/default-windows.json
@@ -386,6 +386,13 @@
"backspace": "agent::RemoveSelectedThread",
},
},
+ {
+ "context": "ThreadsArchiveView",
+ "use_key_equivalents": true,
+ "bindings": {
+ "shift-backspace": "agent::ArchiveSelectedThread",
+ },
+ },
{
"context": "RulesLibrary",
"use_key_equivalents": true,
@@ -720,7 +727,7 @@
"right": "menu::SelectChild",
"enter": "menu::Confirm",
"ctrl-f": "agents_sidebar::FocusSidebarFilter",
- "ctrl-g": "agents_sidebar::ViewAllThreads",
+ "ctrl-g": "agents_sidebar::ToggleThreadHistory",
"shift-backspace": "agent::RemoveSelectedThread",
"ctrl-tab": "agents_sidebar::ToggleThreadSwitcher",
"ctrl-shift-tab": ["agents_sidebar::ToggleThreadSwitcher", { "select_last": true }],
diff --git a/crates/agent_ui/src/agent_ui.rs b/crates/agent_ui/src/agent_ui.rs
index b5b6a7dea69962c2604252913216dcfe72bb2e84..9fa038b6e257393b9377a51b6df8cc17a177641f 100644
--- a/crates/agent_ui/src/agent_ui.rs
+++ b/crates/agent_ui/src/agent_ui.rs
@@ -112,6 +112,8 @@ actions!(
OpenHistory,
/// Adds a context server to the configuration.
AddContextServer,
+ /// Archives the currently selected thread.
+ ArchiveSelectedThread,
/// Removes the currently selected thread.
RemoveSelectedThread,
/// Starts a chat conversation with follow-up enabled.
diff --git a/crates/agent_ui/src/thread_import.rs b/crates/agent_ui/src/thread_import.rs
index cb1234484410a5672c3bf9137ae2b790e181ff5f..944813525b40ed0013972595f992bf96d1876fab 100644
--- a/crates/agent_ui/src/thread_import.rs
+++ b/crates/agent_ui/src/thread_import.rs
@@ -391,7 +391,7 @@ impl Render for ThreadImportModal {
.headline("Import External Agent Threads")
.description(
"Import threads from agents like Claude Agent, Codex, and more, whether started in Zed or another client. \
- Choose which agents to include, and their threads will appear in your list."
+ Choose which agents to include, and their threads will appear in your thread history."
)
.show_dismiss_button(true),
diff --git a/crates/agent_ui/src/threads_archive_view.rs b/crates/agent_ui/src/threads_archive_view.rs
index 351e83bdff7336b2817bdc43da7e5f601539de7b..6547187547c839bb1b804f933296089ffd435c28 100644
--- a/crates/agent_ui/src/threads_archive_view.rs
+++ b/crates/agent_ui/src/threads_archive_view.rs
@@ -7,7 +7,7 @@ use crate::agent_connection_store::AgentConnectionStore;
use crate::thread_metadata_store::{
ThreadId, ThreadMetadata, ThreadMetadataStore, worktree_info_from_thread_paths,
};
-use crate::{Agent, DEFAULT_THREAD_TITLE, RemoveSelectedThread};
+use crate::{Agent, ArchiveSelectedThread, DEFAULT_THREAD_TITLE, RemoveSelectedThread};
use agent::ThreadStore;
use agent_client_protocol as acp;
@@ -45,6 +45,13 @@ use workspace::{
use zed_actions::agents_sidebar::FocusSidebarFilter;
use zed_actions::editor::{MoveDown, MoveUp};
+#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
+enum ThreadFilter {
+ #[default]
+ All,
+ ArchivedOnly,
+}
+
#[derive(Clone)]
enum ArchiveListItem {
BucketSeparator(TimeBucket),
@@ -117,6 +124,7 @@ pub enum ThreadsArchiveViewEvent {
Close,
Activate { thread: ThreadMetadata },
CancelRestore { thread_id: ThreadId },
+ Import,
}
impl EventEmitter for ThreadsArchiveView {}
@@ -139,7 +147,7 @@ pub struct ThreadsArchiveView {
archived_thread_ids: HashSet,
archived_branch_names: HashMap>,
_load_branch_names_task: Task<()>,
- show_archived_only: bool,
+ thread_filter: ThreadFilter,
}
impl ThreadsArchiveView {
@@ -213,7 +221,7 @@ impl ThreadsArchiveView {
archived_thread_ids: HashSet::default(),
archived_branch_names: HashMap::default(),
_load_branch_names_task: Task::ready(()),
- show_archived_only: false,
+ thread_filter: ThreadFilter::All,
};
this.update_items(cx);
@@ -252,11 +260,14 @@ impl ThreadsArchiveView {
}
fn update_items(&mut self, cx: &mut Context) {
- let show_archived_only = self.show_archived_only;
+ let thread_filter = self.thread_filter;
let sessions = ThreadMetadataStore::global(cx)
.read(cx)
.entries()
- .filter(|t| !show_archived_only || t.archived)
+ .filter(|t| match thread_filter {
+ ThreadFilter::All => true,
+ ThreadFilter::ArchivedOnly => t.archived,
+ })
.sorted_by_cached_key(|t| t.created_at.unwrap_or(t.updated_at))
.rev()
.cloned()
@@ -388,6 +399,24 @@ impl ThreadsArchiveView {
ThreadMetadataStore::global(cx).update(cx, |store, cx| store.archive(thread_id, None, cx));
}
+ fn archive_selected_thread(
+ &mut self,
+ _: &ArchiveSelectedThread,
+ _window: &mut Window,
+ cx: &mut Context,
+ ) {
+ let Some(ix) = self.selection else { return };
+ let Some(ArchiveListItem::Entry { thread, .. }) = self.items.get(ix) else {
+ return;
+ };
+
+ if thread.archived {
+ return;
+ }
+
+ self.archive_thread(thread.thread_id, cx);
+ }
+
fn unarchive_thread(
&mut self,
thread: ThreadMetadata,
@@ -605,13 +634,14 @@ impl ThreadsArchiveView {
&branch_names_for_thread,
);
- let archived_color = Color::Custom(cx.theme().colors().text_muted.opacity(0.85));
+ let archived_color = Color::Custom(cx.theme().colors().icon_muted.opacity(0.6));
let base = ThreadItem::new(id, thread.display_title())
.icon(icon)
.when(is_archived, |this| {
- this.icon_color(archived_color)
- .title_label_color(archived_color)
+ this.archived(true)
+ .icon_color(archived_color)
+ .title_label_color(Color::Muted)
})
.when_some(icon_from_external_svg, |this, svg| {
this.custom_icon_from_external_svg(svg)
@@ -693,7 +723,16 @@ impl ThreadsArchiveView {
IconButton::new("archive-thread", IconName::Archive)
.icon_size(IconSize::Small)
.icon_color(Color::Muted)
- .tooltip(Tooltip::text("Archive Thread"))
+ .tooltip({
+ move |_window, cx| {
+ Tooltip::for_action_in(
+ "Archive Thread",
+ &ArchiveSelectedThread,
+ &focus_handle,
+ cx,
+ )
+ }
+ })
.on_click({
let thread_id = thread.thread_id;
cx.listener(move |this, _, _, cx| {
@@ -852,14 +891,13 @@ impl ThreadsArchiveView {
.filter(|item| matches!(item, ArchiveListItem::Entry { .. }))
.count();
+ let has_archived_threads = {
+ let store = ThreadMetadataStore::global(cx).read(cx);
+ store.archived_entries().next().is_some()
+ };
+
let count_label = if entry_count == 1 {
- if self.show_archived_only {
- "1 archived thread".to_string()
- } else {
- "1 thread".to_string()
- }
- } else if self.show_archived_only {
- format!("{} archived threads", entry_count)
+ "1 thread".to_string()
} else {
format!("{} threads", entry_count)
};
@@ -878,18 +916,37 @@ impl ThreadsArchiveView {
.color(Color::Muted),
)
.child(
- IconButton::new("toggle-archived-only", IconName::Archive)
- .icon_size(IconSize::Small)
- .toggle_state(self.show_archived_only)
- .tooltip(Tooltip::text(if self.show_archived_only {
- "Show All Threads"
- } else {
- "Show Archived Only"
- }))
- .on_click(cx.listener(|this, _, _, cx| {
- this.show_archived_only = !this.show_archived_only;
- this.update_items(cx);
- })),
+ h_flex()
+ .child(
+ IconButton::new("thread-import", IconName::Download)
+ .icon_size(IconSize::Small)
+ .tooltip(Tooltip::text("Import Threads"))
+ .on_click(cx.listener(|_this, _, _, cx| {
+ cx.emit(ThreadsArchiveViewEvent::Import);
+ })),
+ )
+ .child(
+ IconButton::new("filter-archived-only", IconName::Archive)
+ .icon_size(IconSize::Small)
+ .disabled(!has_archived_threads)
+ .toggle_state(self.thread_filter == ThreadFilter::ArchivedOnly)
+ .tooltip(Tooltip::text(
+ if self.thread_filter == ThreadFilter::ArchivedOnly {
+ "Show All Threads"
+ } else {
+ "Show Only Archived Threads"
+ },
+ ))
+ .on_click(cx.listener(|this, _, _, cx| {
+ this.thread_filter =
+ if this.thread_filter == ThreadFilter::ArchivedOnly {
+ ThreadFilter::All
+ } else {
+ ThreadFilter::ArchivedOnly
+ };
+ this.update_items(cx);
+ })),
+ ),
)
}
}
@@ -972,6 +1029,7 @@ impl Render for ThreadsArchiveView {
.on_action(cx.listener(Self::select_last))
.on_action(cx.listener(Self::confirm))
.on_action(cx.listener(Self::remove_selected_thread))
+ .on_action(cx.listener(Self::archive_selected_thread))
.size_full()
.child(self.render_header(window, cx))
.when(!has_query, |this| this.child(self.render_toolbar(cx)))
diff --git a/crates/icons/src/icons.rs b/crates/icons/src/icons.rs
index 9fc8d4220bf1d28750928309b20bc167445312eb..20d7b609d8de07c4de4c489eac90b312fbf9c210 100644
--- a/crates/icons/src/icons.rs
+++ b/crates/icons/src/icons.rs
@@ -66,6 +66,7 @@ pub enum IconName {
ChevronUpDown,
Circle,
CircleHelp,
+ Clock,
Close,
CloudDownload,
Code,
@@ -153,7 +154,6 @@ pub enum IconName {
GitWorktree,
Github,
Hash,
- History,
HistoryRerun,
Image,
Inception,
@@ -243,7 +243,6 @@ 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 c8bfa93368ac2607393d2bad1283058866371efd..6fb6358e77704119e2c5b63921b9400e1d93c631 100644
--- a/crates/sidebar/src/sidebar.rs
+++ b/crates/sidebar/src/sidebar.rs
@@ -68,8 +68,8 @@ gpui::actions!(
[
/// Creates a new thread in the currently selected or active project group.
NewThreadInGroup,
- /// Toggles between the thread list and the archive view.
- ViewAllThreads,
+ /// Toggles between the thread list and the thread history.
+ ToggleThreadHistory,
]
);
@@ -89,7 +89,8 @@ const MAX_WIDTH: Pixels = px(800.0);
enum SerializedSidebarView {
#[default]
ThreadList,
- Archive,
+ #[serde(alias = "Archive")]
+ History,
}
#[derive(Default, Serialize, Deserialize)]
@@ -4241,45 +4242,33 @@ impl Sidebar {
fn render_sidebar_bottom_bar(&mut self, cx: &mut Context) -> impl IntoElement {
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()
+ h_flex()
+ .p_1()
.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 External Agent Threads"))
- .on_click(cx.listener(|this, _, window, cx| {
- this.show_archive(window, cx);
- this.show_thread_import_modal(window, cx);
- })),
- )
- })
+ .border_t_1()
+ .border_color(cx.theme().colors().border)
+ .child(self.render_sidebar_toggle_button(cx))
.child(
- IconButton::new("history", IconName::History)
+ IconButton::new("history", IconName::Clock)
.icon_size(IconSize::Small)
.toggle_state(is_archive)
.tooltip(move |_, cx| {
- Tooltip::for_action("View All Threads", &ViewAllThreads, cx)
+ let label = if is_archive {
+ "Hide Thread History"
+ } else {
+ "Show Thread History"
+ };
+ Tooltip::for_action(label, &ToggleThreadHistory, cx)
})
.on_click(cx.listener(|this, _, window, cx| {
- this.toggle_archive(&ViewAllThreads, window, cx);
+ this.toggle_archive(&ToggleThreadHistory, window, cx);
})),
)
- .child(self.render_recent_projects_button(cx));
-
- h_flex()
- .p_1()
- .gap_1()
- .when(on_right, |this| this.flex_row_reverse())
- .justify_between()
- .border_t_1()
- .border_color(cx.theme().colors().border)
- .child(self.render_sidebar_toggle_button(cx))
- .child(action_buttons)
+ .child(div().flex_1())
+ .child(self.render_recent_projects_button(cx))
}
fn active_workspace(&self, cx: &App) -> Option> {
@@ -4405,14 +4394,19 @@ impl Sidebar {
)
}
- fn toggle_archive(&mut self, _: &ViewAllThreads, window: &mut Window, cx: &mut Context) {
+ fn toggle_archive(
+ &mut self,
+ _: &ToggleThreadHistory,
+ window: &mut Window,
+ cx: &mut Context,
+ ) {
match &self.view {
SidebarView::ThreadList => {
let side = match self.side(cx) {
SidebarSide::Left => "left",
SidebarSide::Right => "right",
};
- telemetry::event!("Sidebar Archive Viewed", side = side);
+ telemetry::event!("Thread History Viewed", side = side);
self.show_archive(window, cx);
}
SidebarView::Archive(_) => self.show_thread_list(window, cx),
@@ -4463,6 +4457,9 @@ impl Sidebar {
ThreadsArchiveViewEvent::CancelRestore { thread_id } => {
this.restoring_tasks.remove(thread_id);
}
+ ThreadsArchiveViewEvent::Import => {
+ this.show_thread_import_modal(window, cx);
+ }
},
);
@@ -4535,7 +4532,7 @@ fn render_import_onboarding_banner(
.style(ButtonStyle::OutlinedCustom(cx.theme().colors().border))
.label_size(LabelSize::Small)
.start_icon(
- Icon::new(IconName::ThreadImport)
+ Icon::new(IconName::Download)
.size(IconSize::Small)
.color(Color::Muted),
)
@@ -4592,7 +4589,7 @@ impl WorkspaceSidebar for Sidebar {
width: Some(f32::from(self.width)),
active_view: match self.view {
SidebarView::ThreadList => SerializedSidebarView::ThreadList,
- SidebarView::Archive(_) => SerializedSidebarView::Archive,
+ SidebarView::Archive(_) => SerializedSidebarView::History,
},
};
serde_json::to_string(&serialized).ok()
@@ -4608,7 +4605,7 @@ impl WorkspaceSidebar for Sidebar {
if let Some(width) = serialized.width {
self.width = px(width).clamp(MIN_WIDTH, MAX_WIDTH);
}
- if serialized.active_view == SerializedSidebarView::Archive {
+ if serialized.active_view == SerializedSidebarView::History {
cx.defer_in(window, |this, window, cx| {
this.show_archive(window, cx);
});
diff --git a/crates/sidebar/src/sidebar_tests.rs b/crates/sidebar/src/sidebar_tests.rs
index d687ae5066f74997967559ec5126bdfbf90ddb3d..66820d513a5ceaa5919038d88877c66136397bc4 100644
--- a/crates/sidebar/src/sidebar_tests.rs
+++ b/crates/sidebar/src/sidebar_tests.rs
@@ -481,7 +481,7 @@ async fn test_restore_serialized_archive_view_does_not_panic(cx: &mut TestAppCon
let serialized = serde_json::to_string(&SerializedSidebar {
width: Some(400.0),
- active_view: SerializedSidebarView::Archive,
+ active_view: SerializedSidebarView::History,
})
.expect("serialization should succeed");
diff --git a/crates/ui/src/components/ai/thread_item.rs b/crates/ui/src/components/ai/thread_item.rs
index ccac7a7723b266d4c0707ada506a68f3259a838d..a106d3fa3998b43b6da008f8d1cf2948c811dff2 100644
--- a/crates/ui/src/components/ai/thread_item.rs
+++ b/crates/ui/src/components/ai/thread_item.rs
@@ -55,6 +55,7 @@ pub struct ThreadItem {
project_name: Option,
worktrees: Vec,
is_remote: bool,
+ archived: bool,
on_click: Option>,
on_hover: Box,
action_slot: Option,
@@ -86,6 +87,7 @@ impl ThreadItem {
project_name: None,
worktrees: Vec::new(),
is_remote: false,
+ archived: false,
on_click: None,
on_hover: Box::new(|_, _, _| {}),
action_slot: None,
@@ -183,6 +185,11 @@ impl ThreadItem {
self
}
+ pub fn archived(mut self, archived: bool) -> Self {
+ self.archived = archived;
+ self
+ }
+
pub fn hovered(mut self, hovered: bool) -> Self {
self.hovered = hovered;
self
@@ -431,6 +438,14 @@ impl RenderOnce for ThreadItem {
h_flex()
.gap_1p5()
.child(icon_container()) // Icon Spacing
+ .when(self.archived, |this| {
+ this.child(
+ Icon::new(IconName::Archive).size(IconSize::XSmall).color(
+ Color::Custom(cx.theme().colors().icon_muted.opacity(0.5)),
+ ),
+ )
+ // .child(dot_separator())
+ })
.when(
has_project_name || has_project_paths || has_worktree,
|this| {