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. Load more
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)) }