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