8491 lines · 322.6 KB · Rust
RawDownload
mod thread_switcher; use acp_thread::ThreadStatus;use action_log::DiffStats;use agent::{ThreadStore, ZED_AGENT_ID};use agent_client_protocol::schema::v1 as acp;use agent_settings::AgentSettings;use agent_ui::terminal_thread_metadata_store::{    TerminalThreadMetadata, TerminalThreadMetadataStore, terminal_title_prefix,};use agent_ui::thread_metadata_store::{    ThreadMetadata, ThreadMetadataStore, WorktreePaths, worktree_info_from_thread_paths,};use agent_ui::threads_archive_view::{    ThreadsArchiveView, ThreadsArchiveViewEvent, format_history_entry_timestamp,    fuzzy_match_positions,};use agent_ui::{    AcpThreadImportOnboarding, Agent, AgentPanel, AgentPanelEvent, AgentThreadSource,    ArchiveSelectedThread, CrossChannelImportOnboarding, DEFAULT_THREAD_TITLE, NewTerminalThread,    NewThread, RenameSelectedThread, TerminalId, ThreadId, ThreadImportModal,    ThreadTitleRegenerationResult, channels_with_threads, import_threads_from_other_channels,};use agent_ui::{MessageEditorEvent, StateChange, thread_worktree_archive};use chrono::{DateTime, Utc};use editor::Editor;use feature_flags::{    AgentThreadWorktreeLabel, AgentThreadWorktreeLabelFlag, FeatureFlag, FeatureFlagAppExt as _,};use gpui::{    Action as _, AnyElement, App, ClickEvent, Context, Decorations, DismissEvent, Entity, EntityId,    FocusHandle, Focusable, KeyContext, ListState, Modifiers, Pixels, Render, SharedString, Task,    TaskExt, WeakEntity, Window, WindowBackgroundAppearance, WindowHandle, linear_color_stop,    linear_gradient, list, prelude::*, px,};use itertools::Itertools;use language_model::LanguageModelRegistry;use menu::{    Cancel, Confirm, SelectChild, SelectFirst, SelectLast, SelectNext, SelectParent, SelectPrevious,};use notifications::status_toast::StatusToast;use project::{AgentId, AgentRegistryStore, Event as ProjectEvent, WorktreeId};use recent_projects::sidebar_recent_projects::SidebarRecentProjects;use remote::{RemoteConnectionOptions, same_remote_connection_identity};use ui::utils::platform_title_bar_height; use serde::{Deserialize, Serialize};use settings::Settings as _;use std::cmp::Ordering;use std::collections::{HashMap, HashSet};use std::mem;use std::path::{Path, PathBuf};use std::rc::Rc;use std::sync::Arc;use theme::{ActiveTheme, CLIENT_SIDE_DECORATION_ROUNDING};use ui::{    AgentThreadStatus, CommonAnimationExt, ContextMenu, ContextMenuEntry, Divider, GradientFade,    HighlightedLabel, KeyBinding, PopoverMenu, PopoverMenuHandle, ProjectEmptyState, ScrollAxes,    Scrollbars, Tab, ThreadItem, ThreadItemWorktreeInfo, TintColor, Tooltip, WithScrollbar,    prelude::*, render_modifiers, right_click_menu,};use unicode_segmentation::UnicodeSegmentation as _;use util::ResultExt as _;use util::path_list::PathList;use workspace::{    CloseWindow, FocusWorkspaceSidebar, MultiWorkspace, MultiWorkspaceEvent, NextProject,    NextThread, Open, OpenMode, PreviousProject, PreviousThread, ProjectGroupKey, SaveIntent,    Sidebar as WorkspaceSidebar, SidebarSide, Toast, ToggleWorkspaceSidebar, Workspace,    notifications::NotificationId, sidebar_side_context_menu,}; use git_ui::worktree_service::{RemoteBranchName, worktree_create_targets};use zed_actions::editor::{MoveDown, MoveUp};use zed_actions::{CreateWorktree, NewWorktreeBranchTarget, OpenRecent}; use zed_actions::agents_sidebar::{FocusSidebarFilter, ToggleThreadSwitcher}; use crate::thread_switcher::{    ThreadSwitcher, ThreadSwitcherEntry, ThreadSwitcherEvent, ThreadSwitcherSelection,    ThreadSwitcherTerminalEntry, ThreadSwitcherThreadEntry,}; #[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 thread history.        ToggleThreadHistory,    ]); 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); #[derive(Default, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]enum SerializedSidebarView {    #[default]    ThreadList,    #[serde(alias = "Archive")]    History,} #[derive(Clone, Copy)]enum NewEntryTarget {    LastCreatedKind,    Terminal,} #[derive(Default, Serialize, Deserialize)]struct SerializedSidebar {    #[serde(default)]    width: Option<f32>,    #[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 {        thread_id: agent_ui::ThreadId,        /// Stable remote identifier, used for matching when thread_id        /// differs (e.g. after cross-window activation creates a new        /// local ThreadId).        session_id: Option<acp::SessionId>,        workspace: Entity<Workspace>,    },    Terminal {        terminal_id: TerminalId,        workspace: Entity<Workspace>,    },} impl ActiveEntry {    fn workspace(&self) -> &Entity<Workspace> {        match self {            ActiveEntry::Thread { workspace, .. } | ActiveEntry::Terminal { workspace, .. } => {                workspace            }        }    }     fn is_active_thread(&self, thread_id: &agent_ui::ThreadId) -> bool {        matches!(self, ActiveEntry::Thread { thread_id: active_thread_id, .. } if active_thread_id == thread_id)    }     fn is_active_terminal(&self, terminal_id: TerminalId) -> bool {        matches!(self, ActiveEntry::Terminal { terminal_id: active_terminal_id, .. } if *active_terminal_id == terminal_id)    }     fn matches_entry(&self, entry: &ListEntry) -> bool {        match (self, entry) {            (                ActiveEntry::Thread {                    thread_id,                    session_id,                    ..                },                ListEntry::Thread(thread),            ) => {                *thread_id == thread.metadata.thread_id                    || session_id                        .as_ref()                        .zip(thread.metadata.session_id.as_ref())                        .is_some_and(|(a, b)| a == b)            }            (ActiveEntry::Terminal { terminal_id, .. }, ListEntry::Terminal(terminal)) => {                *terminal_id == terminal.metadata.terminal_id            }            _ => 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 entry uses (may point to linked worktrees).        folder_paths: PathList,        /// The project group this entry 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(),        }    }} /// If the title begins with a decorative prefix (such as a leading emoji,/// spinner glyph, or symbol the agent prefixed the title with), splits that/// prefix off so a single representative glyph can be displayed in place of the/// entry's icon.fn split_leading_icon_char(    title: &SharedString,    highlight_positions: &[usize],) -> Option<(SharedString, SharedString, Vec<usize>)> {    let prefix = terminal_title_prefix(title)?;    let icon_char = pick_icon_glyph(prefix)?;     let stripped_len = prefix.len();    let trimmed_title = &title[stripped_len..];    if trimmed_title.is_empty() {        return None;    }     let adjusted_positions = highlight_positions        .iter()        .filter(|&&position| position >= stripped_len)        .map(|&position| position - stripped_len)        .collect();     Some((        icon_char,        trimmed_title.to_string().into(),        adjusted_positions,    ))} /// Picks a single glyph to render as the icon from a detected title prefix.////// We only ever show one glyph, so this makes a best effort to choose a/// meaningful one by glancing at the leading characters of the prefix:/// runs of `.` are condensed into a single ellipsis, surrounding ASCII brackets/// are stripped (so `[!]` yields `!`), and a leading run of the same character/// is collapsed (so `>>>` yields `>`). The result is the first grapheme cluster/// of whatever remains, keeping multi-codepoint emoji intact.fn pick_icon_glyph(prefix: &str) -> Option<SharedString> {    let prefix = prefix.trim();    if prefix.is_empty() {        return None;    }     // Strip a single pair of surrounding ASCII brackets, e.g. `[!]` -> `!`.    let unwrapped = match prefix.chars().next() {        Some('[') => prefix.strip_prefix('[').and_then(|s| s.strip_suffix(']')),        Some('(') => prefix.strip_prefix('(').and_then(|s| s.strip_suffix(')')),        Some('{') => prefix.strip_prefix('{').and_then(|s| s.strip_suffix('}')),        Some('<') => prefix.strip_prefix('<').and_then(|s| s.strip_suffix('>')),        _ => None,    };    let prefix = unwrapped        .map(str::trim)        .filter(|s| !s.is_empty())        .unwrap_or(prefix);     // Condense a leading run of dots (`...`) into a single ellipsis.    if prefix.starts_with("..") {        return Some("\u{2026}".into());    }     // Take the first grapheme cluster so multi-codepoint emoji stay intact.    let first_grapheme = prefix.graphemes(true).next()?;    if first_grapheme.trim().is_empty() {        return None;    }     Some(first_grapheme.to_string().into())} fn draft_display_label_for_thread_metadata(    metadata: &ThreadMetadata,    workspace: &ThreadEntryWorkspace,    cx: &App,) -> Option<(SharedString, DraftKind)> {    let workspace = match workspace {        ThreadEntryWorkspace::Open(workspace) => Some(workspace),        ThreadEntryWorkspace::Closed { .. } => None,    };     if let Some(label) =        agent_ui::draft_prompt_store::display_label_for_draft(workspace, metadata.thread_id, cx)    {        return Some((label, DraftKind::WithContent));    }     let placeholder = agent_ui::draft_prompt_store::empty_draft_placeholder_label(        workspace,        &metadata.agent_id,        cx,    );    Some((placeholder, DraftKind::Empty))} fn thread_metadata_would_render_sidebar_row(    metadata: &ThreadMetadata,    workspace: &ThreadEntryWorkspace,    cx: &App,) -> bool {    if !metadata.is_draft() {        return true;    }     draft_display_label_for_thread_metadata(metadata, workspace, cx).is_some()} #[derive(Clone, Copy, PartialEq, Eq, Debug)]enum DraftKind {    WithContent,    Empty,} #[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,    draft: Option<DraftKind>,    highlight_positions: Vec<usize>,    worktrees: Vec<ThreadItemWorktreeInfo>,    diff_stats: DiffStats,} #[derive(Clone)]struct TerminalEntry {    metadata: TerminalThreadMetadata,    workspace: ThreadEntryWorkspace,    worktrees: Vec<ThreadItemWorktreeInfo>,    has_notification: bool,    highlight_positions: Vec<usize>,} 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 = Some(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,        has_notifications: bool,        is_active: bool,        has_threads: bool,    },    Thread(Arc<ThreadEntry>),    Terminal(TerminalEntry),} #[derive(Clone)]enum ActivatableEntry {    Thread {        metadata: ThreadMetadata,        workspace: ThreadEntryWorkspace,    },    Terminal {        metadata: TerminalThreadMetadata,        workspace: ThreadEntryWorkspace,    },} impl ActivatableEntry {    fn from_list_entry(entry: &ListEntry) -> Option<Self> {        match entry {            ListEntry::Thread(thread) => Some(Self::Thread {                metadata: thread.metadata.clone(),                workspace: thread.workspace.clone(),            }),            ListEntry::Terminal(terminal) => Some(Self::Terminal {                metadata: terminal.metadata.clone(),                workspace: terminal.workspace.clone(),            }),            ListEntry::ProjectHeader { .. } => None,        }    }     fn project_location(&self, cx: &App) -> (PathList, ProjectGroupKey) {        match self {            Self::Thread {                workspace: ThreadEntryWorkspace::Open(workspace),                ..            }            | Self::Terminal {                workspace: ThreadEntryWorkspace::Open(workspace),                ..            } => (                PathList::new(&workspace.read(cx).root_paths(cx)),                workspace.read(cx).project_group_key(cx),            ),            Self::Thread {                workspace:                    ThreadEntryWorkspace::Closed {                        folder_paths,                        project_group_key,                    },                ..            }            | Self::Terminal {                workspace:                    ThreadEntryWorkspace::Closed {                        folder_paths,                        project_group_key,                    },                ..            } => (folder_paths.clone(), project_group_key.clone()),        }    }} #[cfg(test)]impl ListEntry {    fn session_id(&self) -> Option<&acp::SessionId> {        match self {            ListEntry::Thread(thread_entry) => thread_entry.metadata.session_id.as_ref(),            ListEntry::Terminal(_) | ListEntry::ProjectHeader { .. } => 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::Terminal(terminal) => match &terminal.workspace {                ThreadEntryWorkspace::Open(workspace) => vec![workspace.clone()],                ThreadEntryWorkspace::Closed { .. } => Vec::new(),            },            ListEntry::ProjectHeader { key, .. } => multi_workspace                .workspaces_for_project_group(key, cx)                .unwrap_or_default(),        }    }} impl From<ThreadEntry> for ListEntry {    fn from(thread: ThreadEntry) -> Self {        ListEntry::Thread(Arc::new(thread))    }

Lines 1–500 of 8491.