Eric HolkFix CI test failuresb380a037 months agoHistory
4937 lines · 186.4 KB · Rust
RawDownload
mod thread_switcher; use acp_thread::ThreadStatus;use action_log::DiffStats;use agent_client_protocol::{self as acp};use agent_settings::AgentSettings;use agent_ui::thread_metadata_store::{ThreadMetadata, ThreadMetadataStore};use agent_ui::thread_worktree_archive;use agent_ui::threads_archive_view::{    ThreadsArchiveView, ThreadsArchiveViewEvent, format_history_entry_timestamp,};use agent_ui::{AcpThreadImportOnboarding, ThreadImportModal};use agent_ui::{    Agent, AgentPanel, AgentPanelEvent, DEFAULT_THREAD_TITLE, NewThread, RemoveSelectedThread,};use chrono::{DateTime, Utc};use editor::Editor;use gpui::{    Action as _, AnyElement, App, Context, DismissEvent, Entity, FocusHandle, Focusable,    KeyContext, ListState, Pixels, Render, SharedString, Task, WeakEntity, Window, WindowHandle,    linear_color_stop, linear_gradient, list, prelude::*, px,};use menu::{    Cancel, Confirm, SelectChild, SelectFirst, SelectLast, SelectNext, SelectParent, SelectPrevious,};use project::{    AgentId, AgentRegistryStore, Event as ProjectEvent, ProjectGroupKey, linked_worktree_short_name,};use recent_projects::sidebar_recent_projects::SidebarRecentProjects;use remote::RemoteConnectionOptions;use ui::utils::platform_title_bar_height; use serde::{Deserialize, Serialize};use settings::Settings as _;use std::collections::{HashMap, HashSet};use std::mem;use std::path::PathBuf;use std::rc::Rc;use theme::ActiveTheme;use ui::{    AgentThreadStatus, CommonAnimationExt, ContextMenu, Divider, HighlightedLabel, KeyBinding,    PopoverMenu, PopoverMenuHandle, Tab, ThreadItem, ThreadItemWorktreeInfo, TintColor, Tooltip,    WithScrollbar, prelude::*,};use util::ResultExt as _;use util::path_list::PathList;use workspace::{    AddFolderToProject, CloseWindow, FocusWorkspaceSidebar, MultiWorkspace, MultiWorkspaceEvent,    NextProject, NextThread, Open, PreviousProject, PreviousThread, SerializedProjectGroupKey,    ShowFewerThreads, ShowMoreThreads, Sidebar as WorkspaceSidebar, SidebarSide, Toast,    ToggleWorkspaceSidebar, Workspace, notifications::NotificationId, sidebar_side_context_menu,}; use zed_actions::OpenRecent;use zed_actions::editor::{MoveDown, MoveUp}; use zed_actions::agents_sidebar::{FocusSidebarFilter, ToggleThreadSwitcher}; use crate::thread_switcher::{ThreadSwitcher, ThreadSwitcherEntry, ThreadSwitcherEvent}; #[cfg(test)]mod sidebar_tests; gpui::actions!(    agents_sidebar,    [        /// Creates a new thread in the currently selected or active project group.        NewThreadInGroup,        /// Toggles between the thread list and the archive view.        ToggleArchive,    ]); gpui::actions!(    dev,    [        /// Dumps multi-workspace state (projects, worktrees, active threads) into a new buffer.        DumpWorkspaceInfo,    ]); const DEFAULT_WIDTH: Pixels = px(300.0);const MIN_WIDTH: Pixels = px(200.0);const MAX_WIDTH: Pixels = px(800.0);const DEFAULT_THREADS_SHOWN: usize = 5; #[derive(Default, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]enum SerializedSidebarView {    #[default]    ThreadList,    Archive,} #[derive(Default, Serialize, Deserialize)]struct SerializedSidebar {    #[serde(default)]    width: Option<f32>,    #[serde(default)]    collapsed_groups: Vec<SerializedProjectGroupKey>,    #[serde(default)]    expanded_groups: Vec<(SerializedProjectGroupKey, usize)>,    #[serde(default)]    active_view: SerializedSidebarView,} #[derive(Debug, Default)]enum SidebarView {    #[default]    ThreadList,    Archive(Entity<ThreadsArchiveView>),} enum ArchiveWorktreeOutcome {    Success,    Cancelled,} #[derive(Clone, Debug)]enum ActiveEntry {    Thread {        session_id: acp::SessionId,        workspace: Entity<Workspace>,    },    Draft(Entity<Workspace>),} impl ActiveEntry {    fn workspace(&self) -> &Entity<Workspace> {        match self {            ActiveEntry::Thread { workspace, .. } => workspace,            ActiveEntry::Draft(workspace) => workspace,        }    }     fn is_active_thread(&self, session_id: &acp::SessionId) -> bool {        matches!(self, ActiveEntry::Thread { session_id: id, .. } if id == session_id)    }     fn matches_entry(&self, entry: &ListEntry) -> bool {        match (self, entry) {            (ActiveEntry::Thread { session_id, .. }, ListEntry::Thread(thread)) => {                thread.metadata.session_id == *session_id            }            (                ActiveEntry::Draft(_),                ListEntry::DraftThread {                    workspace: None, ..                },            ) => true,            _ => false,        }    }} #[derive(Clone, Debug)]struct ActiveThreadInfo {    session_id: acp::SessionId,    title: SharedString,    status: AgentThreadStatus,    icon: IconName,    icon_from_external_svg: Option<SharedString>,    is_background: bool,    is_title_generating: bool,    diff_stats: DiffStats,} #[derive(Clone)]enum ThreadEntryWorkspace {    Open(Entity<Workspace>),    Closed {        /// The paths this thread uses (may point to linked worktrees).        folder_paths: PathList,        /// The project group this thread belongs to.        project_group_key: ProjectGroupKey,    },} impl ThreadEntryWorkspace {    fn is_remote(&self, cx: &App) -> bool {        match self {            ThreadEntryWorkspace::Open(workspace) => {                !workspace.read(cx).project().read(cx).is_local()            }            ThreadEntryWorkspace::Closed {                project_group_key, ..            } => project_group_key.host().is_some(),        }    }} #[derive(Clone)]struct WorktreeInfo {    name: SharedString,    full_path: SharedString,    highlight_positions: Vec<usize>,    kind: ui::WorktreeKind,} #[derive(Clone)]struct ThreadEntry {    metadata: ThreadMetadata,    icon: IconName,    icon_from_external_svg: Option<SharedString>,    status: AgentThreadStatus,    workspace: ThreadEntryWorkspace,    is_live: bool,    is_background: bool,    is_title_generating: bool,    highlight_positions: Vec<usize>,    worktrees: Vec<WorktreeInfo>,    diff_stats: DiffStats,} impl ThreadEntry {    /// Updates this thread entry with active thread information.    ///    /// The existing [`ThreadEntry`] was likely deserialized from the database    /// but if we have a correspond thread already loaded we want to apply the    /// live information.    fn apply_active_info(&mut self, info: &ActiveThreadInfo) {        self.metadata.title = info.title.clone();        self.status = info.status;        self.icon = info.icon;        self.icon_from_external_svg = info.icon_from_external_svg.clone();        self.is_live = true;        self.is_background = info.is_background;        self.is_title_generating = info.is_title_generating;        self.diff_stats = info.diff_stats;    }} #[derive(Clone)]enum ListEntry {    ProjectHeader {        key: ProjectGroupKey,        label: SharedString,        highlight_positions: Vec<usize>,        has_running_threads: bool,        waiting_thread_count: usize,        is_active: bool,        has_threads: bool,    },    Thread(ThreadEntry),    ViewMore {        key: ProjectGroupKey,        is_fully_expanded: bool,    },    /// The user's active draft thread. Shows a prefix of the currently-typed    /// prompt, or "Untitled Thread" if the prompt is empty.    DraftThread {        key: project::ProjectGroupKey,        workspace: Option<Entity<Workspace>>,        worktrees: Vec<WorktreeInfo>,    },} #[cfg(test)]impl ListEntry {    fn session_id(&self) -> Option<&acp::SessionId> {        match self {            ListEntry::Thread(thread_entry) => Some(&thread_entry.metadata.session_id),            _ => None,        }    }     fn reachable_workspaces<'a>(        &'a self,        multi_workspace: &'a workspace::MultiWorkspace,        cx: &'a App,    ) -> Vec<Entity<Workspace>> {        match self {            ListEntry::Thread(thread) => match &thread.workspace {                ThreadEntryWorkspace::Open(ws) => vec![ws.clone()],                ThreadEntryWorkspace::Closed { .. } => Vec::new(),            },            ListEntry::DraftThread { workspace, .. } => {                if let Some(ws) = workspace {                    vec![ws.clone()]                } else {                    // workspace: None means this is the active draft,                    // which always lives on the current workspace.                    vec![multi_workspace.workspace().clone()]                }            }            ListEntry::ProjectHeader { key, .. } => multi_workspace                .workspaces_for_project_group(key, cx)                .cloned()                .collect(),            ListEntry::ViewMore { .. } => Vec::new(),        }    }} impl From<ThreadEntry> for ListEntry {    fn from(thread: ThreadEntry) -> Self {        ListEntry::Thread(thread)    }} #[derive(Default)]struct SidebarContents {    entries: Vec<ListEntry>,    notified_threads: HashSet<acp::SessionId>,    project_header_indices: Vec<usize>,    has_open_projects: bool,} impl SidebarContents {    fn is_thread_notified(&self, session_id: &acp::SessionId) -> bool {        self.notified_threads.contains(session_id)    }} fn fuzzy_match_positions(query: &str, candidate: &str) -> Option<Vec<usize>> {    let mut positions = Vec::new();    let mut query_chars = query.chars().peekable();     for (byte_idx, candidate_char) in candidate.char_indices() {        if let Some(&query_char) = query_chars.peek() {            if candidate_char.eq_ignore_ascii_case(&query_char) {                positions.push(byte_idx);                query_chars.next();            }        } else {            break;        }    }     if query_chars.peek().is_none() {        Some(positions)    } else {        None    }} // TODO: The mapping from workspace root paths to git repositories needs a// unified approach across the codebase: this function, `AgentPanel::classify_worktrees`,// thread persistence (which PathList is saved to the database), and thread// querying (which PathList is used to read threads back). All of these need// to agree on how repos are resolved for a given workspace, especially in// multi-root and nested-repo configurations.fn root_repository_snapshots(    workspace: &Entity<Workspace>,    cx: &App,) -> impl Iterator<Item = project::git_store::RepositorySnapshot> {    let path_list = workspace_path_list(workspace, cx);    let project = workspace.read(cx).project().read(cx);    project.repositories(cx).values().filter_map(move |repo| {        let snapshot = repo.read(cx).snapshot();        let is_root = path_list            .paths()            .iter()            .any(|p| p.as_path() == snapshot.work_directory_abs_path.as_ref());        is_root.then_some(snapshot)    })} fn workspace_path_list(workspace: &Entity<Workspace>, cx: &App) -> PathList {    PathList::new(&workspace.read(cx).root_paths(cx))} /// Derives worktree display info from a thread's stored path list.////// For each path in the thread's `folder_paths`, produces a/// [`WorktreeInfo`] with a short display name, full path, and whether/// the worktree is the main checkout or a linked git worktree. When/// multiple main paths exist and a linked worktree's short name alone/// wouldn't identify which main project it belongs to, the main project/// name is prefixed for disambiguation (e.g. `project:feature`).////// `linked_to_main` maps linked worktree abs paths to their main repo/// abs path, used to pick the correct prefix. Falls back to a heuristic/// when no mapping is available.fn worktree_info_from_thread_paths(    folder_paths: &PathList,    main_worktree_paths: &PathList,    linked_to_main: &HashMap<PathBuf, PathBuf>,) -> Vec<WorktreeInfo> {    let main_paths = main_worktree_paths.paths();     let mut infos: Vec<WorktreeInfo> = Vec::new();    let mut linked_short_names: Vec<(SharedString, SharedString)> = Vec::new();     for path in folder_paths.paths().iter() {        let is_main = main_paths.iter().any(|mp| mp.as_path() == path.as_path());        if is_main {            let Some(name) = path.file_name() else {                continue;            };            infos.push(WorktreeInfo {                name: SharedString::from(name.to_string_lossy().to_string()),                full_path: SharedString::from(path.display().to_string()),                highlight_positions: Vec::new(),                kind: ui::WorktreeKind::Main,            });        } else {            let Some(main_path) = linked_to_main                .get(&**path)                .and_then(|main| main_paths.iter().find(|mp| mp.as_path() == main.as_path()))                .or_else(|| {                    main_paths                        .iter()                        .find(|mp| mp.file_name() == path.file_name())                        .or(main_paths.first())                })            else {                continue;            };            let short_name = linked_worktree_short_name(main_path, path).unwrap_or_default();            let project_name = main_path                .file_name()                .map(|n| SharedString::from(n.to_string_lossy().to_string()))                .unwrap_or_default();            linked_short_names.push((short_name.clone(), project_name));            infos.push(WorktreeInfo {                name: short_name,                full_path: SharedString::from(path.display().to_string()),                highlight_positions: Vec::new(),                kind: ui::WorktreeKind::Linked,            });        }    }     // When the group has multiple main worktree paths and the thread's    // folder paths don't all share the same short name, prefix each    // linked worktree chip with its main project name so the user knows    // which project it belongs to.    let all_same_name = infos.len() > 1 && infos.iter().all(|i| i.name == infos[0].name);     if main_paths.len() > 1 && !all_same_name {        for (info, (_short_name, project_name)) in infos            .iter_mut()            .filter(|i| i.kind == ui::WorktreeKind::Linked)            .zip(linked_short_names.iter())        {            info.name = SharedString::from(format!("{}:{}", project_name, info.name));        }    }     infos} /// Shows a [`RemoteConnectionModal`] on the given workspace and establishes/// an SSH connection. Suitable for passing to/// [`MultiWorkspace::find_or_create_workspace`] as the `connect_remote`/// argument.fn connect_remote(    modal_workspace: Entity<Workspace>,    connection_options: RemoteConnectionOptions,    window: &mut Window,    cx: &mut Context<MultiWorkspace>,) -> gpui::Task<anyhow::Result<Option<Entity<remote::RemoteClient>>>> {    remote_connection::connect_with_modal(&modal_workspace, connection_options, window, cx)} /// The sidebar re-derives its entire entry list from scratch on every/// change via `update_entries` → `rebuild_contents`. Avoid adding/// incremental or inter-event coordination state — if something can/// be computed from the current world state, compute it in the rebuild.pub struct Sidebar {    multi_workspace: WeakEntity<MultiWorkspace>,    width: Pixels,    focus_handle: FocusHandle,    filter_editor: Entity<Editor>,    list_state: ListState,    contents: SidebarContents,    /// The index of the list item that currently has the keyboard focus    ///    /// Note: This is NOT the same as the active item.    selection: Option<usize>,    /// Tracks which sidebar entry is currently active (highlighted).    active_entry: Option<ActiveEntry>,    hovered_thread_index: Option<usize>,    collapsed_groups: HashSet<ProjectGroupKey>,    expanded_groups: HashMap<ProjectGroupKey, usize>,    /// Updated only in response to explicit user actions (clicking a    /// thread, confirming in the thread switcher, etc.) — never from    /// background data changes. Used to sort the thread switcher popup.    thread_last_accessed: HashMap<acp::SessionId, DateTime<Utc>>,    /// Updated when the user presses a key to send or queue a message.    /// Used for sorting threads in the sidebar and as a secondary sort    /// key in the thread switcher.    thread_last_message_sent_or_queued: HashMap<acp::SessionId, DateTime<Utc>>,    thread_switcher: Option<Entity<ThreadSwitcher>>,    _thread_switcher_subscriptions: Vec<gpui::Subscription>,    pending_remote_thread_activation: Option<acp::SessionId>,    view: SidebarView,    recent_projects_popover_handle: PopoverMenuHandle<SidebarRecentProjects>,    project_header_menu_ix: Option<usize>,    _subscriptions: Vec<gpui::Subscription>,    _draft_observation: Option<gpui::Subscription>,} impl Sidebar {    pub fn new(        multi_workspace: Entity<MultiWorkspace>,        window: &mut Window,        cx: &mut Context<Self>,    ) -> Self {        let focus_handle = cx.focus_handle();

Lines 1–500 of 4937.