Introduce following for assistant panel (#14479)

Antonio Scandurra , Max , Max Brunsfeld , and Nathan created

Release Notes:

- Added support for following into the assistant panel.

---------

Co-authored-by: Max <max@zed.dev>
Co-authored-by: Max Brunsfeld <maxbrunsfeld@gmail.com>
Co-authored-by: Nathan <nathan@zed.dev>

Change summary

crates/assistant/src/assistant_panel.rs    | 212 ++++++-
crates/collab/src/tests/following_tests.rs |   8 
crates/collab_ui/src/channel_view.rs       | 100 ++-
crates/editor/src/editor.rs                |   2 
crates/editor/src/editor_tests.rs          |   2 
crates/editor/src/items.rs                 | 111 +--
crates/gpui/src/gpui.rs                    |  12 
crates/proto/proto/zed.proto               |  15 
crates/title_bar/src/collab.rs             |   6 
crates/workspace/src/dock.rs               |  27 
crates/workspace/src/item.rs               |  45 
crates/workspace/src/pane_group.rs         |  31 
crates/workspace/src/workspace.rs          | 697 +++++++++++++----------
13 files changed, 785 insertions(+), 483 deletions(-)

Detailed changes

crates/assistant/src/assistant_panel.rs 🔗

@@ -18,6 +18,7 @@ use crate::{
 use anyhow::{anyhow, Result};
 use assistant_slash_command::{SlashCommand, SlashCommandOutputSection};
 use breadcrumbs::Breadcrumbs;
+use client::proto;
 use collections::{BTreeSet, HashMap, HashSet};
 use editor::{
     actions::{FoldAt, MoveToEndOfLine, Newline, ShowCompletions, UnfoldAt},
@@ -58,7 +59,7 @@ use ui::{
 use util::ResultExt;
 use workspace::{
     dock::{DockPosition, Panel, PanelEvent},
-    item::{BreadcrumbText, Item, ItemHandle},
+    item::{self, BreadcrumbText, FollowableItem, Item, ItemHandle},
     pane,
     searchable::{SearchEvent, SearchableItem},
     Pane, Save, ToggleZoom, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, Workspace,
@@ -66,6 +67,7 @@ use workspace::{
 use workspace::{searchable::SearchableItemHandle, NewFile};
 
 pub fn init(cx: &mut AppContext) {
+    workspace::FollowableViewRegistry::register::<ContextEditor>(cx);
     cx.observe_new_views(
         |workspace: &mut Workspace, _cx: &mut ViewContext<Workspace>| {
             workspace
@@ -374,7 +376,7 @@ impl AssistantPanel {
 
     fn handle_pane_event(
         &mut self,
-        _pane: View<Pane>,
+        pane: View<Pane>,
         event: &pane::Event,
         cx: &mut ViewContext<Self>,
     ) {
@@ -384,14 +386,25 @@ impl AssistantPanel {
             pane::Event::ZoomOut => cx.emit(PanelEvent::ZoomOut),
 
             pane::Event::AddItem { item } => {
-                if let Some(workspace) = self.workspace.upgrade() {
-                    workspace.update(cx, |workspace, cx| {
+                self.workspace
+                    .update(cx, |workspace, cx| {
                         item.added_to_pane(workspace, self.pane.clone(), cx)
-                    });
+                    })
+                    .ok();
+            }
+
+            pane::Event::ActivateItem { local } => {
+                if *local {
+                    self.workspace
+                        .update(cx, |workspace, cx| {
+                            workspace.unfollow_in_pane(&pane, cx);
+                        })
+                        .ok();
                 }
+                cx.emit(AssistantPanelEvent::ContextEdited);
             }
 
-            pane::Event::RemoveItem { .. } | pane::Event::ActivateItem { .. } => {
+            pane::Event::RemoveItem { .. } => {
                 cx.emit(AssistantPanelEvent::ContextEdited);
             }
 
@@ -613,12 +626,13 @@ impl AssistantPanel {
     fn handle_context_editor_event(
         &mut self,
         _: View<ContextEditor>,
-        event: &ContextEditorEvent,
+        event: &EditorEvent,
         cx: &mut ViewContext<Self>,
     ) {
         match event {
-            ContextEditorEvent::TabContentChanged => cx.notify(),
-            ContextEditorEvent::Edited => cx.emit(AssistantPanelEvent::ContextEdited),
+            EditorEvent::TitleChanged { .. } => cx.notify(),
+            EditorEvent::Edited { .. } => cx.emit(AssistantPanelEvent::ContextEdited),
+            _ => {}
         }
     }
 
@@ -722,14 +736,17 @@ impl AssistantPanel {
         &mut self,
         id: ContextId,
         cx: &mut ViewContext<Self>,
-    ) -> Task<Result<()>> {
+    ) -> Task<Result<View<ContextEditor>>> {
         let existing_context = self.pane.read(cx).items().find_map(|item| {
             item.downcast::<ContextEditor>()
                 .filter(|editor| *editor.read(cx).context.read(cx).id() == id)
         });
         if let Some(existing_context) = existing_context {
             return cx.spawn(|this, mut cx| async move {
-                this.update(&mut cx, |this, cx| this.show_context(existing_context, cx))
+                this.update(&mut cx, |this, cx| {
+                    this.show_context(existing_context.clone(), cx)
+                })?;
+                Ok(existing_context)
             });
         }
 
@@ -755,10 +772,9 @@ impl AssistantPanel {
                 let editor = cx.new_view(|cx| {
                     ContextEditor::for_context(context, fs, workspace, lsp_adapter_delegate, cx)
                 });
-                this.show_context(editor, cx);
-                anyhow::Ok(())
-            })??;
-            Ok(())
+                this.show_context(editor.clone(), cx);
+                anyhow::Ok(editor)
+            })?
         })
     }
 
@@ -878,6 +894,14 @@ impl Panel for AssistantPanel {
         }
     }
 
+    fn pane(&self) -> Option<View<Pane>> {
+        Some(self.pane.clone())
+    }
+
+    fn remote_id() -> Option<proto::PanelId> {
+        Some(proto::PanelId::AssistantPanel)
+    }
+
     fn icon(&self, cx: &WindowContext) -> Option<IconName> {
         let settings = AssistantSettings::get_global(cx);
         if !settings.enabled || !settings.button {
@@ -924,6 +948,7 @@ pub struct ContextEditor {
     editor: View<Editor>,
     blocks: HashSet<BlockId>,
     scroll_position: Option<ScrollPosition>,
+    remote_id: Option<workspace::ViewId>,
     pending_slash_command_creases: HashMap<Range<language::Anchor>, CreaseId>,
     pending_slash_command_blocks: HashMap<Range<language::Anchor>, BlockId>,
     _subscriptions: Vec<Subscription>,
@@ -971,6 +996,7 @@ impl ContextEditor {
             lsp_adapter_delegate,
             blocks: Default::default(),
             scroll_position: None,
+            remote_id: None,
             fs,
             workspace: workspace.downgrade(),
             pending_slash_command_creases: HashMap::default(),
@@ -1213,7 +1239,7 @@ impl ContextEditor {
                 });
             }
             ContextEvent::SummaryChanged => {
-                cx.emit(ContextEditorEvent::TabContentChanged);
+                cx.emit(EditorEvent::TitleChanged);
                 self.context.update(cx, |context, cx| {
                     context.save(None, self.fs.clone(), cx);
                 });
@@ -1472,9 +1498,9 @@ impl ContextEditor {
             EditorEvent::SelectionsChanged { .. } => {
                 self.scroll_position = self.cursor_scroll_position(cx);
             }
-            EditorEvent::BufferEdited => cx.emit(ContextEditorEvent::Edited),
             _ => {}
         }
+        cx.emit(event.clone());
     }
 
     fn handle_editor_search_event(
@@ -1935,7 +1961,7 @@ impl ContextEditor {
     }
 }
 
-impl EventEmitter<ContextEditorEvent> for ContextEditor {}
+impl EventEmitter<EditorEvent> for ContextEditor {}
 impl EventEmitter<SearchEvent> for ContextEditor {}
 
 impl Render for ContextEditor {
@@ -1977,13 +2003,9 @@ impl FocusableView for ContextEditor {
 }
 
 impl Item for ContextEditor {
-    type Event = ContextEditorEvent;
+    type Event = editor::EditorEvent;
 
-    fn tab_content(
-        &self,
-        params: workspace::item::TabContentParams,
-        cx: &WindowContext,
-    ) -> AnyElement {
+    fn tab_content(&self, params: item::TabContentParams, cx: &WindowContext) -> AnyElement {
         let color = if params.selected {
             Color::Default
         } else {
@@ -1997,15 +2019,16 @@ impl Item for ContextEditor {
         .into_any_element()
     }
 
-    fn to_item_events(event: &Self::Event, mut f: impl FnMut(workspace::item::ItemEvent)) {
+    fn to_item_events(event: &Self::Event, mut f: impl FnMut(item::ItemEvent)) {
         match event {
-            ContextEditorEvent::Edited => {
-                f(workspace::item::ItemEvent::Edit);
-                f(workspace::item::ItemEvent::UpdateBreadcrumbs);
+            EditorEvent::Edited { .. } => {
+                f(item::ItemEvent::Edit);
+                f(item::ItemEvent::UpdateBreadcrumbs);
             }
-            ContextEditorEvent::TabContentChanged => {
-                f(workspace::item::ItemEvent::UpdateTab);
+            EditorEvent::TitleChanged => {
+                f(item::ItemEvent::UpdateTab);
             }
+            _ => {}
         }
     }
 
@@ -2021,7 +2044,7 @@ impl Item for ContextEditor {
         &self,
         theme: &theme::Theme,
         cx: &AppContext,
-    ) -> Option<Vec<workspace::item::BreadcrumbText>> {
+    ) -> Option<Vec<item::BreadcrumbText>> {
         let editor = self.editor.read(cx);
         let cursor = editor.selections.newest_anchor().head();
         let multibuffer = &editor.buffer().read(cx);
@@ -2133,6 +2156,127 @@ impl SearchableItem for ContextEditor {
     }
 }
 
+impl FollowableItem for ContextEditor {
+    fn remote_id(&self) -> Option<workspace::ViewId> {
+        self.remote_id
+    }
+
+    fn to_state_proto(&self, cx: &WindowContext) -> Option<proto::view::Variant> {
+        let context = self.context.read(cx);
+        Some(proto::view::Variant::ContextEditor(
+            proto::view::ContextEditor {
+                context_id: context.id().to_proto(),
+                editor: if let Some(proto::view::Variant::Editor(proto)) =
+                    self.editor.read(cx).to_state_proto(cx)
+                {
+                    Some(proto)
+                } else {
+                    None
+                },
+            },
+        ))
+    }
+
+    fn from_state_proto(
+        workspace: View<Workspace>,
+        id: workspace::ViewId,
+        state: &mut Option<proto::view::Variant>,
+        cx: &mut WindowContext,
+    ) -> Option<Task<Result<View<Self>>>> {
+        let proto::view::Variant::ContextEditor(_) = state.as_ref()? else {
+            return None;
+        };
+        let Some(proto::view::Variant::ContextEditor(state)) = state.take() else {
+            unreachable!()
+        };
+
+        let context_id = ContextId::from_proto(state.context_id);
+        let editor_state = state.editor?;
+
+        let (project, panel) = workspace.update(cx, |workspace, cx| {
+            Some((
+                workspace.project().clone(),
+                workspace.panel::<AssistantPanel>(cx)?,
+            ))
+        })?;
+
+        let context_editor =
+            panel.update(cx, |panel, cx| panel.open_remote_context(context_id, cx));
+
+        Some(cx.spawn(|mut cx| async move {
+            let context_editor = context_editor.await?;
+            context_editor
+                .update(&mut cx, |context_editor, cx| {
+                    context_editor.remote_id = Some(id);
+                    context_editor.editor.update(cx, |editor, cx| {
+                        editor.apply_update_proto(
+                            &project,
+                            proto::update_view::Variant::Editor(proto::update_view::Editor {
+                                selections: editor_state.selections,
+                                pending_selection: editor_state.pending_selection,
+                                scroll_top_anchor: editor_state.scroll_top_anchor,
+                                scroll_x: editor_state.scroll_y,
+                                scroll_y: editor_state.scroll_y,
+                                ..Default::default()
+                            }),
+                            cx,
+                        )
+                    })
+                })?
+                .await?;
+            Ok(context_editor)
+        }))
+    }
+
+    fn to_follow_event(event: &Self::Event) -> Option<item::FollowEvent> {
+        Editor::to_follow_event(event)
+    }
+
+    fn add_event_to_update_proto(
+        &self,
+        event: &Self::Event,
+        update: &mut Option<proto::update_view::Variant>,
+        cx: &WindowContext,
+    ) -> bool {
+        self.editor
+            .read(cx)
+            .add_event_to_update_proto(event, update, cx)
+    }
+
+    fn apply_update_proto(
+        &mut self,
+        project: &Model<Project>,
+        message: proto::update_view::Variant,
+        cx: &mut ViewContext<Self>,
+    ) -> Task<Result<()>> {
+        self.editor.update(cx, |editor, cx| {
+            editor.apply_update_proto(project, message, cx)
+        })
+    }
+
+    fn is_project_item(&self, _cx: &WindowContext) -> bool {
+        true
+    }
+
+    fn set_leader_peer_id(
+        &mut self,
+        leader_peer_id: Option<proto::PeerId>,
+        cx: &mut ViewContext<Self>,
+    ) {
+        self.editor.update(cx, |editor, cx| {
+            editor.set_leader_peer_id(leader_peer_id, cx)
+        })
+    }
+
+    fn dedup(&self, existing: &Self, cx: &WindowContext) -> Option<item::Dedup> {
+        if existing.context.read(cx).id() == self.context.read(cx).id() {
+            Some(item::Dedup::KeepExisting)
+        } else {
+            None
+        }
+    }
+}
+
 pub struct ContextEditorToolbarItem {
     fs: Arc<dyn Fs>,
     workspace: WeakView<Workspace>,
@@ -2369,11 +2513,7 @@ impl EventEmitter<()> for ContextHistory {}
 impl Item for ContextHistory {
     type Event = ();
 
-    fn tab_content(
-        &self,
-        params: workspace::item::TabContentParams,
-        _: &WindowContext,
-    ) -> AnyElement {
+    fn tab_content(&self, params: item::TabContentParams, _: &WindowContext) -> AnyElement {
         let color = if params.selected {
             Color::Default
         } else {

crates/collab/src/tests/following_tests.rs 🔗

@@ -135,7 +135,7 @@ async fn test_basic_following(
         assert_eq!(editor.selections.ranges(cx), vec![2..1]);
     });
 
-    // When client B starts following client A, all visible view states are replicated to client B.
+    // When client B starts following client A, only the active view state is replicated to client B.
     workspace_b.update(cx_b, |workspace, cx| workspace.follow(peer_id_a, cx));
 
     cx_c.executor().run_until_parked();
@@ -156,7 +156,7 @@ async fn test_basic_following(
     );
     assert_eq!(
         editor_b1.update(cx_b, |editor, cx| editor.selections.ranges(cx)),
-        vec![3..2]
+        vec![3..3]
     );
 
     executor.run_until_parked();
@@ -194,7 +194,7 @@ async fn test_basic_following(
 
     // Client C unfollows client A.
     workspace_c.update(cx_c, |workspace, cx| {
-        workspace.unfollow(&workspace.active_pane().clone(), cx);
+        workspace.unfollow(peer_id_a, cx).unwrap();
     });
 
     // All clients see that clients B is following client A.
@@ -398,7 +398,7 @@ async fn test_basic_following(
 
     // After unfollowing, client B stops receiving updates from client A.
     workspace_b.update(cx_b, |workspace, cx| {
-        workspace.unfollow(&workspace.active_pane().clone(), cx)
+        workspace.unfollow(peer_id_a, cx).unwrap()
     });
     workspace_a.update(cx_a, |workspace, cx| {
         workspace.activate_item(&editor_a2, cx)

crates/collab_ui/src/channel_view.rs 🔗

@@ -22,10 +22,9 @@ use std::{
 };
 use ui::{prelude::*, Label};
 use util::ResultExt;
-use workspace::notifications::NotificationId;
+use workspace::{item::Dedup, notifications::NotificationId};
 use workspace::{
     item::{FollowableItem, Item, ItemEvent, ItemHandle, TabContentParams},
-    register_followable_item,
     searchable::SearchableItemHandle,
     ItemNavHistory, Pane, SaveIntent, Toast, ViewId, Workspace, WorkspaceId,
 };
@@ -33,7 +32,7 @@ use workspace::{
 actions!(collab, [CopyLink]);
 
 pub fn init(cx: &mut AppContext) {
-    register_followable_item::<ChannelView>(cx)
+    workspace::FollowableViewRegistry::register::<ChannelView>(cx)
 }
 
 pub struct ChannelView {
@@ -84,31 +83,12 @@ impl ChannelView {
         workspace: View<Workspace>,
         cx: &mut WindowContext,
     ) -> Task<Result<View<Self>>> {
-        let weak_workspace = workspace.downgrade();
-        let workspace = workspace.read(cx);
-        let project = workspace.project().to_owned();
-        let channel_store = ChannelStore::global(cx);
-        let language_registry = workspace.app_state().languages.clone();
-        let markdown = language_registry.language_for_name("Markdown");
-        let channel_buffer =
-            channel_store.update(cx, |store, cx| store.open_channel_buffer(channel_id, cx));
-
+        let channel_view = Self::load(channel_id, workspace, cx);
         cx.spawn(|mut cx| async move {
-            let channel_buffer = channel_buffer.await?;
-            let markdown = markdown.await.log_err();
-
-            channel_buffer.update(&mut cx, |channel_buffer, cx| {
-                channel_buffer.buffer().update(cx, |buffer, cx| {
-                    buffer.set_language_registry(language_registry);
-                    let Some(markdown) = markdown else {
-                        return;
-                    };
-                    buffer.set_language(Some(markdown), cx);
-                })
-            })?;
+            let channel_view = channel_view.await?;
 
             pane.update(&mut cx, |pane, cx| {
-                let buffer_id = channel_buffer.read(cx).remote_id(cx);
+                let buffer_id = channel_view.read(cx).channel_buffer.read(cx).remote_id(cx);
 
                 let existing_view = pane
                     .items_of_type::<Self>()
@@ -116,7 +96,8 @@ impl ChannelView {
 
                 // If this channel buffer is already open in this pane, just return it.
                 if let Some(existing_view) = existing_view.clone() {
-                    if existing_view.read(cx).channel_buffer == channel_buffer {
+                    if existing_view.read(cx).channel_buffer == channel_view.read(cx).channel_buffer
+                    {
                         if let Some(link_position) = link_position {
                             existing_view.update(cx, |channel_view, cx| {
                                 channel_view.focus_position_from_link(link_position, true, cx)
@@ -126,30 +107,60 @@ impl ChannelView {
                     }
                 }
 
-                let view = cx.new_view(|cx| {
-                    let mut this =
-                        Self::new(project, weak_workspace, channel_store, channel_buffer, cx);
-                    this.acknowledge_buffer_version(cx);
-                    this
-                });
-
                 // If the pane contained a disconnected view for this channel buffer,
                 // replace that.
                 if let Some(existing_item) = existing_view {
                     if let Some(ix) = pane.index_for_item(&existing_item) {
                         pane.close_item_by_id(existing_item.entity_id(), SaveIntent::Skip, cx)
                             .detach();
-                        pane.add_item(Box::new(view.clone()), true, true, Some(ix), cx);
+                        pane.add_item(Box::new(channel_view.clone()), true, true, Some(ix), cx);
                     }
                 }
 
                 if let Some(link_position) = link_position {
-                    view.update(cx, |channel_view, cx| {
+                    channel_view.update(cx, |channel_view, cx| {
                         channel_view.focus_position_from_link(link_position, true, cx)
                     });
                 }
 
-                view
+                channel_view
+            })
+        })
+    }
+
+    pub fn load(
+        channel_id: ChannelId,
+        workspace: View<Workspace>,
+        cx: &mut WindowContext,
+    ) -> Task<Result<View<Self>>> {
+        let weak_workspace = workspace.downgrade();
+        let workspace = workspace.read(cx);
+        let project = workspace.project().to_owned();
+        let channel_store = ChannelStore::global(cx);
+        let language_registry = workspace.app_state().languages.clone();
+        let markdown = language_registry.language_for_name("Markdown");
+        let channel_buffer =
+            channel_store.update(cx, |store, cx| store.open_channel_buffer(channel_id, cx));
+
+        cx.spawn(|mut cx| async move {
+            let channel_buffer = channel_buffer.await?;
+            let markdown = markdown.await.log_err();
+
+            channel_buffer.update(&mut cx, |channel_buffer, cx| {
+                channel_buffer.buffer().update(cx, |buffer, cx| {
+                    buffer.set_language_registry(language_registry);
+                    let Some(markdown) = markdown else {
+                        return;
+                    };
+                    buffer.set_language(Some(markdown), cx);
+                })
+            })?;
+
+            cx.new_view(|cx| {
+                let mut this =
+                    Self::new(project, weak_workspace, channel_store, channel_buffer, cx);
+                this.acknowledge_buffer_version(cx);
+                this
             })
         })
     }
@@ -478,7 +489,6 @@ impl FollowableItem for ChannelView {
     }
 
     fn from_state_proto(
-        pane: View<workspace::Pane>,
         workspace: View<workspace::Workspace>,
         remote_id: workspace::ViewId,
         state: &mut Option<proto::view::Variant>,
@@ -491,8 +501,7 @@ impl FollowableItem for ChannelView {
             unreachable!()
         };
 
-        let open =
-            ChannelView::open_in_pane(ChannelId(state.channel_id), None, pane, workspace, cx);
+        let open = ChannelView::load(ChannelId(state.channel_id), workspace, cx);
 
         Some(cx.spawn(|mut cx| async move {
             let this = open.await?;
@@ -563,6 +572,19 @@ impl FollowableItem for ChannelView {
     fn to_follow_event(event: &Self::Event) -> Option<workspace::item::FollowEvent> {
         Editor::to_follow_event(event)
     }
+
+    fn dedup(&self, existing: &Self, cx: &WindowContext) -> Option<Dedup> {
+        let existing = existing.channel_buffer.read(cx);
+        if self.channel_buffer.read(cx).channel_id == existing.channel_id {
+            if existing.is_connected() {
+                Some(Dedup::KeepExisting)
+            } else {
+                Some(Dedup::ReplaceExisting)
+            }
+        } else {
+            None
+        }
+    }
 }
 
 struct ChannelBufferCollaborationHub(Model<ChannelBuffer>);

crates/editor/src/editor.rs 🔗

@@ -271,7 +271,7 @@ pub fn init(cx: &mut AppContext) {
     init_settings(cx);
 
     workspace::register_project_item::<Editor>(cx);
-    workspace::register_followable_item::<Editor>(cx);
+    workspace::FollowableViewRegistry::register::<Editor>(cx);
     workspace::register_deserializable_item::<Editor>(cx);
     cx.observe_new_views(
         |workspace: &mut Workspace, _cx: &mut ViewContext<Workspace>| {

crates/editor/src/editor_tests.rs 🔗

@@ -8812,7 +8812,6 @@ async fn test_following_with_multiple_excerpts(cx: &mut gpui::TestAppContext) {
     let follower_1 = cx
         .update_window(*workspace.deref(), |_, cx| {
             Editor::from_state_proto(
-                pane.clone(),
                 workspace.root_view(cx).unwrap(),
                 ViewId {
                     creator: Default::default(),
@@ -8904,7 +8903,6 @@ async fn test_following_with_multiple_excerpts(cx: &mut gpui::TestAppContext) {
     let follower_2 = cx
         .update_window(*workspace.deref(), |_, cx| {
             Editor::from_state_proto(
-                pane.clone(),
                 workspace.root_view(cx).unwrap().clone(),
                 ViewId {
                     creator: Default::default(),

crates/editor/src/items.rs 🔗

@@ -19,7 +19,7 @@ use multi_buffer::AnchorRangeExt;
 use project::{search::SearchQuery, FormatTrigger, Item as _, Project, ProjectPath};
 use rpc::proto::{self, update_view, PeerId};
 use settings::Settings;
-use workspace::item::{ItemSettings, TabContentParams};
+use workspace::item::{Dedup, ItemSettings, TabContentParams};
 
 use std::{
     any::TypeId,
@@ -34,7 +34,7 @@ use text::{BufferId, Selection};
 use theme::{Theme, ThemeSettings};
 use ui::{h_flex, prelude::*, Label};
 use util::{paths::PathExt, ResultExt, TryFutureExt};
-use workspace::item::{BreadcrumbText, FollowEvent, FollowableItemHandle};
+use workspace::item::{BreadcrumbText, FollowEvent};
 use workspace::{
     item::{FollowableItem, Item, ItemEvent, ItemHandle, ProjectItem},
     searchable::{Direction, SearchEvent, SearchableItem, SearchableItemHandle},
@@ -49,7 +49,6 @@ impl FollowableItem for Editor {
     }
 
     fn from_state_proto(
-        pane: View<workspace::Pane>,
         workspace: View<Workspace>,
         remote_id: ViewId,
         state: &mut Option<proto::view::Variant>,
@@ -63,7 +62,6 @@ impl FollowableItem for Editor {
             unreachable!()
         };
 
-        let client = project.read(cx).client();
         let replica_id = project.read(cx).replica_id();
         let buffer_ids = state
             .excerpts
@@ -77,71 +75,54 @@ impl FollowableItem for Editor {
                 .collect::<Result<Vec<_>>>()
         });
 
-        let pane = pane.downgrade();
         Some(cx.spawn(|mut cx| async move {
             let mut buffers = futures::future::try_join_all(buffers?)
                 .await
                 .debug_assert_ok("leaders don't share views for unshared buffers")?;
 
-            let editor = pane.update(&mut cx, |pane, cx| {
-                let mut editors = pane.items_of_type::<Self>();
-                editors.find(|editor| {
-                    let ids_match = editor.remote_id(&client, cx) == Some(remote_id);
-                    let singleton_buffer_matches = state.singleton
-                        && buffers.first()
-                            == editor.read(cx).buffer.read(cx).as_singleton().as_ref();
-                    ids_match || singleton_buffer_matches
-                })
-            })?;
-
-            let editor = if let Some(editor) = editor {
-                editor
-            } else {
-                pane.update(&mut cx, |_, cx| {
-                    let multibuffer = cx.new_model(|cx| {
-                        let mut multibuffer;
-                        if state.singleton && buffers.len() == 1 {
-                            multibuffer = MultiBuffer::singleton(buffers.pop().unwrap(), cx)
-                        } else {
-                            multibuffer =
-                                MultiBuffer::new(replica_id, project.read(cx).capability());
-                            let mut excerpts = state.excerpts.into_iter().peekable();
-                            while let Some(excerpt) = excerpts.peek() {
-                                let Ok(buffer_id) = BufferId::new(excerpt.buffer_id) else {
-                                    continue;
-                                };
-                                let buffer_excerpts = iter::from_fn(|| {
-                                    let excerpt = excerpts.peek()?;
-                                    (excerpt.buffer_id == u64::from(buffer_id))
-                                        .then(|| excerpts.next().unwrap())
-                                });
-                                let buffer =
-                                    buffers.iter().find(|b| b.read(cx).remote_id() == buffer_id);
-                                if let Some(buffer) = buffer {
-                                    multibuffer.push_excerpts(
-                                        buffer.clone(),
-                                        buffer_excerpts.filter_map(deserialize_excerpt_range),
-                                        cx,
-                                    );
-                                }
+            let editor = cx.update(|cx| {
+                let multibuffer = cx.new_model(|cx| {
+                    let mut multibuffer;
+                    if state.singleton && buffers.len() == 1 {
+                        multibuffer = MultiBuffer::singleton(buffers.pop().unwrap(), cx)
+                    } else {
+                        multibuffer = MultiBuffer::new(replica_id, project.read(cx).capability());
+                        let mut excerpts = state.excerpts.into_iter().peekable();
+                        while let Some(excerpt) = excerpts.peek() {
+                            let Ok(buffer_id) = BufferId::new(excerpt.buffer_id) else {
+                                continue;
+                            };
+                            let buffer_excerpts = iter::from_fn(|| {
+                                let excerpt = excerpts.peek()?;
+                                (excerpt.buffer_id == u64::from(buffer_id))
+                                    .then(|| excerpts.next().unwrap())
+                            });
+                            let buffer =
+                                buffers.iter().find(|b| b.read(cx).remote_id() == buffer_id);
+                            if let Some(buffer) = buffer {
+                                multibuffer.push_excerpts(
+                                    buffer.clone(),
+                                    buffer_excerpts.filter_map(deserialize_excerpt_range),
+                                    cx,
+                                );
                             }
-                        };
-
-                        if let Some(title) = &state.title {
-                            multibuffer = multibuffer.with_title(title.clone())
                         }
+                    };
 
-                        multibuffer
-                    });
+                    if let Some(title) = &state.title {
+                        multibuffer = multibuffer.with_title(title.clone())
+                    }
 
-                    cx.new_view(|cx| {
-                        let mut editor =
-                            Editor::for_multibuffer(multibuffer, Some(project.clone()), true, cx);
-                        editor.remote_id = Some(remote_id);
-                        editor
-                    })
-                })?
-            };
+                    multibuffer
+                });
+
+                cx.new_view(|cx| {
+                    let mut editor =
+                        Editor::for_multibuffer(multibuffer, Some(project.clone()), true, cx);
+                    editor.remote_id = Some(remote_id);
+                    editor
+                })
+            })?;
 
             update_editor_from_message(
                 editor.downgrade(),
@@ -327,6 +308,16 @@ impl FollowableItem for Editor {
     fn is_project_item(&self, _cx: &WindowContext) -> bool {
         true
     }
+
+    fn dedup(&self, existing: &Self, cx: &WindowContext) -> Option<Dedup> {
+        let self_singleton = self.buffer.read(cx).as_singleton()?;
+        let other_singleton = existing.buffer.read(cx).as_singleton()?;
+        if self_singleton == other_singleton {
+            Some(Dedup::KeepExisting)
+        } else {
+            None
+        }
+    }
 }
 
 async fn update_editor_from_message(

crates/gpui/src/gpui.rs 🔗

@@ -291,6 +291,10 @@ pub trait BorrowAppContext {
     fn update_global<G, R>(&mut self, f: impl FnOnce(&mut G, &mut Self) -> R) -> R
     where
         G: Global;
+    /// Updates the global state of the given type, creating a default if it didn't exist before.
+    fn update_default_global<G, R>(&mut self, f: impl FnOnce(&mut G, &mut Self) -> R) -> R
+    where
+        G: Global + Default;
 }
 
 impl<C> BorrowAppContext for C
@@ -310,6 +314,14 @@ where
         self.borrow_mut().end_global_lease(global);
         result
     }
+
+    fn update_default_global<G, R>(&mut self, f: impl FnOnce(&mut G, &mut Self) -> R) -> R
+    where
+        G: Global + Default,
+    {
+        self.borrow_mut().default_global::<G>();
+        self.update_global(f)
+    }
 }
 
 /// A flatten equivalent for anyhow `Result`s.

crates/proto/proto/zed.proto 🔗

@@ -1629,7 +1629,7 @@ message Follow {
 
 message FollowResponse {
     View active_view = 3;
-    // TODO: after 0.124.0 is retired, remove these.
+    // TODO: Remove after version 0.145.x stabilizes.
     optional ViewId active_view_id = 1;
     repeated View views = 2;
 }
@@ -1640,7 +1640,7 @@ message UpdateFollowers {
     reserved 3;
     oneof variant {
         View create_view = 5;
-        // TODO: after 0.124.0 is retired, remove these.
+        // TODO: Remove after version 0.145.x stabilizes.
         UpdateActiveView update_active_view = 4;
         UpdateView update_view = 6;
     }
@@ -1673,6 +1673,10 @@ message UpdateActiveView {
     View view = 3;
 }
 
+enum PanelId {
+    AssistantPanel = 0;
+}
+
 message UpdateView {
     ViewId id = 1;
     optional PeerId leader_id = 2;
@@ -1695,10 +1699,12 @@ message UpdateView {
 message View {
     ViewId id = 1;
     optional PeerId leader_id = 2;
+    optional PanelId panel_id = 6;
 
     oneof variant {
         Editor editor = 3;
         ChannelView channel_view = 4;
+        ContextEditor context_editor = 5;
     }
 
     message Editor {
@@ -1716,6 +1722,11 @@ message View {
         uint64 channel_id = 1;
         Editor editor = 2;
     }
+
+    message ContextEditor {
+        string context_id = 1;
+        Editor editor = 2;
+    }
 }
 
 message Collaborator {

crates/title_bar/src/collab.rs 🔗

@@ -168,7 +168,11 @@ impl TitleBar {
                                     cx.listener(move |this, _, cx| {
                                         this.workspace
                                             .update(cx, |workspace, cx| {
-                                                workspace.follow(peer_id, cx);
+                                                if is_following {
+                                                    workspace.unfollow(peer_id, cx);
+                                                } else {
+                                                    workspace.follow(peer_id, cx);
+                                                }
                                             })
                                             .ok();
                                     })

crates/workspace/src/dock.rs 🔗

@@ -1,6 +1,7 @@
 use crate::persistence::model::DockData;
 use crate::{status_bar::StatusItemView, Workspace};
-use crate::{DraggedDock, Event};
+use crate::{DraggedDock, Event, Pane};
+use client::proto;
 use gpui::{
     deferred, div, px, Action, AnchorCorner, AnyView, AppContext, Axis, Entity, EntityId,
     EventEmitter, FocusHandle, FocusableView, IntoElement, KeyContext, MouseButton, MouseDownEvent,
@@ -23,6 +24,8 @@ pub enum PanelEvent {
     Close,
 }
 
+pub use proto::PanelId;
+
 pub trait Panel: FocusableView + EventEmitter<PanelEvent> {
     fn persistent_name() -> &'static str;
     fn position(&self, cx: &WindowContext) -> DockPosition;
@@ -44,6 +47,12 @@ pub trait Panel: FocusableView + EventEmitter<PanelEvent> {
     }
     fn set_zoomed(&mut self, _zoomed: bool, _cx: &mut ViewContext<Self>) {}
     fn set_active(&mut self, _active: bool, _cx: &mut ViewContext<Self>) {}
+    fn pane(&self) -> Option<View<Pane>> {
+        None
+    }
+    fn remote_id() -> Option<proto::PanelId> {
+        None
+    }
 }
 
 pub trait PanelHandle: Send + Sync {
@@ -55,6 +64,8 @@ pub trait PanelHandle: Send + Sync {
     fn is_zoomed(&self, cx: &WindowContext) -> bool;
     fn set_zoomed(&self, zoomed: bool, cx: &mut WindowContext);
     fn set_active(&self, active: bool, cx: &mut WindowContext);
+    fn remote_id(&self) -> Option<proto::PanelId>;
+    fn pane(&self, cx: &WindowContext) -> Option<View<Pane>>;
     fn size(&self, cx: &WindowContext) -> Pixels;
     fn set_size(&self, size: Option<Pixels>, cx: &mut WindowContext);
     fn icon(&self, cx: &WindowContext) -> Option<ui::IconName>;
@@ -101,6 +112,14 @@ where
         self.update(cx, |this, cx| this.set_active(active, cx))
     }
 
+    fn pane(&self, cx: &WindowContext) -> Option<View<Pane>> {
+        self.read(cx).pane()
+    }
+
+    fn remote_id(&self) -> Option<PanelId> {
+        T::remote_id()
+    }
+
     fn size(&self, cx: &WindowContext) -> Pixels {
         self.read(cx).size(cx)
     }
@@ -296,6 +315,12 @@ impl Dock {
             .position(|entry| entry.panel.persistent_name() == ui_name)
     }
 
+    pub fn panel_index_for_proto_id(&self, panel_id: PanelId) -> Option<usize> {
+        self.panel_entries
+            .iter()
+            .position(|entry| entry.panel.remote_id() == Some(panel_id))
+    }
+
     pub fn active_panel_index(&self) -> usize {
         self.active_panel_index
     }

crates/workspace/src/item.rs 🔗

@@ -3,7 +3,7 @@ use crate::{
     persistence::model::ItemId,
     searchable::SearchableItemHandle,
     workspace_settings::{AutosaveSetting, WorkspaceSettings},
-    DelayedDebouncedEditAction, FollowableItemBuilders, ItemNavHistory, ToolbarItemLocation,
+    DelayedDebouncedEditAction, FollowableViewRegistry, ItemNavHistory, ToolbarItemLocation,
     ViewId, Workspace, WorkspaceId,
 };
 use anyhow::Result;
@@ -472,22 +472,6 @@ impl<T: Item> ItemHandle for View<T> {
             this.added_to_workspace(workspace, cx);
         });
 
-        if let Some(followed_item) = self.to_followable_item_handle(cx) {
-            if let Some(message) = followed_item.to_state_proto(cx) {
-                workspace.update_followers(
-                    followed_item.is_project_item(cx),
-                    proto::update_followers::Variant::CreateView(proto::View {
-                        id: followed_item
-                            .remote_id(&workspace.client(), cx)
-                            .map(|id| id.to_proto()),
-                        variant: Some(message),
-                        leader_id: workspace.leader_for_pane(&pane),
-                    }),
-                    cx,
-                );
-            }
-        }
-
         if workspace
             .panes_by_item
             .insert(self.item_id(), pane.downgrade())
@@ -548,11 +532,11 @@ impl<T: Item> ItemHandle for View<T> {
 
                     if let Some(item) = item.to_followable_item_handle(cx) {
                         let leader_id = workspace.leader_for_pane(&pane);
-                        let follow_event = item.to_follow_event(event);
-                        if leader_id.is_some()
-                            && matches!(follow_event, Some(FollowEvent::Unfollow))
-                        {
-                            workspace.unfollow(&pane, cx);
+
+                        if let Some(leader_id) = leader_id {
+                            if let Some(FollowEvent::Unfollow) = item.to_follow_event(event) {
+                                workspace.unfollow(leader_id, cx);
+                            }
                         }
 
                         if item.focus_handle(cx).contains_focused(cx) {
@@ -682,9 +666,7 @@ impl<T: Item> ItemHandle for View<T> {
     }
 
     fn to_followable_item_handle(&self, cx: &AppContext) -> Option<Box<dyn FollowableItemHandle>> {
-        let builders = cx.try_global::<FollowableItemBuilders>()?;
-        let item = self.to_any();
-        Some(builders.get(&item.entity_type())?.1(&item))
+        FollowableViewRegistry::to_followable_view(self.clone(), cx)
     }
 
     fn on_release(
@@ -769,11 +751,15 @@ pub enum FollowEvent {
     Unfollow,
 }
 
+pub enum Dedup {
+    KeepExisting,
+    ReplaceExisting,
+}
+
 pub trait FollowableItem: Item {
     fn remote_id(&self) -> Option<ViewId>;
     fn to_state_proto(&self, cx: &WindowContext) -> Option<proto::view::Variant>;
     fn from_state_proto(
-        pane: View<Pane>,
         project: View<Workspace>,
         id: ViewId,
         state: &mut Option<proto::view::Variant>,
@@ -794,6 +780,7 @@ pub trait FollowableItem: Item {
     ) -> Task<Result<()>>;
     fn is_project_item(&self, cx: &WindowContext) -> bool;
     fn set_leader_peer_id(&mut self, leader_peer_id: Option<PeerId>, cx: &mut ViewContext<Self>);
+    fn dedup(&self, existing: &Self, cx: &WindowContext) -> Option<Dedup>;
 }
 
 pub trait FollowableItemHandle: ItemHandle {
@@ -815,6 +802,7 @@ pub trait FollowableItemHandle: ItemHandle {
         cx: &mut WindowContext,
     ) -> Task<Result<()>>;
     fn is_project_item(&self, cx: &WindowContext) -> bool;
+    fn dedup(&self, existing: &dyn FollowableItemHandle, cx: &WindowContext) -> Option<Dedup>;
 }
 
 impl<T: FollowableItem> FollowableItemHandle for View<T> {
@@ -868,6 +856,11 @@ impl<T: FollowableItem> FollowableItemHandle for View<T> {
     fn is_project_item(&self, cx: &WindowContext) -> bool {
         self.read(cx).is_project_item(cx)
     }
+
+    fn dedup(&self, existing: &dyn FollowableItemHandle, cx: &WindowContext) -> Option<Dedup> {
+        let existing = existing.to_any().downcast::<T>().ok()?;
+        self.read(cx).dedup(existing.read(cx), cx)
+    }
 }
 
 pub trait WeakFollowableItemHandle: Send + Sync {

crates/workspace/src/pane_group.rs 🔗

@@ -1,6 +1,7 @@
 use crate::{pane_group::element::pane_axis, AppState, FollowerState, Pane, Workspace};
 use anyhow::{anyhow, Result};
 use call::{ActiveCall, ParticipantLocation};
+use client::proto::PeerId;
 use collections::HashMap;
 use gpui::{
     point, size, AnyView, AnyWeakView, Axis, Bounds, IntoElement, Model, MouseButton, Pixels,
@@ -95,7 +96,7 @@ impl PaneGroup {
     pub(crate) fn render(
         &self,
         project: &Model<Project>,
-        follower_states: &HashMap<View<Pane>, FollowerState>,
+        follower_states: &HashMap<PeerId, FollowerState>,
         active_call: Option<&Model<ActiveCall>>,
         active_pane: &View<Pane>,
         zoomed: Option<&AnyWeakView>,
@@ -168,7 +169,7 @@ impl Member {
         &self,
         project: &Model<Project>,
         basis: usize,
-        follower_states: &HashMap<View<Pane>, FollowerState>,
+        follower_states: &HashMap<PeerId, FollowerState>,
         active_call: Option<&Model<ActiveCall>>,
         active_pane: &View<Pane>,
         zoomed: Option<&AnyWeakView>,
@@ -181,19 +182,29 @@ impl Member {
                     return div().into_any();
                 }
 
-                let follower_state = follower_states.get(pane);
+                let follower_state = follower_states.iter().find_map(|(leader_id, state)| {
+                    if state.center_pane == *pane {
+                        Some((*leader_id, state))
+                    } else {
+                        None
+                    }
+                });
 
-                let leader = follower_state.and_then(|state| {
+                let leader = follower_state.as_ref().and_then(|(leader_id, _)| {
                     let room = active_call?.read(cx).room()?.read(cx);
-                    room.remote_participant_for_peer_id(state.leader_id)
+                    room.remote_participant_for_peer_id(*leader_id)
                 });
 
-                let is_in_unshared_view = follower_state.map_or(false, |state| {
+                let is_in_unshared_view = follower_state.as_ref().map_or(false, |(_, state)| {
                     state.active_view_id.is_some_and(|view_id| {
                         !state.items_by_leader_view_id.contains_key(&view_id)
                     })
                 });
 
+                let is_in_panel = follower_state
+                    .as_ref()
+                    .map_or(false, |(_, state)| state.dock_pane.is_some());
+
                 let mut leader_border = None;
                 let mut leader_status_box = None;
                 let mut leader_join_data = None;
@@ -203,7 +214,11 @@ impl Member {
                         .players()
                         .color_for_participant(leader.participant_index.0)
                         .cursor;
-                    leader_color.fade_out(0.3);
+                    if is_in_panel {
+                        leader_color.fade_out(0.75);
+                    } else {
+                        leader_color.fade_out(0.3);
+                    }
                     leader_border = Some(leader_color);
 
                     leader_status_box = match leader.location {
@@ -483,7 +498,7 @@ impl PaneAxis {
         &self,
         project: &Model<Project>,
         basis: usize,
-        follower_states: &HashMap<View<Pane>, FollowerState>,
+        follower_states: &HashMap<PeerId, FollowerState>,
         active_call: Option<&Model<ActiveCall>>,
         active_pane: &View<Pane>,
         zoomed: Option<&AnyWeakView>,

crates/workspace/src/workspace.rs 🔗

@@ -15,7 +15,7 @@ mod workspace_settings;
 use anyhow::{anyhow, Context as _, Result};
 use call::{call_settings::CallSettings, ActiveCall};
 use client::{
-    proto::{self, ErrorCode, PeerId},
+    proto::{self, ErrorCode, PanelId, PeerId},
     ChannelId, Client, DevServerProjectId, ErrorExt, ProjectId, Status, TypedEnvelope, UserStore,
 };
 use collections::{hash_map, HashMap, HashSet};
@@ -81,9 +81,9 @@ use theme::{ActiveTheme, SystemAppearance, ThemeSettings};
 pub use toolbar::{Toolbar, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView};
 pub use ui;
 use ui::{
-    div, h_flex, px, Context as _, Div, FluentBuilder, InteractiveElement as _, IntoElement,
-    ParentElement as _, Pixels, SharedString, Styled as _, ViewContext, VisualContext as _,
-    WindowContext,
+    div, h_flex, px, BorrowAppContext, Context as _, Div, FluentBuilder, InteractiveElement as _,
+    IntoElement, ParentElement as _, Pixels, SharedString, Styled as _, ViewContext,
+    VisualContext as _, WindowContext,
 };
 use util::{maybe, ResultExt};
 use uuid::Uuid;
@@ -354,41 +354,59 @@ pub fn register_project_item<I: ProjectItem>(cx: &mut AppContext) {
     });
 }
 
-type FollowableItemBuilder = fn(
-    View<Pane>,
-    View<Workspace>,
-    ViewId,
-    &mut Option<proto::view::Variant>,
-    &mut WindowContext,
-) -> Option<Task<Result<Box<dyn FollowableItemHandle>>>>;
-
-#[derive(Default, Deref, DerefMut)]
-struct FollowableItemBuilders(
-    HashMap<
-        TypeId,
-        (
-            FollowableItemBuilder,
-            fn(&AnyView) -> Box<dyn FollowableItemHandle>,
-        ),
-    >,
-);
+#[derive(Default)]
+pub struct FollowableViewRegistry(HashMap<TypeId, FollowableViewDescriptor>);
+
+struct FollowableViewDescriptor {
+    from_state_proto: fn(
+        View<Workspace>,
+        ViewId,
+        &mut Option<proto::view::Variant>,
+        &mut WindowContext,
+    ) -> Option<Task<Result<Box<dyn FollowableItemHandle>>>>,
+    to_followable_view: fn(&AnyView) -> Box<dyn FollowableItemHandle>,
+}
 
-impl Global for FollowableItemBuilders {}
-
-pub fn register_followable_item<I: FollowableItem>(cx: &mut AppContext) {
-    let builders = cx.default_global::<FollowableItemBuilders>();
-    builders.insert(
-        TypeId::of::<I>(),
-        (
-            |pane, workspace, id, state, cx| {
-                I::from_state_proto(pane, workspace, id, state, cx).map(|task| {
-                    cx.foreground_executor()
-                        .spawn(async move { Ok(Box::new(task.await?) as Box<_>) })
-                })
+impl Global for FollowableViewRegistry {}
+
+impl FollowableViewRegistry {
+    pub fn register<I: FollowableItem>(cx: &mut AppContext) {
+        cx.default_global::<Self>().0.insert(
+            TypeId::of::<I>(),
+            FollowableViewDescriptor {
+                from_state_proto: |workspace, id, state, cx| {
+                    I::from_state_proto(workspace, id, state, cx).map(|task| {
+                        cx.foreground_executor()
+                            .spawn(async move { Ok(Box::new(task.await?) as Box<_>) })
+                    })
+                },
+                to_followable_view: |view| Box::new(view.clone().downcast::<I>().unwrap()),
             },
-            |this| Box::new(this.clone().downcast::<I>().unwrap()),
-        ),
-    );
+        );
+    }
+
+    pub fn from_state_proto(
+        workspace: View<Workspace>,
+        view_id: ViewId,
+        mut state: Option<proto::view::Variant>,
+        cx: &mut WindowContext,
+    ) -> Option<Task<Result<Box<dyn FollowableItemHandle>>>> {
+        cx.update_default_global(|this: &mut Self, cx| {
+            this.0.values().find_map(|descriptor| {
+                (descriptor.from_state_proto)(workspace.clone(), view_id, &mut state, cx)
+            })
+        })
+    }
+
+    pub fn to_followable_view(
+        view: impl Into<AnyView>,
+        cx: &AppContext,
+    ) -> Option<Box<dyn FollowableItemHandle>> {
+        let this = cx.try_global::<Self>()?;
+        let view = view.into();
+        let descriptor = this.0.get(&view.entity_type())?;
+        Some((descriptor.to_followable_view)(&view))
+    }
 }
 
 #[derive(Default, Deref, DerefMut)]
@@ -593,7 +611,7 @@ pub struct Workspace {
     titlebar_item: Option<AnyView>,
     notifications: Vec<(NotificationId, Box<dyn NotificationHandle>)>,
     project: Model<Project>,
-    follower_states: HashMap<View<Pane>, FollowerState>,
+    follower_states: HashMap<PeerId, FollowerState>,
     last_leaders_by_pane: HashMap<WeakView<Pane>, PeerId>,
     window_edited: bool,
     active_call: Option<(Model<ActiveCall>, Vec<Subscription>)>,
@@ -622,11 +640,16 @@ pub struct ViewId {
     pub id: u64,
 }
 
-#[derive(Default)]
 struct FollowerState {
-    leader_id: PeerId,
+    center_pane: View<Pane>,
+    dock_pane: Option<View<Pane>>,
     active_view_id: Option<ViewId>,
-    items_by_leader_view_id: HashMap<ViewId, Box<dyn FollowableItemHandle>>,
+    items_by_leader_view_id: HashMap<ViewId, FollowerView>,
+}
+
+struct FollowerView {
+    view: Box<dyn FollowableItemHandle>,
+    location: Option<proto::PanelId>,
 }
 
 impl Workspace {
@@ -657,10 +680,10 @@ impl Workspace {
 
                 project::Event::DisconnectedFromHost => {
                     this.update_window_edited(cx);
-                    let panes_to_unfollow: Vec<View<Pane>> =
-                        this.follower_states.keys().map(|k| k.clone()).collect();
-                    for pane in panes_to_unfollow {
-                        this.unfollow(&pane, cx);
+                    let leaders_to_unfollow =
+                        this.follower_states.keys().copied().collect::<Vec<_>>();
+                    for leader_id in leaders_to_unfollow {
+                        this.unfollow(leader_id, cx);
                     }
                 }
 
@@ -1056,7 +1079,11 @@ impl Workspace {
         self.window_edited
     }
 
-    pub fn add_panel<T: Panel>(&mut self, panel: View<T>, cx: &mut WindowContext) {
+    pub fn add_panel<T: Panel>(&mut self, panel: View<T>, cx: &mut ViewContext<Self>) {
+        let focus_handle = panel.focus_handle(cx);
+        cx.on_focus_in(&focus_handle, Self::handle_panel_focused)
+            .detach();
+
         let dock = match panel.position(cx) {
             DockPosition::Left => &self.left_dock,
             DockPosition::Bottom => &self.bottom_dock,
@@ -1975,6 +2002,31 @@ impl Workspace {
         });
     }
 
+    pub fn activate_panel_for_proto_id(
+        &mut self,
+        panel_id: PanelId,
+        cx: &mut ViewContext<Self>,
+    ) -> Option<Arc<dyn PanelHandle>> {
+        let mut panel = None;
+        for dock in [&self.left_dock, &self.bottom_dock, &self.right_dock] {
+            if let Some(panel_index) = dock.read(cx).panel_index_for_proto_id(panel_id) {
+                panel = dock.update(cx, |dock, cx| {
+                    dock.activate_panel(panel_index, cx);
+                    dock.set_open(true, cx);
+                    dock.active_panel().cloned()
+                });
+                break;
+            }
+        }
+
+        if panel.is_some() {
+            cx.notify();
+            self.serialize_workspace(cx);
+        }
+
+        panel
+    }
+
     /// Focus or unfocus the given panel type, depending on the given callback.
     fn focus_or_unfocus_panel<T: Panel>(
         &mut self,
@@ -2032,13 +2084,9 @@ impl Workspace {
     }
 
     pub fn panel<T: Panel>(&self, cx: &WindowContext) -> Option<View<T>> {
-        for dock in [&self.left_dock, &self.bottom_dock, &self.right_dock] {
-            let dock = dock.read(cx);
-            if let Some(panel) = dock.panel::<T>() {
-                return Some(panel);
-            }
-        }
-        None
+        [&self.left_dock, &self.bottom_dock, &self.right_dock]
+            .iter()
+            .find_map(|dock| dock.read(cx).panel::<T>())
     }
 
     fn dismiss_zoomed_items_to_reveal(
@@ -2557,6 +2605,10 @@ impl Workspace {
         cx.notify();
     }
 
+    fn handle_panel_focused(&mut self, cx: &mut ViewContext<Self>) {
+        self.update_active_view_for_followers(cx);
+    }
+
     fn handle_pane_event(
         &mut self,
         pane: View<Pane>,
@@ -2577,7 +2629,7 @@ impl Workspace {
                     pane.track_alternate_file_items();
                 });
                 if *local {
-                    self.unfollow(&pane, cx);
+                    self.unfollow_in_pane(&pane, cx);
                 }
                 if &pane == self.active_pane() {
                     self.active_item_path_changed(cx);
@@ -2626,6 +2678,16 @@ impl Workspace {
         self.serialize_workspace(cx);
     }
 
+    pub fn unfollow_in_pane(
+        &mut self,
+        pane: &View<Pane>,
+        cx: &mut ViewContext<Workspace>,
+    ) -> Option<PeerId> {
+        let leader_id = self.leader_for_pane(pane)?;
+        self.unfollow(leader_id, cx);
+        Some(leader_id)
+    }
+
     pub fn split_pane(
         &mut self,
         pane_to_split: View<Pane>,
@@ -2740,7 +2802,7 @@ impl Workspace {
     fn remove_pane(&mut self, pane: View<Pane>, cx: &mut ViewContext<Self>) {
         if self.center.remove(&pane).unwrap() {
             self.force_remove_pane(&pane, cx);
-            self.unfollow(&pane, cx);
+            self.unfollow_in_pane(&pane, cx);
             self.last_leaders_by_pane.remove(&pane.downgrade());
             for removed_item in pane.read(cx).items() {
                 self.panes_by_item.remove(&removed_item.item_id());
@@ -2774,10 +2836,10 @@ impl Workspace {
     }
 
     fn collaborator_left(&mut self, peer_id: PeerId, cx: &mut ViewContext<Self>) {
-        self.follower_states.retain(|_, state| {
-            if state.leader_id == peer_id {
+        self.follower_states.retain(|leader_id, state| {
+            if *leader_id == peer_id {
                 for item in state.items_by_leader_view_id.values() {
-                    item.set_leader_peer_id(None, cx);
+                    item.view.set_leader_peer_id(None, cx);
                 }
                 false
             } else {
@@ -2796,11 +2858,13 @@ impl Workspace {
 
         self.last_leaders_by_pane
             .insert(pane.downgrade(), leader_id);
-        self.unfollow(&pane, cx);
+        self.unfollow(leader_id, cx);
+        self.unfollow_in_pane(&pane, cx);
         self.follower_states.insert(
-            pane.clone(),
+            leader_id,
             FollowerState {
-                leader_id,
+                center_pane: pane.clone(),
+                dock_pane: None,
                 active_view_id: None,
                 items_by_leader_view_id: Default::default(),
             },
@@ -2820,27 +2884,17 @@ impl Workspace {
             this.update(&mut cx, |this, _| {
                 let state = this
                     .follower_states
-                    .get_mut(&pane)
+                    .get_mut(&leader_id)
                     .ok_or_else(|| anyhow!("following interrupted"))?;
-                state.active_view_id = if let Some(active_view_id) = response.active_view_id {
-                    Some(ViewId::from_proto(active_view_id)?)
-                } else {
-                    None
-                };
+                state.active_view_id = response
+                    .active_view
+                    .as_ref()
+                    .and_then(|view| ViewId::from_proto(view.id.clone()?).ok());
                 Ok::<_, anyhow::Error>(())
             })??;
             if let Some(view) = response.active_view {
-                Self::add_view_from_leader(this.clone(), leader_id, pane.clone(), &view, &mut cx)
-                    .await?;
+                Self::add_view_from_leader(this.clone(), leader_id, &view, &mut cx).await?;
             }
-            Self::add_views_from_leader(
-                this.clone(),
-                leader_id,
-                vec![pane],
-                response.views,
-                &mut cx,
-            )
-            .await?;
             this.update(&mut cx, |this, cx| this.leader_updated(leader_id, cx))?;
             Ok(())
         }))
@@ -2877,7 +2931,7 @@ impl Workspace {
         else {
             return;
         };
-        if Some(leader_id) == self.unfollow(&pane, cx) {
+        if self.unfollow_in_pane(&pane, cx) == Some(leader_id) {
             return;
         }
         if let Some(task) = self.start_following(leader_id, cx) {
@@ -2916,11 +2970,9 @@ impl Workspace {
         }
 
         // if you're already following, find the right pane and focus it.
-        for (pane, state) in &self.follower_states {
-            if leader_id == state.leader_id {
-                cx.focus_view(pane);
-                return;
-            }
+        if let Some(follower_state) = self.follower_states.get(&leader_id) {
+            cx.focus_view(&follower_state.pane());
+            return;
         }
 
         // Otherwise, follow.
@@ -2929,38 +2981,29 @@ impl Workspace {
         }
     }
 
-    pub fn unfollow(&mut self, pane: &View<Pane>, cx: &mut ViewContext<Self>) -> Option<PeerId> {
-        let state = self.follower_states.remove(pane)?;
-        let leader_id = state.leader_id;
+    pub fn unfollow(&mut self, leader_id: PeerId, cx: &mut ViewContext<Self>) -> Option<()> {
+        cx.notify();
+        let state = self.follower_states.remove(&leader_id)?;
         for (_, item) in state.items_by_leader_view_id {
-            item.set_leader_peer_id(None, cx);
+            item.view.set_leader_peer_id(None, cx);
         }
 
-        if self
-            .follower_states
-            .values()
-            .all(|state| state.leader_id != leader_id)
-        {
-            let project_id = self.project.read(cx).remote_id();
-            let room_id = self.active_call()?.read(cx).room()?.read(cx).id();
-            self.app_state
-                .client
-                .send(proto::Unfollow {
-                    room_id,
-                    project_id,
-                    leader_id: Some(leader_id),
-                })
-                .log_err();
-        }
+        let project_id = self.project.read(cx).remote_id();
+        let room_id = self.active_call()?.read(cx).room()?.read(cx).id();
+        self.app_state
+            .client
+            .send(proto::Unfollow {
+                room_id,
+                project_id,
+                leader_id: Some(leader_id),
+            })
+            .log_err();
 
-        cx.notify();
-        Some(leader_id)
+        Some(())
     }
 
     pub fn is_being_followed(&self, peer_id: PeerId) -> bool {
-        self.follower_states
-            .values()
-            .any(|state| state.leader_id == peer_id)
+        self.follower_states.contains_key(&peer_id)
     }
 
     fn active_item_path_changed(&mut self, cx: &mut ViewContext<Self>) {
@@ -3058,7 +3101,8 @@ impl Workspace {
         follower_project_id: Option<u64>,
         cx: &mut ViewContext<Self>,
     ) -> Option<proto::View> {
-        let item = self.active_item(cx)?;
+        let (item, panel_id) = self.active_item_for_followers(cx);
+        let item = item?;
         let leader_id = self
             .pane_for(&*item)
             .and_then(|pane| self.leader_for_pane(&pane));
@@ -3078,6 +3122,7 @@ impl Workspace {
             id: Some(id.to_proto()),
             leader_id,
             variant: Some(variant),
+            panel_id: panel_id.map(|id| id as i32),
         })
     }
 
@@ -3086,52 +3131,14 @@ impl Workspace {
         follower_project_id: Option<u64>,
         cx: &mut ViewContext<Self>,
     ) -> proto::FollowResponse {
-        let client = &self.app_state.client;
-        let project_id = self.project.read(cx).remote_id();
-
         let active_view = self.active_view_for_follower(follower_project_id, cx);
-        let active_view_id = active_view.as_ref().and_then(|view| view.id.clone());
 
         cx.notify();
-
         proto::FollowResponse {
+            // TODO: Remove after version 0.145.x stabilizes.
+            active_view_id: active_view.as_ref().and_then(|view| view.id.clone()),
+            views: active_view.iter().cloned().collect(),
             active_view,
-            // TODO: once v0.124.0 is retired we can stop sending these
-            active_view_id,
-            views: self
-                .panes()
-                .iter()
-                .flat_map(|pane| {
-                    let leader_id = self.leader_for_pane(pane);
-                    pane.read(cx).items().filter_map({
-                        let cx = &cx;
-                        move |item| {
-                            let item = item.to_followable_item_handle(cx)?;
-
-                            // If the item belongs to a particular project, then it should
-                            // only be included if this project is shared, and the follower
-                            // is in the project.
-                            //
-                            // Some items, like channel notes, do not belong to a particular
-                            // project, so they should be included regardless of whether the
-                            // current project is shared, or what project the follower is in.
-                            if item.is_project_item(cx)
-                                && (project_id.is_none() || project_id != follower_project_id)
-                            {
-                                return None;
-                            }
-
-                            let id = item.remote_id(client, cx)?.to_proto();
-                            let variant = item.to_state_proto(cx)?;
-                            Some(proto::View {
-                                id: Some(id),
-                                leader_id,
-                                variant: Some(variant),
-                            })
-                        }
-                    })
-                })
-                .collect(),
         }
     }
 
@@ -3153,34 +3160,43 @@ impl Workspace {
         cx: &mut AsyncWindowContext,
     ) -> Result<()> {
         match update.variant.ok_or_else(|| anyhow!("invalid update"))? {
-            proto::update_followers::Variant::UpdateActiveView(update_active_view) => {
-                let panes_missing_view = this.update(cx, |this, _| {
-                    let mut panes = vec![];
-                    for (pane, state) in &mut this.follower_states {
-                        if state.leader_id != leader_id {
-                            continue;
-                        }
+            proto::update_followers::Variant::CreateView(view) => {
+                let view_id = ViewId::from_proto(view.id.clone().context("invalid view id")?)?;
+                let should_add_view = this.update(cx, |this, _| {
+                    if let Some(state) = this.follower_states.get_mut(&leader_id) {
+                        anyhow::Ok(!state.items_by_leader_view_id.contains_key(&view_id))
+                    } else {
+                        anyhow::Ok(false)
+                    }
+                })??;
 
-                        state.active_view_id =
-                            if let Some(active_view_id) = update_active_view.id.clone() {
-                                Some(ViewId::from_proto(active_view_id)?)
-                            } else {
-                                None
-                            };
+                if should_add_view {
+                    Self::add_view_from_leader(this.clone(), leader_id, &view, cx).await?
+                }
+            }
+            proto::update_followers::Variant::UpdateActiveView(update_active_view) => {
+                let should_add_view = this.update(cx, |this, _| {
+                    if let Some(state) = this.follower_states.get_mut(&leader_id) {
+                        state.active_view_id = update_active_view
+                            .view
+                            .as_ref()
+                            .and_then(|view| ViewId::from_proto(view.id.clone()?).ok());
 
                         if state.active_view_id.is_some_and(|view_id| {
                             !state.items_by_leader_view_id.contains_key(&view_id)
                         }) {
-                            panes.push(pane.clone())
+                            anyhow::Ok(true)
+                        } else {
+                            anyhow::Ok(false)
                         }
+                    } else {
+                        anyhow::Ok(false)
                     }
-                    anyhow::Ok(panes)
                 })??;
 
-                if let Some(view) = update_active_view.view {
-                    for pane in panes_missing_view {
-                        Self::add_view_from_leader(this.clone(), leader_id, pane.clone(), &view, cx)
-                            .await?
+                if should_add_view {
+                    if let Some(view) = update_active_view.view {
+                        Self::add_view_from_leader(this.clone(), leader_id, &view, cx).await?
                     }
                 }
             }
@@ -3194,28 +3210,16 @@ impl Workspace {
                 let mut tasks = Vec::new();
                 this.update(cx, |this, cx| {
                     let project = this.project.clone();
-                    for (_, state) in &mut this.follower_states {
-                        if state.leader_id == leader_id {
-                            let view_id = ViewId::from_proto(id.clone())?;
-                            if let Some(item) = state.items_by_leader_view_id.get(&view_id) {
-                                tasks.push(item.apply_update_proto(&project, variant.clone(), cx));
-                            }
+                    if let Some(state) = this.follower_states.get(&leader_id) {
+                        let view_id = ViewId::from_proto(id.clone())?;
+                        if let Some(item) = state.items_by_leader_view_id.get(&view_id) {
+                            tasks.push(item.view.apply_update_proto(&project, variant.clone(), cx));
                         }
                     }
                     anyhow::Ok(())
                 })??;
                 try_join_all(tasks).await.log_err();
             }
-            proto::update_followers::Variant::CreateView(view) => {
-                let panes = this.update(cx, |this, _| {
-                    this.follower_states
-                        .iter()
-                        .filter_map(|(pane, state)| (state.leader_id == leader_id).then_some(pane))
-                        .cloned()
-                        .collect()
-                })?;
-                Self::add_views_from_leader(this.clone(), leader_id, panes, vec![view], cx).await?;
-            }
         }
         this.update(cx, |this, cx| this.leader_updated(leader_id, cx))?;
         Ok(())
@@ -3224,46 +3228,92 @@ impl Workspace {
     async fn add_view_from_leader(
         this: WeakView<Self>,
         leader_id: PeerId,
-        pane: View<Pane>,
         view: &proto::View,
         cx: &mut AsyncWindowContext,
     ) -> Result<()> {
         let this = this.upgrade().context("workspace dropped")?;
 
-        let item_builders = cx.update(|cx| {
-            cx.default_global::<FollowableItemBuilders>()
-                .values()
-                .map(|b| b.0)
-                .collect::<Vec<_>>()
-        })?;
-
         let Some(id) = view.id.clone() else {
             return Err(anyhow!("no id for view"));
         };
         let id = ViewId::from_proto(id)?;
+        let panel_id = view.panel_id.and_then(|id| proto::PanelId::from_i32(id));
+
+        let pane = this.update(cx, |this, _cx| {
+            let state = this
+                .follower_states
+                .get(&leader_id)
+                .context("stopped following")?;
+            anyhow::Ok(state.pane().clone())
+        })??;
+        let existing_item = pane.update(cx, |pane, cx| {
+            let client = this.read(cx).client().clone();
+            pane.items().find_map(|item| {
+                let item = item.to_followable_item_handle(cx)?;
+                if item.remote_id(&client, cx) == Some(id) {
+                    Some(item)
+                } else {
+                    None
+                }
+            })
+        })?;
+        let item = if let Some(existing_item) = existing_item {
+            existing_item
+        } else {
+            let variant = view.variant.clone();
+            if variant.is_none() {
+                Err(anyhow!("missing view variant"))?;
+            }
 
-        let mut variant = view.variant.clone();
-        if variant.is_none() {
-            Err(anyhow!("missing view variant"))?;
-        }
+            let task = cx.update(|cx| {
+                FollowableViewRegistry::from_state_proto(this.clone(), id, variant, cx)
+            })?;
 
-        let task = item_builders.iter().find_map(|build_item| {
-            cx.update(|cx| build_item(pane.clone(), this.clone(), id, &mut variant, cx))
-                .log_err()
-                .flatten()
-        });
-        let Some(task) = task else {
-            return Err(anyhow!(
-                "failed to construct view from leader (maybe from a different version of zed?)"
-            ));
-        };
+            let Some(task) = task else {
+                return Err(anyhow!(
+                    "failed to construct view from leader (maybe from a different version of zed?)"
+                ));
+            };
 
-        let item = task.await?;
+            let mut new_item = task.await?;
+            pane.update(cx, |pane, cx| {
+                let mut item_ix_to_remove = None;
+                for (ix, item) in pane.items().enumerate() {
+                    if let Some(item) = item.to_followable_item_handle(cx) {
+                        match new_item.dedup(item.as_ref(), cx) {
+                            Some(item::Dedup::KeepExisting) => {
+                                new_item =
+                                    item.boxed_clone().to_followable_item_handle(cx).unwrap();
+                                break;
+                            }
+                            Some(item::Dedup::ReplaceExisting) => {
+                                item_ix_to_remove = Some(ix);
+                                break;
+                            }
+                            None => {}
+                        }
+                    }
+                }
+
+                if let Some(ix) = item_ix_to_remove {
+                    pane.remove_item(ix, false, false, cx);
+                    pane.add_item(new_item.boxed_clone(), false, false, Some(ix), cx);
+                }
+            })?;
+
+            new_item
+        };
 
         this.update(cx, |this, cx| {
-            let state = this.follower_states.get_mut(&pane)?;
+            let state = this.follower_states.get_mut(&leader_id)?;
             item.set_leader_peer_id(Some(leader_id), cx);
-            state.items_by_leader_view_id.insert(id, item);
+            state.items_by_leader_view_id.insert(
+                id,
+                FollowerView {
+                    view: item,
+                    location: panel_id,
+                },
+            );
 
             Some(())
         })?;
@@ -3271,74 +3321,13 @@ impl Workspace {
         Ok(())
     }
 
-    async fn add_views_from_leader(
-        this: WeakView<Self>,
-        leader_id: PeerId,
-        panes: Vec<View<Pane>>,
-        views: Vec<proto::View>,
-        cx: &mut AsyncWindowContext,
-    ) -> Result<()> {
-        let this = this.upgrade().context("workspace dropped")?;
-
-        let item_builders = cx.update(|cx| {
-            cx.default_global::<FollowableItemBuilders>()
-                .values()
-                .map(|b| b.0)
-                .collect::<Vec<_>>()
-        })?;
-
-        let mut item_tasks_by_pane = HashMap::default();
-        for pane in panes {
-            let mut item_tasks = Vec::new();
-            let mut leader_view_ids = Vec::new();
-            for view in &views {
-                let Some(id) = &view.id else {
-                    continue;
-                };
-                let id = ViewId::from_proto(id.clone())?;
-                let mut variant = view.variant.clone();
-                if variant.is_none() {
-                    Err(anyhow!("missing view variant"))?;
-                }
-                for build_item in &item_builders {
-                    let task = cx.update(|cx| {
-                        build_item(pane.clone(), this.clone(), id, &mut variant, cx)
-                    })?;
-                    if let Some(task) = task {
-                        item_tasks.push(task);
-                        leader_view_ids.push(id);
-                        break;
-                    } else if variant.is_none() {
-                        Err(anyhow!(
-                            "failed to construct view from leader (maybe from a different version of zed?)"
-                        ))?;
-                    }
-                }
-            }
-
-            item_tasks_by_pane.insert(pane, (item_tasks, leader_view_ids));
-        }
-
-        for (pane, (item_tasks, leader_view_ids)) in item_tasks_by_pane {
-            let items = futures::future::try_join_all(item_tasks).await?;
-            this.update(cx, |this, cx| {
-                let state = this.follower_states.get_mut(&pane)?;
-                for (id, item) in leader_view_ids.into_iter().zip(items) {
-                    item.set_leader_peer_id(Some(leader_id), cx);
-                    state.items_by_leader_view_id.insert(id, item);
-                }
-
-                Some(())
-            })?;
-        }
-        Ok(())
-    }
-
     pub fn update_active_view_for_followers(&mut self, cx: &mut WindowContext) {
         let mut is_project_item = true;
         let mut update = proto::UpdateActiveView::default();
         if cx.is_window_active() {
-            if let Some(item) = self.active_item(cx) {
+            let (active_item, panel_id) = self.active_item_for_followers(cx);
+
+            if let Some(item) = active_item {
                 if item.focus_handle(cx).contains_focused(cx) {
                     let leader_id = self
                         .pane_for(&*item)
@@ -3355,13 +3344,14 @@ impl Workspace {
                                     id: Some(id.clone()),
                                     leader_id,
                                     variant: Some(variant),
+                                    panel_id: panel_id.map(|id| id as i32),
                                 });
 
                                 is_project_item = item.is_project_item(cx);
                                 update = proto::UpdateActiveView {
                                     view,
-                                    // TODO: once v0.124.0 is retired we can stop sending these
-                                    id: Some(id),
+                                    // TODO: Remove after version 0.145.x stabilizes.
+                                    id: Some(id.clone()),
                                     leader_id,
                                 };
                             }
@@ -3371,8 +3361,9 @@ impl Workspace {
             }
         }
 
-        if &update.id != &self.last_active_view_id {
-            self.last_active_view_id.clone_from(&update.id);
+        let active_view_id = update.view.as_ref().and_then(|view| view.id.as_ref());
+        if active_view_id != self.last_active_view_id.as_ref() {
+            self.last_active_view_id = active_view_id.cloned();
             self.update_followers(
                 is_project_item,
                 proto::update_followers::Variant::UpdateActiveView(update),
@@ -3381,6 +3372,32 @@ impl Workspace {
         }
     }
 
+    fn active_item_for_followers(
+        &self,
+        cx: &mut WindowContext,
+    ) -> (Option<Box<dyn ItemHandle>>, Option<proto::PanelId>) {
+        let mut active_item = None;
+        let mut panel_id = None;
+        for dock in [&self.left_dock, &self.right_dock, &self.bottom_dock] {
+            if dock.focus_handle(cx).contains_focused(cx) {
+                if let Some(panel) = dock.read(cx).active_panel() {
+                    if let Some(pane) = panel.pane(cx) {
+                        if let Some(item) = pane.read(cx).active_item() {
+                            active_item = Some(item);
+                            panel_id = panel.remote_id();
+                            break;
+                        }
+                    }
+                }
+            }
+        }
+
+        if active_item.is_none() {
+            active_item = self.active_pane().read(cx).active_item();
+        }
+        (active_item, panel_id)
+    }
+
     fn update_followers(
         &self,
         project_only: bool,
@@ -3402,7 +3419,13 @@ impl Workspace {
     }
 
     pub fn leader_for_pane(&self, pane: &View<Pane>) -> Option<PeerId> {
-        self.follower_states.get(pane).map(|state| state.leader_id)
+        self.follower_states.iter().find_map(|(leader_id, state)| {
+            if state.center_pane == *pane || state.dock_pane.as_ref() == Some(pane) {
+                Some(*leader_id)
+            } else {
+                None
+            }
+        })
     }
 
     fn leader_updated(&mut self, leader_id: PeerId, cx: &mut ViewContext<Self>) -> Option<()> {
@@ -3411,7 +3434,6 @@ impl Workspace {
         let call = self.active_call()?;
         let room = call.read(cx).room()?.read(cx);
         let participant = room.remote_participant_for_peer_id(leader_id)?;
-        let mut items_to_activate = Vec::new();
 
         let leader_in_this_app;
         let leader_in_this_project;
@@ -3430,38 +3452,48 @@ impl Workspace {
             }
         };
 
-        for (pane, state) in &self.follower_states {
-            if state.leader_id != leader_id {
-                continue;
-            }
-            if let (Some(active_view_id), true) = (state.active_view_id, leader_in_this_app) {
-                if let Some(item) = state.items_by_leader_view_id.get(&active_view_id) {
-                    if leader_in_this_project || !item.is_project_item(cx) {
-                        items_to_activate.push((pane.clone(), item.boxed_clone()));
-                    }
+        let state = self.follower_states.get(&leader_id)?;
+        let mut item_to_activate = None;
+        if let (Some(active_view_id), true) = (state.active_view_id, leader_in_this_app) {
+            if let Some(item) = state.items_by_leader_view_id.get(&active_view_id) {
+                if leader_in_this_project || !item.view.is_project_item(cx) {
+                    item_to_activate = Some((item.location, item.view.boxed_clone()));
                 }
-                continue;
             }
+        } else if let Some(shared_screen) =
+            self.shared_screen_for_peer(leader_id, &state.center_pane, cx)
+        {
+            item_to_activate = Some((None, Box::new(shared_screen)));
+        }
+
+        let (panel_id, item) = item_to_activate?;
 
-            if let Some(shared_screen) = self.shared_screen_for_peer(leader_id, pane, cx) {
-                items_to_activate.push((pane.clone(), Box::new(shared_screen)));
+        let mut transfer_focus = state.center_pane.read(cx).has_focus(cx);
+        let pane;
+        if let Some(panel_id) = panel_id {
+            pane = self.activate_panel_for_proto_id(panel_id, cx)?.pane(cx)?;
+            let state = self.follower_states.get_mut(&leader_id)?;
+            state.dock_pane = Some(pane.clone());
+        } else {
+            pane = state.center_pane.clone();
+            let state = self.follower_states.get_mut(&leader_id)?;
+            if let Some(dock_pane) = state.dock_pane.take() {
+                transfer_focus |= dock_pane.focus_handle(cx).contains_focused(cx);
             }
         }
 
-        for (pane, item) in items_to_activate {
-            let pane_was_focused = pane.read(cx).has_focus(cx);
-            if let Some(index) = pane.update(cx, |pane, _| pane.index_for_item(item.as_ref())) {
-                pane.update(cx, |pane, cx| pane.activate_item(index, false, false, cx));
+        pane.update(cx, |pane, cx| {
+            let focus_active_item = pane.has_focus(cx) || transfer_focus;
+            if let Some(index) = pane.index_for_item(item.as_ref()) {
+                pane.activate_item(index, false, false, cx);
             } else {
-                pane.update(cx, |pane, cx| {
-                    pane.add_item(item.boxed_clone(), false, false, None, cx)
-                });
+                pane.add_item(item.boxed_clone(), false, false, None, cx)
             }
 
-            if pane_was_focused {
-                pane.update(cx, |pane, cx| pane.focus_active_item(cx));
+            if focus_active_item {
+                pane.focus_active_item(cx)
             }
-        }
+        });
 
         None
     }
@@ -3848,7 +3880,7 @@ impl Workspace {
             .on_action(cx.listener(Self::follow_next_collaborator))
             .on_action(cx.listener(|workspace, _: &Unfollow, cx| {
                 let pane = workspace.active_pane().clone();
-                workspace.unfollow(&pane, cx);
+                workspace.unfollow_in_pane(&pane, cx);
             }))
             .on_action(cx.listener(|workspace, action: &Save, cx| {
                 workspace
@@ -3995,6 +4027,65 @@ impl Workspace {
             .unwrap_or(Self::DEFAULT_PADDING)
             .clamp(0.0, Self::MAX_PADDING)
     }
+
+    fn render_dock(
+        &self,
+        position: DockPosition,
+        dock: &View<Dock>,
+        cx: &WindowContext,
+    ) -> Option<Div> {
+        if self.zoomed_position == Some(position) {
+            return None;
+        }
+
+        let leader_border = dock.read(cx).active_panel().and_then(|panel| {
+            let pane = panel.pane(cx)?;
+            let follower_states = &self.follower_states;
+            leader_border_for_pane(follower_states, &pane, cx)
+        });
+
+        Some(
+            div()
+                .flex()
+                .flex_none()
+                .overflow_hidden()
+                .child(dock.clone())
+                .children(leader_border),
+        )
+    }
+}
+
+fn leader_border_for_pane(
+    follower_states: &HashMap<PeerId, FollowerState>,
+    pane: &View<Pane>,
+    cx: &WindowContext,
+) -> Option<Div> {
+    let (leader_id, _follower_state) = follower_states.iter().find_map(|(leader_id, state)| {
+        if state.pane() == pane {
+            Some((*leader_id, state))
+        } else {
+            None
+        }
+    })?;
+
+    let room = ActiveCall::try_global(cx)?.read(cx).room()?.read(cx);
+    let leader = room.remote_participant_for_peer_id(leader_id)?;
+
+    let mut leader_color = cx
+        .theme()
+        .players()
+        .color_for_participant(leader.participant_index.0)
+        .cursor;
+    leader_color.fade_out(0.3);
+    Some(
+        div()
+            .absolute()
+            .size_full()
+            .left_0()
+            .top_0()
+            .border_2()
+            .border_color(leader_color),
+    )
 }
 
 fn window_bounds_env_override() -> Option<Bounds<Pixels>> {