Fix sidebar selections (#50900)

Mikayla Maki and Eric Holk created

Release Notes:

- N/A

---------

Co-authored-by: Eric Holk <eric@zed.dev>

Change summary

crates/agent_ui/src/agent_panel.rs       |  35 +
crates/agent_ui/src/connection_view.rs   |  67 ++
crates/gpui/examples/active_state_bug.rs |  47 ++
crates/gpui/src/elements/div.rs          |  12 
crates/sidebar/Cargo.toml                |   1 
crates/sidebar/src/sidebar.rs            | 525 +++++++++++++------------
crates/workspace/src/multi_workspace.rs  |  26 +
crates/workspace/src/workspace.rs        |  10 
crates/zed/src/zed.rs                    |  21 
9 files changed, 454 insertions(+), 290 deletions(-)

Detailed changes

crates/agent_ui/src/agent_panel.rs πŸ”—

@@ -208,7 +208,7 @@ pub fn init(cx: &mut App) {
                 .register_action(|workspace, _: &OpenAgentDiff, window, cx| {
                     let thread = workspace
                         .panel::<AgentPanel>(cx)
-                        .and_then(|panel| panel.read(cx).active_thread_view().cloned())
+                        .and_then(|panel| panel.read(cx).active_connection_view().cloned())
                         .and_then(|thread_view| {
                             thread_view
                                 .read(cx)
@@ -570,6 +570,7 @@ pub struct AgentPanel {
     start_thread_in: StartThreadIn,
     worktree_creation_status: Option<WorktreeCreationStatus>,
     _thread_view_subscription: Option<Subscription>,
+    _active_thread_focus_subscription: Option<Subscription>,
     _worktree_creation_task: Option<Task<()>>,
     show_trust_workspace_message: bool,
     last_configuration_error_telemetry: Option<String>,
@@ -898,6 +899,7 @@ impl AgentPanel {
             start_thread_in: StartThreadIn::default(),
             worktree_creation_status: None,
             _thread_view_subscription: None,
+            _active_thread_focus_subscription: None,
             _worktree_creation_task: None,
             show_trust_workspace_message: false,
             last_configuration_error_telemetry: None,
@@ -988,7 +990,7 @@ impl AgentPanel {
             .unwrap_or(false)
     }
 
-    pub(crate) fn active_thread_view(&self) -> Option<&Entity<ConnectionView>> {
+    pub fn active_connection_view(&self) -> Option<&Entity<ConnectionView>> {
         match &self.active_view {
             ActiveView::AgentThread { server_view, .. } => Some(server_view),
             ActiveView::Uninitialized
@@ -1173,7 +1175,7 @@ impl AgentPanel {
     }
 
     fn expand_message_editor(&mut self, window: &mut Window, cx: &mut Context<Self>) {
-        let Some(thread_view) = self.active_thread_view() else {
+        let Some(thread_view) = self.active_connection_view() else {
             return;
         };
 
@@ -1432,7 +1434,7 @@ impl AgentPanel {
         cx: &mut Context<Self>,
     ) {
         if let Some(workspace) = self.workspace.upgrade()
-            && let Some(thread_view) = self.active_thread_view()
+            && let Some(thread_view) = self.active_connection_view()
             && let Some(active_thread) = thread_view.read(cx).active_thread().cloned()
         {
             active_thread.update(cx, |thread, cx| {
@@ -1763,6 +1765,12 @@ impl AgentPanel {
             ActiveView::AgentThread { server_view } => {
                 self._thread_view_subscription =
                     Self::subscribe_to_active_thread_view(server_view, window, cx);
+                let focus_handle = server_view.focus_handle(cx);
+                self._active_thread_focus_subscription =
+                    Some(cx.on_focus_in(&focus_handle, window, |_this, _window, cx| {
+                        cx.emit(AgentPanelEvent::ThreadFocused);
+                        cx.notify();
+                    }));
                 Some(
                     cx.observe_in(server_view, window, |this, server_view, window, cx| {
                         this._thread_view_subscription =
@@ -1775,6 +1783,7 @@ impl AgentPanel {
             }
             _ => {
                 self._thread_view_subscription = None;
+                self._active_thread_focus_subscription = None;
                 None
             }
         };
@@ -2035,6 +2044,7 @@ impl AgentPanel {
                 .map(|t| t.read(cx).id.clone())
                 == Some(session_id.clone())
             {
+                cx.emit(AgentPanelEvent::ActiveViewChanged);
                 return;
             }
         }
@@ -2642,6 +2652,7 @@ fn agent_panel_dock_position(cx: &App) -> DockPosition {
 
 pub enum AgentPanelEvent {
     ActiveViewChanged,
+    ThreadFocused,
     BackgroundThreadChanged,
 }
 
@@ -3523,7 +3534,7 @@ impl AgentPanel {
             });
 
         let is_thread_loading = self
-            .active_thread_view()
+            .active_connection_view()
             .map(|thread| thread.read(cx).is_loading())
             .unwrap_or(false);
 
@@ -4077,7 +4088,7 @@ impl Render for AgentPanel {
             .on_action(cx.listener(Self::reset_font_size))
             .on_action(cx.listener(Self::toggle_zoom))
             .on_action(cx.listener(|this, _: &ReauthenticateAgent, window, cx| {
-                if let Some(thread_view) = this.active_thread_view() {
+                if let Some(thread_view) = this.active_connection_view() {
                     thread_view.update(cx, |thread_view, cx| thread_view.reauthenticate(window, cx))
                 }
             }))
@@ -4263,7 +4274,7 @@ impl AgentPanelDelegate for ConcreteAssistantPanelDelegate {
             // Wait to create a new context until the workspace is no longer
             // being updated.
             cx.defer_in(window, move |panel, window, cx| {
-                if let Some(thread_view) = panel.active_thread_view() {
+                if let Some(thread_view) = panel.active_connection_view() {
                     thread_view.update(cx, |thread_view, cx| {
                         thread_view.insert_selections(window, cx);
                     });
@@ -4301,7 +4312,7 @@ impl AgentPanelDelegate for ConcreteAssistantPanelDelegate {
             // Wait to create a new context until the workspace is no longer
             // being updated.
             cx.defer_in(window, move |panel, window, cx| {
-                if let Some(thread_view) = panel.active_thread_view() {
+                if let Some(thread_view) = panel.active_connection_view() {
                     thread_view.update(cx, |thread_view, cx| {
                         thread_view.insert_terminal_text(text, window, cx);
                     });
@@ -4367,7 +4378,7 @@ impl AgentPanel {
     /// This is a test-only accessor that exposes the private `active_thread_view()`
     /// method for test assertions. Not compiled into production builds.
     pub fn active_thread_view_for_tests(&self) -> Option<&Entity<ConnectionView>> {
-        self.active_thread_view()
+        self.active_connection_view()
     }
 
     /// Sets the start_thread_in value directly, bypassing validation.
@@ -4552,7 +4563,7 @@ mod tests {
                 "workspace A agent type should be restored"
             );
             assert!(
-                panel.active_thread_view().is_some(),
+                panel.active_connection_view().is_some(),
                 "workspace A should have its active thread restored"
             );
         });
@@ -4572,7 +4583,7 @@ mod tests {
                 "workspace B agent type should be restored"
             );
             assert!(
-                panel.active_thread_view().is_none(),
+                panel.active_connection_view().is_none(),
                 "workspace B should have no active thread"
             );
         });
@@ -4709,7 +4720,7 @@ mod tests {
         send_message(&panel, &mut cx);
 
         let weak_view_a = panel.read_with(&cx, |panel, _cx| {
-            panel.active_thread_view().unwrap().downgrade()
+            panel.active_connection_view().unwrap().downgrade()
         });
 
         // Thread A should be idle (auto-completed via set_next_prompt_updates).

crates/agent_ui/src/connection_view.rs πŸ”—

@@ -399,7 +399,10 @@ impl ConnectionView {
 
 enum ServerState {
     Loading(Entity<LoadingView>),
-    LoadError(LoadError),
+    LoadError {
+        error: LoadError,
+        session_id: Option<acp::SessionId>,
+    },
     Connected(ConnectedServerState),
 }
 
@@ -430,6 +433,7 @@ impl AuthState {
 }
 
 struct LoadingView {
+    session_id: Option<acp::SessionId>,
     title: SharedString,
     _load_task: Task<()>,
     _update_title_task: Task<anyhow::Result<()>>,
@@ -572,12 +576,18 @@ impl ConnectionView {
         window: &mut Window,
         cx: &mut Context<Self>,
     ) -> ServerState {
+        let session_id = resume_thread
+            .as_ref()
+            .map(|thread| thread.session_id.clone());
         if project.read(cx).is_via_collab()
             && agent.clone().downcast::<NativeAgentServer>().is_none()
         {
-            return ServerState::LoadError(LoadError::Other(
-                "External agents are not yet supported in shared projects.".into(),
-            ));
+            return ServerState::LoadError {
+                error: LoadError::Other(
+                    "External agents are not yet supported in shared projects.".into(),
+                ),
+                session_id,
+            };
         }
         let mut worktrees = project.read(cx).visible_worktrees(cx).collect::<Vec<_>>();
         // Pick the first non-single-file worktree for the root directory if there are any,
@@ -633,17 +643,18 @@ impl ConnectionView {
         );
 
         let connect_task = agent.connect(delegate, cx);
+        let load_session_id = session_id.clone();
         let load_task = cx.spawn_in(window, async move |this, cx| {
             let connection = match connect_task.await {
                 Ok(connection) => connection,
                 Err(err) => {
                     this.update_in(cx, |this, window, cx| {
                         if err.downcast_ref::<LoadError>().is_some() {
-                            this.handle_load_error(err, window, cx);
+                            this.handle_load_error(load_session_id.clone(), err, window, cx);
                         } else if let Some(active) = this.active_thread() {
                             active.update(cx, |active, cx| active.handle_thread_error(err, cx));
                         } else {
-                            this.handle_load_error(err, window, cx);
+                            this.handle_load_error(load_session_id.clone(), err, window, cx);
                         }
                         cx.notify();
                     })
@@ -756,7 +767,7 @@ impl ConnectionView {
                         );
                     }
                     Err(err) => {
-                        this.handle_load_error(err, window, cx);
+                        this.handle_load_error(load_session_id.clone(), err, window, cx);
                     }
                 };
             })
@@ -792,6 +803,7 @@ impl ConnectionView {
             });
 
             LoadingView {
+                session_id,
                 title: "Loading…".into(),
                 _load_task: load_task,
                 _update_title_task: update_title_task,
@@ -1086,6 +1098,7 @@ impl ConnectionView {
 
     fn handle_load_error(
         &mut self,
+        session_id: Option<acp::SessionId>,
         err: anyhow::Error,
         window: &mut Window,
         cx: &mut Context<Self>,
@@ -1106,7 +1119,13 @@ impl ConnectionView {
             LoadError::Other(format!("{:#}", err).into())
         };
         self.emit_load_error_telemetry(&load_error);
-        self.set_server_state(ServerState::LoadError(load_error), cx);
+        self.set_server_state(
+            ServerState::LoadError {
+                error: load_error,
+                session_id,
+            },
+            cx,
+        );
     }
 
     fn handle_agent_servers_updated(
@@ -1121,7 +1140,7 @@ impl ConnectionView {
         // This handles the case where a thread is restored before authentication completes.
         let should_retry = match &self.server_state {
             ServerState::Loading(_) => false,
-            ServerState::LoadError(_) => true,
+            ServerState::LoadError { .. } => true,
             ServerState::Connected(connected) => {
                 connected.auth_state.is_ok() && connected.has_thread_error(cx)
             }
@@ -1145,7 +1164,7 @@ impl ConnectionView {
         match &self.server_state {
             ServerState::Connected(_) => "New Thread".into(),
             ServerState::Loading(loading_view) => loading_view.read(cx).title.clone(),
-            ServerState::LoadError(error) => match error {
+            ServerState::LoadError { error, .. } => match error {
                 LoadError::Unsupported { .. } => format!("Upgrade {}", self.agent.name()).into(),
                 LoadError::FailedToInstall(_) => {
                     format!("Failed to Install {}", self.agent.name()).into()
@@ -1164,6 +1183,17 @@ impl ConnectionView {
         }
     }
 
+    // The parent ID is None if we haven't created a thread yet
+    pub fn parent_id(&self, cx: &App) -> Option<acp::SessionId> {
+        match &self.server_state {
+            ServerState::Connected(_) => self
+                .parent_thread(cx)
+                .map(|thread| thread.read(cx).id.clone()),
+            ServerState::Loading(loading) => loading.read(cx).session_id.clone(),
+            ServerState::LoadError { session_id, .. } => session_id.clone(),
+        }
+    }
+
     pub fn is_loading(&self) -> bool {
         matches!(self.server_state, ServerState::Loading { .. })
     }
@@ -1361,7 +1391,13 @@ impl ConnectionView {
                         self.focus_handle.focus(window, cx)
                     }
                 }
-                self.set_server_state(ServerState::LoadError(error.clone()), cx);
+                self.set_server_state(
+                    ServerState::LoadError {
+                        error: error.clone(),
+                        session_id: Some(thread_id),
+                    },
+                    cx,
+                );
             }
             AcpThreadEvent::TitleUpdated => {
                 let title = thread.read(cx).title();
@@ -2635,7 +2671,7 @@ impl Render for ConnectionView {
                     .flex_1()
                     // .child(self.render_recent_history(cx))
                     .into_any(),
-                ServerState::LoadError(e) => v_flex()
+                ServerState::LoadError { error: e, .. } => v_flex()
                     .flex_1()
                     .size_full()
                     .items_center()
@@ -3126,7 +3162,10 @@ pub(crate) mod tests {
                 "Tab title should show the agent name with an error prefix"
             );
             match &view.server_state {
-                ServerState::LoadError(LoadError::Other(msg)) => {
+                ServerState::LoadError {
+                    error: LoadError::Other(msg),
+                    ..
+                } => {
                     assert!(
                         msg.contains("Invalid gzip header"),
                         "Error callout should contain the underlying extraction error, got: {msg}"
@@ -3136,7 +3175,7 @@ pub(crate) mod tests {
                     "Expected LoadError::Other, got: {}",
                     match other {
                         ServerState::Loading(_) => "Loading (stuck!)",
-                        ServerState::LoadError(_) => "LoadError (wrong variant)",
+                        ServerState::LoadError { .. } => "LoadError (wrong variant)",
                         ServerState::Connected(_) => "Connected",
                     }
                 ),

crates/gpui/examples/active_state_bug.rs πŸ”—

@@ -0,0 +1,47 @@
+/// Click the button β€” the `.active()` background gets stuck on every other click.
+use gpui::*;
+use gpui_platform::application;
+
+struct Example;
+
+impl Render for Example {
+    fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
+        // Colors from Zed's default dark theme
+        let bg = hsla(215. / 360., 0.12, 0.15, 1.);
+        let text = hsla(221. / 360., 0.11, 0.86, 1.);
+        let hover = hsla(225. / 360., 0.118, 0.267, 1.);
+        let active = hsla(220. / 360., 0.118, 0.20, 1.);
+
+        div().bg(bg).size_full().p_1().child(
+            div()
+                .id("button")
+                .px_2()
+                .py_0p5()
+                .rounded_md()
+                .text_sm()
+                .text_color(text)
+                .hover(|s| s.bg(hover))
+                .active(|s| s.bg(active))
+                .on_click(|_, _, _| {})
+                .child("Click me"),
+        )
+    }
+}
+
+fn main() {
+    application().run(|cx: &mut App| {
+        cx.open_window(
+            WindowOptions {
+                window_bounds: Some(WindowBounds::Windowed(Bounds::centered(
+                    None,
+                    size(px(200.), px(60.)),
+                    cx,
+                ))),
+                ..Default::default()
+            },
+            |_, cx| cx.new(|_| Example),
+        )
+        .unwrap();
+        cx.activate(true);
+    });
+}

crates/gpui/src/elements/div.rs πŸ”—

@@ -2517,18 +2517,24 @@ impl Interactivity {
                 );
             }
 
+            // We unconditionally bind both the mouse up and mouse down active state handlers
+            // Because we might not get a chance to render a frame before the mouse up event arrives.
             let active_state = element_state
                 .clicked_state
                 .get_or_insert_with(Default::default)
                 .clone();
-            if active_state.borrow().is_clicked() {
+
+            {
+                let active_state = active_state.clone();
                 window.on_mouse_event(move |_: &MouseUpEvent, phase, window, _cx| {
-                    if phase == DispatchPhase::Capture {
+                    if phase == DispatchPhase::Capture && active_state.borrow().is_clicked() {
                         *active_state.borrow_mut() = ElementClickedState::default();
                         window.refresh();
                     }
                 });
-            } else {
+            }
+
+            {
                 let active_group_hitbox = self
                     .group_active_style
                     .as_ref()

crates/sidebar/Cargo.toml πŸ”—

@@ -21,6 +21,7 @@ agent-client-protocol.workspace = true
 agent_ui.workspace = true
 chrono.workspace = true
 editor.workspace = true
+feature_flags.workspace = true
 fs.workspace = true
 gpui.workspace = true
 menu.workspace = true

crates/sidebar/src/sidebar.rs πŸ”—

@@ -4,10 +4,11 @@ use agent_client_protocol as acp;
 use agent_ui::{AgentPanel, AgentPanelEvent, NewThread};
 use chrono::Utc;
 use editor::{Editor, EditorElement, EditorStyle};
+use feature_flags::{AgentV2FeatureFlag, FeatureFlagViewExt as _};
 use gpui::{
     AnyElement, App, Context, Entity, EventEmitter, FocusHandle, Focusable, FontStyle, ListState,
-    Pixels, Render, SharedString, Subscription, TextStyle, WeakEntity, Window, actions, list,
-    prelude::*, px, relative, rems,
+    Pixels, Render, SharedString, TextStyle, WeakEntity, Window, actions, list, prelude::*, px,
+    relative, rems,
 };
 use menu::{Cancel, Confirm, SelectFirst, SelectLast, SelectNext, SelectPrevious};
 use project::Event as ProjectEvent;
@@ -22,8 +23,8 @@ use ui::{
 };
 use util::path_list::PathList;
 use workspace::{
-    FocusWorkspaceSidebar, MultiWorkspace, Sidebar as WorkspaceSidebar, SidebarEvent,
-    ToggleWorkspaceSidebar, Workspace,
+    FocusWorkspaceSidebar, MultiWorkspace, MultiWorkspaceEvent, Sidebar as WorkspaceSidebar,
+    SidebarEvent, ToggleWorkspaceSidebar, Workspace,
 };
 use zed_actions::editor::{MoveDown, MoveUp};
 
@@ -70,7 +71,7 @@ enum ListEntry {
     ProjectHeader {
         path_list: PathList,
         label: SharedString,
-        workspace_index: usize,
+        workspace: Entity<Workspace>,
         highlight_positions: Vec<usize>,
     },
     Thread {
@@ -79,7 +80,7 @@ enum ListEntry {
         icon_from_external_svg: Option<SharedString>,
         status: AgentThreadStatus,
         diff_stats: Option<(usize, usize)>,
-        workspace_index: usize,
+        workspace: Entity<Workspace>,
         is_live: bool,
         is_background: bool,
         highlight_positions: Vec<usize>,
@@ -90,6 +91,7 @@ enum ListEntry {
     },
     NewThread {
         path_list: PathList,
+        workspace: Entity<Workspace>,
     },
 }
 
@@ -157,20 +159,6 @@ fn workspace_path_list_and_label(
     (PathList::new(&paths), label)
 }
 
-fn workspace_index_for_path_list(
-    workspaces: &[Entity<Workspace>],
-    path_list: &PathList,
-    cx: &App,
-) -> Option<usize> {
-    workspaces
-        .iter()
-        .enumerate()
-        .find_map(|(index, workspace)| {
-            let (candidate, _) = workspace_path_list_and_label(workspace, cx);
-            (candidate == *path_list).then_some(index)
-        })
-}
-
 pub struct Sidebar {
     multi_workspace: WeakEntity<MultiWorkspace>,
     width: Pixels,
@@ -178,13 +166,14 @@ pub struct Sidebar {
     filter_editor: Entity<Editor>,
     list_state: ListState,
     contents: SidebarContents,
+    /// The index of the list item that currently has the keyboard focus
+    ///
+    /// Note: This is NOT the same as the active item.
     selection: Option<usize>,
+    focused_thread: Option<acp::SessionId>,
+    active_entry_index: Option<usize>,
     collapsed_groups: HashSet<PathList>,
     expanded_groups: HashSet<PathList>,
-    _subscriptions: Vec<Subscription>,
-    _project_subscriptions: Vec<Subscription>,
-    _agent_panel_subscriptions: Vec<Subscription>,
-    _thread_store_subscription: Option<Subscription>,
 }
 
 impl EventEmitter<SidebarEvent> for Sidebar {}
@@ -205,22 +194,32 @@ impl Sidebar {
             editor
         });
 
-        let observe_subscription = cx.observe_in(
+        cx.subscribe_in(
             &multi_workspace,
             window,
-            |this, _multi_workspace, window, cx| {
-                this.update_entries(window, cx);
+            |this, _multi_workspace, event: &MultiWorkspaceEvent, window, cx| match event {
+                MultiWorkspaceEvent::ActiveWorkspaceChanged => {
+                    this.focused_thread = None;
+                    this.update_entries(cx);
+                }
+                MultiWorkspaceEvent::WorkspaceAdded(workspace) => {
+                    this.subscribe_to_workspace(workspace, window, cx);
+                    this.update_entries(cx);
+                }
+                MultiWorkspaceEvent::WorkspaceRemoved(_) => {
+                    this.update_entries(cx);
+                }
             },
-        );
+        )
+        .detach();
 
-        let filter_subscription = cx.subscribe(&filter_editor, |this: &mut Self, _, event, cx| {
+        cx.subscribe(&filter_editor, |this: &mut Self, _, event, cx| {
             if let editor::EditorEvent::BufferEdited = event {
                 let query = this.filter_editor.read(cx).text(cx);
                 if !query.is_empty() {
                     this.selection.take();
                 }
-                this.rebuild_contents(cx);
-                this.list_state.reset(this.contents.entries.len());
+                this.update_entries(cx);
                 if !query.is_empty() {
                     this.selection = this
                         .contents
@@ -235,11 +234,30 @@ impl Sidebar {
                             }
                         });
                 }
-                cx.notify();
             }
+        })
+        .detach();
+
+        let thread_store = ThreadStore::global(cx);
+        cx.observe_in(&thread_store, window, |this, _, _window, cx| {
+            this.update_entries(cx);
+        })
+        .detach();
+
+        cx.observe_flag::<AgentV2FeatureFlag, _>(window, |_is_enabled, this, _window, cx| {
+            this.update_entries(cx);
+        })
+        .detach();
+
+        let workspaces = multi_workspace.read(cx).workspaces().to_vec();
+        cx.defer_in(window, move |this, window, cx| {
+            for workspace in &workspaces {
+                this.subscribe_to_workspace(workspace, window, cx);
+            }
+            this.update_entries(cx);
         });
 
-        let mut this = Self {
+        Self {
             multi_workspace: multi_workspace.downgrade(),
             width: DEFAULT_WIDTH,
             focus_handle,
@@ -247,91 +265,86 @@ impl Sidebar {
             list_state: ListState::new(0, gpui::ListAlignment::Top, px(1000.)),
             contents: SidebarContents::default(),
             selection: None,
+            focused_thread: None,
+            active_entry_index: None,
             collapsed_groups: HashSet::new(),
             expanded_groups: HashSet::new(),
-            _subscriptions: vec![observe_subscription, filter_subscription],
-            _project_subscriptions: Vec::new(),
-            _agent_panel_subscriptions: Vec::new(),
-            _thread_store_subscription: None,
-        };
-        this.update_entries(window, cx);
-        this
+        }
     }
 
-    fn subscribe_to_projects(
-        &mut self,
+    fn subscribe_to_workspace(
+        &self,
+        workspace: &Entity<Workspace>,
         window: &mut Window,
         cx: &mut Context<Self>,
-    ) -> Vec<Subscription> {
-        let Some(multi_workspace) = self.multi_workspace.upgrade() else {
-            return Vec::new();
-        };
-        let projects: Vec<_> = multi_workspace
-            .read(cx)
-            .workspaces()
-            .iter()
-            .map(|w| w.read(cx).project().clone())
-            .collect();
+    ) {
+        let project = workspace.read(cx).project().clone();
+        cx.subscribe_in(
+            &project,
+            window,
+            |this, _project, event, _window, cx| match event {
+                ProjectEvent::WorktreeAdded(_)
+                | ProjectEvent::WorktreeRemoved(_)
+                | ProjectEvent::WorktreeOrderChanged => {
+                    this.update_entries(cx);
+                }
+                _ => {}
+            },
+        )
+        .detach();
 
-        projects
-            .iter()
-            .map(|project| {
-                cx.subscribe_in(
-                    project,
-                    window,
-                    |this, _project, event, window, cx| match event {
-                        ProjectEvent::WorktreeAdded(_)
-                        | ProjectEvent::WorktreeRemoved(_)
-                        | ProjectEvent::WorktreeOrderChanged => {
-                            this.update_entries(window, cx);
-                        }
-                        _ => {}
-                    },
-                )
-            })
-            .collect()
+        cx.subscribe_in(
+            workspace,
+            window,
+            |this, _workspace, event: &workspace::Event, window, cx| {
+                if let workspace::Event::PanelAdded(view) = event {
+                    if let Ok(agent_panel) = view.clone().downcast::<AgentPanel>() {
+                        this.subscribe_to_agent_panel(&agent_panel, window, cx);
+                    }
+                }
+            },
+        )
+        .detach();
+
+        if let Some(agent_panel) = workspace.read(cx).panel::<AgentPanel>(cx) {
+            self.subscribe_to_agent_panel(&agent_panel, window, cx);
+        }
     }
 
-    fn subscribe_to_agent_panels(
-        &mut self,
+    fn subscribe_to_agent_panel(
+        &self,
+        agent_panel: &Entity<AgentPanel>,
         window: &mut Window,
         cx: &mut Context<Self>,
-    ) -> Vec<Subscription> {
-        let Some(multi_workspace) = self.multi_workspace.upgrade() else {
-            return Vec::new();
-        };
-        let workspaces: Vec<_> = multi_workspace.read(cx).workspaces().to_vec();
-
-        workspaces
-            .iter()
-            .map(|workspace| {
-                if let Some(agent_panel) = workspace.read(cx).panel::<AgentPanel>(cx) {
-                    cx.subscribe_in(
-                        &agent_panel,
-                        window,
-                        |this, _, _event: &AgentPanelEvent, window, cx| {
-                            this.update_entries(window, cx);
-                        },
-                    )
-                } else {
-                    cx.observe_in(workspace, window, |this, _, window, cx| {
-                        this.update_entries(window, cx);
-                    })
+    ) {
+        cx.subscribe_in(
+            agent_panel,
+            window,
+            |this, agent_panel, event: &AgentPanelEvent, _window, cx| match event {
+                AgentPanelEvent::ActiveViewChanged => {
+                    if let Some(thread) = agent_panel.read(cx).active_connection_view()
+                        && let Some(session_id) = thread.read(cx).parent_id(cx)
+                    {
+                        this.focused_thread = Some(session_id);
+                    }
+                    this.update_entries(cx);
                 }
-            })
-            .collect()
-    }
-
-    fn subscribe_to_thread_store(&mut self, window: &mut Window, cx: &mut Context<Self>) {
-        if self._thread_store_subscription.is_some() {
-            return;
-        }
-        if let Some(thread_store) = ThreadStore::try_global(cx) {
-            self._thread_store_subscription =
-                Some(cx.observe_in(&thread_store, window, |this, _, window, cx| {
-                    this.update_entries(window, cx);
-                }));
-        }
+                AgentPanelEvent::ThreadFocused => {
+                    let new_focused = agent_panel
+                        .read(cx)
+                        .active_connection_view()
+                        .and_then(|thread| thread.read(cx).parent_id(cx));
+                    if new_focused != this.focused_thread {
+                        this.focused_thread = new_focused;
+                        this.update_entries(cx);
+                    }
+                }
+                AgentPanelEvent::BackgroundThreadChanged => {
+                    this.update_entries(cx);
+                }
+            },
+        )
+        .detach();
     }
 
     fn all_thread_infos_for_workspace(
@@ -386,13 +399,6 @@ impl Sidebar {
         let mw = multi_workspace.read(cx);
         let workspaces = mw.workspaces().to_vec();
         let active_workspace = mw.workspaces().get(mw.active_workspace_index()).cloned();
-        let active_workspace_index = active_workspace
-            .and_then(|active| {
-                workspaces
-                    .iter()
-                    .position(|w| w.entity_id() == active.entity_id())
-            })
-            .unwrap_or(0);
 
         let thread_store = ThreadStore::try_global(cx);
         let query = self.filter_editor.read(cx).text(cx);
@@ -416,7 +422,7 @@ impl Sidebar {
         let mut entries = Vec::new();
         let mut notified_threads = previous.notified_threads;
 
-        for (index, workspace) in workspaces.iter().enumerate() {
+        for workspace in workspaces.iter() {
             let (path_list, label) = workspace_path_list_and_label(workspace, cx);
 
             let is_collapsed = self.collapsed_groups.contains(&path_list);
@@ -433,7 +439,7 @@ impl Sidebar {
                             icon_from_external_svg: None,
                             status: AgentThreadStatus::default(),
                             diff_stats: None,
-                            workspace_index: index,
+                            workspace: workspace.clone(),
                             is_live: false,
                             is_background: false,
                             highlight_positions: Vec::new(),
@@ -455,7 +461,7 @@ impl Sidebar {
                         status,
                         icon,
                         icon_from_external_svg,
-                        workspace_index: _,
+                        workspace: _,
                         is_live,
                         is_background,
                         ..
@@ -473,7 +479,7 @@ impl Sidebar {
                 // Update notification state for live threads.
                 for thread in &threads {
                     if let ListEntry::Thread {
-                        workspace_index,
+                        workspace: thread_workspace,
                         session_info,
                         status,
                         is_background,
@@ -484,13 +490,19 @@ impl Sidebar {
                         if *is_background && *status == AgentThreadStatus::Completed {
                             notified_threads.insert(session_id.clone());
                         } else if *status == AgentThreadStatus::Completed
-                            && *workspace_index != active_workspace_index
+                            && active_workspace
+                                .as_ref()
+                                .is_none_or(|active| active != thread_workspace)
                             && old_statuses.get(session_id) == Some(&AgentThreadStatus::Running)
                         {
                             notified_threads.insert(session_id.clone());
                         }
 
-                        if *workspace_index == active_workspace_index && !*is_background {
+                        if active_workspace
+                            .as_ref()
+                            .is_some_and(|active| active == thread_workspace)
+                            && !*is_background
+                        {
                             notified_threads.remove(session_id);
                         }
                     }
@@ -540,7 +552,7 @@ impl Sidebar {
                 entries.push(ListEntry::ProjectHeader {
                     path_list: path_list.clone(),
                     label,
-                    workspace_index: index,
+                    workspace: workspace.clone(),
                     highlight_positions: workspace_highlight_positions,
                 });
                 entries.extend(matched_threads);
@@ -548,7 +560,7 @@ impl Sidebar {
                 entries.push(ListEntry::ProjectHeader {
                     path_list: path_list.clone(),
                     label,
-                    workspace_index: index,
+                    workspace: workspace.clone(),
                     highlight_positions: Vec::new(),
                 });
 
@@ -578,6 +590,7 @@ impl Sidebar {
                 if total == 0 {
                     entries.push(ListEntry::NewThread {
                         path_list: path_list.clone(),
+                        workspace: workspace.clone(),
                     });
                 }
             }
@@ -599,40 +612,46 @@ impl Sidebar {
         };
     }
 
-    fn update_entries(&mut self, window: &mut Window, cx: &mut Context<Self>) {
-        let multi_workspace = self.multi_workspace.clone();
-        cx.defer_in(window, move |this, window, cx| {
-            let Some(multi_workspace) = multi_workspace.upgrade() else {
-                return;
-            };
-            if !multi_workspace.read(cx).multi_workspace_enabled(cx) {
-                return;
-            }
-
-            this._project_subscriptions = this.subscribe_to_projects(window, cx);
-            this._agent_panel_subscriptions = this.subscribe_to_agent_panels(window, cx);
-            this.subscribe_to_thread_store(window, cx);
+    fn update_entries(&mut self, cx: &mut Context<Self>) {
+        let Some(multi_workspace) = self.multi_workspace.upgrade() else {
+            return;
+        };
+        if !multi_workspace.read(cx).multi_workspace_enabled(cx) {
+            return;
+        }
 
-            let had_notifications = this.has_notifications(cx);
+        let had_notifications = self.has_notifications(cx);
 
-            this.rebuild_contents(cx);
+        self.rebuild_contents(cx);
+        self.recompute_active_entry_index(cx);
 
-            this.list_state.reset(this.contents.entries.len());
+        self.list_state.reset(self.contents.entries.len());
 
-            if let Some(selection) = this.selection {
-                if selection >= this.contents.entries.len() {
-                    this.selection = this.contents.entries.len().checked_sub(1);
-                }
-            }
+        if had_notifications != self.has_notifications(cx) {
+            multi_workspace.update(cx, |_, cx| {
+                cx.notify();
+            });
+        }
 
-            if had_notifications != this.has_notifications(cx) {
-                multi_workspace.update(cx, |_, cx| {
-                    cx.notify();
-                });
-            }
+        cx.notify();
+    }
 
-            cx.notify();
-        });
+    fn recompute_active_entry_index(&mut self, cx: &App) {
+        self.active_entry_index = if let Some(session_id) = &self.focused_thread {
+            self.contents.entries.iter().position(|entry| {
+                matches!(entry, ListEntry::Thread { session_info, .. } if &session_info.session_id == session_id)
+            })
+        } else {
+            let active_workspace = self
+                .multi_workspace
+                .upgrade()
+                .map(|mw| mw.read(cx).workspace().clone());
+            active_workspace.and_then(|active| {
+                self.contents.entries.iter().position(|entry| {
+                    matches!(entry, ListEntry::ProjectHeader { workspace, .. } if workspace == &active)
+                })
+            })
+        };
     }
 
     fn render_list_entry(
@@ -646,6 +665,7 @@ impl Sidebar {
         };
         let is_focused = self.focus_handle.is_focused(window)
             || self.filter_editor.focus_handle(cx).is_focused(window);
+        // is_selected means the keyboard selector is here.
         let is_selected = is_focused && self.selection == Some(ix);
 
         let is_group_header_after_first =
@@ -655,23 +675,17 @@ impl Sidebar {
             ListEntry::ProjectHeader {
                 path_list,
                 label,
-                workspace_index,
-                highlight_positions,
-            } => self.render_project_header(
-                ix,
-                path_list,
-                label,
-                *workspace_index,
+                workspace,
                 highlight_positions,
-                is_selected,
-                cx,
-            ),
+            } => {
+                self.render_project_header(ix, path_list, label, workspace, highlight_positions, cx)
+            }
             ListEntry::Thread {
                 session_info,
                 icon,
                 icon_from_external_svg,
                 status,
-                workspace_index,
+                workspace,
                 highlight_positions,
                 ..
             } => self.render_thread(
@@ -680,7 +694,7 @@ impl Sidebar {
                 *icon,
                 icon_from_external_svg.clone(),
                 *status,
-                *workspace_index,
+                workspace,
                 highlight_positions,
                 is_selected,
                 cx,
@@ -689,11 +703,14 @@ impl Sidebar {
                 path_list,
                 remaining_count,
             } => self.render_view_more(ix, path_list, *remaining_count, is_selected, cx),
-            ListEntry::NewThread { path_list } => {
-                self.render_new_thread(ix, path_list, is_selected, cx)
-            }
+            ListEntry::NewThread {
+                path_list,
+                workspace,
+            } => self.render_new_thread(ix, path_list, workspace, is_selected, cx),
         };
 
+        // add the blue border here, not in the sub methods
+
         if is_group_header_after_first {
             v_flex()
                 .w_full()
@@ -711,9 +728,8 @@ impl Sidebar {
         ix: usize,
         path_list: &PathList,
         label: &SharedString,
-        workspace_index: usize,
+        workspace: &Entity<Workspace>,
         highlight_positions: &[usize],
-        is_selected: bool,
         cx: &mut Context<Self>,
     ) -> AnyElement {
         let id = SharedString::from(format!("project-header-{}", ix));
@@ -726,17 +742,24 @@ impl Sidebar {
         } else {
             IconName::ChevronDown
         };
-        let path_list_for_new_thread = path_list.clone();
-        let path_list_for_remove = path_list.clone();
+        let workspace_for_new_thread = workspace.clone();
+        let workspace_for_remove = workspace.clone();
+        let workspace_for_activate = workspace.clone();
         let path_list_for_toggle = path_list.clone();
-        let workspace_count = self
-            .multi_workspace
-            .upgrade()
+        let multi_workspace = self.multi_workspace.upgrade();
+        let workspace_count = multi_workspace
+            .as_ref()
             .map_or(0, |mw| mw.read(cx).workspaces().len());
+        let is_active_workspace = self.focused_thread.is_none()
+            && multi_workspace
+                .as_ref()
+                .is_some_and(|mw| mw.read(cx).workspace() == workspace);
+
+        // TODO: if is_selected, draw a blue border around the item.
 
         ListItem::new(id)
             .group_name(&group)
-            .toggle_state(is_selected)
+            .toggle_state(is_active_workspace)
             .child(
                 h_flex()
                     .px_1()
@@ -783,7 +806,7 @@ impl Sidebar {
                             .tooltip(Tooltip::text("New Thread"))
                             .on_click(cx.listener(move |this, _, window, cx| {
                                 this.selection = None;
-                                this.create_new_thread(&path_list_for_new_thread, window, cx);
+                                this.create_new_thread(&workspace_for_new_thread, window, cx);
                             })),
                     )
                     .when(workspace_count > 1, |this| {
@@ -797,7 +820,7 @@ impl Sidebar {
                             .tooltip(Tooltip::text("Remove Project"))
                             .on_click(cx.listener(
                                 move |this, _, window, cx| {
-                                    this.remove_workspace(&path_list_for_remove, window, cx);
+                                    this.remove_workspace(&workspace_for_remove, window, cx);
                                 },
                             )),
                         )
@@ -805,14 +828,14 @@ impl Sidebar {
             )
             .on_click(cx.listener(move |this, _, window, cx| {
                 this.selection = None;
-                this.activate_workspace(workspace_index, window, cx);
+                this.activate_workspace(&workspace_for_activate, window, cx);
             }))
             .into_any_element()
     }
 
     fn activate_workspace(
         &mut self,
-        workspace_index: usize,
+        workspace: &Entity<Workspace>,
         window: &mut Window,
         cx: &mut Context<Self>,
     ) {
@@ -820,36 +843,43 @@ impl Sidebar {
             return;
         };
 
+        self.focused_thread = None;
+
+        multi_workspace.update(cx, |multi_workspace, cx| {
+            multi_workspace.activate(workspace.clone(), cx);
+        });
+
         multi_workspace.update(cx, |multi_workspace, cx| {
-            multi_workspace.activate_index(workspace_index, window, cx);
+            multi_workspace.focus_active_workspace(window, cx);
         });
     }
 
     fn remove_workspace(
         &mut self,
-        path_list: &PathList,
+        workspace: &Entity<Workspace>,
         window: &mut Window,
         cx: &mut Context<Self>,
     ) {
         let Some(multi_workspace) = self.multi_workspace.upgrade() else {
             return;
         };
-        let workspaces = multi_workspace.read(cx).workspaces().to_vec();
-
-        let Some(workspace_index) = workspace_index_for_path_list(&workspaces, path_list, cx)
-        else {
-            return;
-        };
 
         multi_workspace.update(cx, |multi_workspace, cx| {
-            multi_workspace.remove_workspace(workspace_index, window, cx);
+            let Some(index) = multi_workspace
+                .workspaces()
+                .iter()
+                .position(|w| w == workspace)
+            else {
+                return;
+            };
+            multi_workspace.remove_workspace(index, window, cx);
         });
     }
 
     fn toggle_collapse(
         &mut self,
         path_list: &PathList,
-        window: &mut Window,
+        _window: &mut Window,
         cx: &mut Context<Self>,
     ) {
         if self.collapsed_groups.contains(path_list) {
@@ -857,7 +887,7 @@ impl Sidebar {
         } else {
             self.collapsed_groups.insert(path_list.clone());
         }
-        self.update_entries(window, cx);
+        self.update_entries(cx);
     }
 
     fn focus_in(&mut self, _window: &mut Window, cx: &mut Context<Self>) {
@@ -869,7 +899,7 @@ impl Sidebar {
 
     fn cancel(&mut self, _: &Cancel, window: &mut Window, cx: &mut Context<Self>) {
         if self.reset_filter_editor_text(window, cx) {
-            self.update_entries(window, cx);
+            self.update_entries(cx);
         } else {
             self.focus_handle.focus(window, cx);
         }
@@ -948,29 +978,27 @@ impl Sidebar {
         };
 
         match entry {
-            ListEntry::ProjectHeader {
-                workspace_index, ..
-            } => {
-                let workspace_index = *workspace_index;
-                self.activate_workspace(workspace_index, window, cx);
+            ListEntry::ProjectHeader { workspace, .. } => {
+                let workspace = workspace.clone();
+                self.activate_workspace(&workspace, window, cx);
             }
             ListEntry::Thread {
                 session_info,
-                workspace_index,
+                workspace,
                 ..
             } => {
                 let session_info = session_info.clone();
-                let workspace_index = *workspace_index;
-                self.activate_thread(session_info, workspace_index, window, cx);
+                let workspace = workspace.clone();
+                self.activate_thread(session_info, &workspace, window, cx);
             }
             ListEntry::ViewMore { path_list, .. } => {
                 let path_list = path_list.clone();
                 self.expanded_groups.insert(path_list);
-                self.update_entries(window, cx);
+                self.update_entries(cx);
             }
-            ListEntry::NewThread { path_list } => {
-                let path_list = path_list.clone();
-                self.create_new_thread(&path_list, window, cx);
+            ListEntry::NewThread { workspace, .. } => {
+                let workspace = workspace.clone();
+                self.create_new_thread(&workspace, window, cx);
             }
         }
     }
@@ -978,7 +1006,7 @@ impl Sidebar {
     fn activate_thread(
         &mut self,
         session_info: acp_thread::AgentSessionInfo,
-        workspace_index: usize,
+        workspace: &Entity<Workspace>,
         window: &mut Window,
         cx: &mut Context<Self>,
     ) {
@@ -987,22 +1015,24 @@ impl Sidebar {
         };
 
         multi_workspace.update(cx, |multi_workspace, cx| {
-            multi_workspace.activate_index(workspace_index, window, cx);
+            multi_workspace.activate(workspace.clone(), cx);
         });
-        let workspaces = multi_workspace.read(cx).workspaces().to_vec();
-        if let Some(workspace) = workspaces.get(workspace_index) {
-            if let Some(agent_panel) = workspace.read(cx).panel::<AgentPanel>(cx) {
-                agent_panel.update(cx, |panel, cx| {
-                    panel.load_agent_thread(session_info, window, cx);
-                });
-            }
+
+        workspace.update(cx, |workspace, cx| {
+            workspace.open_panel::<AgentPanel>(window, cx);
+        });
+
+        if let Some(agent_panel) = workspace.read(cx).panel::<AgentPanel>(cx) {
+            agent_panel.update(cx, |panel, cx| {
+                panel.load_agent_thread(session_info, window, cx);
+            });
         }
     }
 
     fn expand_selected_entry(
         &mut self,
         _: &ExpandSelectedEntry,
-        window: &mut Window,
+        _window: &mut Window,
         cx: &mut Context<Self>,
     ) {
         let Some(ix) = self.selection else { return };
@@ -1012,7 +1042,7 @@ impl Sidebar {
                 if self.collapsed_groups.contains(path_list) {
                     let path_list = path_list.clone();
                     self.collapsed_groups.remove(&path_list);
-                    self.update_entries(window, cx);
+                    self.update_entries(cx);
                 } else if ix + 1 < self.contents.entries.len() {
                     self.selection = Some(ix + 1);
                     self.list_state.scroll_to_reveal_item(ix + 1);
@@ -1026,7 +1056,7 @@ impl Sidebar {
     fn collapse_selected_entry(
         &mut self,
         _: &CollapseSelectedEntry,
-        window: &mut Window,
+        _window: &mut Window,
         cx: &mut Context<Self>,
     ) {
         let Some(ix) = self.selection else { return };
@@ -1036,7 +1066,7 @@ impl Sidebar {
                 if !self.collapsed_groups.contains(path_list) {
                     let path_list = path_list.clone();
                     self.collapsed_groups.insert(path_list);
-                    self.update_entries(window, cx);
+                    self.update_entries(cx);
                 }
             }
             Some(
@@ -1049,7 +1079,7 @@ impl Sidebar {
                         let path_list = path_list.clone();
                         self.selection = Some(i);
                         self.collapsed_groups.insert(path_list);
-                        self.update_entries(window, cx);
+                        self.update_entries(cx);
                         break;
                     }
                 }
@@ -1065,9 +1095,9 @@ impl Sidebar {
         icon: IconName,
         icon_from_external_svg: Option<SharedString>,
         status: AgentThreadStatus,
-        workspace_index: usize,
+        workspace: &Entity<Workspace>,
         highlight_positions: &[usize],
-        is_selected: bool,
+        _is_selected: bool,
         cx: &mut Context<Self>,
     ) -> AnyElement {
         let has_notification = self.contents.is_thread_notified(&session_info.session_id);
@@ -1077,6 +1107,7 @@ impl Sidebar {
             .clone()
             .unwrap_or_else(|| "Untitled".into());
         let session_info = session_info.clone();
+        let workspace = workspace.clone();
 
         let id = SharedString::from(format!("thread-entry-{}", ix));
         ThreadItem::new(id, title)
@@ -1087,10 +1118,10 @@ impl Sidebar {
             .highlight_positions(highlight_positions.to_vec())
             .status(status)
             .notified(has_notification)
-            .selected(is_selected)
+            .selected(self.focused_thread.as_ref() == Some(&session_info.session_id))
             .on_click(cx.listener(move |this, _, window, cx| {
                 this.selection = None;
-                this.activate_thread(session_info.clone(), workspace_index, window, cx);
+                this.activate_thread(session_info.clone(), &workspace, window, cx);
             }))
             .into_any_element()
     }
@@ -1147,55 +1178,47 @@ impl Sidebar {
                     .child(Label::new("View More"))
                     .child(Label::new(count).color(Color::Muted).size(LabelSize::Small)),
             )
-            .on_click(cx.listener(move |this, _, window, cx| {
+            .on_click(cx.listener(move |this, _, _window, cx| {
                 this.selection = None;
                 this.expanded_groups.insert(path_list.clone());
-                this.update_entries(window, cx);
+                this.update_entries(cx);
             }))
             .into_any_element()
     }
 
     fn create_new_thread(
         &mut self,
-        path_list: &PathList,
+        workspace: &Entity<Workspace>,
         window: &mut Window,
         cx: &mut Context<Self>,
     ) {
         let Some(multi_workspace) = self.multi_workspace.upgrade() else {
             return;
         };
-        let workspaces = multi_workspace.read(cx).workspaces().to_vec();
-
-        let workspace_index = workspace_index_for_path_list(&workspaces, path_list, cx);
-
-        let Some(workspace_index) = workspace_index else {
-            return;
-        };
 
         multi_workspace.update(cx, |multi_workspace, cx| {
-            multi_workspace.activate_index(workspace_index, window, cx);
+            multi_workspace.activate(workspace.clone(), cx);
         });
 
-        if let Some(workspace) = workspaces.get(workspace_index) {
-            workspace.update(cx, |workspace, cx| {
-                if let Some(agent_panel) = workspace.panel::<AgentPanel>(cx) {
-                    agent_panel.update(cx, |panel, cx| {
-                        panel.new_thread(&NewThread, window, cx);
-                    });
-                }
-                workspace.focus_panel::<AgentPanel>(window, cx);
-            });
-        }
+        workspace.update(cx, |workspace, cx| {
+            if let Some(agent_panel) = workspace.panel::<AgentPanel>(cx) {
+                agent_panel.update(cx, |panel, cx| {
+                    panel.new_thread(&NewThread, window, cx);
+                });
+            }
+            workspace.focus_panel::<AgentPanel>(window, cx);
+        });
     }
 
     fn render_new_thread(
         &self,
         ix: usize,
-        path_list: &PathList,
+        _path_list: &PathList,
+        workspace: &Entity<Workspace>,
         is_selected: bool,
         cx: &mut Context<Self>,
     ) -> AnyElement {
-        let path_list = path_list.clone();
+        let workspace = workspace.clone();
 
         div()
             .w_full()
@@ -1214,7 +1237,7 @@ impl Sidebar {
                 .toggle_state(is_selected)
                 .on_click(cx.listener(move |this, _, window, cx| {
                     this.selection = None;
-                    this.create_new_thread(&path_list, window, cx);
+                    this.create_new_thread(&workspace, window, cx);
                 })),
             )
             .into_any_element()
@@ -1390,7 +1413,7 @@ impl Render for Sidebar {
                                 .tooltip(Tooltip::text("Clear Search"))
                                 .on_click(cx.listener(|this, _, window, cx| {
                                     this.reset_filter_editor_text(window, cx);
-                                    this.update_entries(window, cx);
+                                    this.update_entries(cx);
                                 })),
                         )
                     }),
@@ -1475,10 +1498,9 @@ mod tests {
         multi_workspace: &Entity<MultiWorkspace>,
         cx: &mut gpui::VisualTestContext,
     ) -> Entity<Sidebar> {
-        let sidebar = multi_workspace.update_in(cx, |_mw, window, cx| {
-            let mw_handle = cx.entity();
-            cx.new(|cx| Sidebar::new(mw_handle, window, cx))
-        });
+        let multi_workspace = multi_workspace.clone();
+        let sidebar =
+            cx.update(|window, cx| cx.new(|cx| Sidebar::new(multi_workspace.clone(), window, cx)));
         multi_workspace.update_in(cx, |mw, window, cx| {
             mw.register_sidebar(sidebar.clone(), window, cx);
         });
@@ -1819,6 +1841,7 @@ mod tests {
             cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx));
         let sidebar = setup_sidebar(&multi_workspace, cx);
 
+        let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
         let expanded_path = PathList::new(&[std::path::PathBuf::from("/expanded")]);
         let collapsed_path = PathList::new(&[std::path::PathBuf::from("/collapsed")]);
 
@@ -1832,7 +1855,7 @@ mod tests {
                 ListEntry::ProjectHeader {
                     path_list: expanded_path.clone(),
                     label: "expanded-project".into(),
-                    workspace_index: 0,
+                    workspace: workspace.clone(),
                     highlight_positions: Vec::new(),
                 },
                 // Thread with default (Completed) status, not active
@@ -1848,7 +1871,7 @@ mod tests {
                     icon_from_external_svg: None,
                     status: AgentThreadStatus::Completed,
                     diff_stats: None,
-                    workspace_index: 0,
+                    workspace: workspace.clone(),
                     is_live: false,
                     is_background: false,
                     highlight_positions: Vec::new(),
@@ -1866,7 +1889,7 @@ mod tests {
                     icon_from_external_svg: None,
                     status: AgentThreadStatus::Running,
                     diff_stats: None,
-                    workspace_index: 0,
+                    workspace: workspace.clone(),
                     is_live: true,
                     is_background: false,
                     highlight_positions: Vec::new(),
@@ -1884,7 +1907,7 @@ mod tests {
                     icon_from_external_svg: None,
                     status: AgentThreadStatus::Error,
                     diff_stats: None,
-                    workspace_index: 1,
+                    workspace: workspace.clone(),
                     is_live: true,
                     is_background: false,
                     highlight_positions: Vec::new(),

crates/workspace/src/multi_workspace.rs πŸ”—

@@ -35,6 +35,12 @@ actions!(
     ]
 );
 
+pub enum MultiWorkspaceEvent {
+    ActiveWorkspaceChanged,
+    WorkspaceAdded(Entity<Workspace>),
+    WorkspaceRemoved(EntityId),
+}
+
 pub enum SidebarEvent {
     Open,
     Close,
@@ -109,6 +115,8 @@ pub struct MultiWorkspace {
     _subscriptions: Vec<Subscription>,
 }
 
+impl EventEmitter<MultiWorkspaceEvent> for MultiWorkspace {}
+
 impl MultiWorkspace {
     pub fn new(workspace: Entity<Workspace>, window: &mut Window, cx: &mut Context<Self>) -> Self {
         let release_subscription = cx.on_release(|this: &mut MultiWorkspace, _cx| {
@@ -304,6 +312,7 @@ impl MultiWorkspace {
         if !self.multi_workspace_enabled(cx) {
             self.workspaces[0] = workspace;
             self.active_workspace_index = 0;
+            cx.emit(MultiWorkspaceEvent::ActiveWorkspaceChanged);
             cx.notify();
             return;
         }
@@ -321,7 +330,11 @@ impl MultiWorkspace {
         cx: &mut Context<Self>,
     ) -> usize {
         let index = self.add_workspace(workspace, cx);
+        let changed = self.active_workspace_index != index;
         self.active_workspace_index = index;
+        if changed {
+            cx.emit(MultiWorkspaceEvent::ActiveWorkspaceChanged);
+        }
         cx.notify();
         index
     }
@@ -338,7 +351,8 @@ impl MultiWorkspace {
                 });
             }
             Self::subscribe_to_workspace(&workspace, cx);
-            self.workspaces.push(workspace);
+            self.workspaces.push(workspace.clone());
+            cx.emit(MultiWorkspaceEvent::WorkspaceAdded(workspace));
             cx.notify();
             self.workspaces.len() - 1
         }
@@ -349,9 +363,13 @@ impl MultiWorkspace {
             index < self.workspaces.len(),
             "workspace index out of bounds"
         );
+        let changed = self.active_workspace_index != index;
         self.active_workspace_index = index;
         self.serialize(cx);
         self.focus_active_workspace(window, cx);
+        if changed {
+            cx.emit(MultiWorkspaceEvent::ActiveWorkspaceChanged);
+        }
         cx.notify();
     }
 
@@ -406,7 +424,7 @@ impl MultiWorkspace {
         }
     }
 
-    fn focus_active_workspace(&self, window: &mut Window, cx: &mut App) {
+    pub fn focus_active_workspace(&self, window: &mut Window, cx: &mut App) {
         // If a dock panel is zoomed, focus it instead of the center pane.
         // Otherwise, focusing the center pane triggers dismiss_zoomed_items_to_reveal
         // which closes the zoomed dock.
@@ -633,6 +651,10 @@ impl MultiWorkspace {
 
         self.serialize(cx);
         self.focus_active_workspace(window, cx);
+        cx.emit(MultiWorkspaceEvent::WorkspaceRemoved(
+            removed_workspace.entity_id(),
+        ));
+        cx.emit(MultiWorkspaceEvent::ActiveWorkspaceChanged);
         cx.notify();
     }
 

crates/workspace/src/workspace.rs πŸ”—

@@ -27,9 +27,9 @@ mod workspace_settings;
 pub use crate::notifications::NotificationFrame;
 pub use dock::Panel;
 pub use multi_workspace::{
-    DraggedSidebar, FocusWorkspaceSidebar, MultiWorkspace, NewWorkspaceInWindow,
-    NextWorkspaceInWindow, PreviousWorkspaceInWindow, Sidebar, SidebarEvent, SidebarHandle,
-    ToggleWorkspaceSidebar,
+    DraggedSidebar, FocusWorkspaceSidebar, MultiWorkspace, MultiWorkspaceEvent,
+    NewWorkspaceInWindow, NextWorkspaceInWindow, PreviousWorkspaceInWindow, Sidebar, SidebarEvent,
+    SidebarHandle, ToggleWorkspaceSidebar,
 };
 pub use path_list::{PathList, SerializedPathList};
 pub use toast_layer::{ToastAction, ToastLayer, ToastView};
@@ -1230,6 +1230,7 @@ pub enum Event {
     ZoomChanged,
     ModalOpened,
     Activate,
+    PanelAdded(AnyView),
 }
 
 #[derive(Debug, Clone)]
@@ -2129,10 +2130,13 @@ impl Workspace {
 
         let dock_position = panel.position(window, cx);
         let dock = self.dock_at_position(dock_position);
+        let any_panel = panel.to_any();
 
         dock.update(cx, |dock, cx| {
             dock.add_panel(panel, self.weak_self.clone(), window, cx)
         });
+
+        cx.emit(Event::PanelAdded(any_panel));
     }
 
     pub fn remove_panel<T: Panel>(

crates/zed/src/zed.rs πŸ”—

@@ -371,15 +371,12 @@ pub fn initialize_workspace(
     })
     .detach();
 
-    cx.observe_new(|multi_workspace: &mut MultiWorkspace, window, cx| {
+    cx.observe_new(|_multi_workspace: &mut MultiWorkspace, window, cx| {
         let Some(window) = window else {
             return;
         };
-        let multi_workspace_handle = cx.entity();
-        let sidebar = cx.new(|cx| Sidebar::new(multi_workspace_handle.clone(), window, cx));
-        multi_workspace.register_sidebar(sidebar, window, cx);
 
-        let multi_workspace_handle = multi_workspace_handle.downgrade();
+        let multi_workspace_handle = cx.entity().downgrade();
         window.on_window_should_close(cx, move |window, cx| {
             multi_workspace_handle
                 .update(cx, |multi_workspace, cx| {
@@ -389,6 +386,20 @@ pub fn initialize_workspace(
                 })
                 .unwrap_or(true)
         });
+
+        let window_handle = window.window_handle();
+        let multi_workspace_handle = cx.entity();
+        cx.defer(move |cx| {
+            window_handle
+                .update(cx, |_, window, cx| {
+                    let sidebar =
+                        cx.new(|cx| Sidebar::new(multi_workspace_handle.clone(), window, cx));
+                    multi_workspace_handle.update(cx, |multi_workspace, cx| {
+                        multi_workspace.register_sidebar(sidebar, window, cx);
+                    });
+                })
+                .ok();
+        });
     })
     .detach();