use std::collections::HashSet;use std::path::PathBuf;use std::sync::Arc; use crate::agent_connection_store::AgentConnectionStore; use crate::thread_metadata_store::{ ThreadId, ThreadMetadata, ThreadMetadataStore, worktree_info_from_thread_paths,};use crate::{Agent, ArchiveSelectedThread, DEFAULT_THREAD_TITLE, RemoveSelectedThread}; use agent::ThreadStore;use agent_client_protocol::schema::v1 as acp;use agent_settings::AgentSettings;use chrono::{DateTime, Datelike as _, Local, NaiveDate, TimeDelta, Utc};use collections::HashMap;use editor::Editor;use fs::Fs;use fuzzy::{StringMatch, StringMatchCandidate};use gpui::{ AnyElement, App, Context, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, ListState, Render, SharedString, Subscription, Task, TaskExt, WeakEntity, Window, list, prelude::*, px,};use itertools::Itertools as _;use menu::{Confirm, SelectFirst, SelectLast, SelectNext, SelectPrevious};use picker::{ Picker, PickerDelegate, highlighted_match_with_paths::{HighlightedMatch, HighlightedMatchWithPaths},};use project::{AgentId, AgentServerStore};use settings::Settings as _;use theme::ActiveTheme;use ui::{ AgentThreadStatus, Divider, KeyBinding, ListItem, ListItemSpacing, ListSubHeader, ScrollAxes, Scrollbars, Tab, ThreadItem, Tooltip, WithScrollbar, prelude::*, utils::platform_title_bar_height,};use util::ResultExt;use util::paths::PathExt;use workspace::{ CloseWindow, ModalView, PathList, RecentWorkspace, SerializedWorkspaceLocation, Workspace, WorkspaceDb, WorkspaceId,}; 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), Entry { thread: ThreadMetadata, highlight_positions: Vec<usize>, },} #[derive(Clone, Copy, Debug, PartialEq, Eq)]enum TimeBucket { Today, Yesterday, ThisWeek, PastWeek, Older,} impl TimeBucket { fn from_dates(reference: NaiveDate, date: NaiveDate) -> Self { if date == reference { return TimeBucket::Today; } if date == reference - TimeDelta::days(1) { return TimeBucket::Yesterday; } let week = date.iso_week(); if reference.iso_week() == week { return TimeBucket::ThisWeek; } let last_week = (reference - TimeDelta::days(7)).iso_week(); if week == last_week { return TimeBucket::PastWeek; } TimeBucket::Older } fn label(&self) -> &'static str { match self { TimeBucket::Today => "Today", TimeBucket::Yesterday => "Yesterday", TimeBucket::ThisWeek => "This Week", TimeBucket::PastWeek => "Past Week", TimeBucket::Older => "Older", } }} pub fn fuzzy_match_positions(query: &str, candidate: &str) -> Option<Vec<usize>> { let query_chars: Vec<char> = query.chars().collect(); if query_chars.is_empty() { return Some(Vec::new()); } let candidate_chars: Vec<(usize, char)> = candidate.char_indices().collect(); let window_count = candidate_chars.len().checked_sub(query_chars.len() - 1)?; 'outer: for window_start in 0..window_count { for (qi, &query_char) in query_chars.iter().enumerate() { let (_, cand_char) = candidate_chars[window_start + qi]; if !cand_char.eq_ignore_ascii_case(&query_char) { continue 'outer; } } return Some( (0..query_chars.len()) .map(|qi| candidate_chars[window_start + qi].0) .collect(), ); } None} pub enum ThreadsArchiveViewEvent { Close, Activate { thread: ThreadMetadata }, CancelRestore { thread_id: ThreadId }, Import, NewThread,} impl EventEmitter<ThreadsArchiveViewEvent> for ThreadsArchiveView {} pub struct ThreadsArchiveView { _history_subscription: Subscription, focus_handle: FocusHandle, list_state: ListState, items: Vec<ArchiveListItem>, selection: Option<usize>, hovered_index: Option<usize>, preserve_selection_on_next_update: bool, filter_editor: Entity<Editor>, _subscriptions: Vec<gpui::Subscription>, _refresh_history_task: Task<()>, workspace: WeakEntity<Workspace>, agent_connection_store: WeakEntity<AgentConnectionStore>, agent_server_store: WeakEntity<AgentServerStore>, restoring: HashSet<ThreadId>, archived_thread_ids: HashSet<ThreadId>, archived_branch_names: HashMap<ThreadId, HashMap<PathBuf, String>>, _load_branch_names_task: Task<()>, thread_filter: ThreadFilter,} impl ThreadsArchiveView { pub fn new( workspace: WeakEntity<Workspace>, agent_connection_store: WeakEntity<AgentConnectionStore>, agent_server_store: WeakEntity<AgentServerStore>, window: &mut Window, cx: &mut Context<Self>, ) -> Self { let focus_handle = cx.focus_handle(); let filter_editor = cx.new(|cx| { let mut editor = Editor::single_line(window, cx); editor.set_placeholder_text("Search all threads…", window, cx); editor }); let filter_editor_subscription = cx.subscribe(&filter_editor, |this: &mut Self, _, event, cx| { if let editor::EditorEvent::BufferEdited = event { this.update_items(cx); } }); let filter_focus_handle = filter_editor.read(cx).focus_handle(cx); cx.on_focus_in( &filter_focus_handle, window, |this: &mut Self, _window, cx| { if this.selection.is_some() { this.selection = None; cx.notify(); } }, ) .detach(); let thread_metadata_store_subscription = cx.observe( &ThreadMetadataStore::global(cx), |this: &mut Self, _, cx| { this.update_items(cx); this.reload_branch_names_if_threads_changed(cx); }, ); cx.on_focus_out(&focus_handle, window, |this: &mut Self, _, _window, cx| { this.selection = None; cx.notify(); }) .detach(); let mut this = Self { _history_subscription: Subscription::new(|| {}), focus_handle, list_state: ListState::new(0, gpui::ListAlignment::Top, px(1000.)), items: Vec::new(), selection: None, hovered_index: None, preserve_selection_on_next_update: false, filter_editor, _subscriptions: vec![ filter_editor_subscription, thread_metadata_store_subscription, ], _refresh_history_task: Task::ready(()), workspace, agent_connection_store, agent_server_store, restoring: HashSet::default(), archived_thread_ids: HashSet::default(), archived_branch_names: HashMap::default(), _load_branch_names_task: Task::ready(()), thread_filter: ThreadFilter::All, }; this.update_items(cx); this.reload_branch_names_if_threads_changed(cx); this } pub fn has_selection(&self) -> bool { self.selection.is_some() } pub fn clear_selection(&mut self) { self.selection = None; } pub fn mark_restoring(&mut self, thread_id: &ThreadId, cx: &mut Context<Self>) { self.restoring.insert(*thread_id); cx.notify(); } pub fn clear_restoring(&mut self, thread_id: &ThreadId, cx: &mut Context<Self>) { self.restoring.remove(thread_id); cx.notify(); } pub fn focus_filter_editor(&self, window: &mut Window, cx: &mut App) { let handle = self.filter_editor.read(cx).focus_handle(cx); handle.focus(window, cx); } pub fn is_filter_editor_focused(&self, window: &Window, cx: &App) -> bool { self.filter_editor .read(cx) .focus_handle(cx) .is_focused(window) } fn update_items(&mut self, cx: &mut Context<Self>) { let store = ThreadMetadataStore::global(cx).read(cx); // If we're filtering to archived threads but none remain (e.g. the // user just deleted the last one), fall back to showing all threads // so they aren't stranded with an empty list and a disabled toggle. if self.thread_filter == ThreadFilter::ArchivedOnly && store.archived_entries().next().is_none() { self.thread_filter = ThreadFilter::All; } let thread_filter = self.thread_filter; let sessions = store .entries() .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() .collect::<Vec<_>>(); let query = self.filter_editor.read(cx).text(cx).to_lowercase(); let today = Local::now().naive_local().date(); let mut items = Vec::with_capacity(sessions.len() + 5); let mut current_bucket: Option<TimeBucket> = None; for session in sessions { let highlight_positions = if !query.is_empty() { let title = session .title .as_ref() .map(|t| t.as_ref()) .unwrap_or(DEFAULT_THREAD_TITLE); if let Some(positions) = fuzzy_match_positions(&query, title) { positions } else { // If title didn't match, also try matching the project name // (the basename of any of the thread's worktree paths), so // typing a project name surfaces its threads here too. let worktree_matched = session.folder_paths().paths().iter().any(|p| { p.as_path() .file_name() .and_then(|name| name.to_str()) .is_some_and(|name| fuzzy_match_positions(&query, name).is_some()) }); if !worktree_matched { continue; } Vec::new() } } else { Vec::new() }; let entry_bucket = { let entry_date = session .created_at .unwrap_or(session.updated_at) .with_timezone(&Local) .naive_local() .date(); TimeBucket::from_dates(today, entry_date) }; if Some(entry_bucket) != current_bucket { current_bucket = Some(entry_bucket); items.push(ArchiveListItem::BucketSeparator(entry_bucket)); } items.push(ArchiveListItem::Entry { thread: session, highlight_positions, }); } let preserve = self.preserve_selection_on_next_update; self.preserve_selection_on_next_update = false; let saved_scroll = self.list_state.logical_scroll_top(); self.list_state.reset(items.len()); self.items = items; if let Some(ix) = self.hovered_index { if ix >= self.items.len() || !self.is_selectable_item(ix) { self.hovered_index = None; } } self.list_state.scroll_to(saved_scroll); if preserve { if let Some(ix) = self.selection { let next = self.find_next_selectable(ix).or_else(|| { ix.checked_sub(1) .and_then(|i| self.find_previous_selectable(i)) }); self.selection = next; if let Some(next) = next { self.list_state.scroll_to_reveal_item(next); } } } else { self.selection = None; } cx.notify(); } fn reload_branch_names_if_threads_changed(&mut self, cx: &mut Context<Self>) { let current_ids: HashSet<ThreadId> = self .items .iter() .filter_map(|item| match item { ArchiveListItem::Entry { thread, .. } => Some(thread.thread_id), _ => None, }) .collect(); if current_ids != self.archived_thread_ids { self.archived_thread_ids = current_ids; self.load_archived_branch_names(cx); } } fn load_archived_branch_names(&mut self, cx: &mut Context<Self>) { let task = ThreadMetadataStore::global(cx) .read(cx) .get_all_archived_branch_names(cx); self._load_branch_names_task = cx.spawn(async move |this, cx| { if let Some(branch_names) = task.await.log_err() { this.update(cx, |this, cx| { this.archived_branch_names = branch_names; cx.notify(); }) .log_err(); } }); } fn reset_filter_editor_text(&mut self, window: &mut Window, cx: &mut Context<Self>) { self.filter_editor.update(cx, |editor, cx| { editor.set_text("", window, cx); }); } fn archive_thread(&mut self, thread_id: ThreadId, cx: &mut Context<Self>) { self.preserve_selection_on_next_update = true; 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<Self>, ) { 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, window: &mut Window, cx: &mut Context<Self>, ) { if self.restoring.contains(&thread.thread_id) { return; } if thread.folder_paths().is_empty() { self.show_project_picker_for_thread(thread, window, cx); return; } self.mark_restoring(&thread.thread_id, cx); self.selection = None; self.reset_filter_editor_text(window, cx); cx.emit(ThreadsArchiveViewEvent::Activate { thread }); } fn show_project_picker_for_thread( &mut self, thread: ThreadMetadata, window: &mut Window, cx: &mut Context<Self>, ) { let Some(workspace) = self.workspace.upgrade() else { return; }; let archive_view = cx.weak_entity(); let fs = workspace.read(cx).app_state().fs.clone(); let current_workspace_id = workspace.read(cx).database_id(); let sibling_workspace_ids: HashSet<WorkspaceId> = workspace .read(cx) .multi_workspace() .and_then(|mw| mw.upgrade()) .map(|mw| { mw.read(cx) .workspaces() .filter_map(|ws| ws.read(cx).database_id()) .collect() }) .unwrap_or_default(); workspace.update(cx, |workspace, cx| { workspace.toggle_modal(window, cx, |window, cx| { ProjectPickerModal::new( thread, fs, archive_view, current_workspace_id, sibling_workspace_ids, window, cx, ) });Lines 1–500 of 1689. Load more
use std::collections::HashSet; use std::path::PathBuf; use std::sync::Arc; use crate::agent_connection_store::AgentConnectionStore; use crate::thread_metadata_store::{ ThreadId, ThreadMetadata, ThreadMetadataStore, worktree_info_from_thread_paths, }; use crate::{Agent, ArchiveSelectedThread, DEFAULT_THREAD_TITLE, RemoveSelectedThread}; use agent::ThreadStore; use agent_client_protocol::schema::v1 as acp; use agent_settings::AgentSettings; use chrono::{DateTime, Datelike as _, Local, NaiveDate, TimeDelta, Utc}; use collections::HashMap; use editor::Editor; use fs::Fs; use fuzzy::{StringMatch, StringMatchCandidate}; use gpui::{ AnyElement, App, Context, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, ListState, Render, SharedString, Subscription, Task, TaskExt, WeakEntity, Window, list, prelude::*, px, }; use itertools::Itertools as _; use menu::{Confirm, SelectFirst, SelectLast, SelectNext, SelectPrevious}; use picker::{ Picker, PickerDelegate, highlighted_match_with_paths::{HighlightedMatch, HighlightedMatchWithPaths}, }; use project::{AgentId, AgentServerStore}; use settings::Settings as _; use theme::ActiveTheme; use ui::{ AgentThreadStatus, Divider, KeyBinding, ListItem, ListItemSpacing, ListSubHeader, ScrollAxes, Scrollbars, Tab, ThreadItem, Tooltip, WithScrollbar, prelude::*, utils::platform_title_bar_height, }; use util::ResultExt; use util::paths::PathExt; use workspace::{ CloseWindow, ModalView, PathList, RecentWorkspace, SerializedWorkspaceLocation, Workspace, WorkspaceDb, WorkspaceId, }; 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), Entry { thread: ThreadMetadata, highlight_positions: Vec<usize>, }, } #[derive(Clone, Copy, Debug, PartialEq, Eq)] enum TimeBucket { Today, Yesterday, ThisWeek, PastWeek, Older, } impl TimeBucket { fn from_dates(reference: NaiveDate, date: NaiveDate) -> Self { if date == reference { return TimeBucket::Today; } if date == reference - TimeDelta::days(1) { return TimeBucket::Yesterday; } let week = date.iso_week(); if reference.iso_week() == week { return TimeBucket::ThisWeek; } let last_week = (reference - TimeDelta::days(7)).iso_week(); if week == last_week { return TimeBucket::PastWeek; } TimeBucket::Older } fn label(&self) -> &'static str { match self { TimeBucket::Today => "Today", TimeBucket::Yesterday => "Yesterday", TimeBucket::ThisWeek => "This Week", TimeBucket::PastWeek => "Past Week", TimeBucket::Older => "Older", } } } pub fn fuzzy_match_positions(query: &str, candidate: &str) -> Option<Vec<usize>> { let query_chars: Vec<char> = query.chars().collect(); if query_chars.is_empty() { return Some(Vec::new()); } let candidate_chars: Vec<(usize, char)> = candidate.char_indices().collect(); let window_count = candidate_chars.len().checked_sub(query_chars.len() - 1)?; 'outer: for window_start in 0..window_count { for (qi, &query_char) in query_chars.iter().enumerate() { let (_, cand_char) = candidate_chars[window_start + qi]; if !cand_char.eq_ignore_ascii_case(&query_char) { continue 'outer; } } return Some( (0..query_chars.len()) .map(|qi| candidate_chars[window_start + qi].0) .collect(), ); } None } pub enum ThreadsArchiveViewEvent { Close, Activate { thread: ThreadMetadata }, CancelRestore { thread_id: ThreadId }, Import, NewThread, } impl EventEmitter<ThreadsArchiveViewEvent> for ThreadsArchiveView {} pub struct ThreadsArchiveView { _history_subscription: Subscription, focus_handle: FocusHandle, list_state: ListState, items: Vec<ArchiveListItem>, selection: Option<usize>, hovered_index: Option<usize>, preserve_selection_on_next_update: bool, filter_editor: Entity<Editor>, _subscriptions: Vec<gpui::Subscription>, _refresh_history_task: Task<()>, workspace: WeakEntity<Workspace>, agent_connection_store: WeakEntity<AgentConnectionStore>, agent_server_store: WeakEntity<AgentServerStore>, restoring: HashSet<ThreadId>, archived_thread_ids: HashSet<ThreadId>, archived_branch_names: HashMap<ThreadId, HashMap<PathBuf, String>>, _load_branch_names_task: Task<()>, thread_filter: ThreadFilter, } impl ThreadsArchiveView { pub fn new( workspace: WeakEntity<Workspace>, agent_connection_store: WeakEntity<AgentConnectionStore>, agent_server_store: WeakEntity<AgentServerStore>, window: &mut Window, cx: &mut Context<Self>, ) -> Self { let focus_handle = cx.focus_handle(); let filter_editor = cx.new(|cx| { let mut editor = Editor::single_line(window, cx); editor.set_placeholder_text("Search all threads…", window, cx); editor }); let filter_editor_subscription = cx.subscribe(&filter_editor, |this: &mut Self, _, event, cx| { if let editor::EditorEvent::BufferEdited = event { this.update_items(cx); } }); let filter_focus_handle = filter_editor.read(cx).focus_handle(cx); cx.on_focus_in( &filter_focus_handle, window, |this: &mut Self, _window, cx| { if this.selection.is_some() { this.selection = None; cx.notify(); } }, ) .detach(); let thread_metadata_store_subscription = cx.observe( &ThreadMetadataStore::global(cx), |this: &mut Self, _, cx| { this.update_items(cx); this.reload_branch_names_if_threads_changed(cx); }, ); cx.on_focus_out(&focus_handle, window, |this: &mut Self, _, _window, cx| { this.selection = None; cx.notify(); }) .detach(); let mut this = Self { _history_subscription: Subscription::new(|| {}), focus_handle, list_state: ListState::new(0, gpui::ListAlignment::Top, px(1000.)), items: Vec::new(), selection: None, hovered_index: None, preserve_selection_on_next_update: false, filter_editor, _subscriptions: vec![ filter_editor_subscription, thread_metadata_store_subscription, ], _refresh_history_task: Task::ready(()), workspace, agent_connection_store, agent_server_store, restoring: HashSet::default(), archived_thread_ids: HashSet::default(), archived_branch_names: HashMap::default(), _load_branch_names_task: Task::ready(()), thread_filter: ThreadFilter::All, }; this.update_items(cx); this.reload_branch_names_if_threads_changed(cx); this } pub fn has_selection(&self) -> bool { self.selection.is_some() } pub fn clear_selection(&mut self) { self.selection = None; } pub fn mark_restoring(&mut self, thread_id: &ThreadId, cx: &mut Context<Self>) { self.restoring.insert(*thread_id); cx.notify(); } pub fn clear_restoring(&mut self, thread_id: &ThreadId, cx: &mut Context<Self>) { self.restoring.remove(thread_id); cx.notify(); } pub fn focus_filter_editor(&self, window: &mut Window, cx: &mut App) { let handle = self.filter_editor.read(cx).focus_handle(cx); handle.focus(window, cx); } pub fn is_filter_editor_focused(&self, window: &Window, cx: &App) -> bool { self.filter_editor .read(cx) .focus_handle(cx) .is_focused(window) } fn update_items(&mut self, cx: &mut Context<Self>) { let store = ThreadMetadataStore::global(cx).read(cx); // If we're filtering to archived threads but none remain (e.g. the // user just deleted the last one), fall back to showing all threads // so they aren't stranded with an empty list and a disabled toggle. if self.thread_filter == ThreadFilter::ArchivedOnly && store.archived_entries().next().is_none() { self.thread_filter = ThreadFilter::All; } let thread_filter = self.thread_filter; let sessions = store .entries() .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() .collect::<Vec<_>>(); let query = self.filter_editor.read(cx).text(cx).to_lowercase(); let today = Local::now().naive_local().date(); let mut items = Vec::with_capacity(sessions.len() + 5); let mut current_bucket: Option<TimeBucket> = None; for session in sessions { let highlight_positions = if !query.is_empty() { let title = session .title .as_ref() .map(|t| t.as_ref()) .unwrap_or(DEFAULT_THREAD_TITLE); if let Some(positions) = fuzzy_match_positions(&query, title) { positions } else { // If title didn't match, also try matching the project name // (the basename of any of the thread's worktree paths), so // typing a project name surfaces its threads here too. let worktree_matched = session.folder_paths().paths().iter().any(|p| { p.as_path() .file_name() .and_then(|name| name.to_str()) .is_some_and(|name| fuzzy_match_positions(&query, name).is_some()) }); if !worktree_matched { continue; } Vec::new() } } else { Vec::new() }; let entry_bucket = { let entry_date = session .created_at .unwrap_or(session.updated_at) .with_timezone(&Local) .naive_local() .date(); TimeBucket::from_dates(today, entry_date) }; if Some(entry_bucket) != current_bucket { current_bucket = Some(entry_bucket); items.push(ArchiveListItem::BucketSeparator(entry_bucket)); } items.push(ArchiveListItem::Entry { thread: session, highlight_positions, }); } let preserve = self.preserve_selection_on_next_update; self.preserve_selection_on_next_update = false; let saved_scroll = self.list_state.logical_scroll_top(); self.list_state.reset(items.len()); self.items = items; if let Some(ix) = self.hovered_index { if ix >= self.items.len() || !self.is_selectable_item(ix) { self.hovered_index = None; } } self.list_state.scroll_to(saved_scroll); if preserve { if let Some(ix) = self.selection { let next = self.find_next_selectable(ix).or_else(|| { ix.checked_sub(1) .and_then(|i| self.find_previous_selectable(i)) }); self.selection = next; if let Some(next) = next { self.list_state.scroll_to_reveal_item(next); } } } else { self.selection = None; } cx.notify(); } fn reload_branch_names_if_threads_changed(&mut self, cx: &mut Context<Self>) { let current_ids: HashSet<ThreadId> = self .items .iter() .filter_map(|item| match item { ArchiveListItem::Entry { thread, .. } => Some(thread.thread_id), _ => None, }) .collect(); if current_ids != self.archived_thread_ids { self.archived_thread_ids = current_ids; self.load_archived_branch_names(cx); } } fn load_archived_branch_names(&mut self, cx: &mut Context<Self>) { let task = ThreadMetadataStore::global(cx) .read(cx) .get_all_archived_branch_names(cx); self._load_branch_names_task = cx.spawn(async move |this, cx| { if let Some(branch_names) = task.await.log_err() { this.update(cx, |this, cx| { this.archived_branch_names = branch_names; cx.notify(); }) .log_err(); } }); } fn reset_filter_editor_text(&mut self, window: &mut Window, cx: &mut Context<Self>) { self.filter_editor.update(cx, |editor, cx| { editor.set_text("", window, cx); }); } fn archive_thread(&mut self, thread_id: ThreadId, cx: &mut Context<Self>) { self.preserve_selection_on_next_update = true; 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<Self>, ) { 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, window: &mut Window, cx: &mut Context<Self>, ) { if self.restoring.contains(&thread.thread_id) { return; } if thread.folder_paths().is_empty() { self.show_project_picker_for_thread(thread, window, cx); return; } self.mark_restoring(&thread.thread_id, cx); self.selection = None; self.reset_filter_editor_text(window, cx); cx.emit(ThreadsArchiveViewEvent::Activate { thread }); } fn show_project_picker_for_thread( &mut self, thread: ThreadMetadata, window: &mut Window, cx: &mut Context<Self>, ) { let Some(workspace) = self.workspace.upgrade() else { return; }; let archive_view = cx.weak_entity(); let fs = workspace.read(cx).app_state().fs.clone(); let current_workspace_id = workspace.read(cx).database_id(); let sibling_workspace_ids: HashSet<WorkspaceId> = workspace .read(cx) .multi_workspace() .and_then(|mw| mw.upgrade()) .map(|mw| { mw.read(cx) .workspaces() .filter_map(|ws| ws.read(cx).database_id()) .collect() }) .unwrap_or_default(); workspace.update(cx, |workspace, cx| { workspace.toggle_modal(window, cx, |window, cx| { ProjectPickerModal::new( thread, fs, archive_view, current_workspace_id, sibling_workspace_ids, window, cx, ) });