1368 lines · 46.5 KB · Rust
RawDownload
use acp_thread::ThreadStatus;use agent_ui::{AgentPanel, AgentPanelEvent};use chrono::{Datelike, Local, NaiveDate, TimeDelta};use db::kvp::KEY_VALUE_STORE; use fs::Fs;use fuzzy::StringMatchCandidate;use gpui::{    App, Context, Entity, EventEmitter, FocusHandle, Focusable, Pixels, Render, SharedString,    Subscription, Task, Window, px,};use picker::{Picker, PickerDelegate};use project::Event as ProjectEvent;use recent_projects::{RecentProjectEntry, get_recent_projects};use std::fmt::Display; use std::collections::{HashMap, HashSet}; use std::path::{Path, PathBuf};use std::sync::Arc;use theme::ActiveTheme;use ui::utils::TRAFFIC_LIGHT_PADDING;use ui::{    AgentThreadStatus, Divider, DividerColor, KeyBinding, ListSubHeader, Tab, ThreadItem, Tooltip,    prelude::*,};use ui_input::ErasedEditor;use util::ResultExt as _;use workspace::{    FocusWorkspaceSidebar, MultiWorkspace, NewWorkspaceInWindow, Sidebar as WorkspaceSidebar,    SidebarEvent, ToggleWorkspaceSidebar, Workspace,}; #[derive(Clone, Debug)]struct AgentThreadInfo {    title: SharedString,    status: AgentThreadStatus,    icon: IconName,} const LAST_THREAD_TITLES_KEY: &str = "sidebar-last-thread-titles"; const DEFAULT_WIDTH: Pixels = px(320.0);const MIN_WIDTH: Pixels = px(200.0);const MAX_WIDTH: Pixels = px(800.0);const MAX_MATCHES: usize = 100; #[derive(Clone)]struct WorkspaceThreadEntry {    index: usize,    worktree_label: SharedString,    full_path: SharedString,    thread_info: Option<AgentThreadInfo>,} impl WorkspaceThreadEntry {    fn new(        index: usize,        workspace: &Entity<Workspace>,        persisted_titles: &HashMap<String, String>,        cx: &App,    ) -> Self {        let workspace_ref = workspace.read(cx);         let worktrees: Vec<_> = workspace_ref            .worktrees(cx)            .filter(|worktree| worktree.read(cx).is_visible())            .map(|worktree| worktree.read(cx).abs_path())            .collect();         let worktree_names: Vec<String> = worktrees            .iter()            .filter_map(|path| {                path.file_name()                    .map(|name| name.to_string_lossy().to_string())            })            .collect();         let worktree_label: SharedString = if worktree_names.is_empty() {            format!("Workspace {}", index + 1).into()        } else {            worktree_names.join(", ").into()        };         let full_path: SharedString = worktrees            .iter()            .map(|path| path.to_string_lossy().to_string())            .collect::<Vec<_>>()            .join("\n")            .into();         let thread_info = Self::thread_info(workspace, cx).or_else(|| {            if worktrees.is_empty() {                return None;            }            let path_key = sorted_paths_key(&worktrees);            let title = persisted_titles.get(&path_key)?;            Some(AgentThreadInfo {                title: SharedString::from(title.clone()),                status: AgentThreadStatus::Completed,                icon: IconName::ZedAgent,            })        });         Self {            index,            worktree_label,            full_path,            thread_info,        }    }     fn thread_info(workspace: &Entity<Workspace>, cx: &App) -> Option<AgentThreadInfo> {        let agent_panel = workspace.read(cx).panel::<AgentPanel>(cx)?;        let agent_panel_ref = agent_panel.read(cx);         let thread_view = agent_panel_ref.as_active_thread_view(cx)?.read(cx);        let thread = thread_view.thread.read(cx);         let icon = thread_view.agent_icon;        let title = thread.title();         let status = if thread.is_waiting_for_confirmation() {            AgentThreadStatus::WaitingForConfirmation        } else if thread.had_error() {            AgentThreadStatus::Error        } else {            match thread.status() {                ThreadStatus::Generating => AgentThreadStatus::Running,                ThreadStatus::Idle => AgentThreadStatus::Completed,            }        };        Some(AgentThreadInfo {            title,            status,            icon,        })    }} #[derive(Clone)]enum SidebarEntry {    Separator(SharedString),    WorkspaceThread(WorkspaceThreadEntry),    RecentProject(RecentProjectEntry),} impl SidebarEntry {    fn searchable_text(&self) -> &str {        match self {            SidebarEntry::Separator(_) => "",            SidebarEntry::WorkspaceThread(entry) => entry.worktree_label.as_ref(),            SidebarEntry::RecentProject(entry) => entry.name.as_ref(),        }    }} #[derive(Clone)]struct SidebarMatch {    entry: SidebarEntry,    positions: Vec<usize>,} struct WorkspacePickerDelegate {    multi_workspace: Entity<MultiWorkspace>,    entries: Vec<SidebarEntry>,    active_workspace_index: usize,    workspace_thread_count: usize,    /// All recent projects including what's filtered out of entries    /// used to add unopened projects to entries on rebuild    recent_projects: Vec<RecentProjectEntry>,    recent_project_thread_titles: HashMap<SharedString, SharedString>,    matches: Vec<SidebarMatch>,    selected_index: usize,    query: String,    hovered_thread_item: Option<usize>,    notified_workspaces: HashSet<usize>,} impl WorkspacePickerDelegate {    fn new(multi_workspace: Entity<MultiWorkspace>) -> Self {        Self {            multi_workspace,            entries: Vec::new(),            active_workspace_index: 0,            workspace_thread_count: 0,            recent_projects: Vec::new(),            recent_project_thread_titles: HashMap::new(),            matches: Vec::new(),            selected_index: 0,            query: String::new(),            hovered_thread_item: None,            notified_workspaces: HashSet::new(),        }    }     fn set_entries(        &mut self,        workspace_threads: Vec<WorkspaceThreadEntry>,        active_workspace_index: usize,        cx: &App,    ) {        if let Some(hovered_index) = self.hovered_thread_item {            let still_exists = workspace_threads                .iter()                .any(|thread| thread.index == hovered_index);            if !still_exists {                self.hovered_thread_item = None;            }        }         let old_statuses: HashMap<usize, AgentThreadStatus> = self            .entries            .iter()            .filter_map(|entry| match entry {                SidebarEntry::WorkspaceThread(thread) => thread                    .thread_info                    .as_ref()                    .map(|info| (thread.index, info.status)),                _ => None,            })            .collect();         for thread in &workspace_threads {            if let Some(info) = &thread.thread_info {                if info.status == AgentThreadStatus::Completed                    && thread.index != active_workspace_index                {                    if old_statuses.get(&thread.index) == Some(&AgentThreadStatus::Running) {                        self.notified_workspaces.insert(thread.index);                    }                }            }        }         if self.active_workspace_index != active_workspace_index {            self.notified_workspaces.remove(&active_workspace_index);        }        self.active_workspace_index = active_workspace_index;        self.workspace_thread_count = workspace_threads.len();        self.rebuild_entries(workspace_threads, cx);    }     fn set_recent_projects(&mut self, recent_projects: Vec<RecentProjectEntry>, cx: &App) {        self.recent_project_thread_titles.clear();        if let Some(map) = read_thread_title_map() {            for entry in &recent_projects {                let path_key = sorted_paths_key(&entry.paths);                if let Some(title) = map.get(&path_key) {                    self.recent_project_thread_titles                        .insert(entry.full_path.clone(), title.clone().into());                }            }        }         self.recent_projects = recent_projects;         let workspace_threads: Vec<WorkspaceThreadEntry> = self            .entries            .iter()            .filter_map(|entry| match entry {                SidebarEntry::WorkspaceThread(thread) => Some(thread.clone()),                _ => None,            })            .collect();        self.rebuild_entries(workspace_threads, cx);    }     fn open_workspace_path_sets(&self, cx: &App) -> Vec<Vec<Arc<Path>>> {        self.multi_workspace            .read(cx)            .workspaces()            .iter()            .map(|workspace| {                let mut paths = workspace.read(cx).root_paths(cx);                paths.sort();                paths            })            .collect()    }     fn rebuild_entries(&mut self, workspace_threads: Vec<WorkspaceThreadEntry>, cx: &App) {        let open_path_sets = self.open_workspace_path_sets(cx);         self.entries.clear();         if !workspace_threads.is_empty() {            self.entries                .push(SidebarEntry::Separator("Active Workspaces".into()));            for thread in workspace_threads {                self.entries.push(SidebarEntry::WorkspaceThread(thread));            }        }         let recent: Vec<_> = self            .recent_projects            .iter()            .filter(|project| {                let mut project_paths: Vec<&Path> =                    project.paths.iter().map(|p| p.as_path()).collect();                project_paths.sort();                !open_path_sets.iter().any(|open_paths| {                    open_paths.len() == project_paths.len()                        && open_paths                            .iter()                            .zip(&project_paths)                            .all(|(a, b)| a.as_ref() == *b)                })            })            .cloned()            .collect();         if !recent.is_empty() {            let today = Local::now().naive_local().date();            let mut current_bucket: Option<TimeBucket> = None;             for project in recent {                let entry_date = project.timestamp.with_timezone(&Local).naive_local().date();                let bucket = TimeBucket::from_dates(today, entry_date);                 if current_bucket != Some(bucket) {                    current_bucket = Some(bucket);                    self.entries                        .push(SidebarEntry::Separator(bucket.to_string().into()));                }                 self.entries.push(SidebarEntry::RecentProject(project));            }        }    }} #[derive(Clone, Copy, Debug, PartialEq, Eq)]enum TimeBucket {    Today,    Yesterday,    ThisWeek,    PastWeek,    All,} 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::All    }} impl Display for TimeBucket {    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {        match self {            TimeBucket::Today => write!(f, "Today"),            TimeBucket::Yesterday => write!(f, "Yesterday"),            TimeBucket::ThisWeek => write!(f, "This Week"),            TimeBucket::PastWeek => write!(f, "Past Week"),            TimeBucket::All => write!(f, "All"),        }    }} fn open_recent_project(paths: Vec<PathBuf>, window: &mut Window, cx: &mut App) {    let Some(handle) = window.window_handle().downcast::<MultiWorkspace>() else {        return;    };     cx.defer(move |cx| {        if let Some(task) = handle            .update(cx, |multi_workspace, window, cx| {                multi_workspace.open_project(paths, window, cx)            })            .log_err()        {            task.detach_and_log_err(cx);        }    });} impl PickerDelegate for WorkspacePickerDelegate {    type ListItem = AnyElement;     fn match_count(&self) -> usize {        self.matches.len()    }     fn selected_index(&self) -> usize {        self.selected_index    }     fn set_selected_index(        &mut self,        ix: usize,        _window: &mut Window,        _cx: &mut Context<Picker<Self>>,    ) {        self.selected_index = ix;    }     fn can_select(        &mut self,        ix: usize,        _window: &mut Window,        _cx: &mut Context<Picker<Self>>,    ) -> bool {        match self.matches.get(ix) {            Some(SidebarMatch {                entry: SidebarEntry::Separator(_),                ..            }) => false,            _ => true,        }    }     fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc<str> {        "Search…".into()    }     fn no_matches_text(&self, _window: &mut Window, _cx: &mut App) -> Option<SharedString> {        if self.query.is_empty() {            None        } else {            Some("No threads match your search.".into())        }    }     fn update_matches(        &mut self,        query: String,        window: &mut Window,        cx: &mut Context<Picker<Self>>,    ) -> Task<()> {        let query_changed = self.query != query;        self.query = query.clone();        if query_changed {            self.hovered_thread_item = None;        }        let entries = self.entries.clone();         if query.is_empty() {            self.matches = entries                .into_iter()                .map(|entry| SidebarMatch {                    entry,                    positions: Vec::new(),                })                .collect();             let separator_offset = if self.workspace_thread_count > 0 {                1            } else {                0            };            self.selected_index = (self.active_workspace_index + separator_offset)                .min(self.matches.len().saturating_sub(1));            return Task::ready(());        }         let executor = cx.background_executor().clone();        cx.spawn_in(window, async move |picker, cx| {            let matches = cx                .background_spawn(async move {                    let data_entries: Vec<(usize, &SidebarEntry)> = entries                        .iter()                        .enumerate()                        .filter(|(_, entry)| !matches!(entry, SidebarEntry::Separator(_)))                        .collect();                     let candidates: Vec<StringMatchCandidate> = data_entries                        .iter()                        .enumerate()                        .map(|(candidate_index, (_, entry))| {                            StringMatchCandidate::new(candidate_index, entry.searchable_text())                        })                        .collect();                     let search_matches = fuzzy::match_strings(                        &candidates,                        &query,                        false,                        true,                        MAX_MATCHES,                        &Default::default(),

Lines 1–500 of 1368.