4981 lines · 185.6 KB · Rust
RawDownload
use crate::thread_metadata_store::{ThreadMetadata, ThreadMetadataStore};use crate::threads_archive_view::{ThreadsArchiveView, ThreadsArchiveViewEvent};use crate::{Agent, AgentPanel, AgentPanelEvent, NewThread, RemoveSelectedThread};use acp_thread::ThreadStatus;use action_log::DiffStats;use agent::ThreadStore;use agent_client_protocol::{self as acp};use agent_settings::AgentSettings;use chrono::Utc;use db::kvp::KEY_VALUE_STORE;use editor::Editor;use feature_flags::{AgentV2FeatureFlag, FeatureFlagViewExt as _};use gpui::{    Action as _, AnyElement, App, Context, Entity, FocusHandle, Focusable, ListState, Pixels,    Render, SharedString, WeakEntity, Window, actions, list, prelude::*, px,};use menu::{Cancel, Confirm, SelectFirst, SelectLast, SelectNext, SelectPrevious};use project::{AgentId, Event as ProjectEvent};use settings::Settings;use std::collections::{HashMap, HashSet};use std::mem;use std::path::Path;use std::sync::Arc;use theme::ActiveTheme;use ui::{    AgentThreadStatus, ButtonStyle, HighlightedLabel, IconButtonShape, ListItem, Tab, ThreadItem,    Tooltip, WithScrollbar, prelude::*,};use util::ResultExt as _;use util::path_list::PathList;use workspace::{    MultiWorkspace, MultiWorkspaceEvent, ToggleWorkspaceSidebar, Workspace, multi_workspace_enabled,};use zed_actions::editor::{MoveDown, MoveUp}; actions!(    agents_sidebar,    [        /// Collapses the selected entry in the workspace sidebar.        CollapseSelectedEntry,        /// Expands the selected entry in the workspace sidebar.        ExpandSelectedEntry,    ]); const DEFAULT_WIDTH: Pixels = px(320.0);const MIN_WIDTH: Pixels = px(200.0);const MAX_WIDTH: Pixels = px(800.0);const DEFAULT_THREADS_SHOWN: usize = 5;const SIDEBAR_STATE_KEY: &str = "sidebar_state"; #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]enum SidebarView {    #[default]    ThreadList,    Archive,} fn read_sidebar_open_state(multi_workspace_id: u64) -> bool {    KEY_VALUE_STORE        .scoped(SIDEBAR_STATE_KEY)        .read(&multi_workspace_id.to_string())        .log_err()        .flatten()        .and_then(|json| serde_json::from_str::<bool>(&json).ok())        .unwrap_or(false)} async fn save_sidebar_open_state(multi_workspace_id: u64, is_open: bool) {    if let Ok(json) = serde_json::to_string(&is_open) {        KEY_VALUE_STORE            .scoped(SIDEBAR_STATE_KEY)            .write(multi_workspace_id.to_string(), json)            .await            .log_err();    }} #[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,} impl From<&ActiveThreadInfo> for acp_thread::AgentSessionInfo {    fn from(info: &ActiveThreadInfo) -> Self {        Self {            session_id: info.session_id.clone(),            work_dirs: None,            title: Some(info.title.clone()),            updated_at: Some(Utc::now()),            created_at: Some(Utc::now()),            meta: None,        }    }} #[derive(Clone)]enum ThreadEntryWorkspace {    Open(Entity<Workspace>),    Closed(PathList),} #[derive(Clone)]struct ThreadEntry {    agent: Agent,    session_info: acp_thread::AgentSessionInfo,    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>,    worktree_name: Option<SharedString>,    worktree_highlight_positions: Vec<usize>,    diff_stats: DiffStats,} #[derive(Clone)]enum ListEntry {    ProjectHeader {        path_list: PathList,        label: SharedString,        workspace: Entity<Workspace>,        highlight_positions: Vec<usize>,        has_threads: bool,    },    Thread(ThreadEntry),    ViewMore {        path_list: PathList,        remaining_count: usize,        is_fully_expanded: bool,    },    NewThread {        path_list: PathList,        workspace: Entity<Workspace>,    },} 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>,} 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,) -> Vec<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(|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)        })        .collect()} fn workspace_path_list(workspace: &Entity<Workspace>, cx: &App) -> PathList {    PathList::new(&workspace.read(cx).root_paths(cx))} fn workspace_label_from_path_list(path_list: &PathList) -> SharedString {    let mut names = Vec::with_capacity(path_list.paths().len());    for abs_path in path_list.paths() {        if let Some(name) = abs_path.file_name() {            names.push(name.to_string_lossy().to_string());        }    }    if names.is_empty() {        // TODO: Can we do something better in this case?        "Empty Workspace".into()    } else {        names.join(", ").into()    }} pub struct Sidebar {    multi_workspace: WeakEntity<MultiWorkspace>,    persistence_key: Option<u64>,    is_open: bool,    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>,    focused_thread: Option<acp::SessionId>,    active_entry_index: Option<usize>,    hovered_thread_index: Option<usize>,    collapsed_groups: HashSet<PathList>,    expanded_groups: HashMap<PathList, usize>,    view: SidebarView,    archive_view: Option<Entity<ThreadsArchiveView>>,    _subscriptions: Vec<gpui::Subscription>,    _update_entries_task: Option<gpui::Task<()>>,} impl Sidebar {    pub fn new(        multi_workspace: Entity<MultiWorkspace>,        window: &mut Window,        cx: &mut Context<Self>,    ) -> Self {        let focus_handle = cx.focus_handle();        cx.on_focus_in(&focus_handle, window, Self::focus_in)            .detach();         let filter_editor = cx.new(|cx| {            let mut editor = Editor::single_line(window, cx);            editor.set_placeholder_text("Search…", window, cx);            editor        });         cx.subscribe_in(            &multi_workspace,            window,            |this, _multi_workspace, event: &MultiWorkspaceEvent, window, cx| match event {                MultiWorkspaceEvent::ActiveWorkspaceChanged => {                    this.update_entries(false, cx);                }                MultiWorkspaceEvent::WorkspaceAdded(workspace) => {                    this.subscribe_to_workspace(workspace, window, cx);                    this.update_entries(false, cx);                }                MultiWorkspaceEvent::WorkspaceRemoved(_) => {                    this.update_entries(false, cx);                }            },        )        .detach();         cx.subscribe(&filter_editor, |this: &mut Self, _, event, cx| {            if let editor::EditorEvent::BufferEdited = event {                let query = this.filter_editor.read(cx).text(cx);                if !query.is_empty() {                    this.selection.take();                }                this.update_entries(!query.is_empty(), cx);            }        })        .detach();         cx.observe(&ThreadMetadataStore::global(cx), |this, _store, cx| {            this.update_entries(false, cx);        })        .detach();         cx.observe_flag::<AgentV2FeatureFlag, _>(window, |_is_enabled, this, _window, cx| {            this.update_entries(false, cx);        })        .detach();         let workspaces = multi_workspace.read(cx).workspaces().to_vec();        cx.defer_in(window, move |this, window, cx| {            for workspace in &workspaces {                this.subscribe_to_workspace(workspace, window, cx);            }            this.update_entries(false, cx);        });         let persistence_key = multi_workspace.read(cx).database_id().map(|id| id.0);        let is_open = persistence_key            .map(read_sidebar_open_state)            .unwrap_or(false);         Self {            _update_entries_task: None,            multi_workspace: multi_workspace.downgrade(),            persistence_key,            is_open,            width: DEFAULT_WIDTH,            focus_handle,            filter_editor,            list_state: ListState::new(0, gpui::ListAlignment::Top, px(1000.)),            contents: SidebarContents::default(),            selection: None,            focused_thread: None,            active_entry_index: None,            hovered_thread_index: None,            collapsed_groups: HashSet::new(),            expanded_groups: HashMap::new(),            view: SidebarView::default(),            archive_view: None,            _subscriptions: Vec::new(),        }    }     fn subscribe_to_workspace(        &self,        workspace: &Entity<Workspace>,        window: &mut Window,        cx: &mut Context<Self>,    ) {        let project = workspace.read(cx).project().clone();        cx.subscribe_in(            &project,            window,            |this, _project, event, _window, cx| match event {                ProjectEvent::WorktreeAdded(_)                | ProjectEvent::WorktreeRemoved(_)                | ProjectEvent::WorktreeOrderChanged => {                    this.update_entries(false, cx);                }                _ => {}            },        )        .detach();         let git_store = workspace.read(cx).project().read(cx).git_store().clone();        cx.subscribe_in(            &git_store,            window,            |this, _, event: &project::git_store::GitStoreEvent, window, cx| {                if matches!(                    event,                    project::git_store::GitStoreEvent::RepositoryUpdated(                        _,                        project::git_store::RepositoryEvent::GitWorktreeListChanged,                        _,                    )                ) {                    this.prune_stale_worktree_workspaces(window, cx);                    this.update_entries(false, cx);                }            },        )        .detach();         cx.subscribe_in(            workspace,            window,            |this, _workspace, event: &workspace::Event, window, cx| {                if let workspace::Event::PanelAdded(view) = event {                    if let Ok(agent_panel) = view.clone().downcast::<AgentPanel>() {                        this.subscribe_to_agent_panel(&agent_panel, window, cx);                    }                }            },        )        .detach();         if let Some(agent_panel) = workspace.read(cx).panel::<AgentPanel>(cx) {            self.subscribe_to_agent_panel(&agent_panel, window, cx);        }    }     fn subscribe_to_agent_panel(        &self,        agent_panel: &Entity<AgentPanel>,        window: &mut Window,        cx: &mut Context<Self>,    ) {        cx.subscribe_in(            agent_panel,            window,            |this, _agent_panel, event: &AgentPanelEvent, _window, cx| match event {                AgentPanelEvent::ActiveViewChanged                | AgentPanelEvent::ThreadFocused                | AgentPanelEvent::BackgroundThreadChanged => {                    this.update_entries(false, cx);                }            },        )        .detach();    }     fn all_thread_infos_for_workspace(        workspace: &Entity<Workspace>,        cx: &App,    ) -> Vec<ActiveThreadInfo> {        let Some(agent_panel) = workspace.read(cx).panel::<AgentPanel>(cx) else {            return Vec::new();        };        let agent_panel_ref = agent_panel.read(cx);         agent_panel_ref            .parent_threads(cx)            .into_iter()            .map(|thread_view| {                let thread_view_ref = thread_view.read(cx);                let thread = thread_view_ref.thread.read(cx);                 let icon = thread_view_ref.agent_icon;                let icon_from_external_svg = thread_view_ref.agent_icon_from_external_svg.clone();                let title = thread.title();                let is_native = thread_view_ref.as_native_thread(cx).is_some();                let is_title_generating = is_native && thread.has_provisional_title();                let session_id = thread.session_id().clone();                let is_background = agent_panel_ref.is_background_thread(&session_id);                 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,                    }                };                 let diff_stats = thread.action_log().read(cx).diff_stats(cx);                 ActiveThreadInfo {                    session_id,                    title,                    status,                    icon,                    icon_from_external_svg,                    is_background,                    is_title_generating,                    diff_stats,                }            })            .collect()    }     fn rebuild_contents(&mut self, thread_entries: Vec<ThreadMetadata>, cx: &App) {        let Some(multi_workspace) = self.multi_workspace.upgrade() else {            return;        };        let mw = multi_workspace.read(cx);        let workspaces = mw.workspaces().to_vec();        let active_workspace = mw.workspaces().get(mw.active_workspace_index()).cloned();         self.focused_thread = active_workspace            .as_ref()            .and_then(|ws| ws.read(cx).panel::<AgentPanel>(cx))            .and_then(|panel| panel.read(cx).active_conversation().cloned())            .and_then(|cv| cv.read(cx).parent_id(cx));         let mut threads_by_paths: HashMap<PathList, Vec<ThreadMetadata>> = HashMap::new();        for row in thread_entries {            threads_by_paths                .entry(row.folder_paths.clone())                .or_default()                .push(row);        }         // Build a lookup for agent icons from the first workspace's AgentServerStore.

Lines 1–500 of 4981.