Skip to repository content

tenant.openagents/omega

No repository description is available.

OpenAgents Git authority 2026-07-28T01:52:43.591Z Public web read
NIP-34 coordinate30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omega
MaintainersHidden in public view
References2 branches · 1 tag
Read-only clonegit clone https://openagents.com/git/tenant.openagents/omega.git
Browse files

sidebar.rs

8492 lines · 330.4 KB · rust
1mod thread_switcher;
2
3use acp_thread::ThreadStatus;
4use action_log::DiffStats;
5use agent::{ThreadStore, OMEGA_AGENT_ID};
6use agent_client_protocol::schema::v1 as acp;
7use agent_settings::AgentSettings;
8use agent_ui::terminal_thread_metadata_store::{
9    TerminalThreadMetadata, TerminalThreadMetadataStore, terminal_title_prefix,
10};
11use agent_ui::thread_metadata_store::{
12    ThreadMetadata, ThreadMetadataStore, WorktreePaths, worktree_info_from_thread_paths,
13};
14use agent_ui::threads_archive_view::{
15    ThreadsArchiveView, ThreadsArchiveViewEvent, format_history_entry_timestamp,
16    fuzzy_match_positions,
17};
18use agent_ui::{
19    AcpThreadImportOnboarding, Agent, AgentPanel, AgentPanelEvent, AgentThreadSource,
20    ArchiveSelectedThread, CrossChannelImportOnboarding, DEFAULT_THREAD_TITLE, NewTerminalThread,
21    NewThread, RenameSelectedThread, TerminalId, ThreadId, ThreadImportModal,
22    ThreadTitleRegenerationResult, channels_with_threads, import_threads_from_other_channels,
23};
24use agent_ui::{MessageEditorEvent, StateChange, thread_worktree_archive};
25use chrono::{DateTime, Utc};
26use editor::Editor;
27use feature_flags::{
28    AgentThreadWorktreeLabel, AgentThreadWorktreeLabelFlag, FeatureFlag, FeatureFlagAppExt as _,
29};
30use gpui::{
31    Action as _, AnyElement, App, ClickEvent, Context, Decorations, DismissEvent, Entity, EntityId,
32    FocusHandle, Focusable, KeyContext, ListState, Modifiers, Pixels, Render, SharedString, Task,
33    TaskExt, WeakEntity, Window, WindowBackgroundAppearance, WindowHandle, linear_color_stop,
34    linear_gradient, list, prelude::*, px,
35};
36use itertools::Itertools;
37use language_model::LanguageModelRegistry;
38use menu::{
39    Cancel, Confirm, SelectChild, SelectFirst, SelectLast, SelectNext, SelectParent, SelectPrevious,
40};
41use notifications::status_toast::StatusToast;
42use project::{AgentId, AgentRegistryStore, Event as ProjectEvent, WorktreeId};
43use recent_projects::sidebar_recent_projects::SidebarRecentProjects;
44use remote::{RemoteConnectionOptions, same_remote_connection_identity};
45use ui::utils::platform_title_bar_height;
46
47use serde::{Deserialize, Serialize};
48use settings::Settings as _;
49use std::cmp::Ordering;
50use std::collections::{HashMap, HashSet};
51use std::mem;
52use std::path::{Path, PathBuf};
53use std::rc::Rc;
54use std::sync::Arc;
55use theme::{ActiveTheme, CLIENT_SIDE_DECORATION_ROUNDING};
56use ui::{
57    AgentThreadStatus, CommonAnimationExt, ContextMenu, ContextMenuEntry, Divider, GradientFade,
58    HighlightedLabel, KeyBinding, PopoverMenu, PopoverMenuHandle, ProjectEmptyState, ScrollAxes,
59    Scrollbars, Tab, ThreadItem, ThreadItemWorktreeInfo, TintColor, Tooltip, WithScrollbar,
60    prelude::*, render_modifiers, right_click_menu,
61};
62use unicode_segmentation::UnicodeSegmentation as _;
63use util::ResultExt as _;
64use util::path_list::PathList;
65use workspace::{
66    CloseWindow, FocusWorkspaceSidebar, MultiWorkspace, MultiWorkspaceEvent, NextProject,
67    NextThread, Open, OpenMode, PreviousProject, PreviousThread, ProjectGroupKey, SaveIntent,
68    Sidebar as WorkspaceSidebar, SidebarSide, Toast, ToggleWorkspaceSidebar, Workspace,
69    notifications::NotificationId, sidebar_side_context_menu,
70};
71
72use git_ui::worktree_service::{RemoteBranchName, worktree_create_targets};
73use zed_actions::editor::{MoveDown, MoveUp};
74use zed_actions::{CreateWorktree, NewWorktreeBranchTarget, OpenRecent};
75
76use zed_actions::agents_sidebar::{FocusSidebarFilter, ToggleThreadSwitcher};
77
78use crate::thread_switcher::{
79    ThreadSwitcher, ThreadSwitcherEntry, ThreadSwitcherEvent, ThreadSwitcherSelection,
80    ThreadSwitcherTerminalEntry, ThreadSwitcherThreadEntry,
81};
82
83#[cfg(test)]
84mod sidebar_tests;
85
86gpui::actions!(
87    agents_sidebar,
88    [
89        /// Creates a new thread in the currently selected or active project group.
90        NewThreadInGroup,
91        /// Toggles between the thread list and the thread history.
92        ToggleThreadHistory,
93    ]
94);
95
96gpui::actions!(
97    dev,
98    [
99        /// Dumps multi-workspace state (projects, worktrees, active threads) into a new buffer.
100        DumpWorkspaceInfo,
101    ]
102);
103
104const DEFAULT_WIDTH: Pixels = px(300.0);
105const MIN_WIDTH: Pixels = px(200.0);
106const MAX_WIDTH: Pixels = px(800.0);
107
108#[derive(Default, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
109enum SerializedSidebarView {
110    #[default]
111    ThreadList,
112    #[serde(alias = "Archive")]
113    History,
114}
115
116#[derive(Clone, Copy)]
117enum NewEntryTarget {
118    LastCreatedKind,
119    Terminal,
120}
121
122#[derive(Default, Serialize, Deserialize)]
123struct SerializedSidebar {
124    #[serde(default)]
125    width: Option<f32>,
126    #[serde(default)]
127    active_view: SerializedSidebarView,
128}
129
130#[derive(Debug, Default)]
131enum SidebarView {
132    #[default]
133    ThreadList,
134    Archive(Entity<ThreadsArchiveView>),
135}
136
137enum ArchiveWorktreeOutcome {
138    Success,
139    Cancelled,
140}
141
142#[derive(Clone, Debug)]
143enum ActiveEntry {
144    Thread {
145        thread_id: agent_ui::ThreadId,
146        /// Stable remote identifier, used for matching when thread_id
147        /// differs (e.g. after cross-window activation creates a new
148        /// local ThreadId).
149        session_id: Option<acp::SessionId>,
150        workspace: Entity<Workspace>,
151    },
152    Terminal {
153        terminal_id: TerminalId,
154        workspace: Entity<Workspace>,
155    },
156}
157
158impl ActiveEntry {
159    fn workspace(&self) -> &Entity<Workspace> {
160        match self {
161            ActiveEntry::Thread { workspace, .. } | ActiveEntry::Terminal { workspace, .. } => {
162                workspace
163            }
164        }
165    }
166
167    fn is_active_thread(&self, thread_id: &agent_ui::ThreadId) -> bool {
168        matches!(self, ActiveEntry::Thread { thread_id: active_thread_id, .. } if active_thread_id == thread_id)
169    }
170
171    fn is_active_terminal(&self, terminal_id: TerminalId) -> bool {
172        matches!(self, ActiveEntry::Terminal { terminal_id: active_terminal_id, .. } if *active_terminal_id == terminal_id)
173    }
174
175    fn matches_entry(&self, entry: &ListEntry) -> bool {
176        match (self, entry) {
177            (
178                ActiveEntry::Thread {
179                    thread_id,
180                    session_id,
181                    ..
182                },
183                ListEntry::Thread(thread),
184            ) => {
185                *thread_id == thread.metadata.thread_id
186                    || session_id
187                        .as_ref()
188                        .zip(thread.metadata.session_id.as_ref())
189                        .is_some_and(|(a, b)| a == b)
190            }
191            (ActiveEntry::Terminal { terminal_id, .. }, ListEntry::Terminal(terminal)) => {
192                *terminal_id == terminal.metadata.terminal_id
193            }
194            _ => false,
195        }
196    }
197}
198
199#[derive(Clone, Debug)]
200struct ActiveThreadInfo {
201    session_id: acp::SessionId,
202    title: SharedString,
203    status: AgentThreadStatus,
204    icon: IconName,
205    icon_from_external_svg: Option<SharedString>,
206    is_background: bool,
207    is_title_generating: bool,
208    diff_stats: DiffStats,
209}
210
211#[derive(Clone)]
212enum ThreadEntryWorkspace {
213    Open(Entity<Workspace>),
214    Closed {
215        /// The paths this entry uses (may point to linked worktrees).
216        folder_paths: PathList,
217        /// The project group this entry belongs to.
218        project_group_key: ProjectGroupKey,
219    },
220}
221
222impl ThreadEntryWorkspace {
223    fn is_remote(&self, cx: &App) -> bool {
224        match self {
225            ThreadEntryWorkspace::Open(workspace) => {
226                !workspace.read(cx).project().read(cx).is_local()
227            }
228            ThreadEntryWorkspace::Closed {
229                project_group_key, ..
230            } => project_group_key.host().is_some(),
231        }
232    }
233}
234
235/// If the title begins with a decorative prefix (such as a leading emoji,
236/// spinner glyph, or symbol the agent prefixed the title with), splits that
237/// prefix off so a single representative glyph can be displayed in place of the
238/// entry's icon.
239fn split_leading_icon_char(
240    title: &SharedString,
241    highlight_positions: &[usize],
242) -> Option<(SharedString, SharedString, Vec<usize>)> {
243    let prefix = terminal_title_prefix(title)?;
244    let icon_char = pick_icon_glyph(prefix)?;
245
246    let stripped_len = prefix.len();
247    let trimmed_title = &title[stripped_len..];
248    if trimmed_title.is_empty() {
249        return None;
250    }
251
252    let adjusted_positions = highlight_positions
253        .iter()
254        .filter(|&&position| position >= stripped_len)
255        .map(|&position| position - stripped_len)
256        .collect();
257
258    Some((
259        icon_char,
260        trimmed_title.to_string().into(),
261        adjusted_positions,
262    ))
263}
264
265/// Picks a single glyph to render as the icon from a detected title prefix.
266///
267/// We only ever show one glyph, so this makes a best effort to choose a
268/// meaningful one by glancing at the leading characters of the prefix:
269/// runs of `.` are condensed into a single ellipsis, surrounding ASCII brackets
270/// are stripped (so `[!]` yields `!`), and a leading run of the same character
271/// is collapsed (so `>>>` yields `>`). The result is the first grapheme cluster
272/// of whatever remains, keeping multi-codepoint emoji intact.
273fn pick_icon_glyph(prefix: &str) -> Option<SharedString> {
274    let prefix = prefix.trim();
275    if prefix.is_empty() {
276        return None;
277    }
278
279    // Strip a single pair of surrounding ASCII brackets, e.g. `[!]` -> `!`.
280    let unwrapped = match prefix.chars().next() {
281        Some('[') => prefix.strip_prefix('[').and_then(|s| s.strip_suffix(']')),
282        Some('(') => prefix.strip_prefix('(').and_then(|s| s.strip_suffix(')')),
283        Some('{') => prefix.strip_prefix('{').and_then(|s| s.strip_suffix('}')),
284        Some('<') => prefix.strip_prefix('<').and_then(|s| s.strip_suffix('>')),
285        _ => None,
286    };
287    let prefix = unwrapped
288        .map(str::trim)
289        .filter(|s| !s.is_empty())
290        .unwrap_or(prefix);
291
292    // Condense a leading run of dots (`...`) into a single ellipsis.
293    if prefix.starts_with("..") {
294        return Some("\u{2026}".into());
295    }
296
297    // Take the first grapheme cluster so multi-codepoint emoji stay intact.
298    let first_grapheme = prefix.graphemes(true).next()?;
299    if first_grapheme.trim().is_empty() {
300        return None;
301    }
302
303    Some(first_grapheme.to_string().into())
304}
305
306fn draft_display_label_for_thread_metadata(
307    metadata: &ThreadMetadata,
308    workspace: &ThreadEntryWorkspace,
309    cx: &App,
310) -> Option<(SharedString, DraftKind)> {
311    let workspace = match workspace {
312        ThreadEntryWorkspace::Open(workspace) => Some(workspace),
313        ThreadEntryWorkspace::Closed { .. } => None,
314    };
315
316    if let Some(label) =
317        agent_ui::draft_prompt_store::display_label_for_draft(workspace, metadata.thread_id, cx)
318    {
319        return Some((label, DraftKind::WithContent));
320    }
321
322    let placeholder = agent_ui::draft_prompt_store::empty_draft_placeholder_label(
323        workspace,
324        &metadata.agent_id,
325        cx,
326    );
327    Some((placeholder, DraftKind::Empty))
328}
329
330fn thread_metadata_would_render_sidebar_row(
331    metadata: &ThreadMetadata,
332    workspace: &ThreadEntryWorkspace,
333    cx: &App,
334) -> bool {
335    if !metadata.is_draft() {
336        return true;
337    }
338
339    draft_display_label_for_thread_metadata(metadata, workspace, cx).is_some()
340}
341
342#[derive(Clone, Copy, PartialEq, Eq, Debug)]
343enum DraftKind {
344    WithContent,
345    Empty,
346}
347
348#[derive(Clone)]
349struct ThreadEntry {
350    metadata: ThreadMetadata,
351    icon: IconName,
352    icon_from_external_svg: Option<SharedString>,
353    status: AgentThreadStatus,
354    workspace: ThreadEntryWorkspace,
355    is_live: bool,
356    is_background: bool,
357    is_title_generating: bool,
358    draft: Option<DraftKind>,
359    highlight_positions: Vec<usize>,
360    worktrees: Vec<ThreadItemWorktreeInfo>,
361    diff_stats: DiffStats,
362}
363
364#[derive(Clone)]
365struct TerminalEntry {
366    metadata: TerminalThreadMetadata,
367    workspace: ThreadEntryWorkspace,
368    worktrees: Vec<ThreadItemWorktreeInfo>,
369    has_notification: bool,
370    highlight_positions: Vec<usize>,
371}
372
373impl ThreadEntry {
374    /// Updates this thread entry with active thread information.
375    ///
376    /// The existing [`ThreadEntry`] was likely deserialized from the database
377    /// but if we have a correspond thread already loaded we want to apply the
378    /// live information.
379    fn apply_active_info(&mut self, info: &ActiveThreadInfo) {
380        self.metadata.title = Some(info.title.clone());
381        self.status = info.status;
382        self.icon = info.icon;
383        self.icon_from_external_svg = info.icon_from_external_svg.clone();
384        self.is_live = true;
385        self.is_background = info.is_background;
386        self.is_title_generating = info.is_title_generating;
387        self.diff_stats = info.diff_stats;
388    }
389}
390
391#[derive(Clone)]
392enum ListEntry {
393    ProjectHeader {
394        key: ProjectGroupKey,
395        label: SharedString,
396        highlight_positions: Vec<usize>,
397        has_running_threads: bool,
398        waiting_thread_count: usize,
399        has_notifications: bool,
400        is_active: bool,
401        has_threads: bool,
402    },
403    Thread(Arc<ThreadEntry>),
404    Terminal(TerminalEntry),
405}
406
407#[derive(Clone)]
408enum ActivatableEntry {
409    Thread {
410        metadata: ThreadMetadata,
411        workspace: ThreadEntryWorkspace,
412    },
413    Terminal {
414        metadata: TerminalThreadMetadata,
415        workspace: ThreadEntryWorkspace,
416    },
417}
418
419impl ActivatableEntry {
420    fn from_list_entry(entry: &ListEntry) -> Option<Self> {
421        match entry {
422            ListEntry::Thread(thread) => Some(Self::Thread {
423                metadata: thread.metadata.clone(),
424                workspace: thread.workspace.clone(),
425            }),
426            ListEntry::Terminal(terminal) => Some(Self::Terminal {
427                metadata: terminal.metadata.clone(),
428                workspace: terminal.workspace.clone(),
429            }),
430            ListEntry::ProjectHeader { .. } => None,
431        }
432    }
433
434    fn project_location(&self, cx: &App) -> (PathList, ProjectGroupKey) {
435        match self {
436            Self::Thread {
437                workspace: ThreadEntryWorkspace::Open(workspace),
438                ..
439            }
440            | Self::Terminal {
441                workspace: ThreadEntryWorkspace::Open(workspace),
442                ..
443            } => (
444                PathList::new(&workspace.read(cx).root_paths(cx)),
445                workspace.read(cx).project_group_key(cx),
446            ),
447            Self::Thread {
448                workspace:
449                    ThreadEntryWorkspace::Closed {
450                        folder_paths,
451                        project_group_key,
452                    },
453                ..
454            }
455            | Self::Terminal {
456                workspace:
457                    ThreadEntryWorkspace::Closed {
458                        folder_paths,
459                        project_group_key,
460                    },
461                ..
462            } => (folder_paths.clone(), project_group_key.clone()),
463        }
464    }
465}
466
467#[cfg(test)]
468impl ListEntry {
469    fn session_id(&self) -> Option<&acp::SessionId> {
470        match self {
471            ListEntry::Thread(thread_entry) => thread_entry.metadata.session_id.as_ref(),
472            ListEntry::Terminal(_) | ListEntry::ProjectHeader { .. } => None,
473        }
474    }
475
476    fn reachable_workspaces<'a>(
477        &'a self,
478        multi_workspace: &'a workspace::MultiWorkspace,
479        cx: &'a App,
480    ) -> Vec<Entity<Workspace>> {
481        match self {
482            ListEntry::Thread(thread) => match &thread.workspace {
483                ThreadEntryWorkspace::Open(ws) => vec![ws.clone()],
484                ThreadEntryWorkspace::Closed { .. } => Vec::new(),
485            },
486            ListEntry::Terminal(terminal) => match &terminal.workspace {
487                ThreadEntryWorkspace::Open(workspace) => vec![workspace.clone()],
488                ThreadEntryWorkspace::Closed { .. } => Vec::new(),
489            },
490            ListEntry::ProjectHeader { key, .. } => multi_workspace
491                .workspaces_for_project_group(key, cx)
492                .unwrap_or_default(),
493        }
494    }
495}
496
497impl From<ThreadEntry> for ListEntry {
498    fn from(thread: ThreadEntry) -> Self {
499        ListEntry::Thread(Arc::new(thread))
500    }
501}
502
503impl From<TerminalEntry> for ListEntry {
504    fn from(terminal: TerminalEntry) -> Self {
505        ListEntry::Terminal(terminal)
506    }
507}
508
509#[derive(Default)]
510struct SidebarContents {
511    entries: Vec<ListEntry>,
512    notified_threads: HashSet<agent_ui::ThreadId>,
513    notified_terminals: HashSet<TerminalId>,
514    project_header_indices: Vec<usize>,
515    has_open_projects: bool,
516}
517
518/// Identity-and-layout key for a [`ListEntry`] used to preserve measured list items
519/// across rebuilds. Equal shapes must render to the same height; add any new
520/// height-affecting state here.
521#[derive(Debug, PartialEq, Eq)]
522enum EntryShape {
523    ProjectHeader {
524        key: ProjectGroupKey,
525        // Toggles the "No threads yet" empty-state row when not collapsed.
526        has_threads: bool,
527        // Determines whether the "No threads yet" row is rendered (only shown when
528        // `!is_collapsed && !has_threads`).
529        is_collapsed: bool,
530    },
531    Thread(ThreadId),
532    Terminal(TerminalId),
533}
534
535impl SidebarContents {
536    fn is_thread_notified(&self, thread_id: &agent_ui::ThreadId) -> bool {
537        self.notified_threads.contains(thread_id)
538    }
539
540    fn is_terminal_notified(&self, terminal_id: TerminalId) -> bool {
541        self.notified_terminals.contains(&terminal_id)
542    }
543}
544
545// TODO: The mapping from workspace root paths to git repositories needs a
546// unified approach across the codebase: this function, `AgentPanel::classify_worktrees`,
547// thread persistence (which PathList is saved to the database), and thread
548// querying (which PathList is used to read threads back). All of these need
549// to agree on how repos are resolved for a given workspace, especially in
550// multi-root and nested-repo configurations.
551fn root_repository_snapshots(
552    workspace: &Entity<Workspace>,
553    cx: &App,
554) -> impl Iterator<Item = project::git_store::RepositorySnapshot> {
555    let path_list = workspace_path_list(workspace, cx);
556    let project = workspace.read(cx).project().read(cx);
557    project.repositories(cx).values().filter_map(move |repo| {
558        let snapshot = repo.read(cx).snapshot();
559        let is_root = path_list
560            .paths()
561            .iter()
562            .any(|p| p.as_path() == snapshot.work_directory_abs_path.as_ref());
563        is_root.then_some(snapshot)
564    })
565}
566
567fn workspace_path_list(workspace: &Entity<Workspace>, cx: &App) -> PathList {
568    PathList::new(&workspace.read(cx).root_paths(cx))
569}
570
571fn linked_worktree_path_lists_for_workspaces(
572    workspaces: &[Entity<Workspace>],
573    cx: &App,
574) -> Vec<PathList> {
575    let mut linked_worktree_paths = Vec::new();
576    for workspace in workspaces {
577        if workspace.read(cx).visible_worktrees(cx).count() != 1 {
578            continue;
579        }
580        for snapshot in root_repository_snapshots(workspace, cx) {
581            linked_worktree_paths.extend(
582                snapshot.linked_worktrees().iter().map(|linked_worktree| {
583                    PathList::new(std::slice::from_ref(&linked_worktree.path))
584                }),
585            );
586        }
587    }
588
589    linked_worktree_paths.sort_by(|a, b| a.paths()[0].cmp(&b.paths()[0]));
590    linked_worktree_paths
591}
592
593fn workspace_has_terminal_metadata_except(
594    workspace: &Entity<Workspace>,
595    except_terminal_id: Option<TerminalId>,
596    cx: &App,
597) -> bool {
598    let Some(store) = TerminalThreadMetadataStore::try_global(cx) else {
599        return false;
600    };
601    let path_list = workspace_path_list(workspace, cx);
602    let remote_connection = workspace
603        .read(cx)
604        .project()
605        .read(cx)
606        .remote_connection_options(cx);
607    store
608        .read(cx)
609        .entries_for_path(&path_list, remote_connection.as_ref())
610        .any(|terminal| except_terminal_id != Some(terminal.terminal_id))
611}
612
613#[derive(Clone)]
614struct WorkspaceMenuWorktreeLabel {
615    icon: Option<IconName>,
616    primary_name: SharedString,
617    secondary_name: Option<SharedString>,
618}
619
620impl WorkspaceMenuWorktreeLabel {
621    fn render(&self) -> impl IntoElement {
622        h_flex()
623            .min_w_0()
624            .gap_0p5()
625            .when_some(self.icon, |this, icon| {
626                this.child(Icon::new(icon).size(IconSize::XSmall).color(Color::Muted))
627            })
628            .child(Label::new(self.primary_name.clone()).truncate())
629            .when_some(self.secondary_name.clone(), |this, secondary_name| {
630                this.child(Label::new("/").alpha(0.5))
631                    .child(Label::new(secondary_name).truncate())
632            })
633    }
634}
635
636fn workspace_menu_worktree_labels(
637    workspace: &Entity<Workspace>,
638    cx: &App,
639) -> Vec<WorkspaceMenuWorktreeLabel> {
640    let root_paths = workspace.read(cx).root_paths(cx);
641    let show_folder_name = root_paths.len() > 1;
642    let project = workspace.read(cx).project().clone();
643    let repository_snapshots: Vec<_> = project
644        .read(cx)
645        .repositories(cx)
646        .values()
647        .map(|repo| repo.read(cx).snapshot())
648        .collect();
649
650    root_paths
651        .into_iter()
652        .map(|root_path| {
653            let root_path = root_path.as_ref();
654            let folder_name = root_path
655                .file_name()
656                .map(|name| SharedString::from(name.to_string_lossy().to_string()))
657                .unwrap_or_default();
658            let repository_snapshot = repository_snapshots
659                .iter()
660                .find(|snapshot| snapshot.work_directory_abs_path.as_ref() == root_path);
661
662            if let Some(snapshot) = repository_snapshot {
663                let worktree_name = if snapshot.is_linked_worktree() {
664                    snapshot
665                        .main_worktree_abs_path()
666                        .and_then(|main_worktree_path| {
667                            project::linked_worktree_short_name(main_worktree_path, root_path)
668                        })
669                        .unwrap_or_else(|| folder_name.clone())
670                } else {
671                    "main".into()
672                };
673
674                if show_folder_name {
675                    WorkspaceMenuWorktreeLabel {
676                        icon: Some(IconName::GitWorktree),
677                        primary_name: folder_name,
678                        secondary_name: Some(worktree_name),
679                    }
680                } else {
681                    WorkspaceMenuWorktreeLabel {
682                        icon: Some(IconName::GitWorktree),
683                        primary_name: worktree_name,
684                        secondary_name: None,
685                    }
686                }
687            } else {
688                WorkspaceMenuWorktreeLabel {
689                    icon: None,
690                    primary_name: folder_name,
691                    secondary_name: None,
692                }
693            }
694        })
695        .collect()
696}
697
698fn apply_worktree_label_mode(
699    mut worktrees: Vec<ThreadItemWorktreeInfo>,
700    mode: AgentThreadWorktreeLabel,
701) -> Vec<ThreadItemWorktreeInfo> {
702    match mode {
703        AgentThreadWorktreeLabel::Both => {}
704        AgentThreadWorktreeLabel::Worktree => {
705            for wt in &mut worktrees {
706                wt.branch_name = None;
707            }
708        }
709        AgentThreadWorktreeLabel::Branch => {
710            for wt in &mut worktrees {
711                // Fall back to showing the worktree name when no branch is
712                // known; an empty chip would be worse than a mismatched icon.
713                if wt.branch_name.is_some() {
714                    wt.worktree_name = None;
715                }
716            }
717        }
718    }
719    worktrees
720}
721
722/// Shows a [`RemoteConnectionModal`] on the given workspace and establishes
723/// an SSH connection. Suitable for passing to
724/// [`MultiWorkspace::find_or_create_workspace`] as the `connect_remote`
725/// argument.
726fn connect_remote(
727    modal_workspace: Entity<Workspace>,
728    connection_options: RemoteConnectionOptions,
729    window: &mut Window,
730    cx: &mut Context<MultiWorkspace>,
731) -> gpui::Task<anyhow::Result<Option<Entity<remote::RemoteClient>>>> {
732    remote_connection::connect_with_modal(&modal_workspace, connection_options, window, cx)
733}
734
735// Per-project-group cache of the remote default branch, used to populate the
736// "Create New Worktree" submenu without doing git I/O while the menu is open.
737enum DefaultBranchCache {
738    Pending,
739    Resolved(Option<RemoteBranchName>),
740}
741
742// Mirrors the behavior of the worktree picker's "Create new worktree" entries.
743fn create_worktree_in_workspace(
744    workspace: &Entity<Workspace>,
745    branch_target: NewWorktreeBranchTarget,
746    window: &mut Window,
747    cx: &mut App,
748) {
749    workspace.update(cx, |workspace, cx| {
750        let focused_dock = workspace.focused_dock_position(window, cx);
751        git_ui::worktree_service::handle_create_worktree(
752            workspace,
753            &CreateWorktree {
754                worktree_name: None,
755                branch_target,
756            },
757            window,
758            focused_dock,
759            cx,
760        );
761    });
762}
763
764/// The sidebar re-derives its entire entry list from scratch on every
765/// change via `update_entries` → `rebuild_contents`. Avoid adding
766/// incremental or inter-event coordination state — if something can
767/// be computed from the current world state, compute it in the rebuild.
768pub struct Sidebar {
769    multi_workspace: WeakEntity<MultiWorkspace>,
770    width: Pixels,
771    focus_handle: FocusHandle,
772    filter_editor: Entity<Editor>,
773    thread_rename_editor: Entity<Editor>,
774    list_state: ListState,
775    contents: SidebarContents,
776    /// The index of the list item that currently has the keyboard focus
777    ///
778    /// Note: This is NOT the same as the active item.
779    selection: Option<usize>,
780    /// Tracks which sidebar entry is currently active (highlighted).
781    active_entry: Option<ActiveEntry>,
782    hovered_thread_index: Option<usize>,
783    renaming_thread_id: Option<ThreadId>,
784    /// Threads in the database-backed regeneration path need their own loading
785    /// state because they do not have a live `agent::Thread` to report it.
786    regenerating_titles: HashSet<ThreadId>,
787    /// start_renaming_thread must seed current title into the title editor
788    /// so this prevents that BufferEdited event from being interpreted as user input.
789    suppress_next_rename_edit: bool,
790
791    /// Updated only in response to explicit user actions (clicking a
792    /// thread, confirming in the thread switcher, etc.) — never from
793    /// background data changes. Used to sort the thread switcher popup.
794    thread_last_accessed: HashMap<ThreadId, DateTime<Utc>>,
795    terminal_last_accessed: HashMap<TerminalId, DateTime<Utc>>,
796    thread_switcher: Option<Entity<ThreadSwitcher>>,
797    _thread_switcher_subscriptions: Vec<gpui::Subscription>,
798    pending_thread_activation: Option<agent_ui::ThreadId>,
799    /// Persists live thread statuses across rebuilds so that Running→Completed
800    /// transitions can be detected even when the group is collapsed (and
801    /// thread entries are not present in the list).
802    live_thread_statuses: HashMap<acp::SessionId, (AgentThreadStatus, ThreadId)>,
803    /// Remembers whether each draft last rendered as empty or with content so
804    /// that when a draft that was empty gains content again, we refresh
805    /// its interaction time.
806    draft_kinds: HashMap<ThreadId, DraftKind>,
807    view: SidebarView,
808    restoring_tasks: HashMap<agent_ui::ThreadId, Task<()>>,
809    recent_projects_popover_handle: PopoverMenuHandle<SidebarRecentProjects>,
810    project_header_menu_handles: HashMap<usize, PopoverMenuHandle<ContextMenu>>,
811    project_header_new_thread_menu_handles: HashMap<usize, PopoverMenuHandle<ContextMenu>>,
812    project_header_menu_ix: Option<usize>,
813    worktree_default_branches: HashMap<ProjectGroupKey, DefaultBranchCache>,
814    _subscriptions: Vec<gpui::Subscription>,
815    _draft_editor_observations: Vec<gpui::Subscription>,
816    update_task: Option<Task<()>>,
817    /// For the thread import banners, if there is just one we show "Import
818    /// Threads" but if we are showing both the external agents and other
819    /// channels import banners then we change the text to disambiguate the
820    /// buttons. This field tracks whether we were using verbose labels so they
821    /// can stay stable after dismissing one of the banners.
822    import_banners_use_verbose_labels: Option<bool>,
823    /// Display names of other release channels that have threads available to
824    /// import.
825    cross_channel_import_channels: Vec<SharedString>,
826}
827
828impl Sidebar {
829    pub fn new(
830        multi_workspace: Entity<MultiWorkspace>,
831        window: &mut Window,
832        cx: &mut Context<Self>,
833    ) -> Self {
834        let focus_handle = cx.focus_handle();
835        cx.on_focus_in(&focus_handle, window, Self::focus_in)
836            .detach();
837
838        AgentThreadWorktreeLabelFlag::watch(cx);
839
840        let filter_editor = cx.new(|cx| {
841            let mut editor = Editor::single_line(window, cx);
842            editor.set_placeholder_text("Search threads…", window, cx);
843            editor
844        });
845        let thread_rename_editor = cx.new(|cx| Editor::single_line(window, cx));
846
847        cx.subscribe_in(
848            &multi_workspace,
849            window,
850            |this, _multi_workspace, event: &MultiWorkspaceEvent, window, cx| match event {
851                MultiWorkspaceEvent::ActiveWorkspaceChanged { .. } => {
852                    this.sync_active_entry_from_active_workspace(cx);
853                    this.replace_archived_panel_thread(window, cx);
854                    this.schedule_update_entries(false, cx);
855                }
856                MultiWorkspaceEvent::WorkspaceAdded(workspace) => {
857                    this.subscribe_to_workspace(workspace, window, cx);
858                    this.schedule_update_entries(false, cx);
859                }
860                MultiWorkspaceEvent::WorkspaceRemoved(_)
861                | MultiWorkspaceEvent::ProjectGroupsChanged => {
862                    this.schedule_update_entries(false, cx);
863                }
864            },
865        )
866        .detach();
867
868        cx.subscribe(&filter_editor, |this: &mut Self, _, event, cx| {
869            if let editor::EditorEvent::BufferEdited = event {
870                let query = this.filter_editor.read(cx).text(cx);
871                if !query.is_empty() {
872                    this.selection.take();
873                }
874                this.schedule_update_entries(!query.is_empty(), cx);
875            }
876        })
877        .detach();
878
879        cx.subscribe_in(
880            &thread_rename_editor,
881            window,
882            |this, title_editor, event, window, cx| {
883                this.handle_thread_rename_editor_event(title_editor, event, window, cx);
884            },
885        )
886        .detach();
887
888        cx.observe(&ThreadMetadataStore::global(cx), |this, _store, cx| {
889            this.schedule_update_entries(false, cx);
890        })
891        .detach();
892
893        cx.observe(
894            &TerminalThreadMetadataStore::global(cx),
895            |this, _store, cx| {
896                this.schedule_update_entries(false, cx);
897            },
898        )
899        .detach();
900
901        let channels_with_threads = channels_with_threads(cx);
902        cx.spawn(async move |this, cx| {
903            let channels = channels_with_threads.await;
904            this.update(cx, |this, cx| {
905                this.cross_channel_import_channels = channels;
906                cx.notify();
907            })
908            .ok();
909        })
910        .detach();
911
912        let deferred_multi_workspace = multi_workspace.downgrade();
913        cx.defer_in(window, move |this, window, cx| {
914            if let Some(multi_workspace) = deferred_multi_workspace.upgrade() {
915                let workspaces: Vec<_> = multi_workspace.read(cx).workspaces().cloned().collect();
916                for workspace in &workspaces {
917                    this.subscribe_to_workspace(workspace, window, cx);
918                }
919            }
920            this.schedule_update_entries(false, cx);
921        });
922
923        Self {
924            multi_workspace: multi_workspace.downgrade(),
925            width: DEFAULT_WIDTH,
926            focus_handle,
927            filter_editor,
928            thread_rename_editor,
929            list_state: ListState::new(0, gpui::ListAlignment::Top, px(1000.)),
930            contents: SidebarContents::default(),
931            selection: None,
932            active_entry: None,
933            hovered_thread_index: None,
934            renaming_thread_id: None,
935            regenerating_titles: HashSet::new(),
936            suppress_next_rename_edit: false,
937
938            thread_last_accessed: HashMap::new(),
939            terminal_last_accessed: HashMap::new(),
940            thread_switcher: None,
941            _thread_switcher_subscriptions: Vec::new(),
942            pending_thread_activation: None,
943            live_thread_statuses: HashMap::new(),
944            draft_kinds: HashMap::new(),
945            view: SidebarView::default(),
946            restoring_tasks: HashMap::new(),
947            recent_projects_popover_handle: PopoverMenuHandle::default(),
948            project_header_menu_handles: HashMap::new(),
949            project_header_new_thread_menu_handles: HashMap::new(),
950            project_header_menu_ix: None,
951            worktree_default_branches: HashMap::new(),
952            _subscriptions: Vec::new(),
953            _draft_editor_observations: Vec::new(),
954            update_task: None,
955            import_banners_use_verbose_labels: None,
956            cross_channel_import_channels: Vec::new(),
957        }
958    }
959
960    fn serialize(&mut self, cx: &mut Context<Self>) {
961        cx.emit(workspace::SidebarEvent::SerializeNeeded);
962    }
963
964    fn is_group_collapsed(&self, key: &ProjectGroupKey, cx: &App) -> bool {
965        self.multi_workspace
966            .upgrade()
967            .and_then(|mw| {
968                mw.read(cx)
969                    .group_state_by_key(key)
970                    .map(|state| !state.expanded)
971            })
972            .unwrap_or(false)
973    }
974
975    fn set_group_expanded(&self, key: &ProjectGroupKey, expanded: bool, cx: &mut Context<Self>) {
976        if let Some(mw) = self.multi_workspace.upgrade() {
977            mw.update(cx, |mw, cx| {
978                if let Some(state) = mw.group_state_by_key_mut(key) {
979                    state.expanded = expanded;
980                }
981                mw.serialize(cx);
982            });
983        }
984    }
985
986    fn is_active_workspace(&self, workspace: &Entity<Workspace>, cx: &App) -> bool {
987        self.multi_workspace
988            .upgrade()
989            .map_or(false, |mw| mw.read(cx).workspace() == workspace)
990    }
991
992    fn subscribe_to_workspace(
993        &mut self,
994        workspace: &Entity<Workspace>,
995        window: &mut Window,
996        cx: &mut Context<Self>,
997    ) {
998        let project = workspace.read(cx).project().clone();
999        if project.read(cx).is_via_collab() {
1000            return;
1001        }
1002
1003        cx.subscribe_in(
1004            &project,
1005            window,
1006            |this, project, event, _window, cx| match event {
1007                ProjectEvent::WorktreeAdded(_)
1008                | ProjectEvent::WorktreeRemoved(_)
1009                | ProjectEvent::WorktreeOrderChanged => {
1010                    this.schedule_update_entries(false, cx);
1011                }
1012                ProjectEvent::WorktreePathsChanged { old_worktree_paths } => {
1013                    this.move_entry_paths(project, old_worktree_paths, cx);
1014                    this.schedule_update_entries(false, cx);
1015                }
1016                _ => {}
1017            },
1018        )
1019        .detach();
1020
1021        let git_store = workspace.read(cx).project().read(cx).git_store().clone();
1022        cx.subscribe_in(
1023            &git_store,
1024            window,
1025            |this, _, event: &project::git_store::GitStoreEvent, _window, cx| {
1026                if matches!(
1027                    event,
1028                    project::git_store::GitStoreEvent::RepositoryUpdated(
1029                        _,
1030                        project::git_store::RepositoryEvent::GitWorktreeListChanged
1031                            | project::git_store::RepositoryEvent::HeadChanged,
1032                        _,
1033                    )
1034                ) {
1035                    this.schedule_update_entries(false, cx);
1036                }
1037            },
1038        )
1039        .detach();
1040
1041        cx.subscribe_in(
1042            workspace,
1043            window,
1044            move |this, workspace, event: &workspace::Event, window, cx| {
1045                if let workspace::Event::PanelAdded(view) = event {
1046                    if let Ok(agent_panel) = view.clone().downcast::<AgentPanel>() {
1047                        this.subscribe_to_agent_panel(workspace, &agent_panel, window, cx);
1048                        this.schedule_update_entries(false, cx);
1049                    }
1050                }
1051            },
1052        )
1053        .detach();
1054
1055        self.observe_docks(workspace, cx);
1056
1057        if let Some(agent_panel) = workspace.read(cx).panel::<AgentPanel>(cx) {
1058            self.subscribe_to_agent_panel(workspace, &agent_panel, window, cx);
1059        }
1060    }
1061
1062    fn move_entry_paths(
1063        &mut self,
1064        project: &Entity<project::Project>,
1065        old_paths: &WorktreePaths,
1066        cx: &mut Context<Self>,
1067    ) {
1068        if project.read(cx).is_via_collab() {
1069            return;
1070        }
1071
1072        let new_paths = project.read(cx).worktree_paths(cx);
1073        let old_folder_paths = old_paths.folder_path_list().clone();
1074
1075        let added_pairs: Vec<_> = new_paths
1076            .ordered_pairs()
1077            .filter(|(main, folder)| {
1078                !old_paths
1079                    .ordered_pairs()
1080                    .any(|(old_main, old_folder)| old_main == *main && old_folder == *folder)
1081            })
1082            .map(|(m, f)| (m.clone(), f.clone()))
1083            .collect();
1084
1085        let new_folder_paths = new_paths.folder_path_list();
1086        let removed_folder_paths: Vec<PathBuf> = old_folder_paths
1087            .paths()
1088            .iter()
1089            .filter(|p| !new_folder_paths.paths().contains(p))
1090            .cloned()
1091            .collect();
1092
1093        if added_pairs.is_empty() && removed_folder_paths.is_empty() {
1094            return;
1095        }
1096
1097        let remote_connection = project.read(cx).remote_connection_options(cx);
1098        let apply_path_changes = |paths: &mut WorktreePaths| {
1099            for (main_path, folder_path) in &added_pairs {
1100                paths.add_path(main_path, folder_path);
1101            }
1102            for path in &removed_folder_paths {
1103                paths.remove_folder_path(path);
1104            }
1105        };
1106        ThreadMetadataStore::global(cx).update(cx, |store, store_cx| {
1107            store.change_worktree_paths(
1108                &old_folder_paths,
1109                remote_connection.as_ref(),
1110                &apply_path_changes,
1111                store_cx,
1112            );
1113        });
1114        TerminalThreadMetadataStore::global(cx).update(cx, |store, store_cx| {
1115            store.change_worktree_paths(
1116                &old_folder_paths,
1117                remote_connection.as_ref(),
1118                &apply_path_changes,
1119                store_cx,
1120            );
1121        });
1122    }
1123
1124    fn subscribe_to_agent_panel(
1125        &mut self,
1126        workspace: &Entity<Workspace>,
1127        agent_panel: &Entity<AgentPanel>,
1128        window: &mut Window,
1129        cx: &mut Context<Self>,
1130    ) {
1131        let workspace = workspace.downgrade();
1132        cx.subscribe_in(
1133            agent_panel,
1134            window,
1135            move |this, agent_panel, event: &AgentPanelEvent, window, cx| match event {
1136                AgentPanelEvent::ActiveViewChanged
1137                | AgentPanelEvent::ActiveViewFocused
1138                | AgentPanelEvent::EntryChanged => {
1139                    this.sync_active_entry_from_panel(agent_panel, cx);
1140                    this.schedule_update_entries(false, cx);
1141                }
1142                AgentPanelEvent::TerminalCloseRequested { metadata } => {
1143                    if let Some(workspace) = workspace.upgrade() {
1144                        let workspace = ThreadEntryWorkspace::Open(workspace);
1145                        this.close_terminal(metadata, &workspace, window, cx);
1146                    }
1147                }
1148                AgentPanelEvent::ThreadInteracted { thread_id } => {
1149                    this.record_thread_interacted(thread_id, cx);
1150                    this.schedule_update_entries(false, cx);
1151                }
1152            },
1153        )
1154        .detach();
1155    }
1156
1157    fn sync_active_entry_from_active_workspace(&mut self, cx: &App) {
1158        let panel = self
1159            .active_workspace(cx)
1160            .and_then(|ws| ws.read(cx).panel::<AgentPanel>(cx));
1161        if let Some(panel) = panel {
1162            self.sync_active_entry_from_panel(&panel, cx);
1163        }
1164    }
1165
1166    /// When switching workspaces, the active panel may still be showing
1167    /// a thread that was archived from a different workspace. In that
1168    /// case, create a fresh draft so the panel has valid content and
1169    /// `active_entry` can point at it.
1170    fn replace_archived_panel_thread(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1171        let Some(workspace) = self.active_workspace(cx) else {
1172            return;
1173        };
1174        let Some(panel) = workspace.read(cx).panel::<AgentPanel>(cx) else {
1175            return;
1176        };
1177        let Some(thread_id) = panel.read(cx).active_thread_id(cx) else {
1178            return;
1179        };
1180        let is_archived = ThreadMetadataStore::global(cx)
1181            .read(cx)
1182            .entry(thread_id)
1183            .is_some_and(|m| m.archived);
1184        if is_archived {
1185            self.create_new_thread(&workspace, window, cx);
1186        }
1187    }
1188
1189    /// Syncs `active_entry` from the agent panel's current state.
1190    /// Called from `ActiveViewChanged` — the panel has settled into its
1191    /// new view, so we can safely read it without race conditions.
1192    ///
1193    /// Also resolves `pending_thread_activation` when the panel's
1194    /// active thread matches the pending activation.
1195    fn sync_active_entry_from_panel(&mut self, agent_panel: &Entity<AgentPanel>, cx: &App) -> bool {
1196        let Some(active_workspace) = self.active_workspace(cx) else {
1197            return false;
1198        };
1199
1200        // Only sync when the event comes from the active workspace's panel.
1201        let is_active_panel = active_workspace
1202            .read(cx)
1203            .panel::<AgentPanel>(cx)
1204            .is_some_and(|p| p == *agent_panel);
1205        if !is_active_panel {
1206            return false;
1207        }
1208
1209        let panel = agent_panel.read(cx);
1210
1211        if let Some(pending_thread_id) = self.pending_thread_activation {
1212            let panel_thread_id = panel
1213                .active_conversation_view()
1214                .map(|cv| cv.read(cx).parent_id());
1215
1216            if panel_thread_id == Some(pending_thread_id) {
1217                let session_id = panel
1218                    .active_agent_thread(cx)
1219                    .map(|thread| thread.read(cx).session_id().clone());
1220                self.active_entry = Some(ActiveEntry::Thread {
1221                    thread_id: pending_thread_id,
1222                    session_id,
1223                    workspace: active_workspace,
1224                });
1225                self.pending_thread_activation = None;
1226                return true;
1227            }
1228            // Pending activation not yet resolved — keep current active_entry.
1229            return false;
1230        }
1231
1232        if let Some(terminal_id) = panel.active_terminal_id() {
1233            self.active_entry = Some(ActiveEntry::Terminal {
1234                terminal_id,
1235                workspace: active_workspace,
1236            });
1237        } else if let Some(thread_id) = panel.active_thread_id(cx) {
1238            let is_archived = ThreadMetadataStore::global(cx)
1239                .read(cx)
1240                .entry(thread_id)
1241                .is_some_and(|m| m.archived);
1242            if !is_archived {
1243                let session_id = panel
1244                    .active_agent_thread(cx)
1245                    .map(|thread| thread.read(cx).session_id().clone());
1246                self.active_entry = Some(ActiveEntry::Thread {
1247                    thread_id,
1248                    session_id,
1249                    workspace: active_workspace,
1250                });
1251            }
1252        }
1253
1254        false
1255    }
1256
1257    fn observe_docks(&mut self, workspace: &Entity<Workspace>, cx: &mut Context<Self>) {
1258        let docks: Vec<_> = workspace
1259            .read(cx)
1260            .all_docks()
1261            .into_iter()
1262            .cloned()
1263            .collect();
1264        let workspace = workspace.downgrade();
1265        for dock in docks {
1266            let workspace = workspace.clone();
1267            cx.observe(&dock, move |this, _dock, cx| {
1268                let Some(workspace) = workspace.upgrade() else {
1269                    return;
1270                };
1271                if !this.is_active_workspace(&workspace, cx) {
1272                    return;
1273                }
1274
1275                cx.notify();
1276            })
1277            .detach();
1278        }
1279    }
1280
1281    /// Opens a new workspace for a group that has no open workspaces.
1282    fn open_workspace_for_group(
1283        &mut self,
1284        project_group_key: &ProjectGroupKey,
1285        window: &mut Window,
1286        cx: &mut Context<Self>,
1287    ) {
1288        let Some(multi_workspace) = self.multi_workspace.upgrade() else {
1289            return;
1290        };
1291        let path_list = project_group_key.path_list().clone();
1292        let host = project_group_key.host();
1293        let provisional_key = Some(project_group_key.clone());
1294        let active_workspace = multi_workspace.read(cx).workspace().clone();
1295        let modal_workspace = active_workspace.clone();
1296
1297        let task = multi_workspace.update(cx, |this, cx| {
1298            this.find_or_create_workspace(
1299                path_list,
1300                host,
1301                provisional_key,
1302                |options, window, cx| connect_remote(active_workspace, options, window, cx),
1303                &[],
1304                None,
1305                OpenMode::Activate,
1306                window,
1307                cx,
1308            )
1309        });
1310
1311        cx.spawn_in(window, async move |_this, cx| {
1312            let result = task.await;
1313            remote_connection::dismiss_connection_modal(&modal_workspace, cx);
1314            result?;
1315            anyhow::Ok(())
1316        })
1317        .detach_and_log_err(cx);
1318    }
1319
1320    fn open_workspace_and_create_entry(
1321        &mut self,
1322        project_group_key: &ProjectGroupKey,
1323        target: NewEntryTarget,
1324        window: &mut Window,
1325        cx: &mut Context<Self>,
1326    ) {
1327        let Some(multi_workspace) = self.multi_workspace.upgrade() else {
1328            return;
1329        };
1330
1331        let path_list = project_group_key.path_list().clone();
1332        let host = project_group_key.host();
1333        let provisional_key = Some(project_group_key.clone());
1334        let active_workspace = multi_workspace.read(cx).workspace().clone();
1335
1336        let task = multi_workspace.update(cx, |this, cx| {
1337            this.find_or_create_workspace(
1338                path_list,
1339                host,
1340                provisional_key,
1341                |options, window, cx| connect_remote(active_workspace, options, window, cx),
1342                &[],
1343                None,
1344                OpenMode::Activate,
1345                window,
1346                cx,
1347            )
1348        });
1349
1350        cx.spawn_in(window, async move |this, cx| {
1351            let workspace = task.await?;
1352            this.update_in(cx, |this, window, cx| match target {
1353                NewEntryTarget::LastCreatedKind => this.create_new_entry(&workspace, window, cx),
1354                NewEntryTarget::Terminal => this.create_new_terminal(&workspace, window, cx),
1355            })?;
1356            anyhow::Ok(())
1357        })
1358        .detach_and_log_err(cx);
1359    }
1360
1361    /// Rebuilds the sidebar contents from current workspace and thread state.
1362    ///
1363    /// Iterates [`MultiWorkspace::project_group_keys`] to determine project
1364    /// groups, then populates thread entries from the metadata store and
1365    /// merges live thread info from active agent panels.
1366    ///
1367    /// Aim for a single forward pass over workspaces and threads plus an
1368    /// O(T log T) sort. Avoid adding extra scans over the data.
1369    ///
1370    /// Properties:
1371    ///
1372    /// - Should always show every workspace in the multiworkspace
1373    ///     - If you have no threads, and two workspaces for the worktree and the main workspace, make sure at least one is shown
1374    /// - Should always show every thread, associated with each workspace in the multiworkspace
1375    /// - After every build_contents, our "active" state should exactly match the current workspace's, current agent panel's current thread.
1376    fn rebuild_contents(&mut self, cx: &App) {
1377        let Some(multi_workspace) = self.multi_workspace.upgrade() else {
1378            return;
1379        };
1380        let mw = multi_workspace.read(cx);
1381        let workspaces: Vec<_> = mw.workspaces().cloned().collect();
1382        let active_workspace = Some(mw.workspace().clone());
1383
1384        let agent_server_store = workspaces
1385            .first()
1386            .map(|ws| ws.read(cx).project().read(cx).agent_server_store().clone());
1387
1388        let query = self.filter_editor.read(cx).text(cx);
1389
1390        let previous = mem::take(&mut self.contents);
1391
1392        let old_statuses = &self.live_thread_statuses;
1393
1394        let mut entries = Vec::new();
1395        let mut notified_threads = previous.notified_threads;
1396        let mut notified_terminals: HashSet<TerminalId> = HashSet::new();
1397        let mut new_live_statuses: HashMap<acp::SessionId, (AgentThreadStatus, ThreadId)> =
1398            HashMap::new();
1399        let mut current_session_ids: HashSet<acp::SessionId> = HashSet::new();
1400        let mut current_thread_ids: HashSet<agent_ui::ThreadId> = HashSet::new();
1401        let mut current_terminal_ids: HashSet<TerminalId> = HashSet::new();
1402        let mut project_header_indices: Vec<usize> = Vec::new();
1403        let mut seen_thread_ids: HashSet<agent_ui::ThreadId> = HashSet::new();
1404        let mut seen_terminal_ids: HashSet<TerminalId> = HashSet::new();
1405
1406        let has_open_projects = workspaces
1407            .iter()
1408            .any(|ws| !workspace_path_list(ws, cx).paths().is_empty());
1409
1410        let resolve_agent_icon = |agent_id: &AgentId| -> (IconName, Option<SharedString>) {
1411            let agent = Agent::from(agent_id.clone());
1412            let icon = match agent {
1413                Agent::NativeAgent => IconName::OmegaAgent,
1414                Agent::Custom { .. } => IconName::Terminal,
1415
1416                _ => IconName::OmegaAgent,
1417            };
1418            let icon_from_external_svg = agent_server_store
1419                .as_ref()
1420                .and_then(|store| store.read(cx).agent_icon(&agent_id));
1421            (icon, icon_from_external_svg)
1422        };
1423
1424        let groups = mw.project_groups(cx);
1425        let mut live_notified_terminal_ids: HashSet<TerminalId> = HashSet::new();
1426        for workspace in &workspaces {
1427            if let Some(agent_panel) = workspace.read(cx).panel::<AgentPanel>(cx) {
1428                live_notified_terminal_ids.extend(
1429                    agent_panel
1430                        .read(cx)
1431                        .terminals(cx)
1432                        .into_iter()
1433                        .filter_map(|terminal| terminal.has_notification.then_some(terminal.id)),
1434                );
1435            }
1436        }
1437
1438        let mut all_paths: Vec<PathBuf> = groups
1439            .iter()
1440            .flat_map(|group| group.key.path_list().paths().iter().cloned())
1441            .collect();
1442        all_paths.sort_unstable();
1443        all_paths.dedup();
1444        let path_details =
1445            util::disambiguate::compute_disambiguation_details(&all_paths, |path, detail| {
1446                project::path_suffix(path, detail)
1447            });
1448        let path_detail_map: HashMap<PathBuf, usize> =
1449            all_paths.into_iter().zip(path_details).collect();
1450
1451        let mut branch_by_path: HashMap<PathBuf, SharedString> = HashMap::new();
1452        for ws in &workspaces {
1453            let project = ws.read(cx).project().read(cx);
1454            for repo in project.repositories(cx).values() {
1455                let snapshot = repo.read(cx).snapshot();
1456                if let Some(branch) = &snapshot.branch {
1457                    branch_by_path.insert(
1458                        snapshot.work_directory_abs_path.to_path_buf(),
1459                        SharedString::from(Arc::<str>::from(branch.name())),
1460                    );
1461                }
1462                for linked_wt in snapshot.linked_worktrees() {
1463                    if let Some(branch) = linked_wt.branch_name() {
1464                        branch_by_path.insert(
1465                            linked_wt.path.clone(),
1466                            SharedString::from(Arc::<str>::from(branch)),
1467                        );
1468                    }
1469                }
1470            }
1471        }
1472
1473        for group in &groups {
1474            let group_key = &group.key;
1475            let group_workspaces = &group.workspaces;
1476
1477            let workspace_by_path_list: HashMap<PathList, &Entity<Workspace>> = group_workspaces
1478                .iter()
1479                .map(|ws| (workspace_path_list(ws, cx), ws))
1480                .collect();
1481            let resolve_workspace = |folder_paths: &PathList| -> ThreadEntryWorkspace {
1482                workspace_by_path_list
1483                    .get(folder_paths)
1484                    .map(|ws| ThreadEntryWorkspace::Open((*ws).clone()))
1485                    .unwrap_or_else(|| ThreadEntryWorkspace::Closed {
1486                        folder_paths: folder_paths.clone(),
1487                        project_group_key: group_key.clone(),
1488                    })
1489            };
1490            let linked_worktree_path_lists =
1491                linked_worktree_path_lists_for_workspaces(group_workspaces, cx);
1492            let make_terminal_entry =
1493                |metadata: TerminalThreadMetadata, workspace: ThreadEntryWorkspace| {
1494                    let worktrees =
1495                        worktree_info_from_thread_paths(&metadata.worktree_paths, &branch_by_path);
1496                    let has_notification =
1497                        live_notified_terminal_ids.contains(&metadata.terminal_id);
1498                    TerminalEntry {
1499                        metadata,
1500                        workspace,
1501                        worktrees,
1502                        has_notification,
1503                        highlight_positions: Vec::new(),
1504                    }
1505                };
1506
1507            let mut terminals = Vec::new();
1508            let terminal_store = TerminalThreadMetadataStore::global(cx);
1509            let group_host = group_key.host();
1510            let mut push_terminal_metadata =
1511                |metadata: TerminalThreadMetadata, workspace: ThreadEntryWorkspace| {
1512                    if !seen_terminal_ids.insert(metadata.terminal_id) {
1513                        return;
1514                    }
1515                    terminals.push(make_terminal_entry(metadata, workspace));
1516                };
1517            for row in terminal_store
1518                .read(cx)
1519                .entries_for_main_worktree_path(group_key.path_list(), group_host.as_ref())
1520                .cloned()
1521            {
1522                let workspace = resolve_workspace(row.folder_paths());
1523                push_terminal_metadata(row, workspace);
1524            }
1525            for row in terminal_store
1526                .read(cx)
1527                .entries_for_path(group_key.path_list(), group_host.as_ref())
1528                .cloned()
1529            {
1530                let workspace = resolve_workspace(row.folder_paths());
1531                push_terminal_metadata(row, workspace);
1532            }
1533            for ws in group_workspaces {
1534                let ws_paths = workspace_path_list(ws, cx);
1535                if ws_paths.paths().is_empty() {
1536                    continue;
1537                }
1538                for row in terminal_store
1539                    .read(cx)
1540                    .entries_for_path(&ws_paths, group_host.as_ref())
1541                    .cloned()
1542                {
1543                    push_terminal_metadata(row, ThreadEntryWorkspace::Open(ws.clone()));
1544                }
1545            }
1546            for worktree_path_list in &linked_worktree_path_lists {
1547                for row in terminal_store
1548                    .read(cx)
1549                    .entries_for_path(worktree_path_list, group_host.as_ref())
1550                    .cloned()
1551                {
1552                    push_terminal_metadata(
1553                        row,
1554                        ThreadEntryWorkspace::Closed {
1555                            folder_paths: worktree_path_list.clone(),
1556                            project_group_key: group_key.clone(),
1557                        },
1558                    );
1559                }
1560            }
1561            current_terminal_ids.extend(
1562                terminals
1563                    .iter()
1564                    .map(|terminal| terminal.metadata.terminal_id),
1565            );
1566            notified_terminals.extend(terminals.iter().filter_map(|terminal| {
1567                terminal
1568                    .has_notification
1569                    .then_some(terminal.metadata.terminal_id)
1570            }));
1571            if group_key.path_list().paths().is_empty() {
1572                continue;
1573            }
1574
1575            let label = group_key.display_name(&path_detail_map);
1576
1577            let is_collapsed = self.is_group_collapsed(group_key, cx);
1578            let should_load_threads = !is_collapsed || !query.is_empty();
1579
1580            let is_active = active_workspace
1581                .as_ref()
1582                .is_some_and(|active| group_workspaces.contains(active));
1583
1584            // Collect live thread infos from all workspaces in this group.
1585            let live_infos = group_workspaces
1586                .iter()
1587                .flat_map(|ws| all_thread_infos_for_workspace(ws, cx));
1588
1589            let mut threads: Vec<Arc<ThreadEntry>> = Vec::new();
1590            let mut has_running_threads = false;
1591            let mut waiting_thread_count: usize = 0;
1592            let group_host = group_key.host();
1593
1594            if should_load_threads {
1595                let thread_store = ThreadMetadataStore::global(cx);
1596
1597                let make_thread_entry =
1598                    |row: ThreadMetadata, workspace: ThreadEntryWorkspace| -> Arc<ThreadEntry> {
1599                        let (icon, icon_from_external_svg) = resolve_agent_icon(&row.agent_id);
1600                        let worktrees =
1601                            worktree_info_from_thread_paths(&row.worktree_paths, &branch_by_path);
1602                        // Start drafts as `WithContent`; the post-processing
1603                        // pass below downgrades them to `Empty` if no draft
1604                        // label can be derived.
1605                        let draft = row.is_draft().then_some(DraftKind::WithContent);
1606                        Arc::new(ThreadEntry {
1607                            metadata: row,
1608                            icon,
1609                            icon_from_external_svg,
1610                            status: AgentThreadStatus::default(),
1611                            workspace,
1612                            is_live: false,
1613                            is_background: false,
1614                            is_title_generating: false,
1615                            draft,
1616                            highlight_positions: Vec::new(),
1617                            worktrees,
1618                            diff_stats: DiffStats::default(),
1619                        })
1620                    };
1621
1622                // Main code path: one query per group via main_worktree_paths.
1623                // The main_worktree_paths column is set on all new threads and
1624                // points to the group's canonical paths regardless of which
1625                // linked worktree the thread was opened in.
1626                for row in thread_store
1627                    .read(cx)
1628                    .entries_for_main_worktree_path(group_key.path_list(), group_host.as_ref())
1629                    .cloned()
1630                {
1631                    if !seen_thread_ids.insert(row.thread_id) {
1632                        continue;
1633                    }
1634                    let workspace = resolve_workspace(row.folder_paths());
1635                    threads.push(make_thread_entry(row, workspace));
1636                }
1637
1638                // Legacy threads did not have `main_worktree_paths` populated, so they
1639                // must be queried by their `folder_paths`.
1640
1641                // Load any legacy threads for the main worktrees of this project group.
1642                for row in thread_store
1643                    .read(cx)
1644                    .entries_for_path(group_key.path_list(), group_host.as_ref())
1645                    .cloned()
1646                {
1647                    if !seen_thread_ids.insert(row.thread_id) {
1648                        continue;
1649                    }
1650                    let workspace = resolve_workspace(row.folder_paths());
1651                    threads.push(make_thread_entry(row, workspace));
1652                }
1653
1654                // Also surface any thread whose `folder_paths` equals
1655                // one of this group's open workspaces' root paths.
1656                // The three lookups above can all miss when the
1657                // thread's stored `main_worktree_paths` disagree with
1658                // the group key (for example, a stale row whose main
1659                // paths equal its folder paths for a linked-worktree
1660                // workspace). The thread will be rewritten into the
1661                // correct shape the next time `handle_conversation_event`
1662                // fires, but until then the sidebar should still show
1663                // it under the group whose workspace it actually
1664                // belongs to.
1665                for ws in group_workspaces {
1666                    let ws_paths = workspace_path_list(ws, cx);
1667                    if ws_paths.paths().is_empty() {
1668                        continue;
1669                    }
1670                    for row in thread_store
1671                        .read(cx)
1672                        .entries_for_path(&ws_paths, group_host.as_ref())
1673                        .cloned()
1674                    {
1675                        if !seen_thread_ids.insert(row.thread_id) {
1676                            continue;
1677                        }
1678                        threads.push(make_thread_entry(
1679                            row,
1680                            ThreadEntryWorkspace::Open(ws.clone()),
1681                        ));
1682                    }
1683                }
1684
1685                // Load any legacy threads for any single linked worktree of this project group.
1686                for worktree_path_list in &linked_worktree_path_lists {
1687                    for row in thread_store
1688                        .read(cx)
1689                        .entries_for_path(worktree_path_list, group_host.as_ref())
1690                        .cloned()
1691                    {
1692                        if !seen_thread_ids.insert(row.thread_id) {
1693                            continue;
1694                        }
1695                        threads.push(make_thread_entry(
1696                            row,
1697                            ThreadEntryWorkspace::Closed {
1698                                folder_paths: worktree_path_list.clone(),
1699                                project_group_key: group_key.clone(),
1700                            },
1701                        ));
1702                    }
1703                }
1704
1705                for thread in &mut threads {
1706                    if thread.draft.is_none() {
1707                        continue;
1708                    }
1709                    if let Some((label, kind)) = draft_display_label_for_thread_metadata(
1710                        &thread.metadata,
1711                        &thread.workspace,
1712                        cx,
1713                    ) {
1714                        let thread = Arc::make_mut(thread);
1715                        thread.metadata.title = Some(label);
1716                        thread.draft = Some(kind);
1717                    }
1718                }
1719                threads.retain(|thread| thread.draft.is_none() || thread.metadata.title.is_some());
1720
1721                // Keep empty drafts only while their thread is active; preserve
1722                // drafts with content because they hold user-typed state.
1723                let pending_activation = self.pending_thread_activation;
1724                let active_panel_thread_id = active_workspace
1725                    .as_ref()
1726                    .and_then(|ws| ws.read(cx).panel::<AgentPanel>(cx))
1727                    .and_then(|panel| panel.read(cx).active_thread_id(cx));
1728                threads.retain(|thread| {
1729                    if thread.draft != Some(DraftKind::Empty) {
1730                        return true;
1731                    }
1732                    if pending_activation.is_some() {
1733                        return false;
1734                    }
1735                    Some(thread.metadata.thread_id) == active_panel_thread_id
1736                });
1737
1738                // Build a lookup from live_infos and compute running/waiting
1739                // counts in a single pass.
1740                let mut live_info_by_session: HashMap<acp::SessionId, ActiveThreadInfo> =
1741                    HashMap::new();
1742                for info in live_infos {
1743                    if info.status == AgentThreadStatus::Running {
1744                        has_running_threads = true;
1745                    }
1746                    if info.status == AgentThreadStatus::WaitingForConfirmation {
1747                        waiting_thread_count += 1;
1748                    }
1749                    live_info_by_session.insert(info.session_id.clone(), info);
1750                }
1751
1752                // Merge live info into threads and update notification state
1753                // in a single pass.
1754                for thread in &mut threads {
1755                    if let Some(session_id) = thread.metadata.session_id.clone() {
1756                        if let Some(info) = live_info_by_session.get(&session_id) {
1757                            let status = info.status;
1758                            let thread_id = thread.metadata.thread_id;
1759                            Arc::make_mut(thread).apply_active_info(info);
1760                            new_live_statuses.insert(session_id, (status, thread_id));
1761                        }
1762                    }
1763
1764                    let session_id = &thread.metadata.session_id;
1765                    let is_active_thread = self.active_entry.as_ref().is_some_and(|entry| {
1766                        entry.is_active_thread(&thread.metadata.thread_id)
1767                            && active_workspace
1768                                .as_ref()
1769                                .is_some_and(|active| active == entry.workspace())
1770                    });
1771
1772                    if thread.status == AgentThreadStatus::Completed
1773                        && !is_active_thread
1774                        && session_id
1775                            .as_ref()
1776                            .and_then(|sid| old_statuses.get(sid))
1777                            .is_some_and(|(s, _)| *s == AgentThreadStatus::Running)
1778                    {
1779                        notified_threads.insert(thread.metadata.thread_id);
1780                    }
1781
1782                    if is_active_thread && !thread.is_background {
1783                        notified_threads.remove(&thread.metadata.thread_id);
1784                    }
1785                }
1786
1787                threads.sort_by(|a, b| {
1788                    let a_time = Self::thread_display_time(&a.metadata);
1789                    let b_time = Self::thread_display_time(&b.metadata);
1790                    b_time.cmp(&a_time)
1791                });
1792            } else {
1793                for info in live_infos {
1794                    if info.status == AgentThreadStatus::Running {
1795                        has_running_threads = true;
1796                    }
1797                    if info.status == AgentThreadStatus::WaitingForConfirmation {
1798                        waiting_thread_count += 1;
1799                    }
1800                    // Resolve the thread_id for this session so we can
1801                    // track its status and detect transitions even while
1802                    // the group is collapsed.
1803                    let thread_id = old_statuses
1804                        .get(&info.session_id)
1805                        .map(|(_, tid)| *tid)
1806                        .or_else(|| {
1807                            ThreadMetadataStore::global(cx)
1808                                .read(cx)
1809                                .entry_by_session(&info.session_id)
1810                                .map(|m| m.thread_id)
1811                        });
1812
1813                    if let Some(thread_id) = thread_id {
1814                        let old_status = old_statuses.get(&info.session_id).map(|(s, _)| *s);
1815                        new_live_statuses.insert(info.session_id.clone(), (info.status, thread_id));
1816                        if info.status == AgentThreadStatus::Completed
1817                            && old_status == Some(AgentThreadStatus::Running)
1818                        {
1819                            notified_threads.insert(thread_id);
1820                        }
1821                    }
1822                }
1823
1824                if is_active
1825                    && let Some(ActiveEntry::Thread { thread_id, .. }) = self.active_entry.as_ref()
1826                {
1827                    notified_threads.remove(thread_id);
1828                }
1829            }
1830
1831            let has_visible_rows = !threads.is_empty() || !terminals.is_empty();
1832            let has_stored_thread_rows = !should_load_threads && !has_visible_rows && {
1833                let store = ThreadMetadataStore::global(cx).read(cx);
1834                store
1835                    .entries_for_main_worktree_path(group_key.path_list(), group_host.as_ref())
1836                    .any(|metadata| {
1837                        let workspace = resolve_workspace(metadata.folder_paths());
1838                        thread_metadata_would_render_sidebar_row(metadata, &workspace, cx)
1839                    })
1840                    || store
1841                        .entries_for_path(group_key.path_list(), group_host.as_ref())
1842                        .any(|metadata| {
1843                            let workspace = resolve_workspace(metadata.folder_paths());
1844                            thread_metadata_would_render_sidebar_row(metadata, &workspace, cx)
1845                        })
1846            };
1847            let has_threads = has_visible_rows || has_stored_thread_rows;
1848
1849            if !query.is_empty() {
1850                let workspace_highlight_positions =
1851                    fuzzy_match_positions(&query, &label).unwrap_or_default();
1852                let workspace_matched = !workspace_highlight_positions.is_empty();
1853
1854                let mut matched_threads: Vec<Arc<ThreadEntry>> = Vec::new();
1855                for mut thread in threads {
1856                    let mut worktree_matched = false;
1857                    {
1858                        let thread = Arc::make_mut(&mut thread);
1859                        let title = thread.metadata.display_title();
1860                        if let Some(positions) = fuzzy_match_positions(&query, title.as_ref()) {
1861                            thread.highlight_positions = positions;
1862                        }
1863                        for worktree in &mut thread.worktrees {
1864                            let Some(name) = worktree.worktree_name.as_ref() else {
1865                                continue;
1866                            };
1867                            if let Some(positions) = fuzzy_match_positions(&query, name) {
1868                                worktree.highlight_positions = positions;
1869                                worktree_matched = true;
1870                            }
1871                        }
1872                    }
1873                    if workspace_matched
1874                        || !thread.highlight_positions.is_empty()
1875                        || worktree_matched
1876                    {
1877                        matched_threads.push(thread);
1878                    }
1879                }
1880
1881                let mut matched_terminals: Vec<TerminalEntry> = Vec::new();
1882                for mut terminal in terminals {
1883                    let mut terminal_matched = false;
1884                    let terminal_title = terminal.metadata.display_title();
1885                    if let Some(positions) = fuzzy_match_positions(&query, terminal_title.as_ref())
1886                    {
1887                        terminal.highlight_positions = positions;
1888                        terminal_matched = true;
1889                    }
1890                    let mut worktree_matched = false;
1891                    for worktree in &mut terminal.worktrees {
1892                        let Some(name) = worktree.worktree_name.as_ref() else {
1893                            continue;
1894                        };
1895                        if let Some(positions) = fuzzy_match_positions(&query, name) {
1896                            worktree.highlight_positions = positions;
1897                            worktree_matched = true;
1898                        }
1899                    }
1900                    if workspace_matched || terminal_matched || worktree_matched {
1901                        matched_terminals.push(terminal);
1902                    }
1903                }
1904
1905                if matched_threads.is_empty() && matched_terminals.is_empty() && !workspace_matched
1906                {
1907                    continue;
1908                }
1909
1910                // Check for notifications: threads that completed while not active.
1911                let has_thread_notifications = matched_threads
1912                    .iter()
1913                    .any(|t| notified_threads.contains(&t.metadata.thread_id));
1914                let has_terminal_notifications = matched_terminals
1915                    .iter()
1916                    .any(|t| notified_terminals.contains(&t.metadata.terminal_id));
1917
1918                project_header_indices.push(entries.len());
1919                entries.push(ListEntry::ProjectHeader {
1920                    key: group_key.clone(),
1921                    label,
1922                    highlight_positions: workspace_highlight_positions,
1923                    has_running_threads,
1924                    waiting_thread_count,
1925                    has_notifications: has_thread_notifications || has_terminal_notifications,
1926                    is_active,
1927                    has_threads,
1928                });
1929
1930                Self::push_entries_by_display_time(
1931                    &mut entries,
1932                    matched_terminals,
1933                    matched_threads,
1934                    &mut current_session_ids,
1935                    &mut current_thread_ids,
1936                );
1937            } else {
1938                let has_terminal_notifications = terminals
1939                    .iter()
1940                    .any(|t| notified_terminals.contains(&t.metadata.terminal_id));
1941
1942                // When collapsed, threads aren't loaded into `threads`, so we
1943                // query the store for thread IDs to check notifications and
1944                // to prevent the retain below from purging them.
1945                let has_thread_notifications = if threads.is_empty() && !notified_threads.is_empty()
1946                {
1947                    let thread_store = ThreadMetadataStore::global(cx);
1948                    let store = thread_store.read(cx);
1949                    let group_thread_ids = store
1950                        .entries_for_main_worktree_path(group_key.path_list(), group_host.as_ref())
1951                        .chain(store.entries_for_path(group_key.path_list(), group_host.as_ref()))
1952                        .map(|m| m.thread_id)
1953                        .collect::<HashSet<_>>();
1954                    current_thread_ids.extend(group_thread_ids.iter());
1955                    group_thread_ids
1956                        .iter()
1957                        .any(|id| notified_threads.contains(id))
1958                } else {
1959                    threads
1960                        .iter()
1961                        .any(|t| notified_threads.contains(&t.metadata.thread_id))
1962                };
1963
1964                project_header_indices.push(entries.len());
1965                entries.push(ListEntry::ProjectHeader {
1966                    key: group_key.clone(),
1967                    label,
1968                    highlight_positions: Vec::new(),
1969                    has_running_threads,
1970                    waiting_thread_count,
1971                    has_notifications: has_thread_notifications || has_terminal_notifications,
1972                    is_active,
1973                    has_threads,
1974                });
1975
1976                if is_collapsed {
1977                    continue;
1978                }
1979
1980                Self::push_entries_by_display_time(
1981                    &mut entries,
1982                    terminals,
1983                    threads,
1984                    &mut current_session_ids,
1985                    &mut current_thread_ids,
1986                );
1987            }
1988        }
1989
1990        notified_threads.retain(|id| current_thread_ids.contains(id));
1991
1992        self.thread_last_accessed
1993            .retain(|id, _| current_thread_ids.contains(id));
1994        self.terminal_last_accessed
1995            .retain(|id, _| current_terminal_ids.contains(id));
1996
1997        self.live_thread_statuses = new_live_statuses;
1998
1999        self.contents = SidebarContents {
2000            entries,
2001            notified_threads,
2002            notified_terminals,
2003            project_header_indices,
2004            has_open_projects,
2005        };
2006    }
2007
2008    fn schedule_update_entries(&mut self, select_first_after_update: bool, cx: &mut Context<Self>) {
2009        if self.update_task.is_some() && !select_first_after_update {
2010            return;
2011        }
2012
2013        self.update_task = Some(cx.spawn(async move |this, cx| {
2014            this.update(cx, |this, cx| {
2015                this.update_task = None;
2016                this.update_entries(cx);
2017                if select_first_after_update {
2018                    this.select_first_entry();
2019                    cx.notify();
2020                }
2021            })
2022            .ok();
2023        }));
2024    }
2025
2026    /// Rebuilds the sidebar's visible entries from already-cached state.
2027    fn update_entries(&mut self, cx: &mut Context<Self>) {
2028        let Some(multi_workspace) = self.multi_workspace.upgrade() else {
2029            return;
2030        };
2031        if !multi_workspace.read(cx).multi_workspace_enabled(cx) {
2032            return;
2033        }
2034
2035        let had_notifications = self.has_notifications(cx);
2036        let previous_shapes: Vec<EntryShape> =
2037            self.entry_shapes(multi_workspace.read(cx)).collect();
2038
2039        self.rebuild_contents(cx);
2040        self.refresh_refilled_draft_times(cx);
2041        self.refresh_draft_editor_observations(cx);
2042
2043        // Preserve measurements for unchanged entries so sticky headers do not flicker.
2044        self.apply_list_state_diff(&previous_shapes, multi_workspace.read(cx));
2045
2046        self.prefetch_worktree_default_branches(cx);
2047
2048        if had_notifications != self.has_notifications(cx) {
2049            multi_workspace.update(cx, |_, cx| {
2050                cx.notify();
2051            });
2052        }
2053
2054        cx.notify();
2055    }
2056
2057    /// Splices only the changed entry range, leaving unchanged item measurements intact.
2058    fn apply_list_state_diff(
2059        &self,
2060        previous_shapes: &[EntryShape],
2061        multi_workspace: &MultiWorkspace,
2062    ) {
2063        let mut new_iter = self.entry_shapes(multi_workspace);
2064        let mut prefix_len = 0;
2065        let leading_new = loop {
2066            match (previous_shapes.get(prefix_len), new_iter.next()) {
2067                (Some(prev), Some(next)) if *prev == next => prefix_len += 1,
2068                (None, None) => return,
2069                (_, leading) => break leading,
2070            }
2071        };
2072
2073        let new_tail: Vec<EntryShape> = leading_new.into_iter().chain(new_iter).collect();
2074        let prev_tail = &previous_shapes[prefix_len..];
2075        let suffix_len = prev_tail
2076            .iter()
2077            .rev()
2078            .zip(new_tail.iter().rev())
2079            .take_while(|(prev, next)| prev == next)
2080            .count();
2081
2082        let old_changed = prefix_len..previous_shapes.len() - suffix_len;
2083        let new_changed_count = new_tail.len() - suffix_len;
2084        self.list_state.splice(old_changed, new_changed_count);
2085    }
2086
2087    fn entry_shapes<'a>(
2088        &'a self,
2089        multi_workspace: &'a MultiWorkspace,
2090    ) -> impl Iterator<Item = EntryShape> + 'a {
2091        self.contents.entries.iter().map(move |entry| match entry {
2092            ListEntry::ProjectHeader {
2093                key, has_threads, ..
2094            } => EntryShape::ProjectHeader {
2095                key: key.clone(),
2096                has_threads: *has_threads,
2097                is_collapsed: multi_workspace
2098                    .group_state_by_key(key)
2099                    .map(|state| !state.expanded)
2100                    .unwrap_or(false),
2101            },
2102            ListEntry::Thread(thread) => EntryShape::Thread(thread.metadata.thread_id),
2103            ListEntry::Terminal(terminal) => EntryShape::Terminal(terminal.metadata.terminal_id),
2104        })
2105    }
2106
2107    /// Detects drafts that just went from empty back to having content and
2108    /// refreshes their interaction time to now, so a re-filled draft sorts to
2109    /// the top of the list instead of falling back to its original creation time.
2110    fn refresh_refilled_draft_times(&mut self, cx: &mut Context<Self>) {
2111        let mut new_kinds: HashMap<ThreadId, DraftKind> = HashMap::new();
2112        let mut refilled: Vec<ThreadId> = Vec::new();
2113
2114        for entry in &self.contents.entries {
2115            let ListEntry::Thread(thread) = entry else {
2116                continue;
2117            };
2118            let Some(kind) = thread.draft else {
2119                continue;
2120            };
2121            let thread_id = thread.metadata.thread_id;
2122
2123            if kind == DraftKind::WithContent
2124                && self.draft_kinds.get(&thread_id) == Some(&DraftKind::Empty)
2125            {
2126                refilled.push(thread_id);
2127            }
2128            new_kinds.insert(thread_id, kind);
2129        }
2130        self.draft_kinds = new_kinds;
2131
2132        if refilled.is_empty() {
2133            return;
2134        }
2135
2136        let now = Utc::now();
2137
2138        ThreadMetadataStore::global(cx).update(cx, |store, store_cx| {
2139            for thread_id in refilled {
2140                store.update_interacted_at(&thread_id, now, store_cx);
2141            }
2142        });
2143    }
2144
2145    /// Re-establishes subscriptions to each visible draft's message editor
2146    /// so we rebuild entries (and their displayed titles) as the user types.
2147    fn refresh_draft_editor_observations(&mut self, cx: &mut Context<Self>) {
2148        self._draft_editor_observations.clear();
2149        let Some(multi_workspace) = self.multi_workspace.upgrade() else {
2150            return;
2151        };
2152
2153        let draft_conversation_views: Vec<Entity<agent_ui::ConversationView>> = multi_workspace
2154            .read(cx)
2155            .workspaces()
2156            .filter_map(|ws| ws.read(cx).panel::<AgentPanel>(cx))
2157            .flat_map(|panel| panel.read(cx).conversation_views())
2158            .collect();
2159
2160        for cv in draft_conversation_views {
2161            if let Some(thread_view) = cv.read(cx).active_thread() {
2162                let editor = thread_view.read(cx).message_editor.clone();
2163                self._draft_editor_observations.push(cx.subscribe(
2164                    &editor,
2165                    |this, _editor, event, cx| match event {
2166                        MessageEditorEvent::Edited => this.schedule_update_entries(false, cx),
2167                        _ => (),
2168                    },
2169                ));
2170            }
2171            // Also subscribe to the ConversationView itself so that editor
2172            // replacements during lifecycle transitions (Loading →
2173            // Connected) re-wire the editor observation above.
2174            self._draft_editor_observations.push(cx.subscribe(
2175                &cv,
2176                |this, _cv, _event: &StateChange, cx| {
2177                    this.schedule_update_entries(false, cx);
2178                },
2179            ));
2180        }
2181    }
2182
2183    fn select_first_entry(&mut self) {
2184        self.selection = self
2185            .contents
2186            .entries
2187            .iter()
2188            .position(|entry| matches!(entry, ListEntry::Thread(_) | ListEntry::Terminal(_)))
2189            .or_else(|| {
2190                if self.contents.entries.is_empty() {
2191                    None
2192                } else {
2193                    Some(0)
2194                }
2195            });
2196    }
2197
2198    fn render_list_entry(
2199        &mut self,
2200        ix: usize,
2201        window: &mut Window,
2202        cx: &mut Context<Self>,
2203    ) -> AnyElement {
2204        let Some(entry) = self.contents.entries.get(ix) else {
2205            return div().into_any_element();
2206        };
2207        let is_focused = self.focus_handle.is_focused(window);
2208        // is_selected means the keyboard selector is here.
2209        let is_selected = is_focused && self.selection == Some(ix);
2210
2211        let is_group_header_after_first =
2212            ix > 0 && matches!(entry, ListEntry::ProjectHeader { .. });
2213
2214        let is_active = self
2215            .active_entry
2216            .as_ref()
2217            .is_some_and(|active| active.matches_entry(entry));
2218
2219        let rendered = match entry {
2220            ListEntry::ProjectHeader {
2221                key,
2222                label,
2223                highlight_positions,
2224                has_running_threads,
2225                waiting_thread_count,
2226                has_notifications,
2227                is_active: is_active_group,
2228                has_threads,
2229            } => {
2230                self.project_header_menu_handles.entry(ix).or_default();
2231                self.project_header_new_thread_menu_handles
2232                    .entry(ix)
2233                    .or_default();
2234
2235                self.render_project_header(
2236                    ix,
2237                    false,
2238                    key,
2239                    label,
2240                    highlight_positions,
2241                    *has_running_threads,
2242                    *waiting_thread_count,
2243                    *has_notifications,
2244                    *is_active_group,
2245                    is_selected,
2246                    *has_threads,
2247                    // has_active_draft,
2248                    cx,
2249                )
2250            }
2251            ListEntry::Thread(thread) => self.render_thread(ix, thread, is_active, is_selected, cx),
2252            ListEntry::Terminal(terminal) => {
2253                self.render_terminal(ix, terminal, is_active, is_selected, cx)
2254            }
2255        };
2256
2257        if is_group_header_after_first {
2258            v_flex()
2259                .w_full()
2260                .border_t_1()
2261                .border_color(cx.theme().colors().border)
2262                .child(rendered)
2263                .into_any_element()
2264        } else {
2265            rendered
2266        }
2267    }
2268
2269    fn render_remote_project_icon(
2270        &self,
2271        ix: usize,
2272        host: Option<&RemoteConnectionOptions>,
2273    ) -> Option<AnyElement> {
2274        let remote_icon_per_type = match host? {
2275            RemoteConnectionOptions::Wsl(_) => IconName::Linux,
2276            RemoteConnectionOptions::Docker(_) => IconName::Box,
2277            _ => IconName::Server,
2278        };
2279
2280        Some(
2281            div()
2282                .id(format!("remote-project-icon-{}", ix))
2283                .child(
2284                    Icon::new(remote_icon_per_type)
2285                        .size(IconSize::XSmall)
2286                        .color(Color::Muted),
2287                )
2288                .tooltip(Tooltip::text("Remote Project"))
2289                .into_any_element(),
2290        )
2291    }
2292
2293    fn render_project_header(
2294        &self,
2295        ix: usize,
2296        is_sticky: bool,
2297        key: &ProjectGroupKey,
2298        label: &SharedString,
2299        highlight_positions: &[usize],
2300        has_running_threads: bool,
2301        waiting_thread_count: usize,
2302        has_notifications: bool,
2303        is_active: bool,
2304        is_focused: bool,
2305        has_threads: bool,
2306        cx: &mut Context<Self>,
2307    ) -> AnyElement {
2308        let host = key.host();
2309
2310        let has_filter = self.has_filter_query(cx);
2311
2312        let id_prefix = if is_sticky { "sticky-" } else { "" };
2313        let id = SharedString::from(format!("{id_prefix}project-header-{ix}"));
2314        let group_name = SharedString::from(format!("{id_prefix}header-group-{ix}"));
2315
2316        let is_collapsed = self.is_group_collapsed(key, cx);
2317        let disclosure_icon = if is_collapsed {
2318            IconName::ChevronRight
2319        } else {
2320            IconName::ChevronDown
2321        };
2322
2323        let key_for_toggle = key.clone();
2324        let key_for_focus = key.clone();
2325
2326        // The fade gradient renders as a visible patch on transparent windows,
2327        // so truncate the label instead.
2328        let opaque_window =
2329            cx.theme().window_background_appearance() == WindowBackgroundAppearance::Opaque;
2330
2331        let label = if highlight_positions.is_empty() {
2332            Label::new(label.clone())
2333                .when(!is_active, |this| this.color(Color::Muted))
2334                .when(!opaque_window, |this| this.truncate())
2335                .into_any_element()
2336        } else {
2337            HighlightedLabel::new(label.clone(), highlight_positions.to_vec())
2338                .when(!is_active, |this| this.color(Color::Muted))
2339                .when(!opaque_window, |this| this.truncate())
2340                .into_any_element()
2341        };
2342
2343        let color = cx.theme().colors();
2344        let sidebar_base_bg = color
2345            .title_bar_background
2346            .blend(color.panel_background.opacity(0.25));
2347
2348        let base_bg = color.background.blend(sidebar_base_bg);
2349
2350        let hover_base = color
2351            .element_active
2352            .blend(color.element_background.opacity(0.2));
2353        let hover_solid = base_bg.blend(hover_base);
2354
2355        let group_name_for_gradient = group_name.clone();
2356        let gradient_overlay = move || {
2357            GradientFade::new(base_bg, hover_solid, hover_solid)
2358                .width(px(92.0))
2359                .right(px(-2.0))
2360                .gradient_stop(0.7)
2361                .when(!has_filter, |this| {
2362                    this.group_name(group_name_for_gradient.clone())
2363                })
2364        };
2365
2366        let header = h_flex()
2367            .id(id)
2368            .group(&group_name)
2369            .when(!has_filter, |this| this.cursor_pointer())
2370            .relative()
2371            .h(Tab::content_height(cx))
2372            .w_full()
2373            .pl_2()
2374            .pr_1p5()
2375            .justify_between()
2376            .border_1()
2377            .map(|this| {
2378                if is_focused {
2379                    this.border_color(color.border_focused)
2380                } else {
2381                    this.border_color(gpui::transparent_black())
2382                }
2383            })
2384            .when(!has_filter, |this| this.hover(|s| s.bg(hover_solid)))
2385            .child(
2386                h_flex()
2387                    .relative()
2388                    .min_w_0()
2389                    .w_full()
2390                    .gap_1()
2391                    .child(label)
2392                    .when_some(
2393                        self.render_remote_project_icon(ix, host.as_ref()),
2394                        |this, icon| this.child(icon),
2395                    )
2396                    .when(is_collapsed, |this| {
2397                        this.when(has_running_threads, |this| {
2398                            this.child(
2399                                Icon::new(IconName::LoadCircle)
2400                                    .size(IconSize::XSmall)
2401                                    .color(Color::Muted)
2402                                    .with_rotate_animation(2),
2403                            )
2404                        })
2405                        .when(waiting_thread_count > 0, |this| {
2406                            let tooltip_text = if waiting_thread_count == 1 {
2407                                "1 thread is waiting for confirmation".to_string()
2408                            } else {
2409                                format!(
2410                                    "{waiting_thread_count} threads are waiting for confirmation",
2411                                )
2412                            };
2413                            this.child(
2414                                div()
2415                                    .id(format!("{id_prefix}waiting-indicator-{ix}"))
2416                                    .child(
2417                                        Icon::new(IconName::Warning)
2418                                            .size(IconSize::XSmall)
2419                                            .color(Color::Warning),
2420                                    )
2421                                    .tooltip(Tooltip::text(tooltip_text)),
2422                            )
2423                        })
2424                        .when(
2425                            has_notifications && !has_running_threads && waiting_thread_count == 0,
2426                            |this| {
2427                                this.child(
2428                                    Icon::new(IconName::Circle)
2429                                        .size(IconSize::Small)
2430                                        .color(Color::Accent),
2431                                )
2432                            },
2433                        )
2434                    })
2435                    .when(!has_filter, |this| {
2436                        this.child(
2437                            div()
2438                                .when(!is_focused, |this| this.visible_on_hover(&group_name))
2439                                .child(
2440                                    Icon::new(disclosure_icon)
2441                                        .size(IconSize::Small)
2442                                        .color(Color::Muted),
2443                                ),
2444                        )
2445                    }),
2446            )
2447            .children(opaque_window.then(|| gradient_overlay()))
2448            .child(
2449                h_flex()
2450                    .gap_px()
2451                    .pr_1p5()
2452                    .children(opaque_window.then(|| gradient_overlay()))
2453                    .child(self.render_new_thread_button(ix, id_prefix, key, &group_name, cx))
2454                    .child(self.render_project_header_ellipsis_menu(
2455                        ix,
2456                        id_prefix,
2457                        key,
2458                        is_active,
2459                        has_threads,
2460                        &group_name,
2461                        cx,
2462                    ))
2463                    .on_mouse_down(gpui::MouseButton::Left, |_, _, cx| {
2464                        cx.stop_propagation();
2465                    }),
2466            )
2467            .on_mouse_down(gpui::MouseButton::Right, {
2468                let menu_handle = self
2469                    .project_header_menu_handles
2470                    .get(&ix)
2471                    .cloned()
2472                    .unwrap_or_default();
2473                move |_, window, cx| {
2474                    cx.stop_propagation();
2475                    menu_handle.toggle(window, cx);
2476                }
2477            })
2478            .on_click(
2479                cx.listener(move |this, event: &gpui::ClickEvent, window, cx| {
2480                    if event.modifiers().secondary() {
2481                        this.activate_or_open_workspace_for_group(&key_for_focus, window, cx);
2482                    } else if !this.has_filter_query(cx) {
2483                        this.toggle_collapse(&key_for_toggle, window, cx);
2484                    }
2485                }),
2486            )
2487            .block_mouse_except_scroll();
2488
2489        if !is_collapsed && !has_threads {
2490            v_flex()
2491                .w_full()
2492                .child(header)
2493                .child(
2494                    h_flex()
2495                        .px_2()
2496                        .pt_1()
2497                        .pb_2()
2498                        .gap(px(7.))
2499                        .child(Icon::new(IconName::Circle).size(IconSize::Small).color(
2500                            Color::Custom(cx.theme().colors().icon_placeholder.opacity(0.1)),
2501                        ))
2502                        .child(
2503                            Label::new("No threads yet")
2504                                .size(LabelSize::Small)
2505                                .color(Color::Placeholder),
2506                        ),
2507                )
2508                .into_any_element()
2509        } else {
2510            header.into_any_element()
2511        }
2512    }
2513
2514    fn render_new_thread_button(
2515        &self,
2516        ix: usize,
2517        id_prefix: &str,
2518        key: &ProjectGroupKey,
2519        group_name: &SharedString,
2520        cx: &mut Context<Self>,
2521    ) -> AnyElement {
2522        let focus_handle = self.focus_handle.clone();
2523
2524        let menu_handle = self
2525            .project_header_new_thread_menu_handles
2526            .get(&ix)
2527            .cloned()
2528            .unwrap_or_default();
2529        let is_menu_open = menu_handle.is_deployed();
2530
2531        let button = IconButton::new(
2532            SharedString::from(format!("{id_prefix}project-header-new-thread-{ix}")),
2533            IconName::Plus,
2534        )
2535        .selected_style(ButtonStyle::Tinted(TintColor::Accent))
2536        .icon_size(IconSize::Small)
2537        .when(!is_menu_open, |this| this.visible_on_hover(group_name));
2538
2539        let open_workspaces = self
2540            .multi_workspace
2541            .upgrade()
2542            .and_then(|mw| mw.read(cx).workspaces_for_project_group(key, cx))
2543            .unwrap_or_default();
2544
2545        if open_workspaces.is_empty() {
2546            let key = key.clone();
2547            return button
2548                .tooltip(move |_, cx| {
2549                    Tooltip::for_action_in("Start New Agent Thread", &NewThread, &focus_handle, cx)
2550                })
2551                .on_click(cx.listener(move |this, _, window, cx| {
2552                    this.set_group_expanded(&key, true, cx);
2553                    this.selection = None;
2554                    if let Some(workspace) = this.workspace_for_group(&key, cx) {
2555                        this.create_new_entry(&workspace, window, cx);
2556                    } else {
2557                        this.open_workspace_and_create_entry(
2558                            &key,
2559                            NewEntryTarget::LastCreatedKind,
2560                            window,
2561                            cx,
2562                        );
2563                    }
2564                }))
2565                .into_any_element();
2566        }
2567
2568        let this = cx.weak_entity();
2569        let key = key.clone();
2570
2571        PopoverMenu::new(SharedString::from(format!(
2572            "{id_prefix}project-header-new-thread-menu-{ix}"
2573        )))
2574        .with_handle(menu_handle)
2575        .trigger_with_tooltip(button, move |_, cx| {
2576            Tooltip::for_action_in("Start New Agent Thread", &NewThread, &focus_handle, cx)
2577        })
2578        .anchor(gpui::Anchor::TopLeft)
2579        .on_open(Rc::new({
2580            let this = this.clone();
2581            move |_window, cx| {
2582                this.update(cx, |_sidebar, cx| cx.notify()).ok();
2583            }
2584        }))
2585        .menu(move |window, cx| {
2586            let this = this.clone();
2587            let key = key.clone();
2588            let open_workspaces = open_workspaces.clone();
2589            let active_workspace = this
2590                .read_with(cx, |sidebar, cx| {
2591                    sidebar
2592                        .multi_workspace
2593                        .upgrade()
2594                        .map(|mw| mw.read(cx).workspace().clone())
2595                })
2596                .ok()
2597                .flatten();
2598            let workspace_labels: Vec<_> = open_workspaces
2599                .iter()
2600                .map(|workspace| workspace_menu_worktree_labels(workspace, cx))
2601                .collect();
2602
2603            Some(ContextMenu::build(
2604                window,
2605                cx,
2606                move |mut menu, _window, cx| {
2607                    menu = menu.header("New Thread In…");
2608
2609                    for (workspace, labels) in open_workspaces
2610                        .iter()
2611                        .cloned()
2612                        .zip(workspace_labels.iter().cloned())
2613                    {
2614                        let is_active_workspace = active_workspace.as_ref() == Some(&workspace);
2615                        menu = menu.custom_entry(
2616                            move |_window, _cx| {
2617                                h_flex()
2618                                    .w_full()
2619                                    .gap_2()
2620                                    .justify_between()
2621                                    .child(h_flex().min_w_0().gap_1().children(
2622                                        labels.iter().enumerate().map(|(label_ix, label)| {
2623                                            h_flex()
2624                                                .gap_1()
2625                                                .when(label_ix > 0, |this| {
2626                                                    this.child(Label::new("•").alpha(0.25))
2627                                                })
2628                                                .child(label.render())
2629                                                .into_any_element()
2630                                        }),
2631                                    ))
2632                                    .when(is_active_workspace, |this| {
2633                                        this.child(
2634                                            Icon::new(IconName::Check)
2635                                                .size(IconSize::Small)
2636                                                .color(Color::Accent),
2637                                        )
2638                                    })
2639                                    .into_any_element()
2640                            },
2641                            {
2642                                let this = this.clone();
2643                                let key = key.clone();
2644                                let workspace = workspace.clone();
2645                                move |window, cx| {
2646                                    this.update(cx, |sidebar, cx| {
2647                                        sidebar.set_group_expanded(&key, true, cx);
2648                                        sidebar.selection = None;
2649                                        sidebar.create_new_entry(&workspace, window, cx);
2650                                    })
2651                                    .ok();
2652                                }
2653                            },
2654                        );
2655                    }
2656
2657                    let base_workspace = active_workspace
2658                        .as_ref()
2659                        .filter(|workspace| open_workspaces.contains(workspace))
2660                        .cloned()
2661                        .or_else(|| open_workspaces.first().cloned());
2662
2663                    // Only offer worktree creation when the base project can
2664                    // actually create one; otherwise the submenu would expand to
2665                    // nothing. Mirrors the picker's `creation_blocked_reason`.
2666                    let creation_blocked = base_workspace.as_ref().is_none_or(|base_workspace| {
2667                        let project = base_workspace.read(cx).project().read(cx);
2668                        project.is_via_collab() || project.repositories(cx).is_empty()
2669                    });
2670
2671                    if let Some(base_workspace) = base_workspace.filter(|_| !creation_blocked) {
2672                        menu = menu.separator().submenu("Create New Worktree…", {
2673                            let this = this.clone();
2674                            move |mut submenu, _window, submenu_cx| {
2675                                let project = base_workspace.read(submenu_cx).project().clone();
2676                                let project_ref = project.read(submenu_cx);
2677                                let has_multiple_repositories =
2678                                    project_ref.repositories(submenu_cx).len() > 1;
2679                                let current_branch =
2680                                    project_ref.active_repository(submenu_cx).and_then(|repo| {
2681                                        repo.read(submenu_cx)
2682                                            .branch
2683                                            .as_ref()
2684                                            .map(|branch| branch.name().to_string())
2685                                    });
2686                                let default_branch = this
2687                                    .read_with(submenu_cx, |sidebar, _| {
2688                                        match sidebar.worktree_default_branches.get(&key) {
2689                                            Some(DefaultBranchCache::Resolved(branch)) => {
2690                                                branch.clone()
2691                                            }
2692                                            _ => None,
2693                                        }
2694                                    })
2695                                    .ok()
2696                                    .flatten();
2697
2698                                let targets = worktree_create_targets(
2699                                    has_multiple_repositories,
2700                                    default_branch,
2701                                    current_branch.as_deref(),
2702                                );
2703                                for target in targets {
2704                                    let label = format!(
2705                                        "Based on {}",
2706                                        target.branch_label(
2707                                            has_multiple_repositories,
2708                                            current_branch.as_deref(),
2709                                        )
2710                                    );
2711                                    let branch_target = target.branch_target();
2712                                    let workspace = base_workspace.clone();
2713                                    submenu = submenu.entry(label, None, move |window, cx| {
2714                                        create_worktree_in_workspace(
2715                                            &workspace,
2716                                            branch_target.clone(),
2717                                            window,
2718                                            cx,
2719                                        );
2720                                    });
2721                                }
2722
2723                                submenu
2724                            }
2725                        });
2726                    }
2727
2728                    menu
2729                },
2730            ))
2731        })
2732        .anchor(gpui::Anchor::TopRight)
2733        .offset(gpui::Point {
2734            x: px(0.),
2735            y: px(1.),
2736        })
2737        .into_any_element()
2738    }
2739
2740    // Warms `worktree_default_branches` for every project group with at least one
2741    // open workspace. The git query runs off the menu path so the submenu can read
2742    // the result synchronously when it opens. Worktrees of a repository share the
2743    // same default branch, so any workspace in the group yields the same answer.
2744    fn prefetch_worktree_default_branches(&mut self, cx: &mut Context<Self>) {
2745        let Some(multi_workspace) = self.multi_workspace.upgrade() else {
2746            return;
2747        };
2748        let keys: Vec<ProjectGroupKey> = self
2749            .contents
2750            .entries
2751            .iter()
2752            .filter_map(|entry| match entry {
2753                ListEntry::ProjectHeader { key, .. } => Some(key.clone()),
2754                _ => None,
2755            })
2756            .collect();
2757        for key in keys {
2758            if self.worktree_default_branches.contains_key(&key) {
2759                continue;
2760            }
2761            let Some(base) = multi_workspace
2762                .read(cx)
2763                .workspaces_for_project_group(&key, cx)
2764                .and_then(|workspaces| workspaces.first().cloned())
2765            else {
2766                continue;
2767            };
2768            self.prefetch_worktree_default_branch(&key, &base, cx);
2769        }
2770    }
2771
2772    fn prefetch_worktree_default_branch(
2773        &mut self,
2774        key: &ProjectGroupKey,
2775        workspace: &Entity<Workspace>,
2776        cx: &mut Context<Self>,
2777    ) {
2778        // Presence of the key means the group is already pending or resolved. The
2779        // no-repository case is deliberately not inserted so it retries on a
2780        // later rebuild once the repository has finished loading.
2781        if self.worktree_default_branches.contains_key(key) {
2782            return;
2783        }
2784        let Some(repository) = workspace.read(cx).project().read(cx).active_repository(cx) else {
2785            return;
2786        };
2787        let request = repository.update(cx, |repository, _| repository.default_branch(true));
2788        self.worktree_default_branches
2789            .insert(key.clone(), DefaultBranchCache::Pending);
2790        let key = key.clone();
2791        cx.spawn(async move |this, cx| {
2792            let default_branch = request.await.ok().and_then(Result::ok).flatten();
2793            let parsed = default_branch.as_deref().and_then(RemoteBranchName::parse);
2794            this.update(cx, |sidebar, cx| {
2795                sidebar
2796                    .worktree_default_branches
2797                    .insert(key, DefaultBranchCache::Resolved(parsed));
2798                cx.notify();
2799            })
2800            .ok();
2801        })
2802        .detach();
2803    }
2804
2805    fn render_project_header_ellipsis_menu(
2806        &self,
2807        ix: usize,
2808        id_prefix: &str,
2809        project_group_key: &ProjectGroupKey,
2810        is_active: bool,
2811        has_threads: bool,
2812        group_name: &SharedString,
2813        cx: &mut Context<Self>,
2814    ) -> AnyElement {
2815        let multi_workspace = self.multi_workspace.clone();
2816        let project_group_key = project_group_key.clone();
2817
2818        let show_multi_project_entries = multi_workspace
2819            .read_with(cx, |mw, _| {
2820                project_group_key.host().is_none() && mw.project_group_keys().len() >= 2
2821            })
2822            .unwrap_or(false);
2823
2824        let this = cx.weak_entity();
2825
2826        let trigger_id = SharedString::from(format!("{id_prefix}-ellipsis-menu-{ix}"));
2827        let menu_handle = self
2828            .project_header_menu_handles
2829            .get(&ix)
2830            .cloned()
2831            .unwrap_or_default();
2832        let is_menu_open = menu_handle.is_deployed();
2833
2834        PopoverMenu::new(format!("{id_prefix}project-header-menu-{ix}"))
2835            .with_handle(menu_handle)
2836            .trigger(
2837                IconButton::new(trigger_id, IconName::Ellipsis)
2838                    .selected_style(ButtonStyle::Tinted(TintColor::Accent))
2839                    .icon_size(IconSize::Small)
2840                    .when(!is_menu_open, |el| el.visible_on_hover(group_name)),
2841            )
2842            .on_open(Rc::new({
2843                let this = this.clone();
2844                move |_window, cx| {
2845                    this.update(cx, |sidebar, cx| {
2846                        sidebar.project_header_menu_ix = Some(ix);
2847                        cx.notify();
2848                    })
2849                    .ok();
2850                }
2851            }))
2852            .menu(move |window, cx| {
2853                let multi_workspace = multi_workspace.clone();
2854                let project_group_key = project_group_key.clone();
2855                let this_for_menu = this.clone();
2856
2857                let open_workspaces = multi_workspace
2858                    .read_with(cx, |multi_workspace, cx| {
2859                        multi_workspace
2860                            .workspaces_for_project_group(&project_group_key, cx)
2861                            .unwrap_or_default()
2862                    })
2863                    .unwrap_or_default();
2864
2865                // Compute reorder state at menu-open time so it reflects the
2866                // most recent group ordering.
2867                let (group_index, total_groups) = multi_workspace
2868                    .read_with(cx, |mw, _| {
2869                        let keys = mw.project_group_keys();
2870                        let index = keys.iter().position(|k| k == &project_group_key);
2871                        (index, keys.len())
2872                    })
2873                    .unwrap_or((None, 0));
2874                let show_reorder_entries = total_groups >= 2;
2875                let can_move_up = group_index.is_some_and(|i| i > 0);
2876                let can_move_down = group_index.is_some_and(|i| i + 1 < total_groups);
2877
2878                let active_workspace = multi_workspace
2879                    .read_with(cx, |multi_workspace, _cx| {
2880                        multi_workspace.workspace().clone()
2881                    })
2882                    .ok();
2883                let workspace_labels: Vec<_> = open_workspaces
2884                    .iter()
2885                    .map(|workspace| workspace_menu_worktree_labels(workspace, cx))
2886                    .collect();
2887                let workspace_is_active: Vec<_> = open_workspaces
2888                    .iter()
2889                    .map(|workspace| active_workspace.as_ref() == Some(workspace))
2890                    .collect();
2891
2892                let menu =
2893                    ContextMenu::build_persistent(window, cx, move |menu, _window, menu_cx| {
2894                        let menu = menu.end_slot_action(Box::new(menu::SecondaryConfirm));
2895                        let weak_menu = menu_cx.weak_entity();
2896
2897                        let menu = menu.when(show_multi_project_entries, |this| {
2898                            this.entry(
2899                                "Open Project in New Window",
2900                                Some(Box::new(workspace::MoveProjectToNewWindow)),
2901                                {
2902                                    let project_group_key = project_group_key.clone();
2903                                    let multi_workspace = multi_workspace.clone();
2904                                    move |window, cx| {
2905                                        multi_workspace
2906                                            .update(cx, |multi_workspace, cx| {
2907                                                multi_workspace
2908                                                    .open_project_group_in_new_window(
2909                                                        &project_group_key,
2910                                                        window,
2911                                                        cx,
2912                                                    )
2913                                                    .detach_and_log_err(cx);
2914                                            })
2915                                            .ok();
2916                                    }
2917                                },
2918                            )
2919                        });
2920
2921                        let menu = menu
2922                            .custom_entry(
2923                                {
2924                                    move |_window, cx| {
2925                                        let action = h_flex()
2926                                            .opacity(0.6)
2927                                            .children(render_modifiers(
2928                                                &Modifiers::secondary_key(),
2929                                                PlatformStyle::platform(),
2930                                                None,
2931                                                Some(TextSize::Default.rems(cx).into()),
2932                                                false,
2933                                            ))
2934                                            .child(Label::new("-click").color(Color::Muted));
2935
2936                                        let label = if has_threads {
2937                                            "Focus Last Project"
2938                                        } else {
2939                                            "Focus Project"
2940                                        };
2941
2942                                        h_flex()
2943                                            .w_full()
2944                                            .justify_between()
2945                                            .gap_4()
2946                                            .child(
2947                                                Label::new(label)
2948                                                    .when(is_active, |s| s.color(Color::Disabled)),
2949                                            )
2950                                            .child(action)
2951                                            .into_any_element()
2952                                    }
2953                                },
2954                                {
2955                                    let project_group_key = project_group_key.clone();
2956                                    let this = this_for_menu.clone();
2957                                    move |window, cx| {
2958                                        if is_active {
2959                                            return;
2960                                        }
2961                                        this.update(cx, |sidebar, cx| {
2962                                            if let Some(workspace) =
2963                                                sidebar.workspace_for_group(&project_group_key, cx)
2964                                            {
2965                                                sidebar.activate_workspace(&workspace, window, cx);
2966                                            } else {
2967                                                sidebar.open_workspace_for_group(
2968                                                    &project_group_key,
2969                                                    window,
2970                                                    cx,
2971                                                );
2972                                            }
2973                                            sidebar.selection = None;
2974                                            sidebar.active_entry = None;
2975                                        })
2976                                        .ok();
2977                                    }
2978                                },
2979                            )
2980                            .selectable(!is_active);
2981
2982                        let menu = if open_workspaces.is_empty() {
2983                            menu
2984                        } else {
2985                            let mut menu = menu.separator().header("Open Worktrees");
2986
2987                            for (
2988                                workspace_index,
2989                                ((workspace, workspace_label), is_active_workspace),
2990                            ) in open_workspaces
2991                                .iter()
2992                                .cloned()
2993                                .zip(workspace_labels.iter().cloned())
2994                                .zip(workspace_is_active.iter().copied())
2995                                .enumerate()
2996                            {
2997                                let activate_multi_workspace = multi_workspace.clone();
2998                                let close_multi_workspace = multi_workspace.clone();
2999                                let activate_weak_menu = weak_menu.clone();
3000                                let close_weak_menu = weak_menu.clone();
3001                                let activate_workspace = workspace.clone();
3002                                let close_workspace = workspace.clone();
3003
3004                                menu = menu.custom_entry(
3005                                    move |_window, _cx| {
3006                                        let close_multi_workspace = close_multi_workspace.clone();
3007                                        let close_weak_menu = close_weak_menu.clone();
3008                                        let close_workspace = close_workspace.clone();
3009                                        let row_group_name = SharedString::from(format!(
3010                                            "workspace-menu-row-{workspace_index}"
3011                                        ));
3012
3013                                        h_flex()
3014                                            .group(&row_group_name)
3015                                            .w_full()
3016                                            .gap_2()
3017                                            .justify_between()
3018                                            .child(h_flex().min_w_0().gap_1().children(
3019                                                workspace_label.iter().enumerate().map(
3020                                                    |(label_ix, label)| {
3021                                                        h_flex()
3022                                                            .gap_1()
3023                                                            .when(label_ix > 0, |this| {
3024                                                                this.child(
3025                                                                    Label::new("•").alpha(0.25),
3026                                                                )
3027                                                            })
3028                                                            .child(label.render())
3029                                                            .into_any_element()
3030                                                    },
3031                                                ),
3032                                            ))
3033                                            .when(is_active_workspace, |this| {
3034                                                this.pr_1().child(
3035                                                    Icon::new(IconName::Check)
3036                                                        .size(IconSize::Small)
3037                                                        .color(Color::Accent),
3038                                                )
3039                                            })
3040                                            .when(!is_active_workspace, |this| {
3041                                                let close_multi_workspace =
3042                                                    close_multi_workspace.clone();
3043                                                let close_weak_menu = close_weak_menu.clone();
3044                                                let close_workspace = close_workspace.clone();
3045
3046                                                this.child(
3047                                                    IconButton::new(
3048                                                        ("close-workspace", workspace_index),
3049                                                        IconName::Close,
3050                                                    )
3051                                                    .icon_size(IconSize::Small)
3052                                                    .visible_on_hover(&row_group_name)
3053                                                    .tooltip(Tooltip::text("Close Worktree"))
3054                                                    .on_click(move |_, window, cx| {
3055                                                        cx.stop_propagation();
3056                                                        window.prevent_default();
3057                                                        close_multi_workspace
3058                                                            .update(cx, |multi_workspace, cx| {
3059                                                                multi_workspace
3060                                                                    .close_workspace(
3061                                                                        &close_workspace,
3062                                                                        window,
3063                                                                        cx,
3064                                                                    )
3065                                                                    .detach_and_log_err(cx);
3066                                                            })
3067                                                            .ok();
3068                                                        close_weak_menu
3069                                                            .update(cx, |_, cx| {
3070                                                                cx.emit(DismissEvent)
3071                                                            })
3072                                                            .ok();
3073                                                    }),
3074                                                )
3075                                            })
3076                                            .into_any_element()
3077                                    },
3078                                    move |window, cx| {
3079                                        activate_multi_workspace
3080                                            .update(cx, |multi_workspace, cx| {
3081                                                multi_workspace.activate(
3082                                                    activate_workspace.clone(),
3083                                                    None,
3084                                                    window,
3085                                                    cx,
3086                                                );
3087                                            })
3088                                            .ok();
3089                                        activate_weak_menu
3090                                            .update(cx, |_, cx| cx.emit(DismissEvent))
3091                                            .ok();
3092                                    },
3093                                );
3094                            }
3095
3096                            menu
3097                        };
3098
3099                        let menu = menu.when(show_reorder_entries, |this| {
3100                            let move_up_multi_workspace = multi_workspace.clone();
3101                            let move_up_key = project_group_key.clone();
3102                            let move_up_weak_menu = weak_menu.clone();
3103                            let move_down_multi_workspace = multi_workspace.clone();
3104                            let move_down_key = project_group_key.clone();
3105                            let move_down_weak_menu = weak_menu.clone();
3106
3107                            this.separator()
3108                                .item(
3109                                    ContextMenuEntry::new("Move Up")
3110                                        .disabled(!can_move_up)
3111                                        .handler(move |_window, cx| {
3112                                            move_up_multi_workspace
3113                                                .update(cx, |mw, cx| {
3114                                                    mw.move_project_group_up(&move_up_key, cx);
3115                                                })
3116                                                .ok();
3117                                            move_up_weak_menu
3118                                                .update(cx, |_, cx| cx.emit(DismissEvent))
3119                                                .ok();
3120                                        }),
3121                                )
3122                                .item(
3123                                    ContextMenuEntry::new("Move Down")
3124                                        .disabled(!can_move_down)
3125                                        .handler(move |_window, cx| {
3126                                            move_down_multi_workspace
3127                                                .update(cx, |mw, cx| {
3128                                                    mw.move_project_group_down(&move_down_key, cx);
3129                                                })
3130                                                .ok();
3131                                            move_down_weak_menu
3132                                                .update(cx, |_, cx| cx.emit(DismissEvent))
3133                                                .ok();
3134                                        }),
3135                                )
3136                        });
3137
3138                        let project_group_key = project_group_key.clone();
3139                        let remove_multi_workspace = multi_workspace.clone();
3140                        menu.separator().entry("Remove", None, move |window, cx| {
3141                            remove_multi_workspace
3142                                .update(cx, |multi_workspace, cx| {
3143                                    multi_workspace
3144                                        .remove_project_group(&project_group_key, window, cx)
3145                                        .detach_and_log_err(cx);
3146                                })
3147                                .ok();
3148                            weak_menu.update(cx, |_, cx| cx.emit(DismissEvent)).ok();
3149                        })
3150                    });
3151
3152                let this = this.clone();
3153
3154                window
3155                    .subscribe(&menu, cx, move |_, _: &gpui::DismissEvent, _window, cx| {
3156                        this.update(cx, |sidebar, cx| {
3157                            sidebar.project_header_menu_ix = None;
3158                            cx.notify();
3159                        })
3160                        .ok();
3161                    })
3162                    .detach();
3163
3164                Some(menu)
3165            })
3166            .anchor(gpui::Anchor::TopRight)
3167            .offset(gpui::Point {
3168                x: px(0.),
3169                y: px(1.),
3170            })
3171            .into_any_element()
3172    }
3173
3174    fn render_sticky_header(
3175        &self,
3176        window: &mut Window,
3177        cx: &mut Context<Self>,
3178    ) -> Option<AnyElement> {
3179        let scroll_top = self.list_state.logical_scroll_top();
3180
3181        let &header_idx = self
3182            .contents
3183            .project_header_indices
3184            .iter()
3185            .rev()
3186            .find(|&&idx| idx <= scroll_top.item_ix)?;
3187
3188        let needs_sticky = header_idx < scroll_top.item_ix
3189            || (header_idx == scroll_top.item_ix && scroll_top.offset_in_item > px(0.));
3190
3191        if !needs_sticky {
3192            return None;
3193        }
3194
3195        let ListEntry::ProjectHeader {
3196            key,
3197            label,
3198            highlight_positions,
3199            has_running_threads,
3200            waiting_thread_count,
3201            has_notifications,
3202            is_active,
3203            has_threads,
3204        } = self.contents.entries.get(header_idx)?
3205        else {
3206            return None;
3207        };
3208
3209        let is_focused = self.focus_handle.is_focused(window);
3210        let is_selected = is_focused && self.selection == Some(header_idx);
3211
3212        let header_element = self.render_project_header(
3213            header_idx,
3214            true,
3215            key,
3216            &label,
3217            &highlight_positions,
3218            *has_running_threads,
3219            *waiting_thread_count,
3220            *has_notifications,
3221            *is_active,
3222            is_selected,
3223            *has_threads,
3224            cx,
3225        );
3226
3227        let top_offset = self
3228            .contents
3229            .project_header_indices
3230            .iter()
3231            .find(|&&idx| idx > header_idx)
3232            .and_then(|&next_idx| {
3233                let bounds = self.list_state.bounds_for_item(next_idx)?;
3234                let viewport = self.list_state.viewport_bounds();
3235                let y_in_viewport = bounds.origin.y - viewport.origin.y;
3236                let header_height = bounds.size.height;
3237                (y_in_viewport < header_height).then_some(y_in_viewport - header_height)
3238            })
3239            .unwrap_or(px(0.));
3240
3241        let color = cx.theme().colors();
3242        let background = color
3243            .title_bar_background
3244            .blend(color.panel_background.opacity(0.2));
3245
3246        let element = v_flex()
3247            .absolute()
3248            .top(top_offset)
3249            .left_0()
3250            .w_full()
3251            .bg(background)
3252            .border_b_1()
3253            .border_color(color.border.opacity(0.5))
3254            .child(header_element)
3255            .shadow_sm()
3256            .into_any_element();
3257
3258        Some(element)
3259    }
3260
3261    fn toggle_collapse(
3262        &mut self,
3263        project_group_key: &ProjectGroupKey,
3264        _window: &mut Window,
3265        cx: &mut Context<Self>,
3266    ) {
3267        let is_collapsed = self.is_group_collapsed(project_group_key, cx);
3268        self.set_group_expanded(project_group_key, is_collapsed, cx);
3269        self.update_entries(cx);
3270    }
3271
3272    fn dispatch_context(&self, window: &Window, cx: &Context<Self>) -> KeyContext {
3273        let mut dispatch_context = KeyContext::new_with_defaults();
3274        dispatch_context.add("ThreadsSidebar");
3275        dispatch_context.add("menu");
3276
3277        let is_archived_search_focused = matches!(&self.view, SidebarView::Archive(archive) if archive.read(cx).is_filter_editor_focused(window, cx));
3278
3279        let is_renaming_thread = self
3280            .thread_rename_editor
3281            .focus_handle(cx)
3282            .is_focused(window);
3283
3284        let identifier = if self.filter_editor.focus_handle(cx).is_focused(window)
3285            || is_archived_search_focused
3286        {
3287            "searching"
3288        } else if is_renaming_thread {
3289            "editing"
3290        } else {
3291            "not_searching"
3292        };
3293
3294        dispatch_context.add(identifier);
3295        dispatch_context
3296    }
3297
3298    fn focus_in(&mut self, window: &mut Window, cx: &mut Context<Self>) {
3299        if !self.focus_handle.is_focused(window) {
3300            return;
3301        }
3302
3303        if let SidebarView::Archive(archive) = &self.view {
3304            let has_selection = archive.read(cx).has_selection();
3305            if !has_selection {
3306                archive.update(cx, |view, cx| view.focus_filter_editor(window, cx));
3307            }
3308        } else if self.selection.is_none() {
3309            self.filter_editor.focus_handle(cx).focus(window, cx);
3310        }
3311    }
3312
3313    fn cancel(&mut self, _: &Cancel, window: &mut Window, cx: &mut Context<Self>) {
3314        if self.renaming_thread_id.is_some() {
3315            self.finish_thread_rename(window, cx);
3316            return;
3317        }
3318
3319        if self.filter_editor.read(cx).is_focused(window) {
3320            if self.reset_filter_editor_text(window, cx) {
3321                self.selection = None;
3322                self.update_entries(cx);
3323                return;
3324            }
3325
3326            if self.selection.is_none() {
3327                self.select_first_entry();
3328            }
3329            if self.selection.is_some() {
3330                self.focus_handle.focus(window, cx);
3331                cx.notify();
3332            }
3333            return;
3334        }
3335
3336        if self.reset_filter_editor_text(window, cx) {
3337            self.update_entries(cx);
3338        } else {
3339            self.selection = None;
3340            self.filter_editor.focus_handle(cx).focus(window, cx);
3341            cx.notify();
3342        }
3343    }
3344
3345    fn focus_sidebar_filter(
3346        &mut self,
3347        _: &FocusSidebarFilter,
3348        window: &mut Window,
3349        cx: &mut Context<Self>,
3350    ) {
3351        self.selection = None;
3352        if let SidebarView::Archive(archive) = &self.view {
3353            archive.update(cx, |view, cx| {
3354                view.clear_selection();
3355                view.focus_filter_editor(window, cx);
3356            });
3357        } else {
3358            self.filter_editor.focus_handle(cx).focus(window, cx);
3359        }
3360
3361        cx.notify();
3362    }
3363
3364    fn reset_filter_editor_text(&mut self, window: &mut Window, cx: &mut Context<Self>) -> bool {
3365        self.filter_editor.update(cx, |editor, cx| {
3366            if editor.buffer().read(cx).len(cx).0 > 0 {
3367                editor.set_text("", window, cx);
3368                true
3369            } else {
3370                false
3371            }
3372        })
3373    }
3374
3375    fn has_filter_query(&self, cx: &App) -> bool {
3376        !self.filter_editor.read(cx).text(cx).is_empty()
3377    }
3378
3379    fn start_renaming_thread(
3380        &mut self,
3381        ix: usize,
3382        thread_id: ThreadId,
3383        title: SharedString,
3384        window: &mut Window,
3385        cx: &mut Context<Self>,
3386    ) {
3387        if self.renaming_thread_id.is_some() && self.renaming_thread_id != Some(thread_id) {
3388            self.finish_thread_rename(window, cx);
3389        }
3390
3391        self.selection = Some(ix);
3392        self.renaming_thread_id = Some(thread_id);
3393        self.suppress_next_rename_edit = true;
3394        self.list_state.scroll_to_reveal_item(ix);
3395        self.thread_rename_editor.update(cx, |editor, cx| {
3396            editor.set_text(title, window, cx);
3397            editor.select_all(&editor::actions::SelectAll, window, cx);
3398            editor.focus_handle(cx).focus(window, cx);
3399        });
3400        cx.notify();
3401    }
3402
3403    fn handle_thread_rename_editor_event(
3404        &mut self,
3405        title_editor: &Entity<Editor>,
3406        event: &editor::EditorEvent,
3407        window: &mut Window,
3408        cx: &mut Context<Self>,
3409    ) {
3410        match event {
3411            editor::EditorEvent::BufferEdited => {
3412                if self.suppress_next_rename_edit {
3413                    self.suppress_next_rename_edit = false;
3414                    return;
3415                }
3416                if !title_editor.read(cx).is_focused(window) {
3417                    return;
3418                }
3419                let new_title = title_editor.read(cx).text(cx);
3420                if new_title.is_empty() {
3421                    return;
3422                }
3423                let Some(thread_id) = self.renaming_thread_id else {
3424                    return;
3425                };
3426                self.apply_thread_rename(thread_id, SharedString::from(new_title), window, cx);
3427            }
3428            editor::EditorEvent::Blurred => {
3429                self.finish_thread_rename(window, cx);
3430            }
3431            _ => {}
3432        }
3433    }
3434
3435    fn apply_thread_rename(
3436        &mut self,
3437        thread_id: ThreadId,
3438        title: SharedString,
3439        window: &mut Window,
3440        cx: &mut Context<Self>,
3441    ) {
3442        let mut found = false;
3443        if let Some(multi_workspace) = self.multi_workspace.upgrade() {
3444            let workspaces: Vec<_> = multi_workspace.read(cx).workspaces().cloned().collect();
3445            for workspace in workspaces {
3446                if let Some(agent_panel) = workspace.read(cx).panel::<AgentPanel>(cx) {
3447                    if let Some(view) = agent_panel
3448                        .read(cx)
3449                        .conversation_view_for_id(&thread_id, cx)
3450                        && let Some(thread_view) = view.read(cx).root_thread_view()
3451                    {
3452                        thread_view.update(cx, |thread_view, cx| {
3453                            thread_view.rename(title.clone(), window, cx);
3454                        });
3455                        found = true;
3456                    }
3457                }
3458            }
3459        }
3460
3461        if !found {
3462            ThreadMetadataStore::global(cx).update(cx, |store, cx| {
3463                store.set_title_override(thread_id, title, cx);
3464            });
3465        }
3466    }
3467
3468    fn finish_thread_rename(&mut self, window: &mut Window, cx: &mut Context<Self>) -> bool {
3469        if self.renaming_thread_id.take().is_none() {
3470            return false;
3471        }
3472        self.focus_handle.focus(window, cx);
3473        self.update_entries(cx);
3474        true
3475    }
3476
3477    fn editor_move_down(&mut self, _: &MoveDown, window: &mut Window, cx: &mut Context<Self>) {
3478        self.select_next(&SelectNext, window, cx);
3479        if self.selection.is_some() {
3480            self.focus_handle.focus(window, cx);
3481        }
3482    }
3483
3484    fn editor_move_up(&mut self, _: &MoveUp, window: &mut Window, cx: &mut Context<Self>) {
3485        self.select_previous(&SelectPrevious, window, cx);
3486        if self.selection.is_some() {
3487            self.focus_handle.focus(window, cx);
3488        }
3489    }
3490
3491    fn editor_confirm(&mut self, window: &mut Window, cx: &mut Context<Self>) {
3492        if self.selection.is_none() {
3493            self.select_next(&SelectNext, window, cx);
3494        }
3495        if self.selection.is_some() {
3496            self.focus_handle.focus(window, cx);
3497        }
3498    }
3499
3500    fn select_next(&mut self, _: &SelectNext, _window: &mut Window, cx: &mut Context<Self>) {
3501        let next = match self.selection {
3502            Some(ix) if ix + 1 < self.contents.entries.len() => ix + 1,
3503            Some(_) if !self.contents.entries.is_empty() => 0,
3504            None if !self.contents.entries.is_empty() => 0,
3505            _ => return,
3506        };
3507        self.selection = Some(next);
3508        self.list_state.scroll_to_reveal_item(next);
3509        cx.notify();
3510    }
3511
3512    fn select_previous(&mut self, _: &SelectPrevious, window: &mut Window, cx: &mut Context<Self>) {
3513        match self.selection {
3514            Some(0) => {
3515                self.selection = None;
3516                self.filter_editor.focus_handle(cx).focus(window, cx);
3517                cx.notify();
3518            }
3519            Some(ix) => {
3520                self.selection = Some(ix - 1);
3521                self.list_state.scroll_to_reveal_item(ix - 1);
3522                cx.notify();
3523            }
3524            None if !self.contents.entries.is_empty() => {
3525                let last = self.contents.entries.len() - 1;
3526                self.selection = Some(last);
3527                self.list_state.scroll_to_reveal_item(last);
3528                cx.notify();
3529            }
3530            None => {}
3531        }
3532    }
3533
3534    fn select_first(&mut self, _: &SelectFirst, _window: &mut Window, cx: &mut Context<Self>) {
3535        if !self.contents.entries.is_empty() {
3536            self.selection = Some(0);
3537            self.list_state.scroll_to_reveal_item(0);
3538            cx.notify();
3539        }
3540    }
3541
3542    fn select_last(&mut self, _: &SelectLast, _window: &mut Window, cx: &mut Context<Self>) {
3543        if let Some(last) = self.contents.entries.len().checked_sub(1) {
3544            self.selection = Some(last);
3545            self.list_state.scroll_to_reveal_item(last);
3546            cx.notify();
3547        }
3548    }
3549
3550    fn confirm(&mut self, _: &Confirm, window: &mut Window, cx: &mut Context<Self>) {
3551        if self.finish_thread_rename(window, cx) {
3552            return;
3553        }
3554
3555        let Some(ix) = self.selection else { return };
3556        let Some(entry) = self.contents.entries.get(ix) else {
3557            return;
3558        };
3559
3560        match entry {
3561            ListEntry::ProjectHeader { key, .. } => {
3562                let key = key.clone();
3563                self.toggle_collapse(&key, window, cx);
3564            }
3565            ListEntry::Thread(thread) => {
3566                let metadata = thread.metadata.clone();
3567                match &thread.workspace {
3568                    ThreadEntryWorkspace::Open(workspace) => {
3569                        let workspace = workspace.clone();
3570                        self.activate_thread(metadata, &workspace, false, window, cx);
3571                    }
3572                    ThreadEntryWorkspace::Closed {
3573                        folder_paths,
3574                        project_group_key,
3575                    } => {
3576                        let folder_paths = folder_paths.clone();
3577                        let project_group_key = project_group_key.clone();
3578                        self.open_workspace_and_activate_thread(
3579                            metadata,
3580                            folder_paths,
3581                            &project_group_key,
3582                            window,
3583                            cx,
3584                        );
3585                    }
3586                }
3587            }
3588            ListEntry::Terminal(terminal) => {
3589                let metadata = terminal.metadata.clone();
3590                let workspace = terminal.workspace.clone();
3591                self.activate_terminal_entry(metadata, workspace, false, window, cx);
3592            }
3593        }
3594    }
3595
3596    fn find_workspace_across_windows(
3597        &self,
3598        cx: &App,
3599        predicate: impl Fn(&Entity<Workspace>, &App) -> bool,
3600    ) -> Option<(WindowHandle<MultiWorkspace>, Entity<Workspace>)> {
3601        cx.windows()
3602            .into_iter()
3603            .filter_map(|window| window.downcast::<MultiWorkspace>())
3604            .find_map(|window| {
3605                let workspace = window.read(cx).ok().and_then(|multi_workspace| {
3606                    multi_workspace
3607                        .workspaces()
3608                        .find(|workspace| predicate(workspace, cx))
3609                        .cloned()
3610                })?;
3611                Some((window, workspace))
3612            })
3613    }
3614
3615    fn find_workspace_in_current_window(
3616        &self,
3617        cx: &App,
3618        predicate: impl Fn(&Entity<Workspace>, &App) -> bool,
3619    ) -> Option<Entity<Workspace>> {
3620        self.multi_workspace.upgrade().and_then(|multi_workspace| {
3621            multi_workspace
3622                .read(cx)
3623                .workspaces()
3624                .find(|workspace| predicate(workspace, cx))
3625                .cloned()
3626        })
3627    }
3628
3629    fn load_agent_thread_in_workspace(
3630        workspace: &Entity<Workspace>,
3631        metadata: &ThreadMetadata,
3632        focus: bool,
3633        window: &mut Window,
3634        cx: &mut App,
3635    ) {
3636        let load_thread = |agent_panel: Entity<AgentPanel>,
3637                           metadata: &ThreadMetadata,
3638                           focus: bool,
3639                           window: &mut Window,
3640                           cx: &mut App| {
3641            agent_panel.update(cx, |panel, cx| {
3642                panel.load_agent_thread(
3643                    Agent::from(metadata.agent_id.clone()),
3644                    metadata.thread_id,
3645                    Some(metadata.folder_paths().clone()),
3646                    metadata.title.clone(),
3647                    focus,
3648                    AgentThreadSource::Sidebar,
3649                    window,
3650                    cx,
3651                );
3652            });
3653        };
3654
3655        let mut existing_panel = None;
3656        workspace.update(cx, |workspace, cx| {
3657            if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
3658                existing_panel = Some(panel);
3659            }
3660        });
3661
3662        if let Some(agent_panel) = existing_panel {
3663            load_thread(agent_panel, metadata, focus, window, cx);
3664            workspace.update(cx, |workspace, cx| {
3665                if focus {
3666                    workspace.focus_panel::<AgentPanel>(window, cx);
3667                } else {
3668                    workspace.reveal_panel::<AgentPanel>(window, cx);
3669                }
3670            });
3671            return;
3672        }
3673
3674        let workspace = workspace.downgrade();
3675        let metadata = metadata.clone();
3676        let mut async_window_cx = window.to_async(cx);
3677        cx.spawn(async move |_cx| {
3678            let panel = AgentPanel::load(workspace.clone(), async_window_cx.clone()).await?;
3679
3680            workspace.update_in(&mut async_window_cx, |workspace, window, cx| {
3681                let panel = workspace.panel::<AgentPanel>(cx).unwrap_or_else(|| {
3682                    workspace.add_panel(panel.clone(), window, cx);
3683                    panel.clone()
3684                });
3685                load_thread(panel, &metadata, focus, window, cx);
3686                if focus {
3687                    workspace.focus_panel::<AgentPanel>(window, cx);
3688                } else {
3689                    workspace.reveal_panel::<AgentPanel>(window, cx);
3690                }
3691            })?;
3692
3693            anyhow::Ok(())
3694        })
3695        .detach_and_log_err(cx);
3696    }
3697
3698    fn open_closed_native_thread_as_markdown(
3699        session_id: &acp::SessionId,
3700        title: Option<SharedString>,
3701        workspace: &Entity<Workspace>,
3702        window: &mut Window,
3703        cx: &mut App,
3704    ) {
3705        let thread_store = ThreadStore::global(cx);
3706        let load_task =
3707            thread_store.update(cx, |store, cx| store.load_thread(session_id.clone(), cx));
3708
3709        let thread_title = title
3710            .map(|t| t.to_string())
3711            .unwrap_or_else(|| DEFAULT_THREAD_TITLE.to_string());
3712
3713        let workspace = workspace.clone();
3714
3715        window
3716            .spawn(cx, async move |cx| {
3717                let db_thread = load_task.await?;
3718                let Some(db_thread) = db_thread else {
3719                    anyhow::bail!("Thread not found in database");
3720                };
3721
3722                let markdown = db_thread.to_markdown();
3723
3724                cx.update(|window, cx| {
3725                    agent_ui::open_markdown_in_workspace(
3726                        thread_title,
3727                        markdown,
3728                        workspace,
3729                        window,
3730                        cx,
3731                    )
3732                })?
3733                .await
3734            })
3735            .detach_and_log_err(cx);
3736    }
3737
3738    fn show_thread_title_toast(workspace: Entity<Workspace>, message: &'static str, cx: &mut App) {
3739        workspace.update(cx, |workspace, cx| {
3740            let toast = StatusToast::new(message, cx, |this, _cx| {
3741                this.icon(
3742                    Icon::new(IconName::Warning)
3743                        .size(IconSize::Small)
3744                        .color(Color::Warning),
3745                )
3746                .dismiss_button(true)
3747            });
3748            workspace.toggle_status_toast(toast, cx);
3749        });
3750    }
3751
3752    fn show_no_thread_summary_model_toast(workspace: Entity<Workspace>, cx: &mut App) {
3753        Self::show_thread_title_toast(
3754            workspace,
3755            "No model is configured for summarizing thread titles.",
3756            cx,
3757        );
3758    }
3759
3760    fn regenerate_thread_title(
3761        &mut self,
3762        session_id: &acp::SessionId,
3763        thread_id: ThreadId,
3764        folder_paths: PathList,
3765        thread_workspace: Option<Entity<Workspace>>,
3766        cx: &mut Context<Self>,
3767    ) {
3768        if let Some(panel) = thread_workspace
3769            .as_ref()
3770            .and_then(|w| w.read(cx).panel::<AgentPanel>(cx))
3771        {
3772            match panel.update(cx, |panel, cx| panel.regenerate_thread_title(thread_id, cx)) {
3773                ThreadTitleRegenerationResult::Started
3774                | ThreadTitleRegenerationResult::AlreadyGenerating => return,
3775                ThreadTitleRegenerationResult::NoModel => {
3776                    if let Some(workspace) = self.active_workspace(cx) {
3777                        Self::show_no_thread_summary_model_toast(workspace, cx);
3778                    }
3779                    return;
3780                }
3781                ThreadTitleRegenerationResult::NotOpen => {}
3782            }
3783        }
3784
3785        let Some(configured_model) =
3786            LanguageModelRegistry::read_global(cx).thread_summary_model(cx)
3787        else {
3788            if let Some(workspace) = self.active_workspace(cx) {
3789                Self::show_no_thread_summary_model_toast(workspace, cx);
3790            }
3791            return;
3792        };
3793
3794        if !self.regenerating_titles.insert(thread_id) {
3795            return;
3796        }
3797
3798        let model = configured_model.model;
3799        let temperature = AgentSettings::temperature_for_model(&model, cx);
3800
3801        let thread_store = ThreadStore::global(cx);
3802        let load_task =
3803            thread_store.update(cx, |store, cx| store.load_thread(session_id.clone(), cx));
3804        let session_id = session_id.clone();
3805
3806        cx.notify();
3807
3808        cx.spawn(async move |this, cx| {
3809            let result: anyhow::Result<SharedString> = async {
3810                let Some(db_thread) = load_task.await? else {
3811                    anyhow::bail!("Thread not found in database");
3812                };
3813
3814                let request = agent::build_thread_title_request(&db_thread.messages, temperature);
3815                let title =
3816                    SharedString::from(agent::stream_thread_title(model, request, cx).await?);
3817
3818                let Some(mut db_thread) = thread_store
3819                    .update(cx, |store, cx| store.load_thread(session_id.clone(), cx))
3820                    .await?
3821                else {
3822                    anyhow::bail!("Thread not found in database");
3823                };
3824                db_thread.title = title.clone();
3825
3826                thread_store
3827                    .update(cx, |store, cx| {
3828                        store.save_thread(session_id, db_thread, folder_paths, cx)
3829                    })
3830                    .await?;
3831
3832                anyhow::Ok(title)
3833            }
3834            .await;
3835
3836            this.update(cx, |this, cx| {
3837                this.regenerating_titles.remove(&thread_id);
3838                match &result {
3839                    Ok(title) => {
3840                        ThreadMetadataStore::global(cx).update(cx, |store, cx| {
3841                            store.set_generated_title(thread_id, title.clone(), cx);
3842                        });
3843                    }
3844                    Err(_) => {
3845                        if let Some(workspace) = this.active_workspace(cx) {
3846                            Self::show_thread_title_toast(
3847                                workspace,
3848                                "Failed to regenerate thread title.",
3849                                cx,
3850                            );
3851                        }
3852                    }
3853                }
3854                cx.notify();
3855            })
3856            .ok();
3857
3858            result.map(|_| ())
3859        })
3860        .detach_and_log_err(cx);
3861    }
3862
3863    fn is_thread_active_in_workspace(
3864        &self,
3865        thread_id: &ThreadId,
3866        workspace: &Entity<Workspace>,
3867        cx: &App,
3868    ) -> bool {
3869        self.active_workspace(cx).as_ref() == Some(workspace)
3870            && self.active_entry.as_ref().is_some_and(|entry| {
3871                entry.is_active_thread(thread_id) && entry.workspace() == workspace
3872            })
3873    }
3874
3875    fn activate_thread_locally(
3876        &mut self,
3877        metadata: &ThreadMetadata,
3878        workspace: &Entity<Workspace>,
3879        retain: bool,
3880        window: &mut Window,
3881        cx: &mut Context<Self>,
3882    ) {
3883        let Some(multi_workspace) = self.multi_workspace.upgrade() else {
3884            return;
3885        };
3886
3887        if self.is_thread_active_in_workspace(&metadata.thread_id, workspace, cx) {
3888            workspace.update(cx, |workspace, cx| {
3889                workspace.focus_panel::<AgentPanel>(window, cx);
3890            });
3891            return;
3892        }
3893
3894        // Set active_entry eagerly so the sidebar highlight updates
3895        // immediately, rather than waiting for a deferred AgentPanel
3896        // event which can race with ActiveWorkspaceChanged clearing it.
3897        self.active_entry = Some(ActiveEntry::Thread {
3898            thread_id: metadata.thread_id,
3899            session_id: metadata.session_id.clone(),
3900            workspace: workspace.clone(),
3901        });
3902        self.record_thread_access(&metadata.thread_id);
3903        self.pending_thread_activation = Some(metadata.thread_id);
3904
3905        multi_workspace.update(cx, |multi_workspace, cx| {
3906            multi_workspace.activate(workspace.clone(), None, window, cx);
3907            if retain {
3908                multi_workspace.retain_active_workspace(cx);
3909            }
3910        });
3911
3912        Self::load_agent_thread_in_workspace(workspace, metadata, true, window, cx);
3913
3914        self.update_entries(cx);
3915    }
3916
3917    fn activate_thread_in_other_window(
3918        &self,
3919        metadata: ThreadMetadata,
3920        workspace: Entity<Workspace>,
3921        target_window: WindowHandle<MultiWorkspace>,
3922        cx: &mut Context<Self>,
3923    ) {
3924        let target_session_id = metadata.session_id.clone();
3925        let metadata_thread_id = metadata.thread_id;
3926        let workspace_for_entry = workspace.clone();
3927
3928        let activated = target_window
3929            .update(cx, |multi_workspace, window, cx| {
3930                window.activate_window();
3931                multi_workspace.activate(workspace.clone(), None, window, cx);
3932                Self::load_agent_thread_in_workspace(&workspace, &metadata, true, window, cx);
3933            })
3934            .log_err()
3935            .is_some();
3936
3937        if activated {
3938            if let Some(target_sidebar) = target_window
3939                .read(cx)
3940                .ok()
3941                .and_then(|multi_workspace| {
3942                    multi_workspace.sidebar().map(|sidebar| sidebar.to_any())
3943                })
3944                .and_then(|sidebar| sidebar.downcast::<Self>().ok())
3945            {
3946                target_sidebar.update(cx, |sidebar, cx| {
3947                    sidebar.pending_thread_activation = Some(metadata_thread_id);
3948                    sidebar.active_entry = Some(ActiveEntry::Thread {
3949                        thread_id: metadata_thread_id,
3950                        session_id: target_session_id.clone(),
3951                        workspace: workspace_for_entry.clone(),
3952                    });
3953                    sidebar.record_thread_access(&metadata_thread_id);
3954                    sidebar.update_entries(cx);
3955                });
3956            }
3957        }
3958    }
3959
3960    fn activate_thread(
3961        &mut self,
3962        metadata: ThreadMetadata,
3963        workspace: &Entity<Workspace>,
3964        retain: bool,
3965        window: &mut Window,
3966        cx: &mut Context<Self>,
3967    ) {
3968        if self
3969            .find_workspace_in_current_window(cx, |candidate, _| candidate == workspace)
3970            .is_some()
3971        {
3972            self.activate_thread_locally(&metadata, &workspace, retain, window, cx);
3973            return;
3974        }
3975
3976        let Some((target_window, workspace)) =
3977            self.find_workspace_across_windows(cx, |candidate, _| candidate == workspace)
3978        else {
3979            return;
3980        };
3981
3982        self.activate_thread_in_other_window(metadata, workspace, target_window, cx);
3983    }
3984
3985    fn open_workspace_and_activate_thread(
3986        &mut self,
3987        metadata: ThreadMetadata,
3988        folder_paths: PathList,
3989        project_group_key: &ProjectGroupKey,
3990        window: &mut Window,
3991        cx: &mut Context<Self>,
3992    ) {
3993        let Some(multi_workspace) = self.multi_workspace.upgrade() else {
3994            return;
3995        };
3996
3997        let pending_thread_id = metadata.thread_id;
3998        // Mark the pending thread activation so rebuild_contents
3999        // preserves the Thread active_entry during loading and
4000        // reconciliation cannot synthesize an empty fallback draft.
4001        self.pending_thread_activation = Some(pending_thread_id);
4002
4003        let host = project_group_key.host();
4004        let provisional_key = Some(project_group_key.clone());
4005        let active_workspace = multi_workspace.read(cx).workspace().clone();
4006        let modal_workspace = active_workspace.clone();
4007
4008        let open_task = multi_workspace.update(cx, |this, cx| {
4009            this.find_or_create_workspace(
4010                folder_paths,
4011                host,
4012                provisional_key,
4013                |options, window, cx| connect_remote(active_workspace, options, window, cx),
4014                &[],
4015                None,
4016                OpenMode::Activate,
4017                window,
4018                cx,
4019            )
4020        });
4021
4022        cx.spawn_in(window, async move |this, cx| {
4023            let result = open_task.await;
4024            // Dismiss the modal as soon as the open attempt completes so
4025            // failures or cancellations do not leave a stale connection modal behind.
4026            remote_connection::dismiss_connection_modal(&modal_workspace, cx);
4027
4028            if result.is_err() {
4029                this.update(cx, |this, _cx| {
4030                    if this.pending_thread_activation == Some(pending_thread_id) {
4031                        this.pending_thread_activation = None;
4032                    }
4033                })
4034                .ok();
4035            }
4036
4037            let workspace = result?;
4038            this.update_in(cx, |this, window, cx| {
4039                this.activate_thread(metadata, &workspace, false, window, cx);
4040            })?;
4041            anyhow::Ok(())
4042        })
4043        .detach_and_log_err(cx);
4044    }
4045
4046    fn find_current_workspace_for_path_list(
4047        &self,
4048        path_list: &PathList,
4049        remote_connection: Option<&RemoteConnectionOptions>,
4050        cx: &App,
4051    ) -> Option<Entity<Workspace>> {
4052        self.find_workspace_in_current_window(cx, |workspace, cx| {
4053            workspace_path_list(workspace, cx).paths() == path_list.paths()
4054                && same_remote_connection_identity(
4055                    workspace
4056                        .read(cx)
4057                        .project()
4058                        .read(cx)
4059                        .remote_connection_options(cx)
4060                        .as_ref(),
4061                    remote_connection,
4062                )
4063        })
4064    }
4065
4066    fn find_open_workspace_for_path_list(
4067        &self,
4068        path_list: &PathList,
4069        remote_connection: Option<&RemoteConnectionOptions>,
4070        cx: &App,
4071    ) -> Option<(WindowHandle<MultiWorkspace>, Entity<Workspace>)> {
4072        self.find_workspace_across_windows(cx, |workspace, cx| {
4073            workspace_path_list(workspace, cx).paths() == path_list.paths()
4074                && same_remote_connection_identity(
4075                    workspace
4076                        .read(cx)
4077                        .project()
4078                        .read(cx)
4079                        .remote_connection_options(cx)
4080                        .as_ref(),
4081                    remote_connection,
4082                )
4083        })
4084    }
4085
4086    fn open_thread_from_archive(
4087        &mut self,
4088        metadata: ThreadMetadata,
4089        window: &mut Window,
4090        cx: &mut Context<Self>,
4091    ) {
4092        let thread_id = metadata.thread_id;
4093        let weak_archive_view = match &self.view {
4094            SidebarView::Archive(view) => Some(view.downgrade()),
4095            _ => None,
4096        };
4097
4098        if metadata.folder_paths().paths().is_empty() {
4099            ThreadMetadataStore::global(cx).update(cx, |store, cx| store.unarchive(thread_id, cx));
4100
4101            let active_workspace = self
4102                .multi_workspace
4103                .upgrade()
4104                .map(|w| w.read(cx).workspace().clone());
4105
4106            if let Some(workspace) = active_workspace {
4107                self.activate_thread_locally(&metadata, &workspace, false, window, cx);
4108            } else {
4109                let path_list = metadata.folder_paths().clone();
4110                if let Some((target_window, workspace)) = self.find_open_workspace_for_path_list(
4111                    &path_list,
4112                    metadata.remote_connection.as_ref(),
4113                    cx,
4114                ) {
4115                    self.activate_thread_in_other_window(metadata, workspace, target_window, cx);
4116                } else {
4117                    let key = ProjectGroupKey::from_worktree_paths(
4118                        &metadata.worktree_paths,
4119                        metadata.remote_connection.clone(),
4120                    );
4121                    self.open_workspace_and_activate_thread(metadata, path_list, &key, window, cx);
4122                }
4123            }
4124            self.show_thread_list(window, cx);
4125            return;
4126        }
4127
4128        let store = ThreadMetadataStore::global(cx);
4129        let task = if metadata.archived {
4130            store
4131                .read(cx)
4132                .get_archived_worktrees_for_thread(thread_id, cx)
4133        } else {
4134            Task::ready(Ok(Vec::new()))
4135        };
4136        let path_list = metadata.folder_paths().clone();
4137
4138        let restore_task = cx.spawn_in(window, async move |this, cx| {
4139            let result: anyhow::Result<()> = async {
4140                let archived_worktrees = task.await?;
4141
4142                if archived_worktrees.is_empty() {
4143                    this.update_in(cx, |this, window, cx| {
4144                        this.restoring_tasks.remove(&thread_id);
4145                        if metadata.archived {
4146                            ThreadMetadataStore::global(cx)
4147                                .update(cx, |store, cx| store.unarchive(thread_id, cx));
4148                        }
4149
4150                        if let Some(workspace) = this.find_current_workspace_for_path_list(
4151                            &path_list,
4152                            metadata.remote_connection.as_ref(),
4153                            cx,
4154                        ) {
4155                            this.activate_thread_locally(&metadata, &workspace, false, window, cx);
4156                        } else if let Some((target_window, workspace)) = this
4157                            .find_open_workspace_for_path_list(
4158                                &path_list,
4159                                metadata.remote_connection.as_ref(),
4160                                cx,
4161                            )
4162                        {
4163                            this.activate_thread_in_other_window(
4164                                metadata,
4165                                workspace,
4166                                target_window,
4167                                cx,
4168                            );
4169                        } else {
4170                            let key = ProjectGroupKey::from_worktree_paths(
4171                                &metadata.worktree_paths,
4172                                metadata.remote_connection.clone(),
4173                            );
4174                            this.open_workspace_and_activate_thread(
4175                                metadata, path_list, &key, window, cx,
4176                            );
4177                        }
4178                        this.show_thread_list(window, cx);
4179                    })?;
4180                    return anyhow::Ok(());
4181                }
4182
4183                let mut path_replacements: Vec<(PathBuf, PathBuf)> = Vec::new();
4184                for row in &archived_worktrees {
4185                    match thread_worktree_archive::restore_worktree_via_git(
4186                        row,
4187                        metadata.remote_connection.as_ref(),
4188                        &mut *cx,
4189                    )
4190                    .await
4191                    {
4192                        Ok(restored_path) => {
4193                            thread_worktree_archive::cleanup_archived_worktree_record(
4194                                row,
4195                                metadata.remote_connection.as_ref(),
4196                                &mut *cx,
4197                            )
4198                            .await;
4199                            path_replacements.push((row.worktree_path.clone(), restored_path));
4200                        }
4201                        Err(error) => {
4202                            log::error!("Failed to restore worktree: {error:#}");
4203                            this.update_in(cx, |this, _window, cx| {
4204                                this.restoring_tasks.remove(&thread_id);
4205                                if let Some(weak_archive_view) = &weak_archive_view {
4206                                    weak_archive_view
4207                                        .update(cx, |view, cx| {
4208                                            view.clear_restoring(&thread_id, cx);
4209                                        })
4210                                        .ok();
4211                                }
4212
4213                                if let Some(multi_workspace) = this.multi_workspace.upgrade() {
4214                                    let workspace = multi_workspace.read(cx).workspace().clone();
4215                                    workspace.update(cx, |workspace, cx| {
4216                                        struct RestoreWorktreeErrorToast;
4217                                        workspace.show_toast(
4218                                            Toast::new(
4219                                                NotificationId::unique::<RestoreWorktreeErrorToast>(
4220                                                ),
4221                                                format!("Failed to restore worktree: {error:#}"),
4222                                            )
4223                                            .autohide(),
4224                                            cx,
4225                                        );
4226                                    });
4227                                }
4228                            })
4229                            .ok();
4230                            return anyhow::Ok(());
4231                        }
4232                    }
4233                }
4234
4235                if !path_replacements.is_empty() {
4236                    cx.update(|_window, cx| {
4237                        store.update(cx, |store, cx| {
4238                            store.update_restored_worktree_paths(thread_id, &path_replacements, cx);
4239                        });
4240                    })?;
4241
4242                    let updated_metadata =
4243                        cx.update(|_window, cx| store.read(cx).entry(thread_id).cloned())?;
4244
4245                    if let Some(updated_metadata) = updated_metadata {
4246                        let new_paths = updated_metadata.folder_paths().clone();
4247                        let key = ProjectGroupKey::from_worktree_paths(
4248                            &updated_metadata.worktree_paths,
4249                            updated_metadata.remote_connection.clone(),
4250                        );
4251
4252                        cx.update(|_window, cx| {
4253                            store.update(cx, |store, cx| {
4254                                store.unarchive(updated_metadata.thread_id, cx);
4255                            });
4256                        })?;
4257
4258                        this.update_in(cx, |this, window, cx| {
4259                            this.restoring_tasks.remove(&thread_id);
4260                            this.open_workspace_and_activate_thread(
4261                                updated_metadata,
4262                                new_paths,
4263                                &key,
4264                                window,
4265                                cx,
4266                            );
4267                            this.show_thread_list(window, cx);
4268                        })?;
4269                    }
4270                }
4271
4272                anyhow::Ok(())
4273            }
4274            .await;
4275            if let Err(error) = result {
4276                log::error!("{error:#}");
4277            }
4278        });
4279        self.restoring_tasks.insert(thread_id, restore_task);
4280    }
4281
4282    fn expand_selected_entry(
4283        &mut self,
4284        _: &SelectChild,
4285        _window: &mut Window,
4286        cx: &mut Context<Self>,
4287    ) {
4288        let Some(ix) = self.selection else { return };
4289
4290        match self.contents.entries.get(ix) {
4291            Some(ListEntry::ProjectHeader { key, .. }) => {
4292                let key = key.clone();
4293                if self.is_group_collapsed(&key, cx) {
4294                    self.set_group_expanded(&key, true, cx);
4295                    self.update_entries(cx);
4296                } else if ix + 1 < self.contents.entries.len() {
4297                    self.selection = Some(ix + 1);
4298                    self.list_state.scroll_to_reveal_item(ix + 1);
4299                    cx.notify();
4300                }
4301            }
4302            _ => {}
4303        }
4304    }
4305
4306    fn collapse_selected_entry(
4307        &mut self,
4308        _: &SelectParent,
4309        _window: &mut Window,
4310        cx: &mut Context<Self>,
4311    ) {
4312        let Some(ix) = self.selection else { return };
4313
4314        match self.contents.entries.get(ix) {
4315            Some(ListEntry::ProjectHeader { key, .. }) => {
4316                let key = key.clone();
4317                if !self.is_group_collapsed(&key, cx) {
4318                    self.set_group_expanded(&key, false, cx);
4319                    self.update_entries(cx);
4320                }
4321            }
4322            Some(ListEntry::Thread(_) | ListEntry::Terminal(_)) => {
4323                for i in (0..ix).rev() {
4324                    if let Some(ListEntry::ProjectHeader { key, .. }) = self.contents.entries.get(i)
4325                    {
4326                        let key = key.clone();
4327                        self.selection = Some(i);
4328                        self.set_group_expanded(&key, false, cx);
4329                        self.update_entries(cx);
4330                        break;
4331                    }
4332                }
4333            }
4334            None => {}
4335        }
4336    }
4337
4338    fn toggle_selected_fold(
4339        &mut self,
4340        _: &editor::actions::ToggleFold,
4341        _window: &mut Window,
4342        cx: &mut Context<Self>,
4343    ) {
4344        let Some(ix) = self.selection else { return };
4345
4346        // Find the group header for the current selection.
4347        let header_ix = match self.contents.entries.get(ix) {
4348            Some(ListEntry::ProjectHeader { .. }) => Some(ix),
4349            Some(ListEntry::Thread(_) | ListEntry::Terminal(_)) => (0..ix).rev().find(|&i| {
4350                matches!(
4351                    self.contents.entries.get(i),
4352                    Some(ListEntry::ProjectHeader { .. })
4353                )
4354            }),
4355            None => None,
4356        };
4357
4358        if let Some(header_ix) = header_ix {
4359            if let Some(ListEntry::ProjectHeader { key, .. }) = self.contents.entries.get(header_ix)
4360            {
4361                let key = key.clone();
4362                if self.is_group_collapsed(&key, cx) {
4363                    self.set_group_expanded(&key, true, cx);
4364                } else {
4365                    self.selection = Some(header_ix);
4366                    self.set_group_expanded(&key, false, cx);
4367                }
4368                self.update_entries(cx);
4369            }
4370        }
4371    }
4372
4373    fn fold_all(
4374        &mut self,
4375        _: &editor::actions::FoldAll,
4376        _window: &mut Window,
4377        cx: &mut Context<Self>,
4378    ) {
4379        if let Some(mw) = self.multi_workspace.upgrade() {
4380            mw.update(cx, |mw, _cx| {
4381                mw.set_all_groups_expanded(false);
4382            });
4383        }
4384        self.update_entries(cx);
4385    }
4386
4387    fn unfold_all(
4388        &mut self,
4389        _: &editor::actions::UnfoldAll,
4390        _window: &mut Window,
4391        cx: &mut Context<Self>,
4392    ) {
4393        if let Some(mw) = self.multi_workspace.upgrade() {
4394            mw.update(cx, |mw, _cx| {
4395                mw.set_all_groups_expanded(true);
4396            });
4397        }
4398        self.update_entries(cx);
4399    }
4400
4401    fn stop_thread(&mut self, thread_id: &agent_ui::ThreadId, cx: &mut Context<Self>) {
4402        let Some(multi_workspace) = self.multi_workspace.upgrade() else {
4403            return;
4404        };
4405
4406        let workspaces: Vec<_> = multi_workspace.read(cx).workspaces().cloned().collect();
4407        for workspace in workspaces {
4408            if let Some(agent_panel) = workspace.read(cx).panel::<AgentPanel>(cx) {
4409                let cancelled =
4410                    agent_panel.update(cx, |panel, cx| panel.cancel_thread(thread_id, cx));
4411                if cancelled {
4412                    return;
4413                }
4414            }
4415        }
4416    }
4417
4418    /// Find the neighbor thread in the sidebar (by display position).
4419    /// Look below first, then above, for the nearest thread that isn't
4420    /// the one being archived. We capture both the neighbor's metadata
4421    /// (for activation) and its workspace paths (for the workspace
4422    /// removal fallback).
4423    fn neighboring_activatable_entry(&self, current_position: usize) -> Option<ActivatableEntry> {
4424        let after = self
4425            .contents
4426            .entries
4427            .get(current_position.checked_add(1)?..)?;
4428        let before = self.contents.entries.get(..current_position)?;
4429        after
4430            .iter()
4431            .chain(before.iter().rev())
4432            .find_map(ActivatableEntry::from_list_entry)
4433    }
4434
4435    fn activate_entry(
4436        &mut self,
4437        entry: &ActivatableEntry,
4438        window: &mut Window,
4439        cx: &mut Context<Self>,
4440    ) -> bool {
4441        match entry {
4442            ActivatableEntry::Thread { metadata, .. } => {
4443                let Some(workspace) = self.multi_workspace.upgrade().and_then(|multi_workspace| {
4444                    multi_workspace
4445                        .read(cx)
4446                        .workspace_for_paths(metadata.folder_paths(), None, cx)
4447                }) else {
4448                    return false;
4449                };
4450
4451                self.active_entry = Some(ActiveEntry::Thread {
4452                    thread_id: metadata.thread_id,
4453                    session_id: metadata.session_id.clone(),
4454                    workspace: workspace.clone(),
4455                });
4456                self.activate_workspace(&workspace, window, cx);
4457                Self::load_agent_thread_in_workspace(&workspace, metadata, true, window, cx);
4458                true
4459            }
4460            ActivatableEntry::Terminal {
4461                metadata,
4462                workspace,
4463            } => {
4464                self.activate_terminal_entry(
4465                    metadata.clone(),
4466                    workspace.clone(),
4467                    false,
4468                    window,
4469                    cx,
4470                );
4471                true
4472            }
4473        }
4474    }
4475
4476    fn activate_terminal_entry(
4477        &mut self,
4478        metadata: TerminalThreadMetadata,
4479        workspace: ThreadEntryWorkspace,
4480        retain: bool,
4481        window: &mut Window,
4482        cx: &mut Context<Self>,
4483    ) {
4484        match workspace {
4485            ThreadEntryWorkspace::Open(workspace) => {
4486                self.activate_terminal_in_workspace(&workspace, metadata, retain, window, cx);
4487            }
4488            ThreadEntryWorkspace::Closed {
4489                folder_paths,
4490                project_group_key,
4491            } => {
4492                self.open_workspace_and_activate_terminal(
4493                    metadata,
4494                    folder_paths,
4495                    &project_group_key,
4496                    window,
4497                    cx,
4498                );
4499            }
4500        }
4501    }
4502
4503    fn load_agent_terminal_in_workspace(
4504        workspace: &Entity<Workspace>,
4505        metadata: &TerminalThreadMetadata,
4506        focus: bool,
4507        window: &mut Window,
4508        cx: &mut App,
4509    ) {
4510        let restore_terminal = |agent_panel: Entity<AgentPanel>,
4511                                metadata: &TerminalThreadMetadata,
4512                                focus: bool,
4513                                workspace: Option<&Workspace>,
4514                                window: &mut Window,
4515                                cx: &mut App| {
4516            agent_panel.update(cx, |panel, cx| {
4517                panel.restore_terminal(
4518                    metadata.clone(),
4519                    focus,
4520                    AgentThreadSource::Sidebar,
4521                    workspace,
4522                    window,
4523                    cx,
4524                );
4525            });
4526        };
4527
4528        let mut existing_panel = None;
4529        workspace.update(cx, |workspace, cx| {
4530            if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
4531                existing_panel = Some(panel);
4532            }
4533        });
4534
4535        if let Some(agent_panel) = existing_panel {
4536            restore_terminal(agent_panel, metadata, focus, None, window, cx);
4537            workspace.update(cx, |workspace, cx| {
4538                if focus {
4539                    workspace.focus_panel::<AgentPanel>(window, cx);
4540                } else {
4541                    workspace.reveal_panel::<AgentPanel>(window, cx);
4542                }
4543            });
4544            return;
4545        }
4546
4547        let workspace = workspace.downgrade();
4548        let metadata = metadata.clone();
4549        let mut async_window_cx = window.to_async(cx);
4550        cx.spawn(async move |_cx| {
4551            let panel = AgentPanel::load(workspace.clone(), async_window_cx.clone()).await?;
4552
4553            workspace.update_in(&mut async_window_cx, |workspace, window, cx| {
4554                let panel = workspace.panel::<AgentPanel>(cx).unwrap_or_else(|| {
4555                    workspace.add_panel(panel.clone(), window, cx);
4556                    panel.clone()
4557                });
4558                restore_terminal(panel, &metadata, focus, Some(workspace), window, cx);
4559                if focus {
4560                    workspace.focus_panel::<AgentPanel>(window, cx);
4561                } else {
4562                    workspace.reveal_panel::<AgentPanel>(window, cx);
4563                }
4564            })?;
4565
4566            anyhow::Ok(())
4567        })
4568        .detach_and_log_err(cx);
4569    }
4570
4571    fn activate_terminal_in_workspace(
4572        &mut self,
4573        workspace: &Entity<Workspace>,
4574        metadata: TerminalThreadMetadata,
4575        retain: bool,
4576        window: &mut Window,
4577        cx: &mut Context<Self>,
4578    ) {
4579        let Some(multi_workspace) = self.multi_workspace.upgrade() else {
4580            return;
4581        };
4582
4583        let terminal_id = metadata.terminal_id;
4584        self.record_terminal_access(terminal_id);
4585        self.active_entry = Some(ActiveEntry::Terminal {
4586            terminal_id,
4587            workspace: workspace.clone(),
4588        });
4589
4590        multi_workspace.update(cx, |multi_workspace, cx| {
4591            multi_workspace.activate(workspace.clone(), None, window, cx);
4592            if retain {
4593                multi_workspace.retain_active_workspace(cx);
4594            }
4595        });
4596
4597        Self::load_agent_terminal_in_workspace(workspace, &metadata, true, window, cx);
4598
4599        self.update_entries(cx);
4600    }
4601
4602    fn open_workspace_and_activate_terminal(
4603        &mut self,
4604        metadata: TerminalThreadMetadata,
4605        folder_paths: PathList,
4606        project_group_key: &ProjectGroupKey,
4607        window: &mut Window,
4608        cx: &mut Context<Self>,
4609    ) {
4610        let Some(multi_workspace) = self.multi_workspace.upgrade() else {
4611            return;
4612        };
4613
4614        let host = project_group_key.host();
4615        let provisional_key = Some(project_group_key.clone());
4616        let active_workspace = multi_workspace.read(cx).workspace().clone();
4617        let modal_workspace = active_workspace.clone();
4618
4619        let open_task = multi_workspace.update(cx, |this, cx| {
4620            this.find_or_create_workspace(
4621                folder_paths,
4622                host,
4623                provisional_key,
4624                |options, window, cx| connect_remote(active_workspace, options, window, cx),
4625                &[],
4626                None,
4627                OpenMode::Activate,
4628                window,
4629                cx,
4630            )
4631        });
4632
4633        cx.spawn_in(window, async move |this, cx| {
4634            let result = open_task.await;
4635            remote_connection::dismiss_connection_modal(&modal_workspace, cx);
4636            let workspace = result?;
4637            this.update_in(cx, |this, window, cx| {
4638                this.activate_terminal_in_workspace(&workspace, metadata, false, window, cx);
4639            })?;
4640            anyhow::Ok(())
4641        })
4642        .detach_and_log_err(cx);
4643    }
4644
4645    fn should_load_closed_workspace_for_archive(
4646        &self,
4647        folder_paths: &PathList,
4648        project_group_key: &ProjectGroupKey,
4649        remote_connection: Option<&RemoteConnectionOptions>,
4650        except_thread_id: Option<ThreadId>,
4651        except_terminal_id: Option<TerminalId>,
4652        cx: &App,
4653    ) -> bool {
4654        if folder_paths.is_empty() || folder_paths == project_group_key.path_list() {
4655            return false;
4656        }
4657
4658        let archive_workspaces = self.archive_workspaces(cx);
4659        let thread_store = ThreadMetadataStore::global(cx);
4660        let thread_store = thread_store.read(cx);
4661        if folder_paths.ordered_paths().any(|path| {
4662            Self::path_is_referenced_by_unarchived_threads_for_archive(
4663                &thread_store,
4664                except_thread_id,
4665                path,
4666                remote_connection,
4667                &archive_workspaces,
4668                cx,
4669            )
4670        }) {
4671            return false;
4672        }
4673
4674        TerminalThreadMetadataStore::try_global(cx).is_none_or(|terminal_store| {
4675            let terminal_store = terminal_store.read(cx);
4676            !folder_paths.ordered_paths().any(|path| {
4677                terminal_store.path_is_referenced_by_terminal(
4678                    except_terminal_id,
4679                    path,
4680                    remote_connection,
4681                )
4682            })
4683        })
4684    }
4685
4686    fn path_is_referenced_by_unarchived_threads_for_archive(
4687        thread_store: &ThreadMetadataStore,
4688        except_thread_id: Option<ThreadId>,
4689        path: &Path,
4690        remote_connection: Option<&RemoteConnectionOptions>,
4691        archive_workspaces: &[Entity<Workspace>],
4692        cx: &App,
4693    ) -> bool {
4694        thread_store.path_is_referenced_by_unarchived_threads_matching(
4695            except_thread_id,
4696            path,
4697            remote_connection,
4698            |thread| Self::thread_blocks_worktree_archive(thread, archive_workspaces, cx),
4699        )
4700    }
4701
4702    fn archive_workspaces(&self, cx: &App) -> Vec<Entity<Workspace>> {
4703        let multi_workspace = self.multi_workspace.upgrade();
4704        thread_worktree_archive::workspaces_for_archive(multi_workspace.as_ref(), cx)
4705    }
4706
4707    fn count_threads_blocking_worktree_archive(
4708        &self,
4709        path_list: &PathList,
4710        remote_connection: Option<&RemoteConnectionOptions>,
4711        except_thread_id: Option<ThreadId>,
4712        cx: &App,
4713    ) -> usize {
4714        let archive_workspaces = self.archive_workspaces(cx);
4715        ThreadMetadataStore::global(cx)
4716            .read(cx)
4717            .entries_for_path(path_list, remote_connection)
4718            .filter(|thread| Some(thread.thread_id) != except_thread_id)
4719            .filter(|thread| Self::thread_blocks_worktree_archive(thread, &archive_workspaces, cx))
4720            .count()
4721    }
4722
4723    fn roots_to_archive_for_paths(
4724        &self,
4725        folder_paths: &PathList,
4726        remote_connection: Option<&RemoteConnectionOptions>,
4727        except_thread_id: Option<ThreadId>,
4728        except_terminal_id: Option<TerminalId>,
4729        cx: &App,
4730    ) -> Vec<thread_worktree_archive::RootPlan> {
4731        let workspaces = self.archive_workspaces(cx);
4732        folder_paths
4733            .ordered_paths()
4734            .filter_map(|path| {
4735                thread_worktree_archive::build_root_plan(path, remote_connection, &workspaces, cx)
4736            })
4737            .filter(|plan| {
4738                let store = ThreadMetadataStore::global(cx);
4739                let store = store.read(cx);
4740                !Self::path_is_referenced_by_unarchived_threads_for_archive(
4741                    &store,
4742                    except_thread_id,
4743                    plan.root_path.as_path(),
4744                    remote_connection,
4745                    &workspaces,
4746                    cx,
4747                )
4748            })
4749            .filter(|root| {
4750                TerminalThreadMetadataStore::try_global(cx).is_none_or(|terminal_store| {
4751                    !terminal_store.read(cx).path_is_referenced_by_terminal(
4752                        except_terminal_id,
4753                        root.root_path.as_path(),
4754                        remote_connection,
4755                    )
4756                })
4757            })
4758            .collect()
4759    }
4760
4761    fn linked_worktree_workspace_to_remove(
4762        &self,
4763        folder_paths: &PathList,
4764        remote_connection: Option<&RemoteConnectionOptions>,
4765        except_thread_id: Option<ThreadId>,
4766        except_terminal_id: Option<TerminalId>,
4767        roots_to_archive: &[thread_worktree_archive::RootPlan],
4768        cx: &App,
4769    ) -> Option<Entity<Workspace>> {
4770        if folder_paths.is_empty() {
4771            return None;
4772        }
4773
4774        let remaining = self.count_threads_blocking_worktree_archive(
4775            folder_paths,
4776            remote_connection,
4777            except_thread_id,
4778            cx,
4779        );
4780
4781        if remaining > 0 {
4782            return None;
4783        }
4784
4785        let multi_workspace = self.multi_workspace.upgrade()?;
4786        let workspace =
4787            multi_workspace
4788                .read(cx)
4789                .workspace_for_paths(folder_paths, remote_connection, cx)?;
4790
4791        if workspace_has_terminal_metadata_except(&workspace, except_terminal_id, cx) {
4792            return None;
4793        }
4794
4795        if !roots_to_archive.is_empty() {
4796            let archive_paths: HashSet<&Path> = roots_to_archive
4797                .iter()
4798                .map(|root| root.root_path.as_path())
4799                .collect();
4800            let project = workspace.read(cx).project().clone();
4801            let visible_worktree_paths = project
4802                .read(cx)
4803                .visible_worktrees(cx)
4804                .map(|worktree| worktree.read(cx).abs_path())
4805                .collect::<Vec<_>>();
4806            return (!visible_worktree_paths.is_empty()
4807                && visible_worktree_paths
4808                    .iter()
4809                    .all(|path| archive_paths.contains(path.as_ref())))
4810            .then_some(workspace);
4811        }
4812
4813        let group_key = workspace.read(cx).project_group_key(cx);
4814        (group_key.path_list() != folder_paths).then_some(workspace)
4815    }
4816
4817    fn delete_empty_drafts_for_archive_roots(
4818        &self,
4819        roots: &[thread_worktree_archive::RootPlan],
4820        cx: &mut Context<Self>,
4821    ) {
4822        self.delete_empty_drafts_for_archive_targets(
4823            roots
4824                .iter()
4825                .map(|root| (root.root_path.as_path(), root.remote_connection.as_ref())),
4826            cx,
4827        );
4828    }
4829
4830    fn delete_empty_drafts_for_archive_paths(
4831        &self,
4832        paths: &PathList,
4833        remote_connection: Option<&RemoteConnectionOptions>,
4834        cx: &mut Context<Self>,
4835    ) {
4836        self.delete_empty_drafts_for_archive_targets(
4837            paths
4838                .ordered_paths()
4839                .map(|path| (path.as_path(), remote_connection)),
4840            cx,
4841        );
4842    }
4843
4844    fn delete_empty_drafts_for_archive_targets<'a>(
4845        &self,
4846        targets: impl IntoIterator<Item = (&'a Path, Option<&'a RemoteConnectionOptions>)>,
4847        cx: &mut Context<Self>,
4848    ) {
4849        let targets = targets.into_iter().collect::<Vec<_>>();
4850        if targets.is_empty() {
4851            return;
4852        }
4853
4854        let archive_workspaces = self.archive_workspaces(cx);
4855        let draft_thread_ids = ThreadMetadataStore::global(cx)
4856            .read(cx)
4857            .unarchived_draft_ids_matching(|thread| {
4858                targets.iter().any(|(path, remote_connection)| {
4859                    thread.matches_remote_connection(*remote_connection)
4860                        && thread.references_folder_path(path)
4861                }) && !Self::thread_blocks_worktree_archive(thread, &archive_workspaces, cx)
4862            });
4863        if draft_thread_ids.is_empty() {
4864            return;
4865        }
4866
4867        ThreadMetadataStore::global(cx).update(cx, |store, cx| {
4868            store.delete_all(draft_thread_ids, cx);
4869        });
4870    }
4871
4872    fn thread_blocks_worktree_archive(
4873        thread: &ThreadMetadata,
4874        archive_workspaces: &[Entity<Workspace>],
4875        cx: &App,
4876    ) -> bool {
4877        if !thread.is_draft() {
4878            return true;
4879        }
4880
4881        agent_ui::draft_prompt_store::draft_has_user_content(
4882            thread.thread_id,
4883            archive_workspaces,
4884            cx,
4885        )
4886    }
4887
4888    async fn wait_for_archive_workspace_metadata(
4889        workspace: &Entity<Workspace>,
4890        cx: &mut gpui::AsyncApp,
4891    ) {
4892        let scans_complete =
4893            workspace.read_with(cx, |workspace, cx| workspace.worktree_scans_complete(cx));
4894        scans_complete.await;
4895
4896        let project = workspace.read_with(cx, |workspace, _| workspace.project().clone());
4897        let barriers = project.update(cx, |project, cx| {
4898            let repositories = project
4899                .repositories(cx)
4900                .values()
4901                .cloned()
4902                .collect::<Vec<_>>();
4903            repositories
4904                .into_iter()
4905                .map(|repository| repository.update(cx, |repository, _| repository.barrier()))
4906                .collect::<Vec<_>>()
4907        });
4908        for barrier in barriers {
4909            let result: anyhow::Result<()> = barrier.await.map_err(|_| {
4910                anyhow::anyhow!("git repository barrier canceled while archiving worktree")
4911            });
4912            result.log_err();
4913        }
4914    }
4915
4916    fn open_workspace_for_archive(
4917        &mut self,
4918        folder_paths: PathList,
4919        project_group_key: ProjectGroupKey,
4920        window: &mut Window,
4921        cx: &mut Context<Self>,
4922    ) -> Option<(Task<anyhow::Result<Entity<Workspace>>>, Entity<Workspace>)> {
4923        let Some(multi_workspace) = self.multi_workspace.upgrade() else {
4924            return None;
4925        };
4926
4927        let host = project_group_key.host();
4928        let active_workspace = multi_workspace.read(cx).workspace().clone();
4929        let modal_workspace = active_workspace.clone();
4930
4931        let open_task = multi_workspace.update(cx, |this, cx| {
4932            this.find_or_create_workspace(
4933                folder_paths,
4934                host,
4935                Some(project_group_key),
4936                |options, window, cx| connect_remote(active_workspace, options, window, cx),
4937                &[],
4938                None,
4939                OpenMode::Add,
4940                window,
4941                cx,
4942            )
4943        });
4944
4945        Some((open_task, modal_workspace))
4946    }
4947
4948    fn open_workspace_and_archive_thread(
4949        &mut self,
4950        session_id: acp::SessionId,
4951        folder_paths: PathList,
4952        project_group_key: ProjectGroupKey,
4953        window: &mut Window,
4954        cx: &mut Context<Self>,
4955    ) {
4956        let Some((open_task, modal_workspace)) =
4957            self.open_workspace_for_archive(folder_paths, project_group_key, window, cx)
4958        else {
4959            return;
4960        };
4961
4962        cx.spawn_in(window, async move |this, cx| {
4963            let result = open_task.await;
4964            remote_connection::dismiss_connection_modal(&modal_workspace, cx);
4965            let workspace = result?;
4966            Self::wait_for_archive_workspace_metadata(&workspace, cx).await;
4967
4968            this.update_in(cx, |this, window, cx| {
4969                this.update_entries(cx);
4970                this.archive_thread(&session_id, window, cx);
4971            })?;
4972            anyhow::Ok(())
4973        })
4974        .detach_and_log_err(cx);
4975    }
4976
4977    fn open_workspace_and_close_terminal(
4978        &mut self,
4979        metadata: TerminalThreadMetadata,
4980        folder_paths: PathList,
4981        project_group_key: ProjectGroupKey,
4982        window: &mut Window,
4983        cx: &mut Context<Self>,
4984    ) {
4985        let Some((open_task, modal_workspace)) =
4986            self.open_workspace_for_archive(folder_paths, project_group_key, window, cx)
4987        else {
4988            return;
4989        };
4990
4991        cx.spawn_in(window, async move |this, cx| {
4992            let result = open_task.await;
4993            remote_connection::dismiss_connection_modal(&modal_workspace, cx);
4994            let workspace = result?;
4995            Self::wait_for_archive_workspace_metadata(&workspace, cx).await;
4996
4997            this.update_in(cx, |this, window, cx| {
4998                let workspace = ThreadEntryWorkspace::Open(workspace);
4999                this.close_terminal(&metadata, &workspace, window, cx);
5000            })?;
5001            anyhow::Ok(())
5002        })
5003        .detach_and_log_err(cx);
5004    }
5005
5006    fn close_terminal(
5007        &mut self,
5008        metadata: &TerminalThreadMetadata,
5009        workspace: &ThreadEntryWorkspace,
5010        window: &mut Window,
5011        cx: &mut Context<Self>,
5012    ) {
5013        if let ThreadEntryWorkspace::Closed {
5014            folder_paths,
5015            project_group_key,
5016        } = workspace
5017            && self.should_load_closed_workspace_for_archive(
5018                folder_paths,
5019                project_group_key,
5020                metadata.remote_connection.as_ref(),
5021                None,
5022                Some(metadata.terminal_id),
5023                cx,
5024            )
5025        {
5026            self.open_workspace_and_close_terminal(
5027                metadata.clone(),
5028                folder_paths.clone(),
5029                project_group_key.clone(),
5030                window,
5031                cx,
5032            );
5033            return;
5034        }
5035
5036        let terminal_id = metadata.terminal_id;
5037        let is_active = self
5038            .active_entry
5039            .as_ref()
5040            .is_some_and(|entry| entry.is_active_terminal(terminal_id));
5041        let neighbor = self
5042            .contents
5043            .entries
5044            .iter()
5045            .position(|entry| {
5046                matches!(
5047                    entry,
5048                    ListEntry::Terminal(terminal)
5049                        if terminal.metadata.terminal_id == terminal_id
5050                )
5051            })
5052            .and_then(|position| self.neighboring_activatable_entry(position));
5053
5054        let terminal_folder_paths = metadata.folder_paths().clone();
5055        let roots_to_archive = self.roots_to_archive_for_paths(
5056            metadata.folder_paths(),
5057            metadata.remote_connection.as_ref(),
5058            None,
5059            Some(terminal_id),
5060            cx,
5061        );
5062
5063        let workspace_to_remove = self.linked_worktree_workspace_to_remove(
5064            &terminal_folder_paths,
5065            metadata.remote_connection.as_ref(),
5066            None,
5067            Some(terminal_id),
5068            &roots_to_archive,
5069            cx,
5070        );
5071
5072        let mut workspaces_to_remove: Vec<Entity<Workspace>> =
5073            workspace_to_remove.into_iter().collect();
5074        let close_item_tasks = self.close_items_for_archived_worktrees(
5075            &roots_to_archive,
5076            &mut workspaces_to_remove,
5077            window,
5078            cx,
5079        );
5080
5081        if !workspaces_to_remove.is_empty() {
5082            let multi_workspace = self.multi_workspace.upgrade().unwrap();
5083            let terminal_workspace_removed = matches!(
5084                workspace,
5085                ThreadEntryWorkspace::Open(workspace) if workspaces_to_remove.contains(workspace)
5086            );
5087            let (fallback_paths, project_group_key) = neighbor
5088                .as_ref()
5089                .map(|neighbor| neighbor.project_location(cx))
5090                .unwrap_or_else(|| {
5091                    workspaces_to_remove
5092                        .first()
5093                        .map(|workspace| {
5094                            let key = workspace.read(cx).project_group_key(cx);
5095                            (key.path_list().clone(), key)
5096                        })
5097                        .unwrap_or_default()
5098                });
5099
5100            let excluded = workspaces_to_remove.clone();
5101            let remove_task = multi_workspace.update(cx, |multi_workspace, cx| {
5102                multi_workspace.remove(
5103                    workspaces_to_remove,
5104                    move |this, window, cx| {
5105                        let active_workspace = this.workspace().clone();
5106                        this.find_or_create_workspace(
5107                            fallback_paths,
5108                            project_group_key.host(),
5109                            Some(project_group_key),
5110                            |options, window, cx| {
5111                                connect_remote(active_workspace, options, window, cx)
5112                            },
5113                            &excluded,
5114                            None,
5115                            OpenMode::Activate,
5116                            window,
5117                            cx,
5118                        )
5119                    },
5120                    window,
5121                    cx,
5122                )
5123            });
5124
5125            let metadata = metadata.clone();
5126            let workspace = workspace.clone();
5127            cx.spawn_in(window, async move |this, cx| {
5128                if !remove_task.await? {
5129                    return anyhow::Ok(());
5130                }
5131
5132                for task in close_item_tasks {
5133                    let result: anyhow::Result<()> = task.await;
5134                    result.log_err();
5135                }
5136
5137                this.update_in(cx, |this, window, cx| {
5138                    if terminal_workspace_removed {
5139                        this.delete_empty_drafts_for_archive_paths(
5140                            metadata.folder_paths(),
5141                            metadata.remote_connection.as_ref(),
5142                            cx,
5143                        );
5144                    }
5145                    // If the terminal's workspace has already been removed,
5146                    // don't synthesize a fallback draft in the detached
5147                    // AgentPanel.
5148                    this.close_terminal_entry(
5149                        &metadata,
5150                        &workspace,
5151                        is_active,
5152                        neighbor.as_ref(),
5153                        !terminal_workspace_removed,
5154                        roots_to_archive,
5155                        window,
5156                        cx,
5157                    );
5158                })?;
5159                anyhow::Ok(())
5160            })
5161            .detach_and_log_err(cx);
5162        } else if !close_item_tasks.is_empty() {
5163            let metadata = metadata.clone();
5164            let workspace = workspace.clone();
5165            cx.spawn_in(window, async move |this, cx| {
5166                for task in close_item_tasks {
5167                    let result: anyhow::Result<()> = task.await;
5168                    result.log_err();
5169                }
5170
5171                this.update_in(cx, |this, window, cx| {
5172                    this.close_terminal_entry(
5173                        &metadata,
5174                        &workspace,
5175                        is_active,
5176                        neighbor.as_ref(),
5177                        true,
5178                        roots_to_archive,
5179                        window,
5180                        cx,
5181                    );
5182                })?;
5183                anyhow::Ok(())
5184            })
5185            .detach_and_log_err(cx);
5186        } else {
5187            self.close_terminal_entry(
5188                metadata,
5189                workspace,
5190                is_active,
5191                neighbor.as_ref(),
5192                true,
5193                roots_to_archive,
5194                window,
5195                cx,
5196            );
5197        }
5198    }
5199
5200    fn close_terminal_entry(
5201        &mut self,
5202        metadata: &TerminalThreadMetadata,
5203        workspace: &ThreadEntryWorkspace,
5204        is_active: bool,
5205        neighbor: Option<&ActivatableEntry>,
5206        activate_panel_draft: bool,
5207        roots_to_archive: Vec<thread_worktree_archive::RootPlan>,
5208        window: &mut Window,
5209        cx: &mut Context<Self>,
5210    ) {
5211        let terminal_id = metadata.terminal_id;
5212        let defer_draft_activation = activate_panel_draft && is_active && neighbor.is_some();
5213
5214        // Closing from the sidebar must not steal focus, since the row's
5215        // workspace may not be the active workspace.
5216        if let ThreadEntryWorkspace::Open(workspace) = workspace {
5217            workspace.update(cx, |workspace, cx| {
5218                if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
5219                    panel.update(cx, |panel, cx| {
5220                        if defer_draft_activation || !activate_panel_draft {
5221                            panel.close_terminal_without_activating_draft(terminal_id, window, cx);
5222                        } else {
5223                            panel.close_terminal(terminal_id, window, cx);
5224                        }
5225                    });
5226                }
5227            });
5228        }
5229        if let Some(store) = TerminalThreadMetadataStore::try_global(cx) {
5230            store.update(cx, |store, cx| {
5231                store.delete(terminal_id, cx);
5232            });
5233        }
5234
5235        self.start_detached_archive_worktree_task(roots_to_archive, cx);
5236
5237        if is_active {
5238            self.active_entry = None;
5239            if neighbor
5240                .as_ref()
5241                .is_some_and(|neighbor| self.activate_entry(neighbor, window, cx))
5242            {
5243                return;
5244            }
5245            if defer_draft_activation && let ThreadEntryWorkspace::Open(workspace) = workspace {
5246                workspace.update(cx, |workspace, cx| {
5247                    if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
5248                        panel.update(cx, |panel, cx| {
5249                            panel.activate_draft(false, AgentThreadSource::AgentPanel, window, cx);
5250                        });
5251                    }
5252                });
5253            }
5254            self.sync_active_entry_from_active_workspace(cx);
5255        }
5256        self.update_entries(cx);
5257    }
5258
5259    fn close_items_for_archived_worktrees(
5260        &self,
5261        roots_to_archive: &[thread_worktree_archive::RootPlan],
5262        workspaces_to_remove: &mut Vec<Entity<Workspace>>,
5263        window: &mut Window,
5264        cx: &mut Context<Self>,
5265    ) -> Vec<Task<anyhow::Result<()>>> {
5266        if roots_to_archive.is_empty() {
5267            return Vec::new();
5268        }
5269
5270        let archive_paths: HashSet<&Path> = roots_to_archive
5271            .iter()
5272            .map(|root| root.root_path.as_path())
5273            .collect();
5274
5275        let mut mixed_workspaces: Vec<(Entity<Workspace>, Vec<WorktreeId>)> = Vec::new();
5276
5277        if let Some(multi_workspace) = self.multi_workspace.upgrade() {
5278            let all_workspaces: Vec<_> = multi_workspace.read(cx).workspaces().cloned().collect();
5279
5280            for workspace in all_workspaces {
5281                if workspaces_to_remove.contains(&workspace) {
5282                    continue;
5283                }
5284
5285                let project = workspace.read(cx).project().read(cx);
5286                let visible_worktrees: Vec<_> = project
5287                    .visible_worktrees(cx)
5288                    .map(|worktree| (worktree.read(cx).id(), worktree.read(cx).abs_path()))
5289                    .collect();
5290
5291                let archived_worktree_ids: Vec<WorktreeId> = visible_worktrees
5292                    .iter()
5293                    .filter(|(_, path)| archive_paths.contains(path.as_ref()))
5294                    .map(|(id, _)| *id)
5295                    .collect();
5296
5297                if archived_worktree_ids.is_empty() {
5298                    continue;
5299                }
5300
5301                if visible_worktrees.len() == archived_worktree_ids.len() {
5302                    workspaces_to_remove.push(workspace);
5303                } else {
5304                    mixed_workspaces.push((workspace, archived_worktree_ids));
5305                }
5306            }
5307        }
5308
5309        let mut close_item_tasks = Vec::new();
5310        for (workspace, archived_worktree_ids) in &mixed_workspaces {
5311            let panes: Vec<_> = workspace.read(cx).panes().to_vec();
5312            for pane in panes {
5313                let items_to_close: Vec<EntityId> = pane
5314                    .read(cx)
5315                    .items()
5316                    .filter(|item| {
5317                        item.project_path(cx)
5318                            .is_some_and(|pp| archived_worktree_ids.contains(&pp.worktree_id))
5319                    })
5320                    .map(|item| item.item_id())
5321                    .collect();
5322
5323                if !items_to_close.is_empty() {
5324                    let task = pane.update(cx, |pane, cx| {
5325                        pane.close_items(window, cx, SaveIntent::Close, &|item_id| {
5326                            items_to_close.contains(&item_id)
5327                        })
5328                    });
5329                    close_item_tasks.push(task);
5330                }
5331            }
5332        }
5333
5334        close_item_tasks
5335    }
5336
5337    fn archive_thread(
5338        &mut self,
5339        session_id: &acp::SessionId,
5340        window: &mut Window,
5341        cx: &mut Context<Self>,
5342    ) {
5343        let store = ThreadMetadataStore::global(cx);
5344        let metadata = store.read(cx).entry_by_session(session_id).cloned();
5345        let metadata_thread_id = metadata.as_ref().map(|metadata| metadata.thread_id);
5346        let thread_entry = self.contents.entries.iter().find_map(|entry| match entry {
5347            ListEntry::Thread(thread) => metadata_thread_id
5348                .map_or_else(
5349                    || thread.metadata.session_id.as_ref() == Some(session_id),
5350                    |thread_id| thread.metadata.thread_id == thread_id,
5351                )
5352                .then(|| thread.clone()),
5353            _ => None,
5354        });
5355        let thread_id = metadata_thread_id.or_else(|| {
5356            thread_entry
5357                .as_ref()
5358                .map(|thread| thread.metadata.thread_id)
5359        });
5360        let active_workspace = thread_id.and_then(|thread_id| {
5361            self.active_entry.as_ref().and_then(|entry| {
5362                if entry.is_active_thread(&thread_id) {
5363                    Some(entry.workspace().clone())
5364                } else {
5365                    None
5366                }
5367            })
5368        });
5369        let thread_folder_paths = metadata
5370            .as_ref()
5371            .map(|metadata| metadata.folder_paths().clone())
5372            .or_else(|| {
5373                thread_entry
5374                    .as_ref()
5375                    .map(|thread| thread.metadata.folder_paths().clone())
5376            })
5377            .or_else(|| {
5378                active_workspace
5379                    .as_ref()
5380                    .map(|workspace| PathList::new(&workspace.read(cx).root_paths(cx)))
5381            });
5382        let thread_entry_workspace = thread_entry.map(|thread| thread.workspace.clone());
5383
5384        if let (
5385            Some(metadata),
5386            Some(ThreadEntryWorkspace::Closed {
5387                folder_paths,
5388                project_group_key,
5389            }),
5390        ) = (metadata.as_ref(), thread_entry_workspace)
5391            && self.should_load_closed_workspace_for_archive(
5392                &folder_paths,
5393                &project_group_key,
5394                metadata.remote_connection.as_ref(),
5395                Some(metadata.thread_id),
5396                None,
5397                cx,
5398            )
5399        {
5400            self.open_workspace_and_archive_thread(
5401                session_id.clone(),
5402                folder_paths,
5403                project_group_key,
5404                window,
5405                cx,
5406            );
5407            return;
5408        }
5409
5410        // Compute which linked worktree roots should be archived from disk if
5411        // this thread is archived. This must happen before we remove any
5412        // workspace from the MultiWorkspace, because `build_root_plan` needs
5413        // the currently open workspaces in order to find the affected projects
5414        // and repository handles for each linked worktree.
5415        let roots_to_archive = metadata
5416            .as_ref()
5417            .map(|metadata| {
5418                self.roots_to_archive_for_paths(
5419                    metadata.folder_paths(),
5420                    metadata.remote_connection.as_ref(),
5421                    thread_id,
5422                    None,
5423                    cx,
5424                )
5425            })
5426            .unwrap_or_default();
5427
5428        let current_pos = self.contents.entries.iter().position(|entry| match entry {
5429            ListEntry::Thread(thread) => thread_id.map_or_else(
5430                || thread.metadata.session_id.as_ref() == Some(session_id),
5431                |tid| thread.metadata.thread_id == tid,
5432            ),
5433            _ => false,
5434        });
5435        let neighbor =
5436            current_pos.and_then(|position| self.neighboring_activatable_entry(position));
5437
5438        // Check if archiving this thread would leave its worktree workspace
5439        // with no threads, requiring workspace removal.
5440        let workspace_to_remove = thread_folder_paths.as_ref().and_then(|folder_paths| {
5441            let thread_remote_connection =
5442                metadata.as_ref().and_then(|m| m.remote_connection.as_ref());
5443            self.linked_worktree_workspace_to_remove(
5444                folder_paths,
5445                thread_remote_connection,
5446                thread_id,
5447                None,
5448                &roots_to_archive,
5449                cx,
5450            )
5451        });
5452
5453        // Also find workspaces for root plans that aren't covered by
5454        // workspace_to_remove. For workspaces that exclusively contain
5455        // worktrees being archived, remove the whole workspace. For
5456        // "mixed" workspaces (containing both archived and non-archived
5457        // worktrees), close only the editor items referencing the
5458        // archived worktrees so their Entity<Worktree> handles are
5459        // dropped without destroying the user's workspace layout.
5460        let mut workspaces_to_remove: Vec<Entity<Workspace>> =
5461            workspace_to_remove.into_iter().collect();
5462        let close_item_tasks = self.close_items_for_archived_worktrees(
5463            &roots_to_archive,
5464            &mut workspaces_to_remove,
5465            window,
5466            cx,
5467        );
5468
5469        if !workspaces_to_remove.is_empty() {
5470            let multi_workspace = self.multi_workspace.upgrade().unwrap();
5471            let session_id = session_id.clone();
5472
5473            let (fallback_paths, project_group_key) = neighbor
5474                .as_ref()
5475                .map(|neighbor| neighbor.project_location(cx))
5476                .unwrap_or_else(|| {
5477                    workspaces_to_remove
5478                        .first()
5479                        .map(|workspace| {
5480                            let key = workspace.read(cx).project_group_key(cx);
5481                            (key.path_list().clone(), key)
5482                        })
5483                        .unwrap_or_default()
5484                });
5485
5486            let excluded = workspaces_to_remove.clone();
5487            let remove_task = multi_workspace.update(cx, |mw, cx| {
5488                mw.remove(
5489                    workspaces_to_remove,
5490                    move |this, window, cx| {
5491                        let active_workspace = this.workspace().clone();
5492                        this.find_or_create_workspace(
5493                            fallback_paths,
5494                            project_group_key.host(),
5495                            Some(project_group_key),
5496                            |options, window, cx| {
5497                                connect_remote(active_workspace, options, window, cx)
5498                            },
5499                            &excluded,
5500                            None,
5501                            OpenMode::Activate,
5502                            window,
5503                            cx,
5504                        )
5505                    },
5506                    window,
5507                    cx,
5508                )
5509            });
5510
5511            let thread_folder_paths = thread_folder_paths.clone();
5512            let thread_remote_connection = metadata
5513                .as_ref()
5514                .and_then(|metadata| metadata.remote_connection.clone());
5515            cx.spawn_in(window, async move |this, cx| {
5516                if !remove_task.await? {
5517                    return anyhow::Ok(());
5518                }
5519
5520                for task in close_item_tasks {
5521                    let result: anyhow::Result<()> = task.await;
5522                    result.log_err();
5523                }
5524
5525                this.update_in(cx, |this, window, cx| {
5526                    if let Some(thread_folder_paths) = thread_folder_paths.as_ref() {
5527                        this.delete_empty_drafts_for_archive_paths(
5528                            thread_folder_paths,
5529                            thread_remote_connection.as_ref(),
5530                            cx,
5531                        );
5532                    }
5533                    let in_flight = thread_id.and_then(|tid| {
5534                        this.start_archive_worktree_task(tid, roots_to_archive, cx)
5535                    });
5536                    this.archive_and_activate(
5537                        &session_id,
5538                        thread_id,
5539                        neighbor.as_ref(),
5540                        thread_folder_paths.as_ref(),
5541                        thread_remote_connection.as_ref(),
5542                        in_flight,
5543                        window,
5544                        cx,
5545                    );
5546                })?;
5547                anyhow::Ok(())
5548            })
5549            .detach_and_log_err(cx);
5550        } else if !close_item_tasks.is_empty() {
5551            let session_id = session_id.clone();
5552            let thread_folder_paths = thread_folder_paths.clone();
5553            let thread_remote_connection = metadata
5554                .as_ref()
5555                .and_then(|metadata| metadata.remote_connection.clone());
5556            cx.spawn_in(window, async move |this, cx| {
5557                for task in close_item_tasks {
5558                    let result: anyhow::Result<()> = task.await;
5559                    result.log_err();
5560                }
5561
5562                this.update_in(cx, |this, window, cx| {
5563                    let in_flight = thread_id.and_then(|tid| {
5564                        this.start_archive_worktree_task(tid, roots_to_archive, cx)
5565                    });
5566                    this.archive_and_activate(
5567                        &session_id,
5568                        thread_id,
5569                        neighbor.as_ref(),
5570                        thread_folder_paths.as_ref(),
5571                        thread_remote_connection.as_ref(),
5572                        in_flight,
5573                        window,
5574                        cx,
5575                    );
5576                })?;
5577                anyhow::Ok(())
5578            })
5579            .detach_and_log_err(cx);
5580        } else {
5581            let in_flight = thread_id
5582                .and_then(|tid| self.start_archive_worktree_task(tid, roots_to_archive, cx));
5583            self.archive_and_activate(
5584                session_id,
5585                thread_id,
5586                neighbor.as_ref(),
5587                thread_folder_paths.as_ref(),
5588                metadata
5589                    .as_ref()
5590                    .and_then(|metadata| metadata.remote_connection.as_ref()),
5591                in_flight,
5592                window,
5593                cx,
5594            );
5595        }
5596    }
5597
5598    /// Archive a thread and activate the nearest neighbor or a draft.
5599    ///
5600    /// IMPORTANT: when activating a neighbor or creating a fallback draft,
5601    /// this method also activates the target workspace in the MultiWorkspace.
5602    /// This is critical because `rebuild_contents` derives the active
5603    /// workspace from `mw.workspace()`. If the linked worktree workspace is
5604    /// still active after archiving its last thread, `rebuild_contents` sees
5605    /// the threadless linked worktree as active and emits a spurious
5606    /// "+ New Thread" entry with the worktree chip — keeping the worktree
5607    /// alive and preventing disk cleanup.
5608    ///
5609    /// When `in_flight_archive` is present, it is the background task that
5610    /// persists the linked worktree's git state and deletes it from disk.
5611    /// We attach it to the metadata store at the same time we mark the thread
5612    /// archived so failures can automatically unarchive the thread and user-
5613    /// initiated unarchive can cancel the task.
5614    fn archive_and_activate(
5615        &mut self,
5616        _session_id: &acp::SessionId,
5617        thread_id: Option<agent_ui::ThreadId>,
5618        neighbor: Option<&ActivatableEntry>,
5619        thread_folder_paths: Option<&PathList>,
5620        thread_remote_connection: Option<&RemoteConnectionOptions>,
5621        in_flight_archive: Option<(Task<()>, async_channel::Sender<()>)>,
5622        window: &mut Window,
5623        cx: &mut Context<Self>,
5624    ) {
5625        if let Some(thread_id) = thread_id {
5626            ThreadMetadataStore::global(cx).update(cx, |store, cx| {
5627                store.archive(thread_id, in_flight_archive, cx);
5628            });
5629        }
5630
5631        let is_active = self
5632            .active_entry
5633            .as_ref()
5634            .is_some_and(|entry| thread_id.is_some_and(|tid| entry.is_active_thread(&tid)));
5635
5636        if is_active {
5637            self.active_entry = None;
5638        }
5639
5640        if !is_active {
5641            // The user is looking at a different thread/draft. Clear the
5642            // archived thread from its workspace's panel so that switching
5643            // to that workspace later doesn't show a stale thread.
5644            if let Some(folder_paths) = thread_folder_paths {
5645                if let Some(workspace) = self.multi_workspace.upgrade().and_then(|mw| {
5646                    mw.read(cx)
5647                        .workspace_for_paths(folder_paths, thread_remote_connection, cx)
5648                }) {
5649                    if let Some(panel) = workspace.read(cx).panel::<AgentPanel>(cx) {
5650                        let panel_shows_archived = panel
5651                            .read(cx)
5652                            .active_conversation_view()
5653                            .map(|cv| cv.read(cx).parent_id())
5654                            .is_some_and(|live_thread_id| {
5655                                thread_id.is_some_and(|id| id == live_thread_id)
5656                            });
5657                        if panel_shows_archived {
5658                            panel.update(cx, |panel, cx| {
5659                                panel.clear_base_view(window, cx);
5660                            });
5661                        }
5662                    }
5663                }
5664            }
5665            return;
5666        }
5667
5668        if neighbor.is_some_and(|neighbor| self.activate_entry(neighbor, window, cx)) {
5669            return;
5670        }
5671
5672        // No neighbor or its workspace isn't open — just clear the
5673        // panel so the group is left empty.
5674        if let Some(folder_paths) = thread_folder_paths {
5675            let workspace = self.multi_workspace.upgrade().and_then(|mw| {
5676                mw.read(cx)
5677                    .workspace_for_paths(folder_paths, thread_remote_connection, cx)
5678            });
5679            if let Some(workspace) = workspace {
5680                if let Some(panel) = workspace.read(cx).panel::<AgentPanel>(cx) {
5681                    panel.update(cx, |panel, cx| {
5682                        panel.clear_base_view(window, cx);
5683                    });
5684                }
5685            }
5686        }
5687    }
5688
5689    fn start_archive_worktree_task(
5690        &self,
5691        thread_id: ThreadId,
5692        roots: Vec<thread_worktree_archive::RootPlan>,
5693        cx: &mut Context<Self>,
5694    ) -> Option<(Task<()>, async_channel::Sender<()>)> {
5695        if roots.is_empty() {
5696            return None;
5697        }
5698
5699        self.delete_empty_drafts_for_archive_roots(&roots, cx);
5700
5701        let (cancel_tx, cancel_rx) = async_channel::bounded::<()>(1);
5702        let task = cx.spawn(async move |_this, cx| {
5703            match Self::archive_worktree_roots(roots, cancel_rx, cx).await {
5704                Ok(ArchiveWorktreeOutcome::Success) => {
5705                    cx.update(|cx| {
5706                        ThreadMetadataStore::global(cx).update(cx, |store, _cx| {
5707                            store.cleanup_completed_archive(thread_id);
5708                        });
5709                    });
5710                }
5711                Ok(ArchiveWorktreeOutcome::Cancelled) => {}
5712                Err(error) => {
5713                    log::error!("Failed to archive worktree: {error:#}");
5714                    cx.update(|cx| {
5715                        ThreadMetadataStore::global(cx).update(cx, |store, cx| {
5716                            store.unarchive(thread_id, cx);
5717                        });
5718                    });
5719                }
5720            }
5721        });
5722
5723        Some((task, cancel_tx))
5724    }
5725
5726    fn start_detached_archive_worktree_task(
5727        &self,
5728        roots: Vec<thread_worktree_archive::RootPlan>,
5729        cx: &mut Context<Self>,
5730    ) {
5731        if roots.is_empty() {
5732            return;
5733        }
5734
5735        self.delete_empty_drafts_for_archive_roots(&roots, cx);
5736
5737        let (cancel_tx, cancel_rx) = async_channel::bounded::<()>(1);
5738        cx.spawn(async move |_this, cx| {
5739            let outcome = Self::archive_worktree_roots(roots, cancel_rx, cx).await;
5740            drop(cancel_tx);
5741            match outcome {
5742                Ok(ArchiveWorktreeOutcome::Success | ArchiveWorktreeOutcome::Cancelled) => {}
5743                Err(error) => {
5744                    log::error!("Failed to archive worktree after closing sidebar item: {error:#}");
5745                }
5746            }
5747        })
5748        .detach();
5749    }
5750
5751    async fn archive_worktree_roots(
5752        roots: Vec<thread_worktree_archive::RootPlan>,
5753        cancel_rx: async_channel::Receiver<()>,
5754        cx: &mut gpui::AsyncApp,
5755    ) -> anyhow::Result<ArchiveWorktreeOutcome> {
5756        let mut completed_persists: Vec<(i64, thread_worktree_archive::RootPlan)> = Vec::new();
5757
5758        for root in &roots {
5759            if cancel_rx.is_closed() {
5760                for &(id, ref completed_root) in completed_persists.iter().rev() {
5761                    thread_worktree_archive::rollback_persist(id, completed_root, cx).await;
5762                }
5763                return Ok(ArchiveWorktreeOutcome::Cancelled);
5764            }
5765
5766            match thread_worktree_archive::persist_worktree_state(root, cx).await {
5767                Ok(id) => {
5768                    completed_persists.push((id, root.clone()));
5769                }
5770                Err(error) => {
5771                    for &(id, ref completed_root) in completed_persists.iter().rev() {
5772                        thread_worktree_archive::rollback_persist(id, completed_root, cx).await;
5773                    }
5774                    return Err(error);
5775                }
5776            }
5777
5778            if cancel_rx.is_closed() {
5779                for &(id, ref completed_root) in completed_persists.iter().rev() {
5780                    thread_worktree_archive::rollback_persist(id, completed_root, cx).await;
5781                }
5782                return Ok(ArchiveWorktreeOutcome::Cancelled);
5783            }
5784
5785            if let Err(error) = thread_worktree_archive::remove_root(root.clone(), cx).await {
5786                if let Some(&(id, ref completed_root)) = completed_persists.last() {
5787                    if completed_root.root_path == root.root_path {
5788                        thread_worktree_archive::rollback_persist(id, completed_root, cx).await;
5789                        completed_persists.pop();
5790                    }
5791                }
5792                for &(id, ref completed_root) in completed_persists.iter().rev() {
5793                    thread_worktree_archive::rollback_persist(id, completed_root, cx).await;
5794                }
5795                return Err(error);
5796            }
5797        }
5798
5799        Ok(ArchiveWorktreeOutcome::Success)
5800    }
5801
5802    fn activate_workspace(
5803        &self,
5804        workspace: &Entity<Workspace>,
5805        window: &mut Window,
5806        cx: &mut Context<Self>,
5807    ) {
5808        if let Some(multi_workspace) = self.multi_workspace.upgrade() {
5809            multi_workspace.update(cx, |mw, cx| {
5810                mw.activate(workspace.clone(), None, window, cx);
5811            });
5812        }
5813    }
5814
5815    fn archive_selected_thread(
5816        &mut self,
5817        _: &ArchiveSelectedThread,
5818        window: &mut Window,
5819        cx: &mut Context<Self>,
5820    ) {
5821        let Some(ix) = self.selection else {
5822            return;
5823        };
5824        match self.contents.entries.get(ix) {
5825            Some(ListEntry::Thread(thread)) => {
5826                match thread.status {
5827                    AgentThreadStatus::Running | AgentThreadStatus::WaitingForConfirmation => {
5828                        return;
5829                    }
5830                    AgentThreadStatus::Completed | AgentThreadStatus::Error => {}
5831                }
5832                if thread.draft.is_some() {
5833                    let workspace = thread.workspace.clone();
5834                    let draft_id = thread.metadata.thread_id;
5835                    self.remove_draft(draft_id, &workspace, window, cx);
5836                } else if let Some(session_id) = thread.metadata.session_id.clone() {
5837                    self.archive_thread(&session_id, window, cx);
5838                }
5839            }
5840            Some(ListEntry::Terminal(terminal)) => {
5841                let metadata = terminal.metadata.clone();
5842                let workspace = terminal.workspace.clone();
5843                self.close_terminal(&metadata, &workspace, window, cx);
5844            }
5845            _ => {}
5846        }
5847    }
5848
5849    fn rename_selected_thread(
5850        &mut self,
5851        _: &RenameSelectedThread,
5852        window: &mut Window,
5853        cx: &mut Context<Self>,
5854    ) {
5855        let Some(ix) = self.selection else {
5856            return;
5857        };
5858        let Some(ListEntry::Thread(thread)) = self.contents.entries.get(ix) else {
5859            return;
5860        };
5861        let thread_id = thread.metadata.thread_id;
5862        let title = thread.metadata.display_title();
5863        self.start_renaming_thread(ix, thread_id, title, window, cx);
5864    }
5865
5866    fn record_thread_access(&mut self, id: &ThreadId) {
5867        self.thread_last_accessed.insert(*id, Utc::now());
5868    }
5869
5870    fn record_terminal_access(&mut self, id: TerminalId) {
5871        self.terminal_last_accessed.insert(id, Utc::now());
5872    }
5873
5874    fn record_thread_interacted(&mut self, thread_id: &agent_ui::ThreadId, cx: &mut App) {
5875        let store = ThreadMetadataStore::global(cx);
5876        store.update(cx, |store, cx| {
5877            store.update_interacted_at(thread_id, Utc::now(), cx);
5878        })
5879    }
5880
5881    fn thread_display_time(metadata: &ThreadMetadata) -> DateTime<Utc> {
5882        metadata.interacted_at.unwrap_or(metadata.updated_at)
5883    }
5884
5885    fn push_entries_by_display_time(
5886        entries: &mut Vec<ListEntry>,
5887        terminals: Vec<TerminalEntry>,
5888        threads: Vec<Arc<ThreadEntry>>,
5889        current_session_ids: &mut HashSet<acp::SessionId>,
5890        current_thread_ids: &mut HashSet<agent_ui::ThreadId>,
5891    ) {
5892        fn display_time(entry: &ListEntry) -> DateTime<Utc> {
5893            match entry {
5894                ListEntry::Thread(thread) if thread.draft == Some(DraftKind::Empty) => {
5895                    DateTime::<Utc>::MAX_UTC
5896                }
5897                ListEntry::Thread(thread) => Sidebar::thread_display_time(&thread.metadata),
5898                ListEntry::Terminal(terminal) => terminal.metadata.created_at,
5899                ListEntry::ProjectHeader { .. } => unreachable!(),
5900            }
5901        }
5902
5903        let row_entries = terminals
5904            .into_iter()
5905            .map(ListEntry::Terminal)
5906            .chain(threads.into_iter().map(ListEntry::Thread))
5907            .sorted_by_key(|right| std::cmp::Reverse(display_time(right)));
5908
5909        for entry in row_entries {
5910            if let ListEntry::Thread(thread) = &entry {
5911                if let Some(session_id) = &thread.metadata.session_id {
5912                    current_session_ids.insert(session_id.clone());
5913                }
5914                current_thread_ids.insert(thread.metadata.thread_id);
5915            }
5916            entries.push(entry);
5917        }
5918    }
5919
5920    /// The sort order used by the ctrl-tab switcher
5921    fn switcher_entry_cmp(
5922        &self,
5923        left: &ThreadSwitcherEntry,
5924        right: &ThreadSwitcherEntry,
5925    ) -> Ordering {
5926        let sort_time = |entry: &ThreadSwitcherEntry| match entry {
5927            ThreadSwitcherEntry::Thread(entry) => self
5928                .thread_last_accessed
5929                .get(&entry.metadata.thread_id)
5930                .copied()
5931                .or(entry.metadata.interacted_at)
5932                .unwrap_or(entry.metadata.updated_at),
5933            ThreadSwitcherEntry::Terminal(entry) => self
5934                .terminal_last_accessed
5935                .get(&entry.metadata.terminal_id)
5936                .copied()
5937                .unwrap_or(entry.metadata.created_at),
5938        };
5939
5940        // .reverse() = most recent first
5941        sort_time(left).cmp(&sort_time(right)).reverse()
5942    }
5943
5944    fn mru_entries_for_switcher(&self, cx: &App) -> Vec<ThreadSwitcherEntry> {
5945        let mut current_header_label: Option<SharedString> = None;
5946        let mut current_header_key: Option<ProjectGroupKey> = None;
5947        let mut entries: Vec<ThreadSwitcherEntry> = self
5948            .contents
5949            .entries
5950            .iter()
5951            .filter_map(|entry| match entry {
5952                ListEntry::ProjectHeader { label, key, .. } => {
5953                    current_header_label = Some(label.clone());
5954                    current_header_key = Some(key.clone());
5955                    None
5956                }
5957                ListEntry::Thread(thread) => {
5958                    if thread.draft == Some(DraftKind::Empty) {
5959                        return None;
5960                    }
5961                    let workspace = match &thread.workspace {
5962                        ThreadEntryWorkspace::Open(workspace) => Some(workspace.clone()),
5963                        ThreadEntryWorkspace::Closed { .. } => {
5964                            current_header_key.as_ref().and_then(|key| {
5965                                self.multi_workspace.upgrade().and_then(|mw| {
5966                                    mw.read(cx).workspace_for_paths(
5967                                        key.path_list(),
5968                                        key.host().as_ref(),
5969                                        cx,
5970                                    )
5971                                })
5972                            })
5973                        }
5974                    }?;
5975                    let notified = self.contents.is_thread_notified(&thread.metadata.thread_id);
5976                    let timestamp: SharedString =
5977                        format_history_entry_timestamp(Self::thread_display_time(&thread.metadata))
5978                            .into();
5979                    Some(ThreadSwitcherEntry::Thread(ThreadSwitcherThreadEntry {
5980                        title: thread.metadata.display_title(),
5981                        icon: thread.icon,
5982                        icon_from_external_svg: thread.icon_from_external_svg.clone(),
5983                        status: thread.status,
5984                        metadata: thread.metadata.clone(),
5985                        workspace,
5986                        project_name: current_header_label.clone(),
5987                        worktrees: thread
5988                            .worktrees
5989                            .iter()
5990                            .cloned()
5991                            .map(|mut wt| {
5992                                wt.highlight_positions = Vec::new();
5993                                wt
5994                            })
5995                            .collect(),
5996                        diff_stats: thread.diff_stats,
5997                        is_draft: thread.draft.is_some(),
5998                        is_title_generating: thread.is_title_generating,
5999                        notified,
6000                        timestamp,
6001                    }))
6002                }
6003                ListEntry::Terminal(terminal) => {
6004                    let timestamp: SharedString =
6005                        format_history_entry_timestamp(terminal.metadata.created_at).into();
6006                    Some(ThreadSwitcherEntry::Terminal(ThreadSwitcherTerminalEntry {
6007                        metadata: terminal.metadata.clone(),
6008                        workspace: terminal.workspace.clone(),
6009                        project_name: current_header_label.clone(),
6010                        worktrees: terminal
6011                            .worktrees
6012                            .iter()
6013                            .cloned()
6014                            .map(|mut wt| {
6015                                wt.highlight_positions = Vec::new();
6016                                wt
6017                            })
6018                            .collect(),
6019                        notified: self
6020                            .contents
6021                            .is_terminal_notified(terminal.metadata.terminal_id),
6022                        timestamp,
6023                    }))
6024                }
6025            })
6026            .collect();
6027
6028        entries.sort_by(|a, b| self.switcher_entry_cmp(a, b));
6029
6030        entries
6031    }
6032
6033    fn dismiss_thread_switcher(&mut self, cx: &mut Context<Self>) {
6034        self.thread_switcher = None;
6035        self._thread_switcher_subscriptions.clear();
6036        if let Some(mw) = self.multi_workspace.upgrade() {
6037            mw.update(cx, |mw, cx| {
6038                mw.set_sidebar_overlay(None, cx);
6039            });
6040        }
6041    }
6042
6043    fn on_toggle_thread_switcher(
6044        &mut self,
6045        action: &ToggleThreadSwitcher,
6046        window: &mut Window,
6047        cx: &mut Context<Self>,
6048    ) {
6049        self.toggle_thread_switcher_impl(action.select_last, window, cx);
6050    }
6051
6052    fn preview_switcher_selection(
6053        &mut self,
6054        selection: &ThreadSwitcherSelection,
6055        window: &mut Window,
6056        cx: &mut Context<Self>,
6057    ) {
6058        match selection {
6059            ThreadSwitcherSelection::Thread {
6060                metadata,
6061                workspace,
6062            } => {
6063                if let Some(multi_workspace) = self.multi_workspace.upgrade() {
6064                    multi_workspace.update(cx, |multi_workspace, cx| {
6065                        multi_workspace.activate(workspace.clone(), None, window, cx);
6066                    });
6067                }
6068                self.active_entry = Some(ActiveEntry::Thread {
6069                    thread_id: metadata.thread_id,
6070                    session_id: metadata.session_id.clone(),
6071                    workspace: workspace.clone(),
6072                });
6073                self.update_entries(cx);
6074                Self::load_agent_thread_in_workspace(workspace, metadata, false, window, cx);
6075            }
6076            ThreadSwitcherSelection::Terminal {
6077                metadata,
6078                workspace,
6079            } => {
6080                if let ThreadEntryWorkspace::Open(workspace) = workspace {
6081                    if let Some(multi_workspace) = self.multi_workspace.upgrade() {
6082                        multi_workspace.update(cx, |multi_workspace, cx| {
6083                            multi_workspace.activate(workspace.clone(), None, window, cx);
6084                        });
6085                    }
6086                    self.active_entry = Some(ActiveEntry::Terminal {
6087                        terminal_id: metadata.terminal_id,
6088                        workspace: workspace.clone(),
6089                    });
6090                    self.update_entries(cx);
6091                    Self::load_agent_terminal_in_workspace(workspace, metadata, false, window, cx);
6092                }
6093            }
6094        }
6095    }
6096
6097    fn confirm_switcher_selection(
6098        &mut self,
6099        selection: &ThreadSwitcherSelection,
6100        window: &mut Window,
6101        cx: &mut Context<Self>,
6102    ) {
6103        match selection {
6104            ThreadSwitcherSelection::Thread {
6105                metadata,
6106                workspace,
6107            } => {
6108                if let Some(multi_workspace) = self.multi_workspace.upgrade() {
6109                    multi_workspace.update(cx, |multi_workspace, cx| {
6110                        multi_workspace.activate(workspace.clone(), None, window, cx);
6111                        multi_workspace.retain_active_workspace(cx);
6112                    });
6113                }
6114                self.record_thread_access(&metadata.thread_id);
6115                self.active_entry = Some(ActiveEntry::Thread {
6116                    thread_id: metadata.thread_id,
6117                    session_id: metadata.session_id.clone(),
6118                    workspace: workspace.clone(),
6119                });
6120                self.update_entries(cx);
6121                self.dismiss_thread_switcher(cx);
6122                Self::load_agent_thread_in_workspace(workspace, metadata, true, window, cx);
6123            }
6124            ThreadSwitcherSelection::Terminal {
6125                metadata,
6126                workspace,
6127            } => {
6128                self.dismiss_thread_switcher(cx);
6129                self.activate_terminal_entry(metadata.clone(), workspace.clone(), true, window, cx);
6130            }
6131        }
6132    }
6133
6134    fn toggle_thread_switcher_impl(
6135        &mut self,
6136        select_last: bool,
6137        window: &mut Window,
6138        cx: &mut Context<Self>,
6139    ) {
6140        if let Some(thread_switcher) = &self.thread_switcher {
6141            thread_switcher.update(cx, |switcher, cx| {
6142                if select_last {
6143                    switcher.select_last(cx);
6144                } else {
6145                    switcher.cycle_selection(cx);
6146                }
6147            });
6148            return;
6149        }
6150
6151        let entries = self.mru_entries_for_switcher(cx);
6152        if entries.len() < 2 {
6153            return;
6154        }
6155
6156        let weak_multi_workspace = self.multi_workspace.clone();
6157
6158        // Snapshot the active entry (thread or terminal) so dismissal can
6159        // restore it.
6160        let original_active_entry = self.active_entry.clone();
6161        let original_metadata = match &original_active_entry {
6162            Some(ActiveEntry::Thread { thread_id, .. }) => {
6163                entries.iter().find_map(|entry| match entry {
6164                    ThreadSwitcherEntry::Thread(entry)
6165                        if *thread_id == entry.metadata.thread_id =>
6166                    {
6167                        Some(entry.metadata.clone())
6168                    }
6169                    _ => None,
6170                })
6171            }
6172            _ => None,
6173        };
6174        let original_workspace = self
6175            .multi_workspace
6176            .upgrade()
6177            .map(|mw| mw.read(cx).workspace().clone());
6178
6179        let thread_switcher = cx.new(|cx| ThreadSwitcher::new(entries, select_last, window, cx));
6180
6181        let mut subscriptions = Vec::new();
6182
6183        subscriptions.push(cx.subscribe_in(&thread_switcher, window, {
6184            let thread_switcher = thread_switcher.clone();
6185            move |this, _emitter, event: &ThreadSwitcherEvent, window, cx| match event {
6186                ThreadSwitcherEvent::Preview(selection) => {
6187                    this.preview_switcher_selection(selection, window, cx);
6188                    let focus = thread_switcher.focus_handle(cx);
6189                    window.focus(&focus, cx);
6190                }
6191                ThreadSwitcherEvent::Confirmed(selection) => {
6192                    this.confirm_switcher_selection(selection, window, cx);
6193                }
6194                ThreadSwitcherEvent::Dismissed => {
6195                    if let Some(mw) = weak_multi_workspace.upgrade() {
6196                        if let Some(original_ws) = &original_workspace {
6197                            mw.update(cx, |mw, cx| {
6198                                mw.activate(original_ws.clone(), None, window, cx);
6199                            });
6200                        }
6201                    }
6202                    match &original_active_entry {
6203                        Some(ActiveEntry::Thread { .. }) => {
6204                            if let (Some(metadata), Some(original_ws)) =
6205                                (&original_metadata, &original_workspace)
6206                            {
6207                                this.active_entry = Some(ActiveEntry::Thread {
6208                                    thread_id: metadata.thread_id,
6209                                    session_id: metadata.session_id.clone(),
6210                                    workspace: original_ws.clone(),
6211                                });
6212                                this.update_entries(cx);
6213                                Self::load_agent_thread_in_workspace(
6214                                    original_ws,
6215                                    metadata,
6216                                    false,
6217                                    window,
6218                                    cx,
6219                                );
6220                            }
6221                        }
6222                        Some(ActiveEntry::Terminal {
6223                            terminal_id,
6224                            workspace,
6225                        }) => {
6226                            let terminal_id = *terminal_id;
6227                            let workspace = workspace.clone();
6228                            this.active_entry = Some(ActiveEntry::Terminal {
6229                                terminal_id,
6230                                workspace: workspace.clone(),
6231                            });
6232                            this.update_entries(cx);
6233                            workspace.update(cx, |workspace, cx| {
6234                                if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
6235                                    panel.update(cx, |panel, cx| {
6236                                        panel.activate_terminal(terminal_id, false, window, cx);
6237                                    });
6238                                }
6239                            });
6240                        }
6241                        None => {}
6242                    }
6243                    this.dismiss_thread_switcher(cx);
6244                }
6245            }
6246        }));
6247
6248        subscriptions.push(cx.subscribe_in(
6249            &thread_switcher,
6250            window,
6251            |this, _emitter, _event: &gpui::DismissEvent, _window, cx| {
6252                this.dismiss_thread_switcher(cx);
6253            },
6254        ));
6255
6256        let focus = thread_switcher.focus_handle(cx);
6257        let overlay_view = gpui::AnyView::from(thread_switcher.clone());
6258
6259        // Replay the initial preview that was emitted during construction
6260        // before subscriptions were wired up.
6261        let initial_preview = thread_switcher
6262            .read(cx)
6263            .selected_entry()
6264            .map(ThreadSwitcherEntry::selection);
6265
6266        self.thread_switcher = Some(thread_switcher);
6267        self._thread_switcher_subscriptions = subscriptions;
6268        if let Some(mw) = self.multi_workspace.upgrade() {
6269            mw.update(cx, |mw, cx| {
6270                mw.set_sidebar_overlay(Some(overlay_view), cx);
6271            });
6272        }
6273
6274        if let Some(selection) = initial_preview {
6275            self.preview_switcher_selection(&selection, window, cx);
6276        }
6277
6278        window.focus(&focus, cx);
6279    }
6280
6281    fn render_thread(
6282        &self,
6283        ix: usize,
6284        thread: &ThreadEntry,
6285        is_active: bool,
6286        is_focused: bool,
6287        cx: &mut Context<Self>,
6288    ) -> AnyElement {
6289        let has_notification = self.contents.is_thread_notified(&thread.metadata.thread_id);
6290
6291        let title: SharedString = thread.metadata.display_title();
6292        let metadata = thread.metadata.clone();
6293        let thread_workspace = thread.workspace.clone();
6294
6295        let is_hovered = self.hovered_thread_index == Some(ix);
6296        let is_selected = is_active;
6297        let is_draft = thread.draft.is_some();
6298        let is_empty_draft = thread.draft == Some(DraftKind::Empty);
6299        let is_running = matches!(
6300            thread.status,
6301            AgentThreadStatus::Running | AgentThreadStatus::WaitingForConfirmation
6302        );
6303        let is_renaming = self.renaming_thread_id == Some(thread.metadata.thread_id);
6304
6305        let thread_id_for_actions = thread.metadata.thread_id;
6306        let session_id_for_delete = thread.metadata.session_id.clone();
6307        let focus_handle = self.focus_handle.clone();
6308        let title_editor = self.thread_rename_editor.clone();
6309
6310        let id = SharedString::from(format!("thread-entry-{}", ix));
6311
6312        let color = cx.theme().colors();
6313        let sidebar_bg = color
6314            .title_bar_background
6315            .blend(color.panel_background.opacity(0.25));
6316
6317        let timestamp: SharedString = if is_empty_draft {
6318            SharedString::default()
6319        } else {
6320            format_history_entry_timestamp(Self::thread_display_time(&thread.metadata)).into()
6321        };
6322
6323        let is_remote = thread.workspace.is_remote(cx);
6324
6325        let worktrees = apply_worktree_label_mode(
6326            thread.worktrees.clone(),
6327            cx.flag_value::<AgentThreadWorktreeLabelFlag>(),
6328        );
6329
6330        let (icon, icon_svg) = if is_draft {
6331            (IconName::Circle, None)
6332        } else {
6333            (thread.icon, thread.icon_from_external_svg.clone())
6334        };
6335
6336        let title_generating = thread.is_title_generating
6337            || self
6338                .regenerating_titles
6339                .contains(&thread.metadata.thread_id);
6340
6341        let thread_item = ThreadItem::new(id, title.clone())
6342            .base_bg(sidebar_bg)
6343            .icon(icon)
6344            .when(is_draft, |this| {
6345                this.icon_color(Color::Custom(cx.theme().colors().icon_muted.opacity(0.2)))
6346            })
6347            .status(thread.status)
6348            .is_remote(is_remote)
6349            .when_some(icon_svg, |this, svg| {
6350                this.custom_icon_from_external_svg(svg)
6351            })
6352            .worktrees(worktrees)
6353            .timestamp(timestamp)
6354            .highlight_positions(thread.highlight_positions.to_vec())
6355            .title_generating(title_generating)
6356            .notified(has_notification)
6357            .when(thread.diff_stats.lines_added > 0, |this| {
6358                this.added(thread.diff_stats.lines_added as usize)
6359            })
6360            .when(thread.diff_stats.lines_removed > 0, |this| {
6361                this.removed(thread.diff_stats.lines_removed as usize)
6362            })
6363            .selected(is_selected)
6364            .focused(is_focused)
6365            .hovered(is_hovered)
6366            .on_hover(cx.listener(move |this, is_hovered: &bool, _window, cx| {
6367                if *is_hovered {
6368                    this.hovered_thread_index = Some(ix);
6369                } else if this.hovered_thread_index == Some(ix) {
6370                    this.hovered_thread_index = None;
6371                }
6372                cx.notify();
6373            }))
6374            .when(is_renaming, |this| {
6375                this.is_truncated(false).title_slot(
6376                    div()
6377                        .h_full()
6378                        .min_w_0()
6379                        .flex_1()
6380                        .capture_action(cx.listener(
6381                            |this, _: &editor::actions::Newline, window, cx| {
6382                                this.finish_thread_rename(window, cx);
6383                            },
6384                        ))
6385                        .on_action(cx.listener(|this, _: &Confirm, window, cx| {
6386                            this.finish_thread_rename(window, cx);
6387                        }))
6388                        .on_action(
6389                            cx.listener(|this, _: &editor::actions::Cancel, window, cx| {
6390                                this.finish_thread_rename(window, cx);
6391                            }),
6392                        )
6393                        .child(title_editor),
6394                )
6395            })
6396            .when(is_hovered && !is_renaming, |this| {
6397                let rename_button = IconButton::new(("rename-thread", ix), IconName::Pencil)
6398                    .icon_size(IconSize::Small)
6399                    .tooltip({
6400                        let focus_handle = focus_handle.clone();
6401                        move |_window, cx| {
6402                            Tooltip::for_action_in(
6403                                "Rename Thread",
6404                                &RenameSelectedThread,
6405                                &focus_handle,
6406                                cx,
6407                            )
6408                        }
6409                    })
6410                    .on_click({
6411                        let title = title.clone();
6412                        cx.listener(move |this, _, window, cx| {
6413                            this.start_renaming_thread(
6414                                ix,
6415                                thread_id_for_actions,
6416                                title.clone(),
6417                                window,
6418                                cx,
6419                            );
6420                        })
6421                    });
6422
6423                let contextual_action: Option<AnyElement> = if is_running {
6424                    Some(
6425                        IconButton::new("stop-thread", IconName::Stop)
6426                            .icon_size(IconSize::Small)
6427                            .icon_color(Color::Error)
6428                            .style(ButtonStyle::Tinted(TintColor::Error))
6429                            .tooltip(Tooltip::text("Stop Generation"))
6430                            .on_click(cx.listener(move |this, _, _window, cx| {
6431                                this.stop_thread(&thread_id_for_actions, cx);
6432                            }))
6433                            .into_any_element(),
6434                    )
6435                } else {
6436                    match thread.draft {
6437                        Some(DraftKind::Empty) => None,
6438                        Some(DraftKind::WithContent) => Some(
6439                            IconButton::new("discard_thread", IconName::Close)
6440                                .icon_size(IconSize::Small)
6441                                .tooltip(Tooltip::text("Discard Draft"))
6442                                .on_click({
6443                                    let thread_workspace = thread_workspace.clone();
6444                                    cx.listener(move |this, _, window, cx| {
6445                                        this.remove_draft(
6446                                            thread_id_for_actions,
6447                                            &thread_workspace,
6448                                            window,
6449                                            cx,
6450                                        );
6451                                    })
6452                                })
6453                                .into_any_element(),
6454                        ),
6455                        None => Some(
6456                            IconButton::new("archive-thread", IconName::Archive)
6457                                .icon_size(IconSize::Small)
6458                                .tooltip({
6459                                    let focus_handle = focus_handle.clone();
6460                                    move |_window, cx| {
6461                                        Tooltip::for_action_in(
6462                                            "Archive Thread",
6463                                            &ArchiveSelectedThread,
6464                                            &focus_handle,
6465                                            cx,
6466                                        )
6467                                    }
6468                                })
6469                                .on_click({
6470                                    let session_id = session_id_for_delete.clone();
6471                                    cx.listener(move |this, _, window, cx| {
6472                                        if let Some(ref session_id) = session_id {
6473                                            this.archive_thread(session_id, window, cx);
6474                                        }
6475                                    })
6476                                })
6477                                .into_any_element(),
6478                        ),
6479                    }
6480                };
6481
6482                this.action_slot(
6483                    h_flex()
6484                        .gap_0p5()
6485                        .child(rename_button)
6486                        .when_some(contextual_action, |this, action| this.child(action)),
6487                )
6488            })
6489            .on_click({
6490                let thread_workspace = thread_workspace.clone();
6491                cx.listener(move |this, _, window, cx| {
6492                    this.selection = None;
6493                    match &thread_workspace {
6494                        ThreadEntryWorkspace::Open(workspace) => {
6495                            this.activate_thread(metadata.clone(), workspace, false, window, cx);
6496                        }
6497                        ThreadEntryWorkspace::Closed {
6498                            folder_paths,
6499                            project_group_key,
6500                        } => {
6501                            this.open_workspace_and_activate_thread(
6502                                metadata.clone(),
6503                                folder_paths.clone(),
6504                                project_group_key,
6505                                window,
6506                                cx,
6507                            );
6508                        }
6509                    }
6510                })
6511            });
6512
6513        if is_draft || thread.metadata.session_id.is_none() {
6514            return thread_item.into_any_element();
6515        }
6516
6517        let Some(session_id) = thread.metadata.session_id.clone() else {
6518            return thread_item.into_any_element();
6519        };
6520
6521        let context_menu_id = SharedString::from(format!("thread-context-menu-{}", ix));
6522        let sidebar = cx.weak_entity();
6523
6524        let active_workspace = self.active_workspace(cx);
6525        let thread_workspace = match &thread_workspace {
6526            ThreadEntryWorkspace::Open(workspace) => Some(workspace.clone()),
6527            ThreadEntryWorkspace::Closed { .. } => None,
6528        };
6529
6530        let is_zed_thread = thread.metadata.agent_id.as_ref() == OMEGA_AGENT_ID.as_ref();
6531        let can_open_as_markdown = thread.is_live || is_zed_thread;
6532        let folder_paths = thread.metadata.folder_paths().clone();
6533
6534        right_click_menu(context_menu_id)
6535            .trigger(move |_, _, _| thread_item)
6536            .menu({
6537                let thread_id = thread.metadata.thread_id;
6538                let markdown_title = Some(thread.metadata.display_title());
6539                let rename_title = title;
6540                move |_window, cx| {
6541                    let session_id = session_id.clone();
6542                    let sidebar = sidebar.clone();
6543                    let active_workspace = active_workspace.clone();
6544                    let thread_workspace = thread_workspace.clone();
6545                    let markdown_title = markdown_title.clone();
6546                    let rename_title = rename_title.clone();
6547                    let folder_paths = folder_paths.clone();
6548                    ContextMenu::build(_window, cx, move |mut menu, _window, _cx| {
6549                        menu = menu.entry("Rename Title", None, {
6550                            let sidebar = sidebar.clone();
6551                            let rename_title = rename_title.clone();
6552                            move |window, cx| {
6553                                sidebar
6554                                    .update(cx, |sidebar, cx| {
6555                                        sidebar.start_renaming_thread(
6556                                            ix,
6557                                            thread_id,
6558                                            rename_title.clone(),
6559                                            window,
6560                                            cx,
6561                                        );
6562                                    })
6563                                    .ok();
6564                            }
6565                        });
6566
6567                        if is_zed_thread {
6568                            menu = menu.entry("Regenerate Thread Title", None, {
6569                                let session_id = session_id.clone();
6570                                let sidebar = sidebar.clone();
6571                                let thread_workspace = thread_workspace.clone();
6572                                let folder_paths = folder_paths.clone();
6573                                move |_window, cx| {
6574                                    sidebar
6575                                        .update(cx, |sidebar, cx| {
6576                                            sidebar.regenerate_thread_title(
6577                                                &session_id,
6578                                                thread_id,
6579                                                folder_paths.clone(),
6580                                                thread_workspace.clone(),
6581                                                cx,
6582                                            );
6583                                        })
6584                                        .ok();
6585                                }
6586                            });
6587                        }
6588
6589                        if can_open_as_markdown {
6590                            menu = menu.entry("Open Thread as Markdown", None, {
6591                                let session_id = session_id.clone();
6592                                let markdown_title = markdown_title.clone();
6593                                let thread_workspace = thread_workspace.clone();
6594                                move |window, cx| {
6595                                    if let Some(thread_workspace) = thread_workspace.as_ref()
6596                                        && let Some(panel) =
6597                                            thread_workspace.read(cx).panel::<AgentPanel>(cx)
6598                                    {
6599                                        let opened = panel.update(cx, |panel, cx| {
6600                                            panel.open_thread_as_markdown(
6601                                                thread_id,
6602                                                thread_workspace.clone(),
6603                                                window,
6604                                                cx,
6605                                            )
6606                                        });
6607                                        if opened {
6608                                            return;
6609                                        }
6610                                    }
6611
6612                                    if is_zed_thread
6613                                        && let Some(active_workspace) = &active_workspace
6614                                    {
6615                                        Self::open_closed_native_thread_as_markdown(
6616                                            &session_id,
6617                                            markdown_title.clone(),
6618                                            active_workspace,
6619                                            window,
6620                                            cx,
6621                                        );
6622                                    }
6623                                }
6624                            });
6625                        }
6626
6627                        menu.separator().entry("Archive Thread", None, {
6628                            let session_id = session_id.clone();
6629                            move |window, cx| {
6630                                sidebar
6631                                    .update(cx, |sidebar, cx| {
6632                                        sidebar.archive_thread(&session_id, window, cx);
6633                                    })
6634                                    .ok();
6635                            }
6636                        })
6637                    })
6638                }
6639            })
6640            .into_any_element()
6641    }
6642
6643    fn render_terminal(
6644        &self,
6645        ix: usize,
6646        terminal: &TerminalEntry,
6647        is_active: bool,
6648        is_focused: bool,
6649        cx: &mut Context<Self>,
6650    ) -> AnyElement {
6651        let id = ElementId::from(format!("terminal-{}", terminal.metadata.terminal_id));
6652        let timestamp = format_history_entry_timestamp(terminal.metadata.created_at);
6653        let is_hovered = self.hovered_thread_index == Some(ix);
6654        let color = cx.theme().colors();
6655        let sidebar_bg = color
6656            .title_bar_background
6657            .blend(color.panel_background.opacity(0.25));
6658        let metadata = terminal.metadata.clone();
6659        let workspace = terminal.workspace.clone();
6660        let focus_handle = self.focus_handle.clone();
6661        let worktrees = apply_worktree_label_mode(
6662            terminal.worktrees.clone(),
6663            cx.flag_value::<AgentThreadWorktreeLabelFlag>(),
6664        );
6665        let is_remote = terminal.workspace.is_remote(cx);
6666
6667        let display_title = terminal.metadata.display_title();
6668        let (icon_char, title, highlight_positions) =
6669            match split_leading_icon_char(&display_title, &terminal.highlight_positions) {
6670                Some((icon_char, title, positions)) => (Some(icon_char), title, positions),
6671                None => (None, display_title, terminal.highlight_positions.clone()),
6672            };
6673
6674        ThreadItem::new(id, title)
6675            .base_bg(sidebar_bg)
6676            .icon(IconName::Terminal)
6677            .when_some(icon_char, |this, icon_char| this.icon_char(icon_char))
6678            .is_remote(is_remote)
6679            .worktrees(worktrees)
6680            .timestamp(timestamp)
6681            .notified(terminal.has_notification)
6682            .highlight_positions(highlight_positions)
6683            .selected(is_active)
6684            .focused(is_focused)
6685            .hovered(is_hovered)
6686            .on_hover(cx.listener(move |this, is_hovered: &bool, _window, cx| {
6687                if *is_hovered {
6688                    this.hovered_thread_index = Some(ix);
6689                } else if this.hovered_thread_index == Some(ix) {
6690                    this.hovered_thread_index = None;
6691                }
6692                cx.notify();
6693            }))
6694            .when(is_hovered, |this| {
6695                this.action_slot(
6696                    IconButton::new("close-terminal", IconName::Close)
6697                        .icon_size(IconSize::Small)
6698                        .icon_color(Color::Muted)
6699                        .tooltip({
6700                            let focus_handle = focus_handle.clone();
6701                            move |_window, cx| {
6702                                Tooltip::for_action_in(
6703                                    "Close Terminal",
6704                                    &ArchiveSelectedThread,
6705                                    &focus_handle,
6706                                    cx,
6707                                )
6708                            }
6709                        })
6710                        .on_click(cx.listener(move |this, _, window, cx| {
6711                            this.close_terminal(&metadata, &workspace, window, cx);
6712                        })),
6713                )
6714            })
6715            .on_click(cx.listener({
6716                let metadata = terminal.metadata.clone();
6717                let workspace = terminal.workspace.clone();
6718                move |this, _, window, cx| {
6719                    this.activate_terminal_entry(
6720                        metadata.clone(),
6721                        workspace.clone(),
6722                        false,
6723                        window,
6724                        cx,
6725                    );
6726                }
6727            }))
6728            .into_any_element()
6729    }
6730
6731    fn render_filter_input(&self, cx: &mut Context<Self>) -> impl IntoElement {
6732        div()
6733            .min_w_0()
6734            .flex_1()
6735            .capture_action(
6736                cx.listener(|this, _: &editor::actions::Newline, window, cx| {
6737                    this.editor_confirm(window, cx);
6738                }),
6739            )
6740            .child(self.filter_editor.clone())
6741    }
6742
6743    fn render_recent_projects_button(&self, cx: &mut Context<Self>) -> impl IntoElement {
6744        let multi_workspace = self.multi_workspace.upgrade();
6745
6746        let workspace = multi_workspace
6747            .as_ref()
6748            .map(|mw| mw.read(cx).workspace().downgrade());
6749
6750        let focus_handle = workspace
6751            .as_ref()
6752            .and_then(|ws| ws.upgrade())
6753            .map(|w| w.read(cx).focus_handle(cx))
6754            .unwrap_or_else(|| cx.focus_handle());
6755
6756        let window_project_groups: Vec<ProjectGroupKey> = multi_workspace
6757            .as_ref()
6758            .map(|mw| mw.read(cx).project_group_keys())
6759            .unwrap_or_default();
6760
6761        let popover_handle = self.recent_projects_popover_handle.clone();
6762
6763        PopoverMenu::new("sidebar-recent-projects-menu")
6764            .with_handle(popover_handle)
6765            .menu(move |window, cx| {
6766                workspace.as_ref().map(|ws| {
6767                    SidebarRecentProjects::popover(
6768                        ws.clone(),
6769                        window_project_groups.clone(),
6770                        focus_handle.clone(),
6771                        window,
6772                        cx,
6773                    )
6774                })
6775            })
6776            .trigger_with_tooltip(
6777                IconButton::new("open-project", IconName::FolderAdd)
6778                    .icon_size(IconSize::Small)
6779                    .selected_style(ButtonStyle::Tinted(TintColor::Accent)),
6780                |_window, cx| Tooltip::for_action("Add Project", &OpenRecent::default(), cx),
6781            )
6782            .offset(gpui::Point {
6783                x: px(-2.0),
6784                y: px(-2.0),
6785            })
6786            .anchor(gpui::Anchor::BottomRight)
6787    }
6788
6789    fn new_thread_in_group(
6790        &mut self,
6791        _: &NewThreadInGroup,
6792        window: &mut Window,
6793        cx: &mut Context<Self>,
6794    ) {
6795        if let Some(key) = self.selected_group_key() {
6796            self.set_group_expanded(&key, true, cx);
6797            self.selection = None;
6798            if let Some(workspace) = self.workspace_for_group(&key, cx) {
6799                self.create_new_entry(&workspace, window, cx);
6800            } else {
6801                self.open_workspace_and_create_entry(
6802                    &key,
6803                    NewEntryTarget::LastCreatedKind,
6804                    window,
6805                    cx,
6806                );
6807            }
6808        } else if let Some(workspace) = self.active_workspace(cx) {
6809            self.create_new_entry(&workspace, window, cx);
6810        }
6811    }
6812
6813    fn new_terminal_thread(
6814        &mut self,
6815        _: &NewTerminalThread,
6816        window: &mut Window,
6817        cx: &mut Context<Self>,
6818    ) {
6819        cx.stop_propagation();
6820
6821        if let Some(key) = self.selected_group_key() {
6822            self.set_group_expanded(&key, true, cx);
6823            self.selection = None;
6824            if let Some(workspace) = self.workspace_for_group(&key, cx) {
6825                self.create_new_terminal(&workspace, window, cx);
6826            } else {
6827                self.open_workspace_and_create_entry(&key, NewEntryTarget::Terminal, window, cx);
6828            }
6829        } else if let Some(workspace) = self.active_workspace(cx) {
6830            self.create_new_terminal(&workspace, window, cx);
6831        }
6832    }
6833
6834    /// Closed linked-worktree drafts need an open workspace so archive root
6835    /// planning can inspect repositories before deleting the worktree.
6836    fn open_workspace_and_remove_draft(
6837        &mut self,
6838        draft_id: ThreadId,
6839        folder_paths: PathList,
6840        project_group_key: ProjectGroupKey,
6841        window: &mut Window,
6842        cx: &mut Context<Self>,
6843    ) {
6844        let Some((open_task, modal_workspace)) =
6845            self.open_workspace_for_archive(folder_paths, project_group_key, window, cx)
6846        else {
6847            return;
6848        };
6849
6850        cx.spawn_in(window, async move |this, cx| {
6851            let result = open_task.await;
6852            remote_connection::dismiss_connection_modal(&modal_workspace, cx);
6853            let workspace = result?;
6854            Self::wait_for_archive_workspace_metadata(&workspace, cx).await;
6855
6856            this.update_in(cx, |this, window, cx| {
6857                let workspace = ThreadEntryWorkspace::Open(workspace);
6858                this.remove_draft(draft_id, &workspace, window, cx);
6859            })?;
6860            anyhow::Ok(())
6861        })
6862        .detach_and_log_err(cx);
6863    }
6864
6865    fn remove_draft(
6866        &mut self,
6867        draft_id: ThreadId,
6868        workspace: &ThreadEntryWorkspace,
6869        window: &mut Window,
6870        cx: &mut Context<Self>,
6871    ) {
6872        let metadata = ThreadMetadataStore::global(cx)
6873            .read(cx)
6874            .entry(draft_id)
6875            .cloned();
6876
6877        if let ThreadEntryWorkspace::Closed {
6878            folder_paths,
6879            project_group_key,
6880        } = workspace
6881            && self.should_load_closed_workspace_for_archive(
6882                folder_paths,
6883                project_group_key,
6884                metadata
6885                    .as_ref()
6886                    .and_then(|metadata| metadata.remote_connection.as_ref()),
6887                Some(draft_id),
6888                None,
6889                cx,
6890            )
6891        {
6892            self.open_workspace_and_remove_draft(
6893                draft_id,
6894                folder_paths.clone(),
6895                project_group_key.clone(),
6896                window,
6897                cx,
6898            );
6899            return;
6900        }
6901
6902        let draft_folder_paths = metadata
6903            .as_ref()
6904            .map(|metadata| metadata.folder_paths().clone())
6905            .or_else(|| match workspace {
6906                ThreadEntryWorkspace::Open(workspace) => {
6907                    Some(PathList::new(&workspace.read(cx).root_paths(cx)))
6908                }
6909                ThreadEntryWorkspace::Closed { folder_paths, .. } => Some(folder_paths.clone()),
6910            });
6911        let draft_remote_connection = metadata
6912            .as_ref()
6913            .and_then(|metadata| metadata.remote_connection.clone());
6914        let roots_to_archive = metadata
6915            .as_ref()
6916            .map(|metadata| {
6917                self.roots_to_archive_for_paths(
6918                    metadata.folder_paths(),
6919                    metadata.remote_connection.as_ref(),
6920                    Some(draft_id),
6921                    None,
6922                    cx,
6923                )
6924            })
6925            .unwrap_or_default();
6926
6927        let was_active = self
6928            .active_entry
6929            .as_ref()
6930            .is_some_and(|entry| entry.is_active_thread(&draft_id));
6931        let neighbor = self
6932            .contents
6933            .entries
6934            .iter()
6935            .position(|entry| {
6936                matches!(
6937                    entry,
6938                    ListEntry::Thread(thread) if thread.metadata.thread_id == draft_id
6939                )
6940            })
6941            .and_then(|position| self.neighboring_activatable_entry(position));
6942
6943        let workspace_to_remove = draft_folder_paths.as_ref().and_then(|folder_paths| {
6944            self.linked_worktree_workspace_to_remove(
6945                folder_paths,
6946                draft_remote_connection.as_ref(),
6947                Some(draft_id),
6948                None,
6949                &roots_to_archive,
6950                cx,
6951            )
6952        });
6953        let mut workspaces_to_remove: Vec<Entity<Workspace>> =
6954            workspace_to_remove.into_iter().collect();
6955        let close_item_tasks = self.close_items_for_archived_worktrees(
6956            &roots_to_archive,
6957            &mut workspaces_to_remove,
6958            window,
6959            cx,
6960        );
6961
6962        if !workspaces_to_remove.is_empty() {
6963            let Some(multi_workspace) = self.multi_workspace.upgrade() else {
6964                return;
6965            };
6966            let draft_workspace_removed = matches!(
6967                workspace,
6968                ThreadEntryWorkspace::Open(workspace) if workspaces_to_remove.contains(workspace)
6969            );
6970            let (fallback_paths, project_group_key) = neighbor
6971                .as_ref()
6972                .map(|neighbor| neighbor.project_location(cx))
6973                .unwrap_or_else(|| {
6974                    workspaces_to_remove
6975                        .first()
6976                        .map(|workspace| {
6977                            let key = workspace.read(cx).project_group_key(cx);
6978                            (key.path_list().clone(), key)
6979                        })
6980                        .unwrap_or_default()
6981                });
6982
6983            let excluded = workspaces_to_remove.clone();
6984            let remove_task = multi_workspace.update(cx, |multi_workspace, cx| {
6985                multi_workspace.remove(
6986                    workspaces_to_remove,
6987                    move |this, window, cx| {
6988                        let active_workspace = this.workspace().clone();
6989                        this.find_or_create_workspace(
6990                            fallback_paths,
6991                            project_group_key.host(),
6992                            Some(project_group_key),
6993                            |options, window, cx| {
6994                                connect_remote(active_workspace, options, window, cx)
6995                            },
6996                            &excluded,
6997                            None,
6998                            OpenMode::Activate,
6999                            window,
7000                            cx,
7001                        )
7002                    },
7003                    window,
7004                    cx,
7005                )
7006            });
7007
7008            let workspace = workspace.clone();
7009            cx.spawn_in(window, async move |this, cx| {
7010                if !remove_task.await? {
7011                    return anyhow::Ok(());
7012                }
7013
7014                for task in close_item_tasks {
7015                    let result: anyhow::Result<()> = task.await;
7016                    result.log_err();
7017                }
7018
7019                this.update_in(cx, |this, window, cx| {
7020                    if draft_workspace_removed {
7021                        if let Some(draft_folder_paths) = draft_folder_paths.as_ref() {
7022                            this.delete_empty_drafts_for_archive_paths(
7023                                draft_folder_paths,
7024                                draft_remote_connection.as_ref(),
7025                                cx,
7026                            );
7027                        }
7028                    }
7029                    this.remove_draft_entry(
7030                        draft_id,
7031                        &workspace,
7032                        was_active,
7033                        neighbor.as_ref(),
7034                        !draft_workspace_removed,
7035                        roots_to_archive,
7036                        window,
7037                        cx,
7038                    );
7039                })?;
7040                anyhow::Ok(())
7041            })
7042            .detach_and_log_err(cx);
7043        } else if !close_item_tasks.is_empty() {
7044            let workspace = workspace.clone();
7045            cx.spawn_in(window, async move |this, cx| {
7046                for task in close_item_tasks {
7047                    let result: anyhow::Result<()> = task.await;
7048                    result.log_err();
7049                }
7050
7051                this.update_in(cx, |this, window, cx| {
7052                    this.remove_draft_entry(
7053                        draft_id,
7054                        &workspace,
7055                        was_active,
7056                        neighbor.as_ref(),
7057                        true,
7058                        roots_to_archive,
7059                        window,
7060                        cx,
7061                    );
7062                })?;
7063                anyhow::Ok(())
7064            })
7065            .detach_and_log_err(cx);
7066        } else {
7067            self.remove_draft_entry(
7068                draft_id,
7069                workspace,
7070                was_active,
7071                neighbor.as_ref(),
7072                true,
7073                roots_to_archive,
7074                window,
7075                cx,
7076            );
7077        }
7078    }
7079
7080    fn remove_draft_entry(
7081        &mut self,
7082        draft_id: ThreadId,
7083        workspace: &ThreadEntryWorkspace,
7084        was_active: bool,
7085        neighbor: Option<&ActivatableEntry>,
7086        activate_panel_draft: bool,
7087        roots_to_archive: Vec<thread_worktree_archive::RootPlan>,
7088        window: &mut Window,
7089        cx: &mut Context<Self>,
7090    ) {
7091        // Fallback to a neighbor thread when the discarded
7092        // draft was the active entry.
7093        let activate_panel_draft = activate_panel_draft && !(was_active && neighbor.is_some());
7094
7095        let removed_from_panel = if let ThreadEntryWorkspace::Open(workspace) = workspace {
7096            workspace.update(cx, |workspace, cx| {
7097                if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
7098                    panel.update(cx, |panel, cx| {
7099                        if activate_panel_draft {
7100                            panel.remove_thread(draft_id, window, cx);
7101                        } else {
7102                            panel.remove_thread_without_activating_draft(draft_id, window, cx);
7103                        }
7104                    });
7105                    true
7106                } else {
7107                    false
7108                }
7109            })
7110        } else {
7111            false
7112        };
7113
7114        if !removed_from_panel {
7115            ThreadMetadataStore::global(cx).update(cx, |store, cx| {
7116                store.delete(draft_id, cx);
7117            });
7118        }
7119
7120        self.start_detached_archive_worktree_task(roots_to_archive, cx);
7121
7122        if was_active {
7123            self.active_entry = None;
7124            if !activate_panel_draft {
7125                if neighbor
7126                    .as_ref()
7127                    .is_some_and(|neighbor| self.activate_entry(neighbor, window, cx))
7128                {
7129                    return;
7130                }
7131                self.sync_active_entry_from_active_workspace(cx);
7132            }
7133        }
7134
7135        self.update_entries(cx);
7136    }
7137
7138    fn create_new_entry(
7139        &mut self,
7140        workspace: &Entity<Workspace>,
7141        window: &mut Window,
7142        cx: &mut Context<Self>,
7143    ) {
7144        if workspace_path_list(workspace, cx).paths().is_empty() {
7145            return;
7146        }
7147
7148        if self.should_create_terminal_for_workspace(workspace, cx) {
7149            self.create_new_terminal(workspace, window, cx);
7150        } else {
7151            self.create_new_thread(workspace, window, cx);
7152        }
7153    }
7154
7155    fn should_create_terminal_for_workspace(
7156        &self,
7157        workspace: &Entity<Workspace>,
7158        cx: &App,
7159    ) -> bool {
7160        workspace
7161            .read(cx)
7162            .panel::<AgentPanel>(cx)
7163            .is_some_and(|panel| panel.read(cx).should_create_terminal_for_new_entry(cx))
7164    }
7165
7166    fn create_new_thread(
7167        &mut self,
7168        workspace: &Entity<Workspace>,
7169        window: &mut Window,
7170        cx: &mut Context<Self>,
7171    ) {
7172        if workspace_path_list(workspace, cx).paths().is_empty() {
7173            return;
7174        }
7175
7176        let Some(multi_workspace) = self.multi_workspace.upgrade() else {
7177            return;
7178        };
7179
7180        multi_workspace.update(cx, |multi_workspace, cx| {
7181            multi_workspace.activate(workspace.clone(), None, window, cx);
7182        });
7183
7184        let draft_id = workspace.update(cx, |workspace, cx| {
7185            let panel = workspace.panel::<AgentPanel>(cx)?;
7186            let draft_id = panel.update(cx, |panel, cx| {
7187                panel.activate_new_thread(true, AgentThreadSource::Sidebar, window, cx);
7188                panel.active_thread_id(cx)
7189            });
7190            workspace.focus_panel::<AgentPanel>(window, cx);
7191            draft_id
7192        });
7193
7194        if let Some(draft_id) = draft_id {
7195            self.active_entry = Some(ActiveEntry::Thread {
7196                thread_id: draft_id,
7197                session_id: None,
7198                workspace: workspace.clone(),
7199            });
7200        }
7201    }
7202
7203    fn create_new_terminal(
7204        &mut self,
7205        workspace: &Entity<Workspace>,
7206        window: &mut Window,
7207        cx: &mut Context<Self>,
7208    ) {
7209        if workspace_path_list(workspace, cx).paths().is_empty() {
7210            return;
7211        }
7212
7213        let Some(multi_workspace) = self.multi_workspace.upgrade() else {
7214            return;
7215        };
7216
7217        multi_workspace.update(cx, |multi_workspace, cx| {
7218            multi_workspace.activate(workspace.clone(), None, window, cx);
7219        });
7220
7221        workspace.update(cx, |workspace, cx| {
7222            if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
7223                panel.update(cx, |panel, cx| {
7224                    panel.new_terminal(Some(workspace), AgentThreadSource::Sidebar, window, cx);
7225                });
7226            }
7227            workspace.focus_panel::<AgentPanel>(window, cx);
7228        });
7229    }
7230
7231    fn selected_group_key(&self) -> Option<ProjectGroupKey> {
7232        let ix = self.selection?;
7233        match self.contents.entries.get(ix) {
7234            Some(ListEntry::ProjectHeader { key, .. }) => Some(key.clone()),
7235            Some(ListEntry::Thread(_) | ListEntry::Terminal(_)) => {
7236                (0..ix)
7237                    .rev()
7238                    .find_map(|i| match self.contents.entries.get(i) {
7239                        Some(ListEntry::ProjectHeader { key, .. }) => Some(key.clone()),
7240                        _ => None,
7241                    })
7242            }
7243            _ => None,
7244        }
7245    }
7246
7247    fn workspace_for_group(&self, key: &ProjectGroupKey, cx: &App) -> Option<Entity<Workspace>> {
7248        let mw = self.multi_workspace.upgrade()?;
7249        let mw = mw.read(cx);
7250        let active = mw.workspace().clone();
7251        let active_key = active.read(cx).project_group_key(cx);
7252        if active_key == *key {
7253            Some(active)
7254        } else {
7255            mw.workspace_for_paths(key.path_list(), key.host().as_ref(), cx)
7256        }
7257    }
7258
7259    pub(crate) fn activate_or_open_workspace_for_group(
7260        &mut self,
7261        key: &ProjectGroupKey,
7262        window: &mut Window,
7263        cx: &mut Context<Self>,
7264    ) {
7265        let workspace = self
7266            .multi_workspace
7267            .upgrade()
7268            .and_then(|mw| mw.read(cx).last_active_workspace_for_group(key, cx))
7269            .or_else(|| self.workspace_for_group(key, cx));
7270        if let Some(workspace) = workspace {
7271            if self.is_active_workspace(&workspace, cx) {
7272                return;
7273            }
7274            self.activate_workspace(&workspace, window, cx);
7275        } else {
7276            self.open_workspace_for_group(key, window, cx);
7277        }
7278        self.selection = None;
7279        self.active_entry = None;
7280    }
7281
7282    fn active_project_group_key(&self, cx: &App) -> Option<ProjectGroupKey> {
7283        let multi_workspace = self.multi_workspace.upgrade()?;
7284        let multi_workspace = multi_workspace.read(cx);
7285        Some(multi_workspace.project_group_key_for_workspace(multi_workspace.workspace(), cx))
7286    }
7287
7288    fn active_project_header_position(&self, cx: &App) -> Option<usize> {
7289        let active_key = self.active_project_group_key(cx)?;
7290        self.contents
7291            .project_header_indices
7292            .iter()
7293            .position(|&entry_ix| {
7294                matches!(
7295                    &self.contents.entries[entry_ix],
7296                    ListEntry::ProjectHeader { key, .. } if *key == active_key
7297                )
7298            })
7299    }
7300
7301    fn cycle_project_impl(&mut self, forward: bool, window: &mut Window, cx: &mut Context<Self>) {
7302        let Some(multi_workspace) = self.multi_workspace.upgrade() else {
7303            return;
7304        };
7305
7306        let header_count = self.contents.project_header_indices.len();
7307        if header_count == 0 {
7308            return;
7309        }
7310
7311        let current_pos = self.active_project_header_position(cx);
7312
7313        let next_pos = match current_pos {
7314            Some(pos) => {
7315                if forward {
7316                    (pos + 1) % header_count
7317                } else {
7318                    (pos + header_count - 1) % header_count
7319                }
7320            }
7321            None => 0,
7322        };
7323
7324        let header_entry_ix = self.contents.project_header_indices[next_pos];
7325        let Some(ListEntry::ProjectHeader { key, .. }) = self.contents.entries.get(header_entry_ix)
7326        else {
7327            return;
7328        };
7329        let key = key.clone();
7330
7331        // Uncollapse the target group so that threads become visible.
7332        self.set_group_expanded(&key, true, cx);
7333
7334        if let Some(workspace) = self.multi_workspace.upgrade().and_then(|mw| {
7335            mw.read(cx)
7336                .workspace_for_paths(key.path_list(), key.host().as_ref(), cx)
7337        }) {
7338            multi_workspace.update(cx, |multi_workspace, cx| {
7339                multi_workspace.activate(workspace, None, window, cx);
7340                multi_workspace.retain_active_workspace(cx);
7341            });
7342        } else {
7343            self.open_workspace_for_group(&key, window, cx);
7344        }
7345    }
7346
7347    fn on_next_project(&mut self, _: &NextProject, window: &mut Window, cx: &mut Context<Self>) {
7348        self.cycle_project_impl(true, window, cx);
7349    }
7350
7351    fn on_previous_project(
7352        &mut self,
7353        _: &PreviousProject,
7354        window: &mut Window,
7355        cx: &mut Context<Self>,
7356    ) {
7357        self.cycle_project_impl(false, window, cx);
7358    }
7359
7360    fn cycle_thread_impl(&mut self, forward: bool, window: &mut Window, cx: &mut Context<Self>) {
7361        let thread_indices: Vec<usize> = self
7362            .contents
7363            .entries
7364            .iter()
7365            .enumerate()
7366            .filter_map(|(ix, entry)| match entry {
7367                ListEntry::Thread(_) | ListEntry::Terminal(_) => Some(ix),
7368                _ => None,
7369            })
7370            .collect();
7371
7372        if thread_indices.is_empty() {
7373            return;
7374        }
7375
7376        let current_thread_pos = self.active_entry.as_ref().and_then(|active| {
7377            thread_indices
7378                .iter()
7379                .position(|&ix| active.matches_entry(&self.contents.entries[ix]))
7380        });
7381
7382        let next_pos = match current_thread_pos {
7383            Some(pos) => {
7384                let count = thread_indices.len();
7385                if forward {
7386                    (pos + 1) % count
7387                } else {
7388                    (pos + count - 1) % count
7389                }
7390            }
7391            None => 0,
7392        };
7393
7394        let entry_ix = thread_indices[next_pos];
7395        match &self.contents.entries[entry_ix] {
7396            ListEntry::Thread(thread) => {
7397                let metadata = thread.metadata.clone();
7398                match &thread.workspace {
7399                    ThreadEntryWorkspace::Open(workspace) => {
7400                        let workspace = workspace.clone();
7401                        self.activate_thread(metadata, &workspace, true, window, cx);
7402                    }
7403                    ThreadEntryWorkspace::Closed {
7404                        folder_paths,
7405                        project_group_key,
7406                    } => {
7407                        let folder_paths = folder_paths.clone();
7408                        let project_group_key = project_group_key.clone();
7409                        self.open_workspace_and_activate_thread(
7410                            metadata,
7411                            folder_paths,
7412                            &project_group_key,
7413                            window,
7414                            cx,
7415                        );
7416                    }
7417                }
7418            }
7419            ListEntry::Terminal(terminal) => {
7420                let metadata = terminal.metadata.clone();
7421                let workspace = terminal.workspace.clone();
7422                self.activate_terminal_entry(metadata, workspace, true, window, cx);
7423            }
7424            ListEntry::ProjectHeader { .. } => {}
7425        }
7426    }
7427
7428    fn on_next_thread(&mut self, _: &NextThread, window: &mut Window, cx: &mut Context<Self>) {
7429        self.cycle_thread_impl(true, window, cx);
7430    }
7431
7432    fn on_previous_thread(
7433        &mut self,
7434        _: &PreviousThread,
7435        window: &mut Window,
7436        cx: &mut Context<Self>,
7437    ) {
7438        self.cycle_thread_impl(false, window, cx);
7439    }
7440
7441    fn render_no_results(&self, cx: &mut Context<Self>) -> impl IntoElement {
7442        let has_query = self.has_filter_query(cx);
7443        let message = if has_query {
7444            "No threads match your search."
7445        } else {
7446            "No threads yet"
7447        };
7448
7449        v_flex()
7450            .id("sidebar-no-results")
7451            .p_4()
7452            .size_full()
7453            .items_center()
7454            .justify_center()
7455            .child(
7456                Label::new(message)
7457                    .size(LabelSize::Small)
7458                    .color(Color::Muted),
7459            )
7460    }
7461
7462    fn render_empty_state(&self, cx: &mut Context<Self>) -> impl IntoElement {
7463        ProjectEmptyState::new(
7464            "Threads Sidebar",
7465            self.focus_handle(cx),
7466            KeyBinding::for_action(&workspace::Open::default(), cx),
7467        )
7468        .on_open_project(|_, window, cx| {
7469            let side = match AgentSettings::get_global(cx).sidebar_side() {
7470                SidebarSide::Left => "left",
7471                SidebarSide::Right => "right",
7472            };
7473            telemetry::event!("Sidebar Add Project Clicked", side = side);
7474            window.dispatch_action(
7475                Open {
7476                    create_new_window: Some(false),
7477                }
7478                .boxed_clone(),
7479                cx,
7480            );
7481        })
7482        .on_clone_repo(|_, window, cx| {
7483            window.dispatch_action(git::Clone.boxed_clone(), cx);
7484        })
7485    }
7486
7487    fn render_sidebar_header(
7488        &self,
7489        no_open_projects: bool,
7490        window: &Window,
7491        cx: &mut Context<Self>,
7492    ) -> impl IntoElement {
7493        let has_query = self.has_filter_query(cx);
7494        let sidebar_on_left = self.side(cx) == SidebarSide::Left;
7495        let sidebar_on_right = self.side(cx) == SidebarSide::Right;
7496        let not_fullscreen = !window.is_fullscreen();
7497        let traffic_lights = cfg!(target_os = "macos") && not_fullscreen && sidebar_on_left;
7498        let left_window_controls = !cfg!(target_os = "macos") && not_fullscreen && sidebar_on_left;
7499        let right_window_controls =
7500            !cfg!(target_os = "macos") && not_fullscreen && sidebar_on_right;
7501        let header_height = platform_title_bar_height(window);
7502
7503        h_flex()
7504            .h(header_height)
7505            .mt_px()
7506            .pb_px()
7507            .when(left_window_controls, |this| {
7508                this.children(Self::render_left_window_controls(window, cx))
7509            })
7510            .map(|this| {
7511                if traffic_lights {
7512                    this.pl(px(ui::utils::TRAFFIC_LIGHT_PADDING))
7513                } else if !left_window_controls {
7514                    this.pl_1p5()
7515                } else {
7516                    this
7517                }
7518            })
7519            .when(!right_window_controls, |this| this.pr_1p5())
7520            .gap_1()
7521            .when(!no_open_projects, |this| {
7522                this.border_b_1()
7523                    .border_color(cx.theme().colors().border)
7524                    .when(traffic_lights, |this| {
7525                        this.child(Divider::vertical().color(ui::DividerColor::Border))
7526                    })
7527                    .child(
7528                        div().ml_1().child(
7529                            Icon::new(IconName::MagnifyingGlass)
7530                                .size(IconSize::Small)
7531                                .color(Color::Muted),
7532                        ),
7533                    )
7534                    .child(self.render_filter_input(cx))
7535                    .child(
7536                        h_flex()
7537                            .gap_1()
7538                            .when(
7539                                self.selection.is_some()
7540                                    && !self.filter_editor.focus_handle(cx).is_focused(window),
7541                                |this| this.child(KeyBinding::for_action(&FocusSidebarFilter, cx)),
7542                            )
7543                            .when(has_query, |this| {
7544                                this.child(
7545                                    IconButton::new("clear_filter", IconName::Close)
7546                                        .icon_size(IconSize::Small)
7547                                        .tooltip(Tooltip::text("Clear Search"))
7548                                        .on_click(cx.listener(|this, _, window, cx| {
7549                                            this.reset_filter_editor_text(window, cx);
7550                                            this.update_entries(cx);
7551                                        })),
7552                                )
7553                            }),
7554                    )
7555            })
7556            .when(right_window_controls, |this| {
7557                this.children(Self::render_right_window_controls(window, cx))
7558            })
7559    }
7560
7561    fn render_left_window_controls(window: &Window, cx: &mut App) -> Option<AnyElement> {
7562        platform_title_bar::render_left_window_controls(
7563            cx.button_layout(),
7564            Box::new(CloseWindow),
7565            window,
7566        )
7567    }
7568
7569    fn render_right_window_controls(window: &Window, cx: &mut App) -> Option<AnyElement> {
7570        platform_title_bar::render_right_window_controls(
7571            cx.button_layout(),
7572            Box::new(CloseWindow),
7573            window,
7574        )
7575    }
7576
7577    fn render_sidebar_toggle_button(&self, _cx: &mut Context<Self>) -> impl IntoElement {
7578        let on_right = AgentSettings::get_global(_cx).sidebar_side() == SidebarSide::Right;
7579
7580        sidebar_side_context_menu("sidebar-toggle-menu", _cx)
7581            .anchor(if on_right {
7582                gpui::Anchor::BottomRight
7583            } else {
7584                gpui::Anchor::BottomLeft
7585            })
7586            .attach(if on_right {
7587                gpui::Anchor::TopRight
7588            } else {
7589                gpui::Anchor::TopLeft
7590            })
7591            .trigger(move |_is_active, _window, _cx| {
7592                let icon = if on_right {
7593                    IconName::ThreadsSidebarRightOpen
7594                } else {
7595                    IconName::ThreadsSidebarLeftOpen
7596                };
7597                IconButton::new("sidebar-close-toggle", icon)
7598                    .icon_size(IconSize::Small)
7599                    .tooltip(Tooltip::element(move |_window, cx| {
7600                        v_flex()
7601                            .gap_1()
7602                            .child(
7603                                h_flex()
7604                                    .gap_2()
7605                                    .justify_between()
7606                                    .child(Label::new("Toggle Sidebar"))
7607                                    .child(KeyBinding::for_action(&ToggleWorkspaceSidebar, cx)),
7608                            )
7609                            .child(
7610                                h_flex()
7611                                    .pt_1()
7612                                    .gap_2()
7613                                    .border_t_1()
7614                                    .border_color(cx.theme().colors().border_variant)
7615                                    .justify_between()
7616                                    .child(Label::new("Focus Sidebar"))
7617                                    .child(KeyBinding::for_action(&FocusWorkspaceSidebar, cx)),
7618                            )
7619                            .into_any_element()
7620                    }))
7621                    .on_click(|_, window, cx| {
7622                        if let Some(multi_workspace) = window.root::<MultiWorkspace>().flatten() {
7623                            multi_workspace.update(cx, |multi_workspace, cx| {
7624                                multi_workspace.close_sidebar(window, cx);
7625                            });
7626                        }
7627                    })
7628            })
7629    }
7630
7631    fn render_sidebar_bottom_bar(&mut self, cx: &mut Context<Self>) -> impl IntoElement {
7632        let is_archive = matches!(self.view, SidebarView::Archive(..));
7633        let on_right = self.side(cx) == SidebarSide::Right;
7634
7635        h_flex()
7636            .p_1()
7637            .gap_1()
7638            .when(on_right, |this| this.flex_row_reverse())
7639            .border_t_1()
7640            .border_color(cx.theme().colors().border)
7641            .child(self.render_sidebar_toggle_button(cx))
7642            .child(
7643                IconButton::new("history", IconName::Clock)
7644                    .icon_size(IconSize::Small)
7645                    .toggle_state(is_archive)
7646                    .tooltip(move |_, cx| {
7647                        let label = if is_archive {
7648                            "Hide Thread History"
7649                        } else {
7650                            "Show Thread History"
7651                        };
7652                        Tooltip::for_action(label, &ToggleThreadHistory, cx)
7653                    })
7654                    .on_click(cx.listener(|this, _, window, cx| {
7655                        this.toggle_archive(&ToggleThreadHistory, window, cx);
7656                    })),
7657            )
7658            .child(div().flex_1())
7659            .child(self.render_recent_projects_button(cx))
7660    }
7661
7662    fn active_workspace(&self, cx: &App) -> Option<Entity<Workspace>> {
7663        self.multi_workspace
7664            .upgrade()
7665            .map(|w| w.read(cx).workspace().clone())
7666    }
7667
7668    fn show_thread_import_modal(
7669        &mut self,
7670        source: &'static str,
7671        window: &mut Window,
7672        cx: &mut Context<Self>,
7673    ) {
7674        telemetry::event!(
7675            "Agent Threads Import Clicked",
7676            source = source,
7677            side = match self.side(cx) {
7678                SidebarSide::Left => "left",
7679                SidebarSide::Right => "right",
7680            }
7681        );
7682
7683        let Some(active_workspace) = self.active_workspace(cx) else {
7684            return;
7685        };
7686
7687        let Some(agent_registry_store) = AgentRegistryStore::try_global(cx) else {
7688            return;
7689        };
7690
7691        let agent_server_store = active_workspace
7692            .read(cx)
7693            .project()
7694            .read(cx)
7695            .agent_server_store()
7696            .clone();
7697
7698        let workspace_handle = active_workspace.downgrade();
7699        let multi_workspace = self.multi_workspace.clone();
7700
7701        active_workspace.update(cx, |workspace, cx| {
7702            workspace.toggle_modal(window, cx, |window, cx| {
7703                ThreadImportModal::new(
7704                    agent_server_store,
7705                    agent_registry_store,
7706                    workspace_handle.clone(),
7707                    multi_workspace.clone(),
7708                    window,
7709                    cx,
7710                )
7711            });
7712        });
7713    }
7714
7715    fn should_render_acp_import_onboarding(&self, cx: &App) -> bool {
7716        let has_external_agents = self
7717            .active_workspace(cx)
7718            .map(|ws| {
7719                ws.read(cx)
7720                    .project()
7721                    .read(cx)
7722                    .agent_server_store()
7723                    .read(cx)
7724                    .has_external_agents()
7725            })
7726            .unwrap_or(false);
7727
7728        has_external_agents && !AcpThreadImportOnboarding::dismissed(cx)
7729    }
7730
7731    fn render_acp_import_onboarding(
7732        &mut self,
7733        verbose_labels: bool,
7734        cx: &mut Context<Self>,
7735    ) -> impl IntoElement {
7736        let on_import = cx.listener(|this, _, window, cx| {
7737            this.show_archive(window, cx);
7738            this.show_thread_import_modal("external_agent_onboarding", window, cx);
7739        });
7740        render_import_onboarding_banner(
7741            "acp",
7742            "Looking for threads from external agents?",
7743            "Import threads from agents like Claude Agent, Codex, and more, whether started in Omega or another client.",
7744            if verbose_labels {
7745                "Import Threads from External Agents"
7746            } else {
7747                "Import Threads"
7748            },
7749            |_, _window, cx| AcpThreadImportOnboarding::dismiss(cx),
7750            on_import,
7751            cx,
7752        )
7753    }
7754
7755    fn should_render_cross_channel_import_onboarding(&self, cx: &App) -> bool {
7756        !CrossChannelImportOnboarding::dismissed(cx)
7757            && !self.cross_channel_import_channels.is_empty()
7758    }
7759
7760    fn render_cross_channel_import_onboarding(
7761        &mut self,
7762        verbose_labels: bool,
7763        cx: &mut Context<Self>,
7764    ) -> impl IntoElement {
7765        let channel_names = self
7766            .cross_channel_import_channels
7767            .iter()
7768            .map(SharedString::as_str)
7769            .join(" and ");
7770
7771        let description = format!(
7772            "Import threads from {} to continue where you left off.",
7773            channel_names
7774        );
7775
7776        let on_import = cx.listener(|this, _, _window, cx| {
7777            telemetry::event!(
7778                "Agent Threads Import Clicked",
7779                source = "cross_channel_onboarding",
7780                side = match this.side(cx) {
7781                    SidebarSide::Left => "left",
7782                    SidebarSide::Right => "right",
7783                }
7784            );
7785            CrossChannelImportOnboarding::dismiss(cx);
7786            if let Some(workspace) = this.active_workspace(cx) {
7787                workspace.update(cx, |workspace, cx| {
7788                    import_threads_from_other_channels(workspace, cx);
7789                });
7790            }
7791        });
7792        render_import_onboarding_banner(
7793            "channel",
7794            "Threads found from other channels",
7795            description,
7796            if verbose_labels {
7797                "Import Threads from Other Channels"
7798            } else {
7799                "Import Threads"
7800            },
7801            |_, _window, cx| CrossChannelImportOnboarding::dismiss(cx),
7802            on_import,
7803            cx,
7804        )
7805    }
7806
7807    fn toggle_archive(
7808        &mut self,
7809        _: &ToggleThreadHistory,
7810        window: &mut Window,
7811        cx: &mut Context<Self>,
7812    ) {
7813        match &self.view {
7814            SidebarView::ThreadList => {
7815                self.show_archive(window, cx);
7816            }
7817            SidebarView::Archive(_) => self.show_thread_list(window, cx),
7818        }
7819    }
7820
7821    fn show_archive(&mut self, window: &mut Window, cx: &mut Context<Self>) {
7822        let side = match self.side(cx) {
7823            SidebarSide::Left => "left",
7824            SidebarSide::Right => "right",
7825        };
7826        telemetry::event!("Thread History Viewed", side = side);
7827
7828        let Some(active_workspace) = self
7829            .multi_workspace
7830            .upgrade()
7831            .map(|w| w.read(cx).workspace().clone())
7832        else {
7833            return;
7834        };
7835        let Some(agent_panel) = active_workspace.read(cx).panel::<AgentPanel>(cx) else {
7836            return;
7837        };
7838
7839        let agent_server_store = active_workspace
7840            .read(cx)
7841            .project()
7842            .read(cx)
7843            .agent_server_store()
7844            .downgrade();
7845
7846        let agent_connection_store = agent_panel.read(cx).connection_store().downgrade();
7847
7848        let archive_view = cx.new(|cx| {
7849            ThreadsArchiveView::new(
7850                active_workspace.downgrade(),
7851                agent_connection_store.clone(),
7852                agent_server_store.clone(),
7853                window,
7854                cx,
7855            )
7856        });
7857
7858        let subscription = cx.subscribe_in(
7859            &archive_view,
7860            window,
7861            |this, _, event: &ThreadsArchiveViewEvent, window, cx| match event {
7862                ThreadsArchiveViewEvent::Close => {
7863                    this.show_thread_list(window, cx);
7864                }
7865                ThreadsArchiveViewEvent::Activate { thread } => {
7866                    this.open_thread_from_archive(thread.clone(), window, cx);
7867                }
7868                ThreadsArchiveViewEvent::CancelRestore { thread_id } => {
7869                    this.restoring_tasks.remove(thread_id);
7870                }
7871                ThreadsArchiveViewEvent::Import => {
7872                    this.show_thread_import_modal("thread_history", window, cx);
7873                }
7874                ThreadsArchiveViewEvent::NewThread => {
7875                    this.show_thread_list(window, cx);
7876                    if let Some(workspace) = this.active_workspace(cx) {
7877                        this.create_new_entry(&workspace, window, cx);
7878                    }
7879                }
7880            },
7881        );
7882
7883        self._subscriptions.push(subscription);
7884        self.view = SidebarView::Archive(archive_view.clone());
7885        archive_view.update(cx, |view, cx| view.focus_filter_editor(window, cx));
7886        self.serialize(cx);
7887        cx.notify();
7888    }
7889
7890    fn show_thread_list(&mut self, window: &mut Window, cx: &mut Context<Self>) {
7891        self.view = SidebarView::ThreadList;
7892        self._subscriptions.clear();
7893        let handle = self.filter_editor.read(cx).focus_handle(cx);
7894        handle.focus(window, cx);
7895        self.serialize(cx);
7896        cx.notify();
7897    }
7898}
7899
7900fn render_import_onboarding_banner(
7901    id: impl Into<SharedString>,
7902    title: impl Into<SharedString>,
7903    description: impl Into<SharedString>,
7904    button_label: impl Into<SharedString>,
7905    on_dismiss: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
7906    on_import: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
7907    cx: &App,
7908) -> impl IntoElement {
7909    let id: SharedString = id.into();
7910    let bg = cx.theme().colors().text_accent;
7911
7912    v_flex()
7913        .min_w_0()
7914        .w_full()
7915        .p_2()
7916        .border_t_1()
7917        .border_color(cx.theme().colors().border)
7918        .bg(linear_gradient(
7919            360.,
7920            linear_color_stop(bg.opacity(0.06), 1.),
7921            linear_color_stop(bg.opacity(0.), 0.),
7922        ))
7923        .child(
7924            h_flex()
7925                .min_w_0()
7926                .w_full()
7927                .gap_1()
7928                .justify_between()
7929                .flex_wrap()
7930                .child(Label::new(title).size(LabelSize::Small))
7931                .child(
7932                    IconButton::new(
7933                        SharedString::from(format!("close-{id}-onboarding")),
7934                        IconName::Close,
7935                    )
7936                    .icon_size(IconSize::Small)
7937                    .on_click(on_dismiss),
7938                ),
7939        )
7940        .child(
7941            Label::new(description)
7942                .size(LabelSize::Small)
7943                .color(Color::Muted)
7944                .mb_2(),
7945        )
7946        .child(
7947            Button::new(SharedString::from(format!("import-{id}")), button_label)
7948                .full_width()
7949                .style(ButtonStyle::OutlinedCustom(cx.theme().colors().border))
7950                .label_size(LabelSize::Small)
7951                .start_icon(
7952                    Icon::new(IconName::Download)
7953                        .size(IconSize::Small)
7954                        .color(Color::Muted),
7955                )
7956                .on_click(on_import),
7957        )
7958}
7959
7960impl WorkspaceSidebar for Sidebar {
7961    fn width(&self, _cx: &App) -> Pixels {
7962        self.width
7963    }
7964
7965    fn set_width(&mut self, width: Option<Pixels>, cx: &mut Context<Self>) {
7966        self.width = width.unwrap_or(DEFAULT_WIDTH).clamp(MIN_WIDTH, MAX_WIDTH);
7967        cx.notify();
7968    }
7969
7970    fn has_notifications(&self, _cx: &App) -> bool {
7971        !self.contents.notified_threads.is_empty() || !self.contents.notified_terminals.is_empty()
7972    }
7973
7974    fn is_threads_list_view_active(&self) -> bool {
7975        matches!(self.view, SidebarView::ThreadList)
7976    }
7977
7978    fn side(&self, cx: &App) -> SidebarSide {
7979        AgentSettings::get_global(cx).sidebar_side()
7980    }
7981
7982    fn prepare_for_focus(&mut self, _window: &mut Window, cx: &mut Context<Self>) {
7983        self.selection = None;
7984        cx.notify();
7985    }
7986
7987    fn toggle_thread_switcher(
7988        &mut self,
7989        select_last: bool,
7990        window: &mut Window,
7991        cx: &mut Context<Self>,
7992    ) {
7993        self.toggle_thread_switcher_impl(select_last, window, cx);
7994    }
7995
7996    fn cycle_project(&mut self, forward: bool, window: &mut Window, cx: &mut Context<Self>) {
7997        self.cycle_project_impl(forward, window, cx);
7998    }
7999
8000    fn cycle_thread(&mut self, forward: bool, window: &mut Window, cx: &mut Context<Self>) {
8001        self.cycle_thread_impl(forward, window, cx);
8002    }
8003
8004    fn serialized_state(&self, _cx: &App) -> Option<String> {
8005        let serialized = SerializedSidebar {
8006            width: Some(f32::from(self.width)),
8007            active_view: match self.view {
8008                SidebarView::ThreadList => SerializedSidebarView::ThreadList,
8009                SidebarView::Archive(_) => SerializedSidebarView::History,
8010            },
8011        };
8012        serde_json::to_string(&serialized).ok()
8013    }
8014
8015    fn restore_serialized_state(
8016        &mut self,
8017        state: &str,
8018        window: &mut Window,
8019        cx: &mut Context<Self>,
8020    ) {
8021        if let Some(serialized) = serde_json::from_str::<SerializedSidebar>(state).log_err() {
8022            if let Some(width) = serialized.width {
8023                self.width = px(width).clamp(MIN_WIDTH, MAX_WIDTH);
8024            }
8025            if serialized.active_view == SerializedSidebarView::History {
8026                cx.defer_in(window, |this, window, cx| {
8027                    this.show_archive(window, cx);
8028                });
8029            }
8030        }
8031        cx.notify();
8032    }
8033}
8034
8035impl gpui::EventEmitter<workspace::SidebarEvent> for Sidebar {}
8036
8037impl Focusable for Sidebar {
8038    fn focus_handle(&self, _cx: &App) -> FocusHandle {
8039        self.focus_handle.clone()
8040    }
8041}
8042
8043impl Render for Sidebar {
8044    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
8045        let _titlebar_height = ui::utils::platform_title_bar_height(window);
8046        let ui_font = theme_settings::setup_ui_font(window, cx);
8047        let sticky_header = self.render_sticky_header(window, cx);
8048
8049        let color = cx.theme().colors();
8050        let bg = color
8051            .title_bar_background
8052            .blend(color.panel_background.opacity(0.25));
8053
8054        let no_open_projects = !self.contents.has_open_projects;
8055        let no_search_results = self.contents.entries.is_empty();
8056
8057        v_flex()
8058            .id("workspace-sidebar")
8059            .key_context(self.dispatch_context(window, cx))
8060            .track_focus(&self.focus_handle)
8061            .on_action(cx.listener(Self::select_next))
8062            .on_action(cx.listener(Self::select_previous))
8063            .on_action(cx.listener(Self::editor_move_down))
8064            .on_action(cx.listener(Self::editor_move_up))
8065            .on_action(cx.listener(Self::select_first))
8066            .on_action(cx.listener(Self::select_last))
8067            .on_action(cx.listener(Self::confirm))
8068            .on_action(cx.listener(Self::expand_selected_entry))
8069            .on_action(cx.listener(Self::collapse_selected_entry))
8070            .on_action(cx.listener(Self::toggle_selected_fold))
8071            .on_action(cx.listener(Self::fold_all))
8072            .on_action(cx.listener(Self::unfold_all))
8073            .on_action(cx.listener(Self::cancel))
8074            .on_action(cx.listener(Self::archive_selected_thread))
8075            .on_action(cx.listener(Self::rename_selected_thread))
8076            .on_action(cx.listener(Self::new_thread_in_group))
8077            .on_action(cx.listener(Self::new_terminal_thread))
8078            .on_action(cx.listener(Self::toggle_archive))
8079            .on_action(cx.listener(Self::focus_sidebar_filter))
8080            .on_action(cx.listener(Self::on_toggle_thread_switcher))
8081            .on_action(cx.listener(Self::on_next_project))
8082            .on_action(cx.listener(Self::on_previous_project))
8083            .on_action(cx.listener(Self::on_next_thread))
8084            .on_action(cx.listener(Self::on_previous_thread))
8085            .on_action(cx.listener(|this, _: &OpenRecent, window, cx| {
8086                this.recent_projects_popover_handle.toggle(window, cx);
8087            }))
8088            .font(ui_font)
8089            .map(|el| {
8090                let on_left = self.side(cx) == SidebarSide::Left;
8091                match window.window_decorations() {
8092                    Decorations::Server => el.h_full().w(self.width),
8093                    // With client-side decorations the sidebar owns the window
8094                    // corners on its side, so round them like the title bar and
8095                    // status bar do. The sidebar is stretched 1px outwards over
8096                    // the window border on untiled edges (with compensating
8097                    // padding) so its rounded background lines up exactly with
8098                    // the window shape, avoiding a transparent gap in the
8099                    // rounded corners.
8100                    Decorations::Client { tiling, .. } => el
8101                        .absolute()
8102                        .top(if tiling.top { px(0.) } else { px(-1.) })
8103                        .bottom(if tiling.bottom { px(0.) } else { px(-1.) })
8104                        .when(!tiling.top, |el| el.pt_px())
8105                        .when(!tiling.bottom, |el| el.pb_px())
8106                        .map(|el| {
8107                            if on_left {
8108                                el.right(px(0.))
8109                                    .left(if tiling.left { px(0.) } else { px(-1.) })
8110                                    .when(!tiling.left, |el| el.pl(px(1.)))
8111                            } else {
8112                                el.left(px(0.))
8113                                    .right(if tiling.right { px(0.) } else { px(-1.) })
8114                                    .when(!tiling.right, |el| el.pr(px(1.)))
8115                            }
8116                        })
8117                        .when(on_left && !(tiling.top || tiling.left), |el| {
8118                            el.rounded_tl(CLIENT_SIDE_DECORATION_ROUNDING)
8119                        })
8120                        .when(on_left && !(tiling.bottom || tiling.left), |el| {
8121                            el.rounded_bl(CLIENT_SIDE_DECORATION_ROUNDING)
8122                        })
8123                        .when(!on_left && !(tiling.top || tiling.right), |el| {
8124                            el.rounded_tr(CLIENT_SIDE_DECORATION_ROUNDING)
8125                        })
8126                        .when(!on_left && !(tiling.bottom || tiling.right), |el| {
8127                            el.rounded_br(CLIENT_SIDE_DECORATION_ROUNDING)
8128                        }),
8129                }
8130            })
8131            .bg(bg)
8132            .when(self.side(cx) == SidebarSide::Left, |el| el.border_r_1())
8133            .when(self.side(cx) == SidebarSide::Right, |el| el.border_l_1())
8134            .border_color(color.border)
8135            .map(|this| match &self.view {
8136                SidebarView::ThreadList => this
8137                    .child(self.render_sidebar_header(no_open_projects, window, cx))
8138                    .map(|this| {
8139                        if no_open_projects {
8140                            this.child(self.render_empty_state(cx))
8141                        } else {
8142                            this.child(
8143                                v_flex()
8144                                    .relative()
8145                                    .flex_1()
8146                                    .overflow_hidden()
8147                                    .child(
8148                                        list(
8149                                            self.list_state.clone(),
8150                                            cx.processor(Self::render_list_entry),
8151                                        )
8152                                        .flex_1()
8153                                        .size_full(),
8154                                    )
8155                                    .when(no_search_results, |this| {
8156                                        this.child(self.render_no_results(cx))
8157                                    })
8158                                    .when_some(sticky_header, |this, header| this.child(header))
8159                                    .custom_scrollbars(
8160                                        Scrollbars::new(ScrollAxes::Vertical)
8161                                            .tracked_scroll_handle(&self.list_state),
8162                                        window,
8163                                        cx,
8164                                    ),
8165                            )
8166                        }
8167                    }),
8168                SidebarView::Archive(archive_view) => this.child(archive_view.clone()),
8169            })
8170            .map(|this| {
8171                let show_acp = self.should_render_acp_import_onboarding(cx);
8172                let show_cross_channel = self.should_render_cross_channel_import_onboarding(cx);
8173
8174                let verbose = *self
8175                    .import_banners_use_verbose_labels
8176                    .get_or_insert(show_acp && show_cross_channel);
8177
8178                this.when(show_acp, |this| {
8179                    this.child(self.render_acp_import_onboarding(verbose, cx))
8180                })
8181                .when(show_cross_channel, |this| {
8182                    this.child(self.render_cross_channel_import_onboarding(verbose, cx))
8183                })
8184            })
8185            .child(self.render_sidebar_bottom_bar(cx))
8186    }
8187}
8188
8189fn all_thread_infos_for_workspace(
8190    workspace: &Entity<Workspace>,
8191    cx: &App,
8192) -> impl Iterator<Item = ActiveThreadInfo> {
8193    let Some(agent_panel) = workspace.read(cx).panel::<AgentPanel>(cx) else {
8194        return None.into_iter().flatten();
8195    };
8196    let agent_panel = agent_panel.read(cx);
8197    let threads = agent_panel
8198        .conversation_views()
8199        .into_iter()
8200        .filter_map(|conversation_view| {
8201            let has_pending_tool_call = conversation_view
8202                .read(cx)
8203                .root_thread_has_pending_tool_call(cx);
8204            let conversation_thread_id = conversation_view.read(cx).parent_id();
8205            let thread_view = conversation_view.read(cx).root_thread_view()?;
8206            let thread_view_ref = thread_view.read(cx);
8207            let thread = thread_view_ref.thread.read(cx);
8208
8209            let icon = thread_view_ref.agent_icon;
8210            let icon_from_external_svg = thread_view_ref.agent_icon_from_external_svg.clone();
8211            let title = thread
8212                .title()
8213                .unwrap_or_else(|| DEFAULT_THREAD_TITLE.into());
8214            let is_title_generating = thread_view_ref
8215                .as_native_thread(cx)
8216                .is_some_and(|native_thread| native_thread.read(cx).is_generating_title());
8217            let session_id = thread.session_id().clone();
8218            let is_background = agent_panel.is_retained_thread(&conversation_thread_id);
8219
8220            let status = if has_pending_tool_call {
8221                AgentThreadStatus::WaitingForConfirmation
8222            } else if thread.had_error() {
8223                AgentThreadStatus::Error
8224            } else {
8225                match thread.status() {
8226                    ThreadStatus::Generating => AgentThreadStatus::Running,
8227                    ThreadStatus::Idle => AgentThreadStatus::Completed,
8228                }
8229            };
8230
8231            let diff_stats = thread.action_log().read(cx).diff_stats(cx);
8232
8233            Some(ActiveThreadInfo {
8234                session_id,
8235                title,
8236                status,
8237                icon,
8238                icon_from_external_svg,
8239                is_background,
8240                is_title_generating,
8241                diff_stats,
8242            })
8243        });
8244
8245    Some(threads).into_iter().flatten()
8246}
8247
8248pub fn dump_workspace_info(
8249    workspace: &mut Workspace,
8250    _: &DumpWorkspaceInfo,
8251    window: &mut gpui::Window,
8252    cx: &mut gpui::Context<Workspace>,
8253) {
8254    use std::fmt::Write;
8255
8256    let mut output = String::new();
8257    let this_entity = cx.entity();
8258
8259    let multi_workspace = workspace.multi_workspace().and_then(|weak| weak.upgrade());
8260    let workspaces: Vec<gpui::Entity<Workspace>> = match &multi_workspace {
8261        Some(mw) => mw.read(cx).workspaces().cloned().collect(),
8262        None => vec![this_entity.clone()],
8263    };
8264    let active_workspace = multi_workspace
8265        .as_ref()
8266        .map(|mw| mw.read(cx).workspace().clone());
8267
8268    writeln!(output, "MultiWorkspace: {} workspace(s)", workspaces.len()).ok();
8269
8270    if let Some(mw) = &multi_workspace {
8271        let keys: Vec<_> = mw.read(cx).project_group_keys();
8272        writeln!(output, "Project group keys ({}):", keys.len()).ok();
8273        for key in keys {
8274            writeln!(output, "  - {key:?}").ok();
8275        }
8276    }
8277
8278    writeln!(output).ok();
8279
8280    for (index, ws) in workspaces.iter().enumerate() {
8281        let is_active = active_workspace.as_ref() == Some(ws);
8282        writeln!(
8283            output,
8284            "--- Workspace {index}{} ---",
8285            if is_active { " (active)" } else { "" }
8286        )
8287        .ok();
8288
8289        // project_group_key_for_workspace internally reads the workspace,
8290        // so we can only call it for workspaces other than this_entity
8291        // (which is already being updated).
8292        if let Some(mw) = &multi_workspace {
8293            if *ws == this_entity {
8294                let workspace_key = workspace.project_group_key(cx);
8295                writeln!(output, "ProjectGroupKey: {workspace_key:?}").ok();
8296            } else {
8297                let effective_key = mw.read(cx).project_group_key_for_workspace(ws, cx);
8298                let workspace_key = ws.read(cx).project_group_key(cx);
8299                if effective_key != workspace_key {
8300                    writeln!(
8301                        output,
8302                        "ProjectGroupKey (multi_workspace): {effective_key:?}"
8303                    )
8304                    .ok();
8305                    writeln!(
8306                        output,
8307                        "ProjectGroupKey (workspace, DISAGREES): {workspace_key:?}"
8308                    )
8309                    .ok();
8310                } else {
8311                    writeln!(output, "ProjectGroupKey: {effective_key:?}").ok();
8312                }
8313            }
8314        } else {
8315            let workspace_key = workspace.project_group_key(cx);
8316            writeln!(output, "ProjectGroupKey: {workspace_key:?}").ok();
8317        }
8318
8319        // The action handler is already inside an update on `this_entity`,
8320        // so we must avoid a nested read/update on that same entity.
8321        if *ws == this_entity {
8322            dump_single_workspace(workspace, &mut output, cx);
8323        } else {
8324            ws.read_with(cx, |ws, cx| {
8325                dump_single_workspace(ws, &mut output, cx);
8326            });
8327        }
8328    }
8329
8330    let project = workspace.project().clone();
8331    cx.spawn_in(window, async move |_this, cx| {
8332        let buffer = project
8333            .update(cx, |project, cx| project.create_buffer(None, false, cx))
8334            .await?;
8335
8336        buffer.update(cx, |buffer, cx| {
8337            buffer.set_text(output, cx);
8338        });
8339
8340        let buffer = cx.new(|cx| {
8341            editor::MultiBuffer::singleton(buffer, cx).with_title("Workspace Info".into())
8342        });
8343
8344        _this.update_in(cx, |workspace, window, cx| {
8345            workspace.add_item_to_active_pane(
8346                Box::new(cx.new(|cx| {
8347                    let mut editor =
8348                        editor::Editor::for_multibuffer(buffer, Some(project.clone()), window, cx);
8349                    editor.set_read_only(true);
8350                    editor.set_should_serialize(false, cx);
8351                    editor.set_breadcrumb_header("Workspace Info".into());
8352                    editor
8353                })),
8354                None,
8355                true,
8356                window,
8357                cx,
8358            );
8359        })
8360    })
8361    .detach_and_log_err(cx);
8362}
8363
8364fn dump_single_workspace(workspace: &Workspace, output: &mut String, cx: &gpui::App) {
8365    use std::fmt::Write;
8366
8367    let workspace_db_id = workspace.database_id();
8368    match workspace_db_id {
8369        Some(id) => writeln!(output, "Workspace DB ID: {id:?}").ok(),
8370        None => writeln!(output, "Workspace DB ID: (none)").ok(),
8371    };
8372
8373    let project = workspace.project().read(cx);
8374
8375    let repos: Vec<_> = project
8376        .repositories(cx)
8377        .values()
8378        .map(|repo| repo.read(cx).snapshot())
8379        .collect();
8380
8381    writeln!(output, "Worktrees:").ok();
8382    for worktree in project.worktrees(cx) {
8383        let worktree = worktree.read(cx);
8384        let abs_path = worktree.abs_path();
8385        let visible = worktree.is_visible();
8386
8387        let repo_info = repos
8388            .iter()
8389            .find(|snapshot| abs_path.starts_with(&*snapshot.work_directory_abs_path));
8390
8391        let is_linked = repo_info.map(|s| s.is_linked_worktree()).unwrap_or(false);
8392        let main_worktree_path = repo_info.and_then(|s| s.main_worktree_abs_path());
8393        let branch = repo_info.and_then(|s| s.branch.as_ref().map(|b| b.ref_name.clone()));
8394
8395        write!(output, "  - {}", abs_path.display()).ok();
8396        if !visible {
8397            write!(output, " (hidden)").ok();
8398        }
8399        if let Some(branch) = &branch {
8400            write!(output, " [branch: {branch}]").ok();
8401        }
8402        if is_linked {
8403            if let Some(main_worktree_path) = main_worktree_path {
8404                write!(
8405                    output,
8406                    " [linked worktree -> {}]",
8407                    main_worktree_path.display()
8408                )
8409                .ok();
8410            } else {
8411                write!(output, " [linked worktree]").ok();
8412            }
8413        }
8414        writeln!(output).ok();
8415    }
8416
8417    if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
8418        let panel = panel.read(cx);
8419
8420        let panel_workspace_id = panel.workspace_id();
8421        if panel_workspace_id != workspace_db_id {
8422            writeln!(
8423                output,
8424                "  \u{26a0} workspace ID mismatch! panel has {panel_workspace_id:?}, workspace has {workspace_db_id:?}"
8425            )
8426            .ok();
8427        }
8428
8429        if let Some(thread) = panel.active_agent_thread(cx) {
8430            let thread = thread.read(cx);
8431            let title = thread.title().unwrap_or_else(|| "(untitled)".into());
8432            let session_id = thread.session_id();
8433            let status = match thread.status() {
8434                ThreadStatus::Idle => "idle",
8435                ThreadStatus::Generating => "generating",
8436            };
8437            let entry_count = thread.entries().len();
8438            write!(output, "Active thread: {title} (session: {session_id})").ok();
8439            write!(output, " [{status}, {entry_count} entries").ok();
8440            if panel
8441                .active_conversation_view()
8442                .is_some_and(|conversation_view| {
8443                    conversation_view
8444                        .read(cx)
8445                        .root_thread_has_pending_tool_call(cx)
8446                })
8447            {
8448                write!(output, ", awaiting confirmation").ok();
8449            }
8450            writeln!(output, "]").ok();
8451        } else {
8452            writeln!(output, "Active thread: (none)").ok();
8453        }
8454
8455        let background_threads = panel.retained_threads();
8456        if !background_threads.is_empty() {
8457            writeln!(
8458                output,
8459                "Background threads ({}): ",
8460                background_threads.len()
8461            )
8462            .ok();
8463            for (session_id, conversation_view) in background_threads {
8464                if let Some(thread_view) = conversation_view.read(cx).root_thread_view() {
8465                    let thread = thread_view.read(cx).thread.read(cx);
8466                    let title = thread.title().unwrap_or_else(|| "(untitled)".into());
8467                    let status = match thread.status() {
8468                        ThreadStatus::Idle => "idle",
8469                        ThreadStatus::Generating => "generating",
8470                    };
8471                    let entry_count = thread.entries().len();
8472                    write!(output, "  - {title} (thread: {session_id:?})").ok();
8473                    write!(output, " [{status}, {entry_count} entries").ok();
8474                    if conversation_view
8475                        .read(cx)
8476                        .root_thread_has_pending_tool_call(cx)
8477                    {
8478                        write!(output, ", awaiting confirmation").ok();
8479                    }
8480                    writeln!(output, "]").ok();
8481                } else {
8482                    writeln!(output, "  - (not connected) (thread: {session_id:?})").ok();
8483                }
8484            }
8485        }
8486    } else {
8487        writeln!(output, "Agent panel: not loaded").ok();
8488    }
8489
8490    writeln!(output).ok();
8491}
8492
Served at tenant.openagents/omega Member data and write actions are omitted.