Add the ability to follow the agent as it makes edits (#29839)

Antonio Scandurra , Nathan Sobo , and Max Brunsfeld created

Nathan here: I also tacked on a bunch of UI refinement.

Release Notes:

- Introduced the ability to follow the agent around as it reads and
edits files.

---------

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

Change summary

Cargo.lock                                             |   1 
assets/icons/crosshair.svg                             |   1 
crates/agent/src/assistant.rs                          |   3 
crates/agent/src/assistant_model_selector.rs           |  12 
crates/agent/src/assistant_panel.rs                    |   7 
crates/agent/src/context_strip.rs                      |  28 
crates/agent/src/message_editor.rs                     | 115 +
crates/agent/src/profile_selector.rs                   |  33 
crates/agent/src/thread.rs                             |  11 
crates/assistant_context_editor/src/context_editor.rs  |  14 
crates/assistant_tool/src/action_log.rs                |   4 
crates/assistant_tools/src/edit_agent.rs               | 332 +++++
crates/assistant_tools/src/edit_agent/evals.rs         |   6 
crates/assistant_tools/src/edit_file_tool.rs           |  31 
crates/assistant_tools/src/read_file_tool.rs           |  47 
crates/assistant_tools/src/streaming_edit_file_tool.rs |   2 
crates/clock/src/clock.rs                              |   1 
crates/collab/src/tests/channel_buffer_tests.rs        |  12 
crates/collab/src/tests/following_tests.rs             |  36 
crates/collab_ui/src/channel_view.rs                   |  11 
crates/debugger_ui/src/session.rs                      |   8 
crates/editor/src/editor.rs                            |  60 
crates/editor/src/editor_tests.rs                      |   4 
crates/editor/src/element.rs                           |  61 
crates/editor/src/items.rs                             |  40 
crates/icons/src/icons.rs                              |   1 
crates/image_viewer/src/image_viewer.rs                |   2 
crates/language/src/buffer.rs                          |  27 
crates/project/src/project.rs                          |  62 +
crates/project_panel/src/project_panel_tests.rs        |   2 
crates/repl/src/notebook/notebook_ui.rs                |   2 
crates/theme/src/styles/players.rs                     |   4 
crates/vim/src/vim.rs                                  |   4 
crates/workspace/Cargo.toml                            |   1 
crates/workspace/src/item.rs                           |  79 +
crates/workspace/src/pane_group.rs                     | 170 +-
crates/workspace/src/workspace.rs                      | 592 ++++++++---
37 files changed, 1,257 insertions(+), 569 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -18092,7 +18092,6 @@ dependencies = [
  "component",
  "dap",
  "db",
- "derive_more",
  "env_logger 0.11.8",
  "fs",
  "futures 0.3.31",

assets/icons/crosshair.svg 🔗

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-crosshair-icon lucide-crosshair"><circle cx="12" cy="12" r="10"/><line x1="22" x2="18" y1="12" y2="12"/><line x1="6" x2="2" y1="12" y2="12"/><line x1="12" x2="12" y1="6" y2="2"/><line x1="12" x2="12" y1="22" y2="18"/></svg>

crates/agent/src/assistant_model_selector.rs 🔗

@@ -104,10 +104,9 @@ impl Render for AssistantModelSelector {
         let focus_handle = self.focus_handle.clone();
 
         let model = self.selector.read(cx).active_model(cx);
-        let (model_name, model_icon) = match model {
-            Some(model) => (model.model.name().0, Some(model.provider.icon())),
-            _ => (SharedString::from("No model selected"), None),
-        };
+        let model_name = model
+            .map(|model| model.model.name().0)
+            .unwrap_or_else(|| SharedString::from("No model selected"));
 
         LanguageModelSelectorPopoverMenu::new(
             self.selector.clone(),
@@ -116,11 +115,6 @@ impl Render for AssistantModelSelector {
                 .child(
                     h_flex()
                         .gap_0p5()
-                        .children(
-                            model_icon.map(|icon| {
-                                Icon::new(icon).color(Color::Muted).size(IconSize::Small)
-                            }),
-                        )
                         .child(
                             Label::new(model_name)
                                 .size(LabelSize::Small)

crates/agent/src/assistant_panel.rs 🔗

@@ -37,8 +37,8 @@ use ui::{
     Banner, ContextMenu, KeyBinding, PopoverMenu, PopoverMenuHandle, Tab, Tooltip, prelude::*,
 };
 use util::ResultExt as _;
-use workspace::Workspace;
 use workspace::dock::{DockPosition, Panel, PanelEvent};
+use workspace::{CollaboratorId, Workspace};
 use zed_actions::agent::OpenConfiguration;
 use zed_actions::assistant::{OpenRulesLibrary, ToggleFocus};
 
@@ -52,7 +52,7 @@ use crate::thread_history::{PastContext, PastThread, ThreadHistory};
 use crate::thread_store::ThreadStore;
 use crate::ui::UsageBanner;
 use crate::{
-    AddContextServer, AgentDiffPane, DeleteRecentlyOpenThread, ExpandMessageEditor,
+    AddContextServer, AgentDiffPane, DeleteRecentlyOpenThread, ExpandMessageEditor, Follow,
     InlineAssistant, NewTextThread, NewThread, OpenActiveThreadAsMarkdown, OpenAgentDiff,
     OpenHistory, ThreadEvent, ToggleContextPicker, ToggleNavigationMenu, ToggleOptionsMenu,
 };
@@ -107,6 +107,9 @@ pub fn init(cx: &mut App) {
                         AgentDiffPane::deploy_in_workspace(thread, workspace, window, cx);
                     }
                 })
+                .register_action(|workspace, _: &Follow, window, cx| {
+                    workspace.follow(CollaboratorId::Agent, window, cx);
+                })
                 .register_action(|workspace, _: &ExpandMessageEditor, window, cx| {
                     if let Some(panel) = workspace.panel::<AssistantPanel>(cx) {
                         workspace.focus_panel::<AssistantPanel>(window, cx);

crates/agent/src/context_strip.rs 🔗

@@ -11,7 +11,7 @@ use gpui::{
 use itertools::Itertools;
 use language::Buffer;
 use project::ProjectItem;
-use ui::{KeyBinding, PopoverMenu, PopoverMenuHandle, Tooltip, prelude::*};
+use ui::{PopoverMenu, PopoverMenuHandle, Tooltip, prelude::*};
 use workspace::Workspace;
 
 use crate::context::{AgentContextHandle, ContextKind};
@@ -357,7 +357,7 @@ impl Focusable for ContextStrip {
 }
 
 impl Render for ContextStrip {
-    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
+    fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
         let context_picker = self.context_picker.clone();
         let focus_handle = self.focus_handle.clone();
 
@@ -434,30 +434,6 @@ impl Render for ContextStrip {
                     })
                     .with_handle(self.context_picker_menu_handle.clone()),
             )
-            .when(no_added_context && suggested_context.is_none(), {
-                |parent| {
-                    parent.child(
-                        h_flex()
-                            .ml_1p5()
-                            .gap_2()
-                            .child(
-                                Label::new("Add Context")
-                                    .size(LabelSize::Small)
-                                    .color(Color::Muted),
-                            )
-                            .opacity(0.5)
-                            .children(
-                                KeyBinding::for_action_in(
-                                    &ToggleContextPicker,
-                                    &focus_handle,
-                                    window,
-                                    cx,
-                                )
-                                .map(|binding| binding.into_any_element()),
-                            ),
-                    )
-                }
-            })
             .children(
                 added_contexts
                     .into_iter()

crates/agent/src/message_editor.rs 🔗

@@ -32,7 +32,7 @@ use std::time::Duration;
 use theme::ThemeSettings;
 use ui::{Disclosure, KeyBinding, PopoverMenuHandle, Tooltip, prelude::*};
 use util::ResultExt as _;
-use workspace::Workspace;
+use workspace::{CollaboratorId, Workspace};
 use zed_llm_client::CompletionMode;
 
 use crate::context_picker::{ContextPicker, ContextPickerCompletionProvider, crease_for_mention};
@@ -42,7 +42,7 @@ use crate::profile_selector::ProfileSelector;
 use crate::thread::{MessageCrease, Thread, TokenUsageRatio};
 use crate::thread_store::ThreadStore;
 use crate::{
-    ActiveThread, AgentDiffPane, Chat, ExpandMessageEditor, NewThread, OpenAgentDiff,
+    ActiveThread, AgentDiffPane, Chat, ExpandMessageEditor, Follow, NewThread, OpenAgentDiff,
     RemoveAllContext, ToggleContextPicker, ToggleProfileSelector, register_agent_preview,
 };
 
@@ -97,7 +97,7 @@ pub(crate) fn create_editor(
             window,
             cx,
         );
-        editor.set_placeholder_text("Ask anything, @ to mention, ↑ to select", cx);
+        editor.set_placeholder_text("Message the agent – @ to include context", cx);
         editor.set_show_indent_guides(false, cx);
         editor.set_soft_wrap();
         editor.set_context_menu_options(ContextMenuOptions {
@@ -200,8 +200,7 @@ impl MessageEditor {
             model_selector,
             edits_expanded: false,
             editor_is_expanded: false,
-            profile_selector: cx
-                .new(|cx| ProfileSelector::new(fs, thread_store, editor.focus_handle(cx), cx)),
+            profile_selector: cx.new(|cx| ProfileSelector::new(fs, thread_store, cx)),
             last_estimated_token_count: None,
             update_token_count_task: None,
             _subscriptions: subscriptions,
@@ -457,6 +456,44 @@ impl MessageEditor {
         )
     }
 
+    fn render_follow_toggle(&self, cx: &mut Context<Self>) -> impl IntoElement {
+        let following = self
+            .workspace
+            .read_with(cx, |workspace, _| {
+                workspace.is_being_followed(CollaboratorId::Agent)
+            })
+            .unwrap_or(false);
+        IconButton::new("follow-agent", IconName::Crosshair)
+            .icon_size(IconSize::Small)
+            .icon_color(Color::Muted)
+            .toggle_state(following)
+            .selected_icon_color(Some(Color::Custom(cx.theme().players().agent().cursor)))
+            .tooltip(move |window, cx| {
+                if following {
+                    Tooltip::for_action("Stop Following Agent", &Follow, window, cx)
+                } else {
+                    Tooltip::with_meta(
+                        "Follow Agent",
+                        Some(&Follow),
+                        "Track the agent's location as it reads and edits files.",
+                        window,
+                        cx,
+                    )
+                }
+            })
+            .on_click(cx.listener(move |this, _, window, cx| {
+                this.workspace
+                    .update(cx, |workspace, cx| {
+                        if following {
+                            workspace.unfollow(CollaboratorId::Agent, window, cx);
+                        } else {
+                            workspace.follow(CollaboratorId::Agent, window, cx);
+                        }
+                    })
+                    .ok();
+            }))
+    }
+
     fn render_editor(
         &self,
         font_size: Rems,
@@ -522,34 +559,39 @@ impl MessageEditor {
                     .items_start()
                     .justify_between()
                     .child(self.context_strip.clone())
-                    .when(focus_handle.is_focused(window), |this| {
-                        this.child(
-                            IconButton::new("toggle-height", expand_icon)
-                                .icon_size(IconSize::XSmall)
-                                .icon_color(Color::Muted)
-                                .tooltip({
-                                    let focus_handle = focus_handle.clone();
-                                    move |window, cx| {
-                                        let expand_label = if is_editor_expanded {
-                                            "Minimize Message Editor".to_string()
-                                        } else {
-                                            "Expand Message Editor".to_string()
-                                        };
-
-                                        Tooltip::for_action_in(
-                                            expand_label,
-                                            &ExpandMessageEditor,
-                                            &focus_handle,
-                                            window,
-                                            cx,
-                                        )
-                                    }
-                                })
-                                .on_click(cx.listener(|_, _, window, cx| {
-                                    window.dispatch_action(Box::new(ExpandMessageEditor), cx);
-                                })),
-                        )
-                    }),
+                    .child(
+                        h_flex()
+                            .gap_1()
+                            .when(focus_handle.is_focused(window), |this| {
+                                this.child(
+                                    IconButton::new("toggle-height", expand_icon)
+                                        .icon_size(IconSize::XSmall)
+                                        .icon_color(Color::Muted)
+                                        .tooltip({
+                                            let focus_handle = focus_handle.clone();
+                                            move |window, cx| {
+                                                let expand_label = if is_editor_expanded {
+                                                    "Minimize Message Editor".to_string()
+                                                } else {
+                                                    "Expand Message Editor".to_string()
+                                                };
+
+                                                Tooltip::for_action_in(
+                                                    expand_label,
+                                                    &ExpandMessageEditor,
+                                                    &focus_handle,
+                                                    window,
+                                                    cx,
+                                                )
+                                            }
+                                        })
+                                        .on_click(cx.listener(|_, _, window, cx| {
+                                            window
+                                                .dispatch_action(Box::new(ExpandMessageEditor), cx);
+                                        })),
+                                )
+                            }),
+                    ),
             )
             .child(
                 v_flex()
@@ -592,7 +634,12 @@ impl MessageEditor {
                         h_flex()
                             .flex_none()
                             .justify_between()
-                            .child(h_flex().gap_2().child(self.profile_selector.clone()))
+                            .child(
+                                h_flex()
+                                    .gap_1()
+                                    .child(self.render_follow_toggle(cx))
+                                    .child(self.profile_selector.clone()),
+                            )
                             .child(
                                 h_flex()
                                     .gap_1()

crates/agent/src/profile_selector.rs 🔗

@@ -4,22 +4,20 @@ use assistant_settings::{
     AgentProfile, AgentProfileId, AssistantSettings, GroupedAgentProfiles, builtin_profiles,
 };
 use fs::Fs;
-use gpui::{Action, Entity, FocusHandle, Subscription, WeakEntity, prelude::*};
+use gpui::{Action, Entity, Subscription, WeakEntity, prelude::*};
 use language_model::LanguageModelRegistry;
 use settings::{Settings as _, SettingsStore, update_settings_file};
 use ui::{
-    ButtonLike, ContextMenu, ContextMenuEntry, KeyBinding, PopoverMenu, PopoverMenuHandle, Tooltip,
-    prelude::*,
+    ButtonLike, ContextMenu, ContextMenuEntry, PopoverMenu, PopoverMenuHandle, Tooltip, prelude::*,
 };
 use util::ResultExt as _;
 
-use crate::{ManageProfiles, ThreadStore, ToggleProfileSelector};
+use crate::{ManageProfiles, ThreadStore};
 
 pub struct ProfileSelector {
     profiles: GroupedAgentProfiles,
     fs: Arc<dyn Fs>,
     thread_store: WeakEntity<ThreadStore>,
-    focus_handle: FocusHandle,
     menu_handle: PopoverMenuHandle<ContextMenu>,
     _subscriptions: Vec<Subscription>,
 }
@@ -28,7 +26,6 @@ impl ProfileSelector {
     pub fn new(
         fs: Arc<dyn Fs>,
         thread_store: WeakEntity<ThreadStore>,
-        focus_handle: FocusHandle,
         cx: &mut Context<Self>,
     ) -> Self {
         let settings_subscription = cx.observe_global::<SettingsStore>(move |this, cx| {
@@ -39,7 +36,6 @@ impl ProfileSelector {
             profiles: GroupedAgentProfiles::from_settings(AssistantSettings::get_global(cx)),
             fs,
             thread_store,
-            focus_handle,
             menu_handle: PopoverMenuHandle::default(),
             _subscriptions: vec![settings_subscription],
         }
@@ -132,7 +128,7 @@ impl ProfileSelector {
 }
 
 impl Render for ProfileSelector {
-    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
+    fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
         let settings = AssistantSettings::get_global(cx);
         let profile_id = &settings.default_profile;
         let profile = settings.profiles.get(profile_id);
@@ -146,15 +142,7 @@ impl Render for ProfileSelector {
             .default_model()
             .map_or(false, |default| default.model.supports_tools());
 
-        let icon = match profile_id.as_str() {
-            builtin_profiles::WRITE => IconName::Pencil,
-            builtin_profiles::ASK => IconName::MessageBubbles,
-            builtin_profiles::MANUAL => IconName::MessageBubbleDashed,
-            _ => IconName::UserRoundPen,
-        };
-
         let this = cx.entity().clone();
-        let focus_handle = self.focus_handle.clone();
 
         PopoverMenu::new("profile-selector")
             .menu(move |window, cx| {
@@ -164,7 +152,6 @@ impl Render for ProfileSelector {
                 ButtonLike::new("profile-selector-button").child(
                     h_flex()
                         .gap_1()
-                        .child(Icon::new(icon).size(IconSize::XSmall).color(Color::Muted))
                         .child(
                             Label::new(selected_profile)
                                 .size(LabelSize::Small)
@@ -174,17 +161,7 @@ impl Render for ProfileSelector {
                             Icon::new(IconName::ChevronDown)
                                 .size(IconSize::XSmall)
                                 .color(Color::Muted),
-                        )
-                        .child(div().opacity(0.5).children({
-                            let focus_handle = focus_handle.clone();
-                            KeyBinding::for_action_in(
-                                &ToggleProfileSelector,
-                                &focus_handle,
-                                window,
-                                cx,
-                            )
-                            .map(|kb| kb.size(rems_from_px(10.)))
-                        })),
+                        ),
                 )
             } else {
                 ButtonLike::new("tools-not-supported-button")

crates/agent/src/thread.rs 🔗

@@ -1582,10 +1582,17 @@ impl Thread {
                                 let tool_uses = thread.use_pending_tools(window, cx, model.clone());
                                 cx.emit(ThreadEvent::UsePendingTools { tool_uses });
                             }
-                            StopReason::EndTurn => {}
-                            StopReason::MaxTokens => {}
+                            StopReason::EndTurn | StopReason::MaxTokens  => {
+                                thread.project.update(cx, |project, cx| {
+                                    project.set_agent_location(None, cx);
+                                });
+                            }
                         },
                         Err(error) => {
+                            thread.project.update(cx, |project, cx| {
+                                project.set_agent_location(None, cx);
+                            });
+
                             if error.is::<PaymentRequiredError>() {
                                 cx.emit(ThreadEvent::ShowError(ThreadError::PaymentRequired));
                             } else if error.is::<MaxMonthlySpendReachedError>() {

crates/assistant_context_editor/src/context_editor.rs 🔗

@@ -62,7 +62,10 @@ use ui::{
     prelude::*,
 };
 use util::{ResultExt, maybe};
-use workspace::searchable::{Direction, SearchableItemHandle};
+use workspace::{
+    CollaboratorId,
+    searchable::{Direction, SearchableItemHandle},
+};
 use workspace::{
     Save, Toast, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, Workspace,
     item::{self, FollowableItem, Item, ItemHandle},
@@ -3417,15 +3420,14 @@ impl FollowableItem for ContextEditor {
         true
     }
 
-    fn set_leader_peer_id(
+    fn set_leader_id(
         &mut self,
-        leader_peer_id: Option<proto::PeerId>,
+        leader_id: Option<CollaboratorId>,
         window: &mut Window,
         cx: &mut Context<Self>,
     ) {
-        self.editor.update(cx, |editor, cx| {
-            editor.set_leader_peer_id(leader_peer_id, window, cx)
-        })
+        self.editor
+            .update(cx, |editor, cx| editor.set_leader_id(leader_id, window, cx))
     }
 
     fn dedup(&self, existing: &Self, _window: &Window, cx: &App) -> Option<item::Dedup> {

crates/assistant_tool/src/action_log.rs 🔗

@@ -29,6 +29,10 @@ impl ActionLog {
         }
     }
 
+    pub fn project(&self) -> &Entity<Project> {
+        &self.project
+    }
+
     /// Notifies a diagnostics check
     pub fn checked_project_diagnostics(&mut self) {
         self.edited_since_project_diagnostics_check = false;

crates/assistant_tools/src/edit_agent.rs 🔗

@@ -19,6 +19,7 @@ use language_model::{
     LanguageModel, LanguageModelCompletionError, LanguageModelRequest, LanguageModelRequestMessage,
     MessageContent, Role,
 };
+use project::{AgentLocation, Project};
 use serde::Serialize;
 use std::{cmp, iter, mem, ops::Range, path::PathBuf, sync::Arc, task::Poll};
 use streaming_diff::{CharOperation, StreamingDiff};
@@ -59,17 +60,20 @@ pub struct EditAgentOutput {
 pub struct EditAgent {
     model: Arc<dyn LanguageModel>,
     action_log: Entity<ActionLog>,
+    project: Entity<Project>,
     templates: Arc<Templates>,
 }
 
 impl EditAgent {
     pub fn new(
         model: Arc<dyn LanguageModel>,
+        project: Entity<Project>,
         action_log: Entity<ActionLog>,
         templates: Arc<Templates>,
     ) -> Self {
         EditAgent {
             model,
+            project,
             action_log,
             templates,
         }
@@ -118,37 +122,72 @@ impl EditAgent {
         let (output_events_tx, output_events_rx) = mpsc::unbounded();
         let this = self.clone();
         let task = cx.spawn(async move |cx| {
-            // Ensure the buffer is tracked by the action log.
             this.action_log
                 .update(cx, |log, cx| log.track_buffer(buffer.clone(), cx))?;
+            let output = this
+                .replace_text_with_chunks_internal(buffer, edit_chunks, output_events_tx, cx)
+                .await;
+            this.project
+                .update(cx, |project, cx| project.set_agent_location(None, cx))?;
+            output
+        });
+        (task, output_events_rx)
+    }
 
+    async fn replace_text_with_chunks_internal(
+        &self,
+        buffer: Entity<Buffer>,
+        edit_chunks: impl 'static + Send + Stream<Item = Result<String, LanguageModelCompletionError>>,
+        output_events_tx: mpsc::UnboundedSender<EditAgentOutputEvent>,
+        cx: &mut AsyncApp,
+    ) -> Result<EditAgentOutput> {
+        cx.update(|cx| {
+            buffer.update(cx, |buffer, cx| buffer.set_text("", cx));
+            self.action_log.update(cx, |log, cx| {
+                log.buffer_edited(buffer.clone(), cx);
+            });
+            self.project.update(cx, |project, cx| {
+                project.set_agent_location(
+                    Some(AgentLocation {
+                        buffer: buffer.downgrade(),
+                        position: language::Anchor::MAX,
+                    }),
+                    cx,
+                )
+            });
+            output_events_tx
+                .unbounded_send(EditAgentOutputEvent::Edited)
+                .ok();
+        })?;
+
+        let mut raw_edits = String::new();
+        pin_mut!(edit_chunks);
+        while let Some(chunk) = edit_chunks.next().await {
+            let chunk = chunk?;
+            raw_edits.push_str(&chunk);
             cx.update(|cx| {
-                buffer.update(cx, |buffer, cx| buffer.set_text("", cx));
-                this.action_log
+                buffer.update(cx, |buffer, cx| buffer.append(chunk, cx));
+                self.action_log
                     .update(cx, |log, cx| log.buffer_edited(buffer.clone(), cx));
+                self.project.update(cx, |project, cx| {
+                    project.set_agent_location(
+                        Some(AgentLocation {
+                            buffer: buffer.downgrade(),
+                            position: language::Anchor::MAX,
+                        }),
+                        cx,
+                    )
+                });
             })?;
+            output_events_tx
+                .unbounded_send(EditAgentOutputEvent::Edited)
+                .ok();
+        }
 
-            let mut raw_edits = String::new();
-            pin_mut!(edit_chunks);
-            while let Some(chunk) = edit_chunks.next().await {
-                let chunk = chunk?;
-                raw_edits.push_str(&chunk);
-                cx.update(|cx| {
-                    buffer.update(cx, |buffer, cx| buffer.append(chunk, cx));
-                    this.action_log
-                        .update(cx, |log, cx| log.buffer_edited(buffer.clone(), cx));
-                })?;
-                output_events_tx
-                    .unbounded_send(EditAgentOutputEvent::Edited)
-                    .ok();
-            }
-
-            Ok(EditAgentOutput {
-                _raw_edits: raw_edits,
-                _parser_metrics: EditParserMetrics::default(),
-            })
-        });
-        (task, output_events_rx)
+        Ok(EditAgentOutput {
+            _raw_edits: raw_edits,
+            _parser_metrics: EditParserMetrics::default(),
+        })
     }
 
     pub fn edit(
@@ -161,6 +200,18 @@ impl EditAgent {
         Task<Result<EditAgentOutput>>,
         mpsc::UnboundedReceiver<EditAgentOutputEvent>,
     ) {
+        self.project
+            .update(cx, |project, cx| {
+                project.set_agent_location(
+                    Some(AgentLocation {
+                        buffer: buffer.downgrade(),
+                        position: language::Anchor::MIN,
+                    }),
+                    cx,
+                );
+            })
+            .ok();
+
         let this = self.clone();
         let (events_tx, events_rx) = mpsc::unbounded();
         let output = cx.spawn(async move |cx| {
@@ -194,8 +245,14 @@ impl EditAgent {
         let (output_events_tx, output_events_rx) = mpsc::unbounded();
         let this = self.clone();
         let task = cx.spawn(async move |mut cx| {
-            this.apply_edits_internal(buffer, edit_chunks, output_events_tx, &mut cx)
-                .await
+            this.action_log
+                .update(cx, |log, cx| log.track_buffer(buffer.clone(), cx))?;
+            let output = this
+                .apply_edits_internal(buffer, edit_chunks, output_events_tx, &mut cx)
+                .await;
+            this.project
+                .update(cx, |project, cx| project.set_agent_location(None, cx))?;
+            output
         });
         (task, output_events_rx)
     }
@@ -207,10 +264,6 @@ impl EditAgent {
         output_events: mpsc::UnboundedSender<EditAgentOutputEvent>,
         cx: &mut AsyncApp,
     ) -> Result<EditAgentOutput> {
-        // Ensure the buffer is tracked by the action log.
-        self.action_log
-            .update(cx, |log, cx| log.track_buffer(buffer.clone(), cx))?;
-
         let (output, mut edit_events) = Self::parse_edit_chunks(edit_chunks, cx);
         while let Some(edit_event) = edit_events.next().await {
             let EditParserEvent::OldText(old_text_query) = edit_event? else {
@@ -275,14 +328,15 @@ impl EditAgent {
                         match op {
                             CharOperation::Insert { text } => {
                                 let edit_start = snapshot.anchor_after(edit_start);
-                                edits_tx.unbounded_send((edit_start..edit_start, text))?;
+                                edits_tx
+                                    .unbounded_send((edit_start..edit_start, Arc::from(text)))?;
                             }
                             CharOperation::Delete { bytes } => {
                                 let edit_end = edit_start + bytes;
                                 let edit_range = snapshot.anchor_after(edit_start)
                                     ..snapshot.anchor_before(edit_end);
                                 edit_start = edit_end;
-                                edits_tx.unbounded_send((edit_range, String::new()))?;
+                                edits_tx.unbounded_send((edit_range, Arc::from("")))?;
                             }
                             CharOperation::Keep { bytes } => edit_start += bytes,
                         }
@@ -296,13 +350,35 @@ impl EditAgent {
             // TODO: group all edits into one transaction
             let mut edits_rx = edits_rx.ready_chunks(32);
             while let Some(edits) = edits_rx.next().await {
+                if edits.is_empty() {
+                    continue;
+                }
+
                 // Edit the buffer and report edits to the action log as part of the
                 // same effect cycle, otherwise the edit will be reported as if the
                 // user made it.
                 cx.update(|cx| {
-                    buffer.update(cx, |buffer, cx| buffer.edit(edits, None, cx));
+                    let max_edit_end = buffer.update(cx, |buffer, cx| {
+                        buffer.edit(edits.iter().cloned(), None, cx);
+                        let max_edit_end = buffer
+                            .summaries_for_anchors::<Point, _>(
+                                edits.iter().map(|(range, _)| &range.end),
+                            )
+                            .max()
+                            .unwrap();
+                        buffer.anchor_before(max_edit_end)
+                    });
                     self.action_log
-                        .update(cx, |log, cx| log.buffer_edited(buffer.clone(), cx))
+                        .update(cx, |log, cx| log.buffer_edited(buffer.clone(), cx));
+                    self.project.update(cx, |project, cx| {
+                        project.set_agent_location(
+                            Some(AgentLocation {
+                                buffer: buffer.downgrade(),
+                                position: max_edit_end,
+                            }),
+                            cx,
+                        );
+                    });
                 })?;
                 output_events
                     .unbounded_send(EditAgentOutputEvent::Edited)
@@ -657,7 +733,7 @@ mod tests {
     use gpui::{App, AppContext, TestAppContext};
     use indoc::indoc;
     use language_model::fake_provider::FakeLanguageModel;
-    use project::Project;
+    use project::{AgentLocation, Project};
     use rand::prelude::*;
     use rand::rngs::StdRng;
     use std::cmp;
@@ -775,8 +851,11 @@ mod tests {
     }
 
     #[gpui::test]
-    async fn test_events(cx: &mut TestAppContext) {
+    async fn test_edit_events(cx: &mut TestAppContext) {
         let agent = init_test(cx).await;
+        let project = agent
+            .action_log
+            .read_with(cx, |log, _| log.project().clone());
         let buffer = cx.new(|cx| Buffer::local("abc\ndef\nghi", cx));
         let (chunks_tx, chunks_rx) = mpsc::unbounded();
         let (apply, mut events) = agent.apply_edit_chunks(
@@ -792,6 +871,10 @@ mod tests {
             buffer.read_with(cx, |buffer, _| buffer.snapshot().text()),
             "abc\ndef\nghi"
         );
+        assert_eq!(
+            project.read_with(cx, |project, _| project.agent_location()),
+            None
+        );
 
         chunks_tx.unbounded_send("bc</old_text>").unwrap();
         cx.run_until_parked();
@@ -800,6 +883,10 @@ mod tests {
             buffer.read_with(cx, |buffer, _| buffer.snapshot().text()),
             "abc\ndef\nghi"
         );
+        assert_eq!(
+            project.read_with(cx, |project, _| project.agent_location()),
+            None
+        );
 
         chunks_tx.unbounded_send("<new_text>abX").unwrap();
         cx.run_until_parked();
@@ -808,6 +895,13 @@ mod tests {
             buffer.read_with(cx, |buffer, _| buffer.snapshot().text()),
             "abXc\ndef\nghi"
         );
+        assert_eq!(
+            project.read_with(cx, |project, _| project.agent_location()),
+            Some(AgentLocation {
+                buffer: buffer.downgrade(),
+                position: buffer.read_with(cx, |buffer, _| buffer.anchor_before(Point::new(0, 3)))
+            })
+        );
 
         chunks_tx.unbounded_send("cY").unwrap();
         cx.run_until_parked();
@@ -816,6 +910,13 @@ mod tests {
             buffer.read_with(cx, |buffer, _| buffer.snapshot().text()),
             "abXcY\ndef\nghi"
         );
+        assert_eq!(
+            project.read_with(cx, |project, _| project.agent_location()),
+            Some(AgentLocation {
+                buffer: buffer.downgrade(),
+                position: buffer.read_with(cx, |buffer, _| buffer.anchor_before(Point::new(0, 5)))
+            })
+        );
 
         chunks_tx.unbounded_send("</new_text>").unwrap();
         chunks_tx.unbounded_send("<old_text>hall").unwrap();
@@ -825,6 +926,13 @@ mod tests {
             buffer.read_with(cx, |buffer, _| buffer.snapshot().text()),
             "abXcY\ndef\nghi"
         );
+        assert_eq!(
+            project.read_with(cx, |project, _| project.agent_location()),
+            Some(AgentLocation {
+                buffer: buffer.downgrade(),
+                position: buffer.read_with(cx, |buffer, _| buffer.anchor_before(Point::new(0, 5)))
+            })
+        );
 
         chunks_tx.unbounded_send("ucinated old</old_text>").unwrap();
         chunks_tx.unbounded_send("<new_text>").unwrap();
@@ -839,6 +947,13 @@ mod tests {
             buffer.read_with(cx, |buffer, _| buffer.snapshot().text()),
             "abXcY\ndef\nghi"
         );
+        assert_eq!(
+            project.read_with(cx, |project, _| project.agent_location()),
+            Some(AgentLocation {
+                buffer: buffer.downgrade(),
+                position: buffer.read_with(cx, |buffer, _| buffer.anchor_before(Point::new(0, 5)))
+            })
+        );
 
         chunks_tx.unbounded_send("hallucinated new</new_").unwrap();
         chunks_tx.unbounded_send("text>").unwrap();
@@ -848,6 +963,13 @@ mod tests {
             buffer.read_with(cx, |buffer, _| buffer.snapshot().text()),
             "abXcY\ndef\nghi"
         );
+        assert_eq!(
+            project.read_with(cx, |project, _| project.agent_location()),
+            Some(AgentLocation {
+                buffer: buffer.downgrade(),
+                position: buffer.read_with(cx, |buffer, _| buffer.anchor_before(Point::new(0, 5)))
+            })
+        );
 
         chunks_tx.unbounded_send("<old_text>gh").unwrap();
         chunks_tx.unbounded_send("i</old_text>").unwrap();
@@ -858,6 +980,13 @@ mod tests {
             buffer.read_with(cx, |buffer, _| buffer.snapshot().text()),
             "abXcY\ndef\nghi"
         );
+        assert_eq!(
+            project.read_with(cx, |project, _| project.agent_location()),
+            Some(AgentLocation {
+                buffer: buffer.downgrade(),
+                position: buffer.read_with(cx, |buffer, _| buffer.anchor_before(Point::new(0, 5)))
+            })
+        );
 
         chunks_tx.unbounded_send("GHI</new_text>").unwrap();
         cx.run_until_parked();
@@ -869,6 +998,13 @@ mod tests {
             buffer.read_with(cx, |buffer, _| buffer.snapshot().text()),
             "abXcY\ndef\nGHI"
         );
+        assert_eq!(
+            project.read_with(cx, |project, _| project.agent_location()),
+            Some(AgentLocation {
+                buffer: buffer.downgrade(),
+                position: buffer.read_with(cx, |buffer, _| buffer.anchor_before(Point::new(2, 3)))
+            })
+        );
 
         drop(chunks_tx);
         apply.await.unwrap();
@@ -877,16 +1013,108 @@ mod tests {
             "abXcY\ndef\nGHI"
         );
         assert_eq!(drain_events(&mut events), vec![]);
+        assert_eq!(
+            project.read_with(cx, |project, _| project.agent_location()),
+            None
+        );
+    }
 
-        fn drain_events(
-            stream: &mut UnboundedReceiver<EditAgentOutputEvent>,
-        ) -> Vec<EditAgentOutputEvent> {
-            let mut events = Vec::new();
-            while let Ok(Some(event)) = stream.try_next() {
-                events.push(event);
-            }
-            events
-        }
+    #[gpui::test]
+    async fn test_overwrite_events(cx: &mut TestAppContext) {
+        let agent = init_test(cx).await;
+        let project = agent
+            .action_log
+            .read_with(cx, |log, _| log.project().clone());
+        let buffer = cx.new(|cx| Buffer::local("abc\ndef\nghi", cx));
+        let (chunks_tx, chunks_rx) = mpsc::unbounded();
+        let (apply, mut events) = agent.replace_text_with_chunks(
+            buffer.clone(),
+            chunks_rx.map(|chunk: &str| Ok(chunk.to_string())),
+            &mut cx.to_async(),
+        );
+
+        cx.run_until_parked();
+        assert_eq!(
+            drain_events(&mut events),
+            vec![EditAgentOutputEvent::Edited]
+        );
+        assert_eq!(
+            buffer.read_with(cx, |buffer, _| buffer.snapshot().text()),
+            ""
+        );
+        assert_eq!(
+            project.read_with(cx, |project, _| project.agent_location()),
+            Some(AgentLocation {
+                buffer: buffer.downgrade(),
+                position: language::Anchor::MAX
+            })
+        );
+
+        chunks_tx.unbounded_send("jkl\n").unwrap();
+        cx.run_until_parked();
+        assert_eq!(
+            drain_events(&mut events),
+            vec![EditAgentOutputEvent::Edited]
+        );
+        assert_eq!(
+            buffer.read_with(cx, |buffer, _| buffer.snapshot().text()),
+            "jkl\n"
+        );
+        assert_eq!(
+            project.read_with(cx, |project, _| project.agent_location()),
+            Some(AgentLocation {
+                buffer: buffer.downgrade(),
+                position: language::Anchor::MAX
+            })
+        );
+
+        chunks_tx.unbounded_send("mno\n").unwrap();
+        cx.run_until_parked();
+        assert_eq!(
+            drain_events(&mut events),
+            vec![EditAgentOutputEvent::Edited]
+        );
+        assert_eq!(
+            buffer.read_with(cx, |buffer, _| buffer.snapshot().text()),
+            "jkl\nmno\n"
+        );
+        assert_eq!(
+            project.read_with(cx, |project, _| project.agent_location()),
+            Some(AgentLocation {
+                buffer: buffer.downgrade(),
+                position: language::Anchor::MAX
+            })
+        );
+
+        chunks_tx.unbounded_send("pqr").unwrap();
+        cx.run_until_parked();
+        assert_eq!(
+            drain_events(&mut events),
+            vec![EditAgentOutputEvent::Edited]
+        );
+        assert_eq!(
+            buffer.read_with(cx, |buffer, _| buffer.snapshot().text()),
+            "jkl\nmno\npqr"
+        );
+        assert_eq!(
+            project.read_with(cx, |project, _| project.agent_location()),
+            Some(AgentLocation {
+                buffer: buffer.downgrade(),
+                position: language::Anchor::MAX
+            })
+        );
+
+        drop(chunks_tx);
+        apply.await.unwrap();
+        assert_eq!(
+            buffer.read_with(cx, |buffer, _| buffer.snapshot().text()),
+            "jkl\nmno\npqr"
+        );
+        assert_eq!(drain_events(&mut events), vec![]);
+        assert_eq!(
+            project.read_with(cx, |project, _| project.agent_location()),
+            None
+        );
     }
 
     #[gpui::test]
@@ -1173,7 +1401,17 @@ mod tests {
         cx.update(Project::init_settings);
         let project = Project::test(FakeFs::new(cx.executor()), [], cx).await;
         let model = Arc::new(FakeLanguageModel::default());
-        let action_log = cx.new(|_| ActionLog::new(project));
-        EditAgent::new(model, action_log, Templates::new())
+        let action_log = cx.new(|_| ActionLog::new(project.clone()));
+        EditAgent::new(model, project, action_log, Templates::new())
+    }
+
+    fn drain_events(
+        stream: &mut UnboundedReceiver<EditAgentOutputEvent>,
+    ) -> Vec<EditAgentOutputEvent> {
+        let mut events = Vec::new();
+        while let Ok(Some(event)) = stream.try_next() {
+            events.push(event);
+        }
+        events
     }
 }

crates/assistant_tools/src/edit_agent/evals.rs 🔗

@@ -517,7 +517,7 @@ fn eval_from_pixels_constructor() {
             input_path: input_file_path.into(),
             input_content: Some(input_file_content.into()),
             edit_description: edit_description.into(),
-            assertion: EvalAssertion::assert_eq(indoc! {"
+            assertion: EvalAssertion::judge_diff(indoc! {"
                 - The diff contains a new `from_pixels` constructor
                 - The diff contains new tests for the `from_pixels` constructor
             "}),
@@ -957,7 +957,7 @@ impl EditAgentTest {
 
                 cx.spawn(async move |cx| {
                     let agent_model =
-                        Self::load_model("anthropic", "claude-3-7-sonnet-latest", cx).await;
+                        Self::load_model("google", "gemini-2.5-pro-preview-03-25", cx).await;
                     let judge_model =
                         Self::load_model("anthropic", "claude-3-7-sonnet-latest", cx).await;
                     (agent_model.unwrap(), judge_model.unwrap())
@@ -967,7 +967,7 @@ impl EditAgentTest {
         let action_log = cx.new(|_| ActionLog::new(project.clone()));
 
         Self {
-            agent: EditAgent::new(agent_model, action_log, Templates::new()),
+            agent: EditAgent::new(agent_model, project.clone(), action_log, Templates::new()),
             project,
             judge_model,
         }

crates/assistant_tools/src/edit_file_tool.rs 🔗

@@ -15,7 +15,7 @@ use language::{
     language_settings::SoftWrap,
 };
 use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
-use project::Project;
+use project::{AgentLocation, Project};
 use schemars::JsonSchema;
 use serde::{Deserialize, Serialize};
 use std::{
@@ -164,6 +164,19 @@ impl Tool for EditFileTool {
                 })?
                 .await?;
 
+            // Set the agent's location to the top of the file
+            project
+                .update(cx, |project, cx| {
+                    project.set_agent_location(
+                        Some(AgentLocation {
+                            buffer: buffer.downgrade(),
+                            position: language::Anchor::MIN,
+                        }),
+                        cx,
+                    );
+                })
+                .ok();
+
             let snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot())?;
 
             if input.old_string.is_empty() {
@@ -226,6 +239,7 @@ impl Tool for EditFileTool {
             let snapshot = cx.update(|cx| {
                 action_log.update(cx, |log, cx| log.track_buffer(buffer.clone(), cx));
 
+                let base_version = diff.base_version.clone();
                 let snapshot = buffer.update(cx, |buffer, cx| {
                     buffer.finalize_last_transaction();
                     buffer.apply_diff(diff, cx);
@@ -233,6 +247,21 @@ impl Tool for EditFileTool {
                     buffer.snapshot()
                 });
                 action_log.update(cx, |log, cx| log.buffer_edited(buffer.clone(), cx));
+
+                // Set the agent's location to the position of the first edit
+                if let Some(first_edit) = snapshot.edits_since::<usize>(&base_version).next() {
+                    let position = snapshot.anchor_before(first_edit.new.start);
+                    project.update(cx, |project, cx| {
+                        project.set_agent_location(
+                            Some(AgentLocation {
+                                buffer: buffer.downgrade(),
+                                position,
+                            }),
+                            cx,
+                        );
+                    })
+                }
+
                 snapshot
             })?;
 

crates/assistant_tools/src/read_file_tool.rs 🔗

@@ -6,8 +6,9 @@ use gpui::{AnyWindowHandle, App, Entity, Task};
 
 use indoc::formatdoc;
 use itertools::Itertools;
+use language::{Anchor, Point};
 use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
-use project::Project;
+use project::{AgentLocation, Project};
 use schemars::JsonSchema;
 use serde::{Deserialize, Serialize};
 use std::sync::Arc;
@@ -35,11 +36,11 @@ pub struct ReadFileToolInput {
 
     /// Optional line number to start reading on (1-based index)
     #[serde(default)]
-    pub start_line: Option<usize>,
+    pub start_line: Option<u32>,
 
     /// Optional line number to end reading on (1-based index, inclusive)
     #[serde(default)]
-    pub end_line: Option<usize>,
+    pub end_line: Option<u32>,
 }
 
 pub struct ReadFileTool;
@@ -109,7 +110,7 @@ impl Tool for ReadFileTool {
         let file_path = input.path.clone();
         cx.spawn(async move |cx| {
             if !exists.await? {
-                return Err(anyhow!("{} not found", file_path))
+                return Err(anyhow!("{} not found", file_path));
             }
 
             let buffer = cx
@@ -118,25 +119,54 @@ impl Tool for ReadFileTool {
                 })?
                 .await?;
 
+            project.update(cx, |project, cx| {
+                project.set_agent_location(
+                    Some(AgentLocation {
+                        buffer: buffer.downgrade(),
+                        position: Anchor::MIN,
+                    }),
+                    cx,
+                );
+            })?;
+
             // Check if specific line ranges are provided
             if input.start_line.is_some() || input.end_line.is_some() {
+                let mut anchor = None;
                 let result = buffer.read_with(cx, |buffer, _cx| {
                     let text = buffer.text();
                     // .max(1) because despite instructions to be 1-indexed, sometimes the model passes 0.
                     let start = input.start_line.unwrap_or(1).max(1);
-                    let lines = text.split('\n').skip(start - 1);
+                    let start_row = start - 1;
+                    if start_row <= buffer.max_point().row {
+                        let column = buffer.line_indent_for_row(start_row).raw_len();
+                        anchor = Some(buffer.anchor_before(Point::new(start_row, column)));
+                    }
+
+                    let lines = text.split('\n').skip(start_row as usize);
                     if let Some(end) = input.end_line {
                         let count = end.saturating_sub(start).saturating_add(1); // Ensure at least 1 line
-                        Itertools::intersperse(lines.take(count), "\n").collect()
+                        Itertools::intersperse(lines.take(count as usize), "\n").collect()
                     } else {
                         Itertools::intersperse(lines, "\n").collect()
                     }
                 })?;
 
                 action_log.update(cx, |log, cx| {
-                    log.track_buffer(buffer, cx);
+                    log.track_buffer(buffer.clone(), cx);
                 })?;
 
+                if let Some(anchor) = anchor {
+                    project.update(cx, |project, cx| {
+                        project.set_agent_location(
+                            Some(AgentLocation {
+                                buffer: buffer.downgrade(),
+                                position: anchor,
+                            }),
+                            cx,
+                        );
+                    })?;
+                }
+
                 Ok(result)
             } else {
                 // No line ranges specified, so check file size to see if it's too big.
@@ -165,7 +195,8 @@ impl Tool for ReadFileTool {
                     })
                 }
             }
-        }).into()
+        })
+        .into()
     }
 }
 

crates/assistant_tools/src/streaming_edit_file_tool.rs 🔗

@@ -170,7 +170,7 @@ impl Tool for StreamingEditFileTool {
                 .update(|cx| LanguageModelRegistry::read_global(cx).default_model())?
                 .context("default model not set")?
                 .model;
-            let edit_agent = EditAgent::new(model, action_log, Templates::new());
+            let edit_agent = EditAgent::new(model, project.clone(), action_log, Templates::new());
 
             let buffer = project
                 .update(cx, |project, cx| {

crates/clock/src/clock.rs 🔗

@@ -10,6 +10,7 @@ use std::{
 pub use system_clock::*;
 
 pub const LOCAL_BRANCH_REPLICA_ID: u16 = u16::MAX;
+pub const AGENT_REPLICA_ID: u16 = u16::MAX - 1;
 
 /// A unique identifier for each distributed node.
 pub type ReplicaId = u16;

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

@@ -13,6 +13,7 @@ use gpui::{BackgroundExecutor, Context, Entity, TestAppContext, Window};
 use rpc::{RECEIVE_TIMEOUT, proto::PeerId};
 use serde_json::json;
 use std::ops::Range;
+use workspace::CollaboratorId;
 
 #[gpui::test]
 async fn test_core_channel_buffers(
@@ -300,13 +301,20 @@ fn assert_remote_selections(
     cx: &mut Context<Editor>,
 ) {
     let snapshot = editor.snapshot(window, cx);
+    let hub = editor.collaboration_hub().unwrap();
+    let collaborators = hub.collaborators(cx);
     let range = Anchor::min()..Anchor::max();
     let remote_selections = snapshot
-        .remote_selections_in_range(&range, editor.collaboration_hub().unwrap(), cx)
+        .remote_selections_in_range(&range, hub, cx)
         .map(|s| {
+            let CollaboratorId::PeerId(peer_id) = s.collaborator_id else {
+                panic!("unexpected collaborator id");
+            };
             let start = s.selection.start.to_offset(&snapshot.buffer_snapshot);
             let end = s.selection.end.to_offset(&snapshot.buffer_snapshot);
-            (s.participant_index, start..end)
+            let user_id = collaborators.get(&peer_id).unwrap().user_id;
+            let participant_index = hub.user_participant_indices(cx).get(&user_id).copied();
+            (participant_index, start..end)
         })
         .collect::<Vec<_>>();
     assert_eq!(

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

@@ -18,7 +18,7 @@ use serde_json::json;
 use settings::SettingsStore;
 use text::{Point, ToPoint};
 use util::{path, test::sample_text};
-use workspace::{SplitDirection, Workspace, item::ItemHandle as _};
+use workspace::{CollaboratorId, SplitDirection, Workspace, item::ItemHandle as _};
 
 use super::TestClient;
 
@@ -425,7 +425,7 @@ async fn test_basic_following(
     executor.run_until_parked();
     assert_eq!(
         workspace_a.update(cx_a, |workspace, _| workspace.leader_for_pane(&pane_a)),
-        Some(peer_id_b)
+        Some(peer_id_b.into())
     );
     assert_eq!(
         workspace_a.update_in(cx_a, |workspace, _, cx| workspace
@@ -1267,7 +1267,7 @@ async fn test_auto_unfollowing(cx_a: &mut TestAppContext, cx_b: &mut TestAppCont
     executor.run_until_parked();
     assert_eq!(
         workspace_b.update(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
-        Some(leader_id)
+        Some(leader_id.into())
     );
     let editor_b2 = workspace_b.update(cx_b, |workspace, cx| {
         workspace
@@ -1292,7 +1292,7 @@ async fn test_auto_unfollowing(cx_a: &mut TestAppContext, cx_b: &mut TestAppCont
     executor.run_until_parked();
     assert_eq!(
         workspace_b.update(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
-        Some(leader_id)
+        Some(leader_id.into())
     );
 
     // When client B edits, it automatically stops following client A.
@@ -1308,7 +1308,7 @@ async fn test_auto_unfollowing(cx_a: &mut TestAppContext, cx_b: &mut TestAppCont
     executor.run_until_parked();
     assert_eq!(
         workspace_b.update(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
-        Some(leader_id)
+        Some(leader_id.into())
     );
 
     // When client B scrolls, it automatically stops following client A.
@@ -1326,7 +1326,7 @@ async fn test_auto_unfollowing(cx_a: &mut TestAppContext, cx_b: &mut TestAppCont
     executor.run_until_parked();
     assert_eq!(
         workspace_b.update(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
-        Some(leader_id)
+        Some(leader_id.into())
     );
 
     // When client B activates a different pane, it continues following client A in the original pane.
@@ -1335,7 +1335,7 @@ async fn test_auto_unfollowing(cx_a: &mut TestAppContext, cx_b: &mut TestAppCont
     });
     assert_eq!(
         workspace_b.update(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
-        Some(leader_id)
+        Some(leader_id.into())
     );
 
     workspace_b.update_in(cx_b, |workspace, window, cx| {
@@ -1343,7 +1343,7 @@ async fn test_auto_unfollowing(cx_a: &mut TestAppContext, cx_b: &mut TestAppCont
     });
     assert_eq!(
         workspace_b.update(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
-        Some(leader_id)
+        Some(leader_id.into())
     );
 
     // When client B activates a different item in the original pane, it automatically stops following client A.
@@ -1406,13 +1406,13 @@ async fn test_peers_simultaneously_following_each_other(
     workspace_a.update(cx_a, |workspace, _| {
         assert_eq!(
             workspace.leader_for_pane(workspace.active_pane()),
-            Some(client_b_id)
+            Some(client_b_id.into())
         );
     });
     workspace_b.update(cx_b, |workspace, _| {
         assert_eq!(
             workspace.leader_for_pane(workspace.active_pane()),
-            Some(client_a_id)
+            Some(client_a_id.into())
         );
     });
 }
@@ -1513,7 +1513,7 @@ async fn test_following_across_workspaces(cx_a: &mut TestAppContext, cx_b: &mut
     workspace_b_project_a.update(&mut cx_b2, |workspace, cx| {
         assert!(workspace.is_being_followed(client_a.peer_id().unwrap()));
         assert_eq!(
-            client_a.peer_id(),
+            client_a.peer_id().map(Into::into),
             workspace.leader_for_pane(workspace.active_pane())
         );
         let item = workspace.active_item(cx).unwrap();
@@ -1554,7 +1554,7 @@ async fn test_following_across_workspaces(cx_a: &mut TestAppContext, cx_b: &mut
     workspace_a.update(cx_a, |workspace, cx| {
         assert!(workspace.is_being_followed(client_b.peer_id().unwrap()));
         assert_eq!(
-            client_b.peer_id(),
+            client_b.peer_id().map(Into::into),
             workspace.leader_for_pane(workspace.active_pane())
         );
         let item = workspace.active_pane().read(cx).active_item().unwrap();
@@ -1615,7 +1615,7 @@ async fn test_following_across_workspaces(cx_a: &mut TestAppContext, cx_b: &mut
         assert_eq!(workspace.project().read(cx).remote_id(), Some(project_b_id));
         assert!(workspace.is_being_followed(client_b.peer_id().unwrap()));
         assert_eq!(
-            client_b.peer_id(),
+            client_b.peer_id().map(Into::into),
             workspace.leader_for_pane(workspace.active_pane())
         );
         let item = workspace.active_item(cx).unwrap();
@@ -1866,7 +1866,11 @@ fn pane_summaries(workspace: &Entity<Workspace>, cx: &mut VisualTestContext) ->
             .panes()
             .iter()
             .map(|pane| {
-                let leader = workspace.leader_for_pane(pane);
+                let leader = match workspace.leader_for_pane(pane) {
+                    Some(CollaboratorId::PeerId(peer_id)) => Some(peer_id),
+                    Some(CollaboratorId::Agent) => unimplemented!(),
+                    None => None,
+                };
                 let active = pane == active_pane;
                 let pane = pane.read(cx);
                 let active_ix = pane.active_item_index();
@@ -1985,7 +1989,7 @@ async fn test_following_to_channel_notes_without_a_shared_project(
     let channel_notes_1_b = workspace_b.update(cx_b, |workspace, cx| {
         assert_eq!(
             workspace.leader_for_pane(workspace.active_pane()),
-            Some(client_a.peer_id().unwrap())
+            Some(client_a.peer_id().unwrap().into())
         );
         workspace
             .active_item(cx)
@@ -2015,7 +2019,7 @@ async fn test_following_to_channel_notes_without_a_shared_project(
     let channel_notes_2_b = workspace_b.update(cx_b, |workspace, cx| {
         assert_eq!(
             workspace.leader_for_pane(workspace.active_pane()),
-            Some(client_a.peer_id().unwrap())
+            Some(client_a.peer_id().unwrap().into())
         );
         workspace
             .active_item(cx)

crates/collab_ui/src/channel_view.rs 🔗

@@ -22,7 +22,7 @@ use std::{
 };
 use ui::prelude::*;
 use util::ResultExt;
-use workspace::item::TabContentParams;
+use workspace::{CollaboratorId, item::TabContentParams};
 use workspace::{
     ItemNavHistory, Pane, SaveIntent, Toast, ViewId, Workspace, WorkspaceId,
     item::{FollowableItem, Item, ItemEvent, ItemHandle},
@@ -654,15 +654,14 @@ impl FollowableItem for ChannelView {
         })
     }
 
-    fn set_leader_peer_id(
+    fn set_leader_id(
         &mut self,
-        leader_peer_id: Option<PeerId>,
+        leader_id: Option<CollaboratorId>,
         window: &mut Window,
         cx: &mut Context<Self>,
     ) {
-        self.editor.update(cx, |editor, cx| {
-            editor.set_leader_peer_id(leader_peer_id, window, cx)
-        })
+        self.editor
+            .update(cx, |editor, cx| editor.set_leader_id(leader_id, window, cx))
     }
 
     fn is_project_item(&self, _window: &Window, _cx: &App) -> bool {

crates/debugger_ui/src/session.rs 🔗

@@ -7,11 +7,11 @@ use gpui::{App, Entity, EventEmitter, FocusHandle, Focusable, Subscription, Task
 use project::Project;
 use project::debugger::session::Session;
 use project::worktree_store::WorktreeStore;
-use rpc::proto::{self, PeerId};
+use rpc::proto;
 use running::RunningState;
 use ui::{Indicator, prelude::*};
 use workspace::{
-    FollowableItem, ViewId, Workspace,
+    CollaboratorId, FollowableItem, ViewId, Workspace,
     item::{self, Item},
 };
 
@@ -189,9 +189,9 @@ impl FollowableItem for DebugSession {
         Task::ready(Ok(()))
     }
 
-    fn set_leader_peer_id(
+    fn set_leader_id(
         &mut self,
-        _leader_peer_id: Option<PeerId>,
+        _leader_id: Option<CollaboratorId>,
         _window: &mut Window,
         _cx: &mut Context<Self>,
     ) {

crates/editor/src/editor.rs 🔗

@@ -56,7 +56,7 @@ use anyhow::{Context as _, Result, anyhow};
 use blink_manager::BlinkManager;
 use buffer_diff::DiffHunkStatus;
 use client::{Collaborator, ParticipantIndex};
-use clock::ReplicaId;
+use clock::{AGENT_REPLICA_ID, ReplicaId};
 use collections::{BTreeMap, HashMap, HashSet, VecDeque};
 use convert_case::{Case, Casing};
 use display_map::*;
@@ -201,7 +201,7 @@ use ui::{
 };
 use util::{RangeExt, ResultExt, TryFutureExt, maybe, post_inc};
 use workspace::{
-    Item as WorkspaceItem, ItemId, ItemNavHistory, OpenInTerminal, OpenTerminal,
+    CollaboratorId, Item as WorkspaceItem, ItemId, ItemNavHistory, OpenInTerminal, OpenTerminal,
     RestoreOnStartupBehavior, SERIALIZATION_THROTTLE_TIME, SplitDirection, TabBarSettings, Toast,
     ViewId, Workspace, WorkspaceId, WorkspaceSettings,
     item::{ItemHandle, PreviewTabsSettings},
@@ -914,7 +914,7 @@ pub struct Editor {
     input_enabled: bool,
     use_modal_editing: bool,
     read_only: bool,
-    leader_peer_id: Option<PeerId>,
+    leader_id: Option<CollaboratorId>,
     remote_id: Option<ViewId>,
     pub hover_state: HoverState,
     pending_mouse_down: Option<Rc<RefCell<Option<MouseDownEvent>>>>,
@@ -1059,10 +1059,10 @@ pub struct RemoteSelection {
     pub replica_id: ReplicaId,
     pub selection: Selection<Anchor>,
     pub cursor_shape: CursorShape,
-    pub peer_id: PeerId,
+    pub collaborator_id: CollaboratorId,
     pub line_mode: bool,
-    pub participant_index: Option<ParticipantIndex>,
     pub user_name: Option<SharedString>,
+    pub color: PlayerColor,
 }
 
 #[derive(Clone, Debug)]
@@ -1723,7 +1723,7 @@ impl Editor {
             use_auto_surround: true,
             auto_replace_emoji_shortcode: false,
             jsx_tag_auto_close_enabled_in_any_buffer: false,
-            leader_peer_id: None,
+            leader_id: None,
             remote_id: None,
             hover_state: Default::default(),
             pending_mouse_down: None,
@@ -2175,8 +2175,8 @@ impl Editor {
         });
     }
 
-    pub fn leader_peer_id(&self) -> Option<PeerId> {
-        self.leader_peer_id
+    pub fn leader_id(&self) -> Option<CollaboratorId> {
+        self.leader_id
     }
 
     pub fn buffer(&self) -> &Entity<MultiBuffer> {
@@ -2517,7 +2517,7 @@ impl Editor {
             }
         }
 
-        if self.focus_handle.is_focused(window) && self.leader_peer_id.is_none() {
+        if self.focus_handle.is_focused(window) && self.leader_id.is_none() {
             self.buffer.update(cx, |buffer, cx| {
                 buffer.set_active_selections(
                     &self.selections.disjoint_anchors(),
@@ -18490,7 +18490,7 @@ impl Editor {
             self.show_cursor_names(window, cx);
             self.buffer.update(cx, |buffer, cx| {
                 buffer.finalize_last_transaction(cx);
-                if self.leader_peer_id.is_none() {
+                if self.leader_id.is_none() {
                     buffer.set_active_selections(
                         &self.selections.disjoint_anchors(),
                         self.selections.line_mode,
@@ -19928,18 +19928,34 @@ impl EditorSnapshot {
         self.buffer_snapshot
             .selections_in_range(range, false)
             .filter_map(move |(replica_id, line_mode, cursor_shape, selection)| {
-                let collaborator = collaborators_by_replica_id.get(&replica_id)?;
-                let participant_index = participant_indices.get(&collaborator.user_id).copied();
-                let user_name = participant_names.get(&collaborator.user_id).cloned();
-                Some(RemoteSelection {
-                    replica_id,
-                    selection,
-                    cursor_shape,
-                    line_mode,
-                    participant_index,
-                    peer_id: collaborator.peer_id,
-                    user_name,
-                })
+                if replica_id == AGENT_REPLICA_ID {
+                    Some(RemoteSelection {
+                        replica_id,
+                        selection,
+                        cursor_shape,
+                        line_mode,
+                        collaborator_id: CollaboratorId::Agent,
+                        user_name: Some("Agent".into()),
+                        color: cx.theme().players().agent(),
+                    })
+                } else {
+                    let collaborator = collaborators_by_replica_id.get(&replica_id)?;
+                    let participant_index = participant_indices.get(&collaborator.user_id).copied();
+                    let user_name = participant_names.get(&collaborator.user_id).cloned();
+                    Some(RemoteSelection {
+                        replica_id,
+                        selection,
+                        cursor_shape,
+                        line_mode,
+                        collaborator_id: CollaboratorId::PeerId(collaborator.peer_id),
+                        user_name,
+                        color: if let Some(index) = participant_index {
+                            cx.theme().players().color_for_participant(index.0)
+                        } else {
+                            cx.theme().players().absent()
+                        },
+                    })
+                }
             })
     }
 

crates/editor/src/editor_tests.rs 🔗

@@ -12650,7 +12650,7 @@ async fn test_following_with_multiple_excerpts(cx: &mut TestAppContext) {
             Editor::from_state_proto(
                 workspace_entity,
                 ViewId {
-                    creator: Default::default(),
+                    creator: CollaboratorId::PeerId(PeerId::default()),
                     id: 0,
                 },
                 &mut state_message,
@@ -12737,7 +12737,7 @@ async fn test_following_with_multiple_excerpts(cx: &mut TestAppContext) {
             Editor::from_state_proto(
                 workspace_entity,
                 ViewId {
-                    creator: Default::default(),
+                    creator: CollaboratorId::PeerId(PeerId::default()),
                     id: 0,
                 },
                 &mut state_message,

crates/editor/src/element.rs 🔗

@@ -28,7 +28,6 @@ use crate::{
     scroll::scroll_amount::ScrollAmount,
 };
 use buffer_diff::{DiffHunkStatus, DiffHunkStatusKind};
-use client::ParticipantIndex;
 use collections::{BTreeMap, HashMap};
 use feature_flags::{DebuggerFeatureFlag, FeatureFlagAppExt};
 use file_icons::FileIcons;
@@ -82,7 +81,7 @@ use theme::{ActiveTheme, Appearance, BufferLineHeight, PlayerColor};
 use ui::{ButtonLike, KeyBinding, POPOVER_Y_PADDING, Tooltip, h_flex, prelude::*};
 use unicode_segmentation::UnicodeSegmentation;
 use util::{RangeExt, ResultExt, debug_panic};
-use workspace::{Workspace, item::Item, notifications::NotifyTaskExt};
+use workspace::{CollaboratorId, Workspace, item::Item, notifications::NotifyTaskExt};
 
 const INLINE_BLAME_PADDING_EM_WIDTHS: f32 = 7.;
 
@@ -1126,7 +1125,7 @@ impl EditorElement {
                         editor.cursor_shape,
                         &snapshot.display_snapshot,
                         is_newest,
-                        editor.leader_peer_id.is_none(),
+                        editor.leader_id.is_none(),
                         None,
                     );
                     if is_newest {
@@ -1150,18 +1149,29 @@ impl EditorElement {
 
             if let Some(collaboration_hub) = &editor.collaboration_hub {
                 // When following someone, render the local selections in their color.
-                if let Some(leader_id) = editor.leader_peer_id {
-                    if let Some(collaborator) = collaboration_hub.collaborators(cx).get(&leader_id)
-                    {
-                        if let Some(participant_index) = collaboration_hub
-                            .user_participant_indices(cx)
-                            .get(&collaborator.user_id)
-                        {
+                if let Some(leader_id) = editor.leader_id {
+                    match leader_id {
+                        CollaboratorId::PeerId(peer_id) => {
+                            if let Some(collaborator) =
+                                collaboration_hub.collaborators(cx).get(&peer_id)
+                            {
+                                if let Some(participant_index) = collaboration_hub
+                                    .user_participant_indices(cx)
+                                    .get(&collaborator.user_id)
+                                {
+                                    if let Some((local_selection_style, _)) = selections.first_mut()
+                                    {
+                                        *local_selection_style = cx
+                                            .theme()
+                                            .players()
+                                            .color_for_participant(participant_index.0);
+                                    }
+                                }
+                            }
+                        }
+                        CollaboratorId::Agent => {
                             if let Some((local_selection_style, _)) = selections.first_mut() {
-                                *local_selection_style = cx
-                                    .theme()
-                                    .players()
-                                    .color_for_participant(participant_index.0);
+                                *local_selection_style = cx.theme().players().agent();
                             }
                         }
                     }
@@ -1173,12 +1183,9 @@ impl EditorElement {
                     collaboration_hub.as_ref(),
                     cx,
                 ) {
-                    let selection_style =
-                        Self::get_participant_color(selection.participant_index, cx);
-
                     // Don't re-render the leader's selections, since the local selections
                     // match theirs.
-                    if Some(selection.peer_id) == editor.leader_peer_id {
+                    if Some(selection.collaborator_id) == editor.leader_id {
                         continue;
                     }
                     let key = HoveredCursor {
@@ -1191,7 +1198,7 @@ impl EditorElement {
 
                     remote_selections
                         .entry(selection.replica_id)
-                        .or_insert((selection_style, Vec::new()))
+                        .or_insert((selection.color, Vec::new()))
                         .1
                         .push(SelectionLayout::new(
                             selection.selection,
@@ -1246,9 +1253,11 @@ impl EditorElement {
                 collaboration_hub.deref(),
                 cx,
             ) {
-                let color = Self::get_participant_color(remote_selection.participant_index, cx);
-                add_cursor(remote_selection.selection.head(), color.cursor);
-                if Some(remote_selection.peer_id) == editor.leader_peer_id {
+                add_cursor(
+                    remote_selection.selection.head(),
+                    remote_selection.color.cursor,
+                );
+                if Some(remote_selection.collaborator_id) == editor.leader_id {
                     skip_local = true;
                 }
             }
@@ -2446,14 +2455,6 @@ impl EditorElement {
         Some(button)
     }
 
-    fn get_participant_color(participant_index: Option<ParticipantIndex>, cx: &App) -> PlayerColor {
-        if let Some(index) = participant_index {
-            cx.theme().players().color_for_participant(index.0)
-        } else {
-            cx.theme().players().absent()
-        }
-    }
-
     fn calculate_relative_line_numbers(
         &self,
         snapshot: &EditorSnapshot,

crates/editor/src/items.rs 🔗

@@ -23,7 +23,7 @@ use project::{
     Project, ProjectItem as _, ProjectPath, lsp_store::FormatTrigger,
     project_settings::ProjectSettings, search::SearchQuery,
 };
-use rpc::proto::{self, PeerId, update_view};
+use rpc::proto::{self, update_view};
 use settings::Settings;
 use std::{
     any::TypeId,
@@ -39,7 +39,7 @@ use theme::{Theme, ThemeSettings};
 use ui::{IconDecorationKind, prelude::*};
 use util::{ResultExt, TryFutureExt, paths::PathExt};
 use workspace::{
-    ItemId, ItemNavHistory, ToolbarItemLocation, ViewId, Workspace, WorkspaceId,
+    CollaboratorId, ItemId, ItemNavHistory, ToolbarItemLocation, ViewId, Workspace, WorkspaceId,
     item::{FollowableItem, Item, ItemEvent, ProjectItem},
     searchable::{Direction, SearchEvent, SearchableItem, SearchableItemHandle},
 };
@@ -170,14 +170,14 @@ impl FollowableItem for Editor {
         }))
     }
 
-    fn set_leader_peer_id(
+    fn set_leader_id(
         &mut self,
-        leader_peer_id: Option<PeerId>,
+        leader_id: Option<CollaboratorId>,
         window: &mut Window,
         cx: &mut Context<Self>,
     ) {
-        self.leader_peer_id = leader_peer_id;
-        if self.leader_peer_id.is_some() {
+        self.leader_id = leader_id;
+        if self.leader_id.is_some() {
             self.buffer.update(cx, |buffer, cx| {
                 buffer.remove_active_selections(cx);
             });
@@ -350,6 +350,30 @@ impl FollowableItem for Editor {
             None
         }
     }
+
+    fn update_agent_location(
+        &mut self,
+        location: language::Anchor,
+        window: &mut Window,
+        cx: &mut Context<Self>,
+    ) {
+        let buffer = self.buffer.read(cx);
+        let buffer = buffer.read(cx);
+        let Some((excerpt_id, _, _)) = buffer.as_singleton() else {
+            return;
+        };
+        let position = buffer.anchor_in_excerpt(*excerpt_id, location).unwrap();
+        let selection = Selection {
+            id: 0,
+            reversed: false,
+            start: position,
+            end: position,
+            goal: SelectionGoal::None,
+        };
+        drop(buffer);
+        self.set_selections_from_remote(vec![selection], None, window, cx);
+        self.request_autoscroll_remotely(Autoscroll::center(), cx);
+    }
 }
 
 async fn update_editor_from_message(
@@ -1293,7 +1317,7 @@ impl ProjectItem for Editor {
 
     fn for_project_item(
         project: Entity<Project>,
-        pane: &Pane,
+        pane: Option<&Pane>,
         buffer: Entity<Buffer>,
         window: &mut Window,
         cx: &mut Context<Self>,
@@ -1304,7 +1328,7 @@ impl ProjectItem for Editor {
         {
             if WorkspaceSettings::get(None, cx).restore_on_file_reopen {
                 if let Some(restoration_data) = Self::project_item_kind()
-                    .and_then(|kind| pane.project_item_restoration_data.get(&kind))
+                    .and_then(|kind| pane.as_ref()?.project_item_restoration_data.get(&kind))
                     .and_then(|data| data.downcast_ref::<EditorRestorationData>())
                     .and_then(|data| {
                         let file = project::File::from_dyn(buffer.read(cx).file())?;

crates/icons/src/icons.rs 🔗

@@ -72,6 +72,7 @@ pub enum IconName {
     CopilotInit,
     Copy,
     CountdownTimer,
+    Crosshair,
     CursorIBeam,
     Dash,
     DatabaseZap,

crates/image_viewer/src/image_viewer.rs 🔗

@@ -375,7 +375,7 @@ impl ProjectItem for ImageView {
 
     fn for_project_item(
         project: Entity<Project>,
-        _: &Pane,
+        _: Option<&Pane>,
         item: Entity<Self::Item>,
         window: &mut Window,
         cx: &mut Context<Self>,

crates/language/src/buffer.rs 🔗

@@ -19,8 +19,8 @@ pub use crate::{
 };
 use anyhow::{Context as _, Result, anyhow};
 use async_watch as watch;
-use clock::Lamport;
 pub use clock::ReplicaId;
+use clock::{AGENT_REPLICA_ID, Lamport};
 use collections::HashMap;
 use fs::MTime;
 use futures::channel::oneshot;
@@ -2132,6 +2132,31 @@ impl Buffer {
         }
     }
 
+    pub fn set_agent_selections(
+        &mut self,
+        selections: Arc<[Selection<Anchor>]>,
+        line_mode: bool,
+        cursor_shape: CursorShape,
+        cx: &mut Context<Self>,
+    ) {
+        let lamport_timestamp = self.text.lamport_clock.tick();
+        self.remote_selections.insert(
+            AGENT_REPLICA_ID,
+            SelectionSet {
+                selections: selections.clone(),
+                lamport_timestamp,
+                line_mode,
+                cursor_shape,
+            },
+        );
+        self.non_text_state_update_count += 1;
+        cx.notify();
+    }
+
+    pub fn remove_agent_selections(&mut self, cx: &mut Context<Self>) {
+        self.set_agent_selections(Arc::default(), false, Default::default(), cx);
+    }
+
     /// Replaces the buffer's entire text.
     pub fn set_text<T>(&mut self, text: T, cx: &mut Context<Self>) -> Option<clock::Lamport>
     where

crates/project/src/project.rs 🔗

@@ -68,9 +68,9 @@ use gpui::{
 };
 use itertools::Itertools;
 use language::{
-    Buffer, BufferEvent, Capability, CodeLabel, Language, LanguageName, LanguageRegistry,
-    PointUtf16, ToOffset, ToPointUtf16, Toolchain, ToolchainList, Transaction, Unclipped,
-    language_settings::InlayHintKind, proto::split_operations,
+    Buffer, BufferEvent, Capability, CodeLabel, CursorShape, Language, LanguageName,
+    LanguageRegistry, PointUtf16, ToOffset, ToPointUtf16, Toolchain, ToolchainList, Transaction,
+    Unclipped, language_settings::InlayHintKind, proto::split_operations,
 };
 use lsp::{
     CodeActionKind, CompletionContext, CompletionItemKind, DocumentHighlightKind, InsertTextMode,
@@ -138,7 +138,7 @@ const MAX_PROJECT_SEARCH_HISTORY_SIZE: usize = 500;
 const MAX_SEARCH_RESULT_FILES: usize = 5_000;
 const MAX_SEARCH_RESULT_RANGES: usize = 10_000;
 
-pub trait ProjectItem {
+pub trait ProjectItem: 'static {
     fn try_open(
         project: &Entity<Project>,
         path: &ProjectPath,
@@ -197,6 +197,13 @@ pub struct Project {
     environment: Entity<ProjectEnvironment>,
     settings_observer: Entity<SettingsObserver>,
     toolchain_store: Option<Entity<ToolchainStore>>,
+    agent_location: Option<AgentLocation>,
+}
+
+#[derive(Clone, Debug, PartialEq, Eq)]
+pub struct AgentLocation {
+    pub buffer: WeakEntity<Buffer>,
+    pub position: Anchor,
 }
 
 #[derive(Default)]
@@ -304,8 +311,11 @@ pub enum Event {
     RevealInProjectPanel(ProjectEntryId),
     SnippetEdit(BufferId, Vec<(lsp::Range, Snippet)>),
     ExpandedAllForEntry(WorktreeId, ProjectEntryId),
+    AgentLocationChanged,
 }
 
+pub struct AgentLocationChanged;
+
 pub enum DebugAdapterClientState {
     Starting(Task<Option<Arc<DebugAdapterClient>>>),
     Running(Arc<DebugAdapterClient>),
@@ -986,6 +996,8 @@ impl Project {
                 search_excluded_history: Self::new_search_history(),
 
                 toolchain_store: Some(toolchain_store),
+
+                agent_location: None,
             }
         })
     }
@@ -1142,6 +1154,7 @@ impl Project {
                 search_excluded_history: Self::new_search_history(),
 
                 toolchain_store: Some(toolchain_store),
+                agent_location: None,
             };
 
             // ssh -> local machine handlers
@@ -1381,6 +1394,7 @@ impl Project {
                 environment,
                 remotely_created_models: Arc::new(Mutex::new(RemotelyCreatedModels::default())),
                 toolchain_store: None,
+                agent_location: None,
             };
             this.set_role(role, cx);
             for worktree in worktrees {
@@ -4875,6 +4889,46 @@ impl Project {
     pub fn status_for_buffer_id(&self, buffer_id: BufferId, cx: &App) -> Option<FileStatus> {
         self.git_store.read(cx).status_for_buffer_id(buffer_id, cx)
     }
+
+    pub fn set_agent_location(
+        &mut self,
+        new_location: Option<AgentLocation>,
+        cx: &mut Context<Self>,
+    ) {
+        if let Some(old_location) = self.agent_location.as_ref() {
+            old_location
+                .buffer
+                .update(cx, |buffer, cx| buffer.remove_agent_selections(cx))
+                .ok();
+        }
+
+        if let Some(location) = new_location.as_ref() {
+            location
+                .buffer
+                .update(cx, |buffer, cx| {
+                    buffer.set_agent_selections(
+                        Arc::from([language::Selection {
+                            id: 0,
+                            start: location.position,
+                            end: location.position,
+                            reversed: false,
+                            goal: language::SelectionGoal::None,
+                        }]),
+                        false,
+                        CursorShape::Hollow,
+                        cx,
+                    )
+                })
+                .ok();
+        }
+
+        self.agent_location = new_location;
+        cx.emit(Event::AgentLocationChanged);
+    }
+
+    pub fn agent_location(&self) -> Option<AgentLocation> {
+        self.agent_location.clone()
+    }
 }
 
 pub struct PathMatchCandidateSet {

crates/project_panel/src/project_panel_tests.rs 🔗

@@ -5291,7 +5291,7 @@ impl ProjectItem for TestProjectItemView {
 
     fn for_project_item(
         _: Entity<Project>,
-        _: &Pane,
+        _: Option<&Pane>,
         project_item: Entity<Self::Item>,
         _: &mut Window,
         cx: &mut Context<Self>,

crates/repl/src/notebook/notebook_ui.rs 🔗

@@ -829,7 +829,7 @@ impl ProjectItem for NotebookEditor {
 
     fn for_project_item(
         project: Entity<Project>,
-        _: &Pane,
+        _: Option<&Pane>,
         item: Entity<Self::Item>,
         window: &mut Window,
         cx: &mut Context<Self>,

crates/theme/src/styles/players.rs 🔗

@@ -129,6 +129,10 @@ impl PlayerColors {
         *self.0.first().unwrap()
     }
 
+    pub fn agent(&self) -> PlayerColor {
+        *self.0.last().unwrap()
+    }
+
     pub fn absent(&self) -> PlayerColor {
         *self.0.last().unwrap()
     }

crates/vim/src/vim.rs 🔗

@@ -1143,7 +1143,7 @@ impl Vim {
                 && !newest_selection_empty
                 && self.mode == Mode::Normal
                 // When following someone, don't switch vim mode.
-                && editor.leader_peer_id().is_none()
+                && editor.leader_id().is_none()
         {
             if preserve_selection {
                 self.switch_mode(Mode::Visual, true, window, cx);
@@ -1468,7 +1468,7 @@ impl Vim {
     fn local_selections_changed(&mut self, window: &mut Window, cx: &mut Context<Self>) {
         let Some(editor) = self.editor() else { return };
 
-        if editor.read(cx).leader_peer_id().is_some() {
+        if editor.read(cx).leader_id().is_some() {
             return;
         }
 

crates/workspace/Cargo.toml 🔗

@@ -36,7 +36,6 @@ clock.workspace = true
 collections.workspace = true
 component.workspace = true
 db.workspace = true
-derive_more.workspace = true
 fs.workspace = true
 futures.workspace = true
 gpui.workspace = true

crates/workspace/src/item.rs 🔗

@@ -1,16 +1,13 @@
 use crate::{
-    DelayedDebouncedEditAction, FollowableViewRegistry, ItemNavHistory, SerializableItemRegistry,
-    ToolbarItemLocation, ViewId, Workspace, WorkspaceId,
+    CollaboratorId, DelayedDebouncedEditAction, FollowableViewRegistry, ItemNavHistory,
+    SerializableItemRegistry, ToolbarItemLocation, ViewId, Workspace, WorkspaceId,
     pane::{self, Pane},
     persistence::model::ItemId,
     searchable::SearchableItemHandle,
     workspace_settings::{AutosaveSetting, WorkspaceSettings},
 };
 use anyhow::Result;
-use client::{
-    Client,
-    proto::{self, PeerId},
-};
+use client::{Client, proto};
 use futures::{StreamExt, channel::mpsc};
 use gpui::{
     Action, AnyElement, AnyView, App, Context, Entity, EntityId, EventEmitter, FocusHandle,
@@ -770,7 +767,7 @@ impl<T: Item> ItemHandle for Entity<T> {
                                         proto::UpdateView {
                                             id: item
                                                 .remote_id(workspace.client(), window, cx)
-                                                .map(|id| id.to_proto()),
+                                                .and_then(|id| id.to_proto()),
                                             variant: pending_update.borrow_mut().take(),
                                             leader_id,
                                         },
@@ -810,13 +807,27 @@ impl<T: Item> ItemHandle for Entity<T> {
                         }
 
                         if item.item_focus_handle(cx).contains_focused(window, cx) {
-                            item.add_event_to_update_proto(
-                                event,
-                                &mut pending_update.borrow_mut(),
-                                window,
-                                cx,
-                            );
-                            pending_update_tx.unbounded_send(leader_id).ok();
+                            match leader_id {
+                                Some(CollaboratorId::Agent) => {}
+                                Some(CollaboratorId::PeerId(leader_peer_id)) => {
+                                    item.add_event_to_update_proto(
+                                        event,
+                                        &mut pending_update.borrow_mut(),
+                                        window,
+                                        cx,
+                                    );
+                                    pending_update_tx.unbounded_send(Some(leader_peer_id)).ok();
+                                }
+                                None => {
+                                    item.add_event_to_update_proto(
+                                        event,
+                                        &mut pending_update.borrow_mut(),
+                                        window,
+                                        cx,
+                                    );
+                                    pending_update_tx.unbounded_send(None).ok();
+                                }
+                            }
                         }
                     }
 
@@ -1081,7 +1092,7 @@ pub trait ProjectItem: Item {
 
     fn for_project_item(
         project: Entity<Project>,
-        pane: &Pane,
+        pane: Option<&Pane>,
         item: Entity<Self::Item>,
         window: &mut Window,
         cx: &mut Context<Self>,
@@ -1126,19 +1137,31 @@ pub trait FollowableItem: Item {
         cx: &mut Context<Self>,
     ) -> Task<Result<()>>;
     fn is_project_item(&self, window: &Window, cx: &App) -> bool;
-    fn set_leader_peer_id(
+    fn set_leader_id(
         &mut self,
-        leader_peer_id: Option<PeerId>,
+        leader_peer_id: Option<CollaboratorId>,
         window: &mut Window,
         cx: &mut Context<Self>,
     );
     fn dedup(&self, existing: &Self, window: &Window, cx: &App) -> Option<Dedup>;
+    fn update_agent_location(
+        &mut self,
+        _location: language::Anchor,
+        _window: &mut Window,
+        _cx: &mut Context<Self>,
+    ) {
+    }
 }
 
 pub trait FollowableItemHandle: ItemHandle {
     fn remote_id(&self, client: &Arc<Client>, window: &mut Window, cx: &mut App) -> Option<ViewId>;
     fn downgrade(&self) -> Box<dyn WeakFollowableItemHandle>;
-    fn set_leader_peer_id(&self, leader_peer_id: Option<PeerId>, window: &mut Window, cx: &mut App);
+    fn set_leader_id(
+        &self,
+        leader_peer_id: Option<CollaboratorId>,
+        window: &mut Window,
+        cx: &mut App,
+    );
     fn to_state_proto(&self, window: &mut Window, cx: &mut App) -> Option<proto::view::Variant>;
     fn add_event_to_update_proto(
         &self,
@@ -1162,13 +1185,14 @@ pub trait FollowableItemHandle: ItemHandle {
         window: &mut Window,
         cx: &mut App,
     ) -> Option<Dedup>;
+    fn update_agent_location(&self, location: language::Anchor, window: &mut Window, cx: &mut App);
 }
 
 impl<T: FollowableItem> FollowableItemHandle for Entity<T> {
     fn remote_id(&self, client: &Arc<Client>, _: &mut Window, cx: &mut App) -> Option<ViewId> {
         self.read(cx).remote_id().or_else(|| {
             client.peer_id().map(|creator| ViewId {
-                creator,
+                creator: CollaboratorId::PeerId(creator),
                 id: self.item_id().as_u64(),
             })
         })
@@ -1178,15 +1202,8 @@ impl<T: FollowableItem> FollowableItemHandle for Entity<T> {
         Box::new(self.downgrade())
     }
 
-    fn set_leader_peer_id(
-        &self,
-        leader_peer_id: Option<PeerId>,
-        window: &mut Window,
-        cx: &mut App,
-    ) {
-        self.update(cx, |this, cx| {
-            this.set_leader_peer_id(leader_peer_id, window, cx)
-        })
+    fn set_leader_id(&self, leader_id: Option<CollaboratorId>, window: &mut Window, cx: &mut App) {
+        self.update(cx, |this, cx| this.set_leader_id(leader_id, window, cx))
     }
 
     fn to_state_proto(&self, window: &mut Window, cx: &mut App) -> Option<proto::view::Variant> {
@@ -1237,6 +1254,12 @@ impl<T: FollowableItem> FollowableItemHandle for Entity<T> {
         let existing = existing.to_any().downcast::<T>().ok()?;
         self.read(cx).dedup(existing.read(cx), window, cx)
     }
+
+    fn update_agent_location(&self, location: language::Anchor, window: &mut Window, cx: &mut App) {
+        self.update(cx, |this, cx| {
+            this.update_agent_location(location, window, cx)
+        })
+    }
 }
 
 pub trait WeakFollowableItemHandle: Send + Sync {

crates/workspace/src/pane_group.rs 🔗

@@ -1,11 +1,10 @@
 use crate::{
-    AppState, FollowerState, Pane, Workspace, WorkspaceSettings,
+    AppState, CollaboratorId, FollowerState, Pane, Workspace, WorkspaceSettings,
     pane_group::element::pane_axis,
     workspace_settings::{PaneSplitDirectionHorizontal, PaneSplitDirectionVertical},
 };
 use anyhow::{Result, anyhow};
 use call::{ActiveCall, ParticipantLocation};
-use client::proto::PeerId;
 use collections::HashMap;
 use gpui::{
     Along, AnyView, AnyWeakView, Axis, Bounds, Entity, Hsla, IntoElement, MouseButton, Pixels,
@@ -188,7 +187,7 @@ pub enum Member {
 #[derive(Clone, Copy)]
 pub struct PaneRenderContext<'a> {
     pub project: &'a Entity<Project>,
-    pub follower_states: &'a HashMap<PeerId, FollowerState>,
+    pub follower_states: &'a HashMap<CollaboratorId, FollowerState>,
     pub active_call: Option<&'a Entity<ActiveCall>>,
     pub active_pane: &'a Entity<Pane>,
     pub app_state: &'a Arc<AppState>,
@@ -243,88 +242,104 @@ impl PaneLeaderDecorator for PaneRenderContext<'_> {
                 None
             }
         });
-        let leader = follower_state.as_ref().and_then(|(leader_id, _)| {
-            let room = self.active_call?.read(cx).room()?.read(cx);
-            room.remote_participant_for_peer_id(*leader_id)
-        });
-        let Some(leader) = leader else {
+        let Some((leader_id, follower_state)) = follower_state else {
             return LeaderDecoration::default();
         };
-        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_join_data = None;
-        let leader_status_box = match leader.location {
-            ParticipantLocation::SharedProject {
-                project_id: leader_project_id,
-            } => {
-                if Some(leader_project_id) == self.project.read(cx).remote_id() {
-                    is_in_unshared_view.then(|| {
-                        Label::new(format!(
-                            "{} is in an unshared pane",
-                            leader.user.github_login
-                        ))
-                    })
-                } else {
-                    leader_join_data = Some((leader_project_id, leader.user.id));
-                    Some(Label::new(format!(
-                        "Follow {} to their active project",
-                        leader.user.github_login,
-                    )))
-                }
+
+        let mut leader_color;
+        let status_box;
+        match leader_id {
+            CollaboratorId::PeerId(peer_id) => {
+                let Some(leader) = self.active_call.as_ref().and_then(|call| {
+                    let room = call.read(cx).room()?.read(cx);
+                    room.remote_participant_for_peer_id(peer_id)
+                }) else {
+                    return LeaderDecoration::default();
+                };
+
+                let is_in_unshared_view = follower_state.active_view_id.is_some_and(|view_id| {
+                    !follower_state
+                        .items_by_leader_view_id
+                        .contains_key(&view_id)
+                });
+
+                let mut leader_join_data = None;
+                let leader_status_box = match leader.location {
+                    ParticipantLocation::SharedProject {
+                        project_id: leader_project_id,
+                    } => {
+                        if Some(leader_project_id) == self.project.read(cx).remote_id() {
+                            is_in_unshared_view.then(|| {
+                                Label::new(format!(
+                                    "{} is in an unshared pane",
+                                    leader.user.github_login
+                                ))
+                            })
+                        } else {
+                            leader_join_data = Some((leader_project_id, leader.user.id));
+                            Some(Label::new(format!(
+                                "Follow {} to their active project",
+                                leader.user.github_login,
+                            )))
+                        }
+                    }
+                    ParticipantLocation::UnsharedProject => Some(Label::new(format!(
+                        "{} is viewing an unshared Zed project",
+                        leader.user.github_login
+                    ))),
+                    ParticipantLocation::External => Some(Label::new(format!(
+                        "{} is viewing a window outside of Zed",
+                        leader.user.github_login
+                    ))),
+                };
+                status_box = leader_status_box.map(|status| {
+                    div()
+                        .absolute()
+                        .w_96()
+                        .bottom_3()
+                        .right_3()
+                        .elevation_2(cx)
+                        .p_1()
+                        .child(status)
+                        .when_some(
+                            leader_join_data,
+                            |this, (leader_project_id, leader_user_id)| {
+                                let app_state = self.app_state.clone();
+                                this.cursor_pointer().on_mouse_down(
+                                    MouseButton::Left,
+                                    move |_, _, cx| {
+                                        crate::join_in_room_project(
+                                            leader_project_id,
+                                            leader_user_id,
+                                            app_state.clone(),
+                                            cx,
+                                        )
+                                        .detach_and_log_err(cx);
+                                    },
+                                )
+                            },
+                        )
+                        .into_any_element()
+                });
+                leader_color = cx
+                    .theme()
+                    .players()
+                    .color_for_participant(leader.participant_index.0)
+                    .cursor;
             }
-            ParticipantLocation::UnsharedProject => Some(Label::new(format!(
-                "{} is viewing an unshared Zed project",
-                leader.user.github_login
-            ))),
-            ParticipantLocation::External => Some(Label::new(format!(
-                "{} is viewing a window outside of Zed",
-                leader.user.github_login
-            ))),
-        };
-        let mut leader_color = cx
-            .theme()
-            .players()
-            .color_for_participant(leader.participant_index.0)
-            .cursor;
+            CollaboratorId::Agent => {
+                status_box = None;
+                leader_color = cx.theme().players().agent().cursor;
+            }
+        }
+
+        let is_in_panel = follower_state.dock_pane.is_some();
         if is_in_panel {
             leader_color.fade_out(0.75);
         } else {
             leader_color.fade_out(0.3);
         }
-        let status_box = leader_status_box.map(|status| {
-            div()
-                .absolute()
-                .w_96()
-                .bottom_3()
-                .right_3()
-                .elevation_2(cx)
-                .p_1()
-                .child(status)
-                .when_some(
-                    leader_join_data,
-                    |this, (leader_project_id, leader_user_id)| {
-                        let app_state = self.app_state.clone();
-                        this.cursor_pointer()
-                            .on_mouse_down(MouseButton::Left, move |_, _, cx| {
-                                crate::join_in_room_project(
-                                    leader_project_id,
-                                    leader_user_id,
-                                    app_state.clone(),
-                                    cx,
-                                )
-                                .detach_and_log_err(cx);
-                            })
-                    },
-                )
-                .into_any_element()
-        });
+
         LeaderDecoration {
             status_box,
             border: Some(leader_color),
@@ -339,6 +354,7 @@ impl PaneLeaderDecorator for PaneRenderContext<'_> {
         self.workspace
     }
 }
+
 impl Member {
     fn new_axis(old_pane: Entity<Pane>, new_pane: Entity<Pane>, direction: SplitDirection) -> Self {
         use Axis::*;

crates/workspace/src/workspace.rs 🔗

@@ -24,7 +24,6 @@ use client::{
     proto::{self, ErrorCode, PanelId, PeerId},
 };
 use collections::{HashMap, HashSet, hash_map};
-use derive_more::{Deref, DerefMut};
 pub use dock::Panel;
 use dock::{Dock, DockPosition, PanelButtons, PanelHandle, RESIZE_HANDLE_SIZE};
 use futures::{
@@ -36,12 +35,12 @@ use futures::{
     future::try_join_all,
 };
 use gpui::{
-    Action, AnyView, AnyWeakView, App, AsyncApp, AsyncWindowContext, Bounds, Context, CursorStyle,
-    Decorations, DragMoveEvent, Entity, EntityId, EventEmitter, FocusHandle, Focusable, Global,
-    Hsla, KeyContext, Keystroke, ManagedView, MouseButton, PathPromptOptions, Point, PromptLevel,
-    Render, ResizeEdge, Size, Stateful, Subscription, Task, Tiling, WeakEntity, WindowBounds,
-    WindowHandle, WindowId, WindowOptions, action_as, actions, canvas, impl_action_as,
-    impl_actions, point, relative, size, transparent_black,
+    Action, AnyEntity, AnyView, AnyWeakView, App, AsyncApp, AsyncWindowContext, Bounds, Context,
+    CursorStyle, Decorations, DragMoveEvent, Entity, EntityId, EventEmitter, FocusHandle,
+    Focusable, Global, Hsla, KeyContext, Keystroke, ManagedView, MouseButton, PathPromptOptions,
+    Point, PromptLevel, Render, ResizeEdge, Size, Stateful, Subscription, Task, Tiling, WeakEntity,
+    WindowBounds, WindowHandle, WindowId, WindowOptions, action_as, actions, canvas,
+    impl_action_as, impl_actions, point, relative, size, transparent_black,
 };
 pub use history_manager::*;
 pub use item::{
@@ -451,44 +450,97 @@ pub fn init(app_state: Arc<AppState>, cx: &mut App) {
     });
 }
 
-#[derive(Clone, Default, Deref, DerefMut)]
-struct ProjectItemOpeners(Vec<ProjectItemOpener>);
+type BuildProjectItemFn =
+    fn(AnyEntity, Entity<Project>, Option<&Pane>, &mut Window, &mut App) -> Box<dyn ItemHandle>;
 
-type ProjectItemOpener = fn(
-    &Entity<Project>,
-    &ProjectPath,
-    &mut Window,
-    &mut App,
-)
-    -> Option<Task<Result<(Option<ProjectEntryId>, WorkspaceItemBuilder)>>>;
+type BuildProjectItemForPathFn =
+    fn(
+        &Entity<Project>,
+        &ProjectPath,
+        &mut Window,
+        &mut App,
+    ) -> Option<Task<Result<(Option<ProjectEntryId>, WorkspaceItemBuilder)>>>;
+
+#[derive(Clone, Default)]
+struct ProjectItemRegistry {
+    build_project_item_fns_by_type: HashMap<TypeId, BuildProjectItemFn>,
+    build_project_item_for_path_fns: Vec<BuildProjectItemForPathFn>,
+}
+
+impl ProjectItemRegistry {
+    fn register<T: ProjectItem>(&mut self) {
+        self.build_project_item_fns_by_type.insert(
+            TypeId::of::<T::Item>(),
+            |item, project, pane, window, cx| {
+                let item = item.downcast().unwrap();
+                Box::new(cx.new(|cx| T::for_project_item(project, pane, item, window, cx)))
+                    as Box<dyn ItemHandle>
+            },
+        );
+        self.build_project_item_for_path_fns
+            .push(|project, project_path, window, cx| {
+                let project_item =
+                    <T::Item as project::ProjectItem>::try_open(project, project_path, cx)?;
+                let project = project.clone();
+                Some(window.spawn(cx, async move |cx| {
+                    let project_item = project_item.await?;
+                    let project_entry_id: Option<ProjectEntryId> =
+                        project_item.read_with(cx, project::ProjectItem::entry_id)?;
+                    let build_workspace_item = Box::new(
+                        |pane: &mut Pane, window: &mut Window, cx: &mut Context<Pane>| {
+                            Box::new(cx.new(|cx| {
+                                T::for_project_item(project, Some(pane), project_item, window, cx)
+                            })) as Box<dyn ItemHandle>
+                        },
+                    ) as Box<_>;
+                    Ok((project_entry_id, build_workspace_item))
+                }))
+            });
+    }
+
+    fn open_path(
+        &self,
+        project: &Entity<Project>,
+        path: &ProjectPath,
+        window: &mut Window,
+        cx: &mut App,
+    ) -> Task<Result<(Option<ProjectEntryId>, WorkspaceItemBuilder)>> {
+        let Some(open_project_item) = self
+            .build_project_item_for_path_fns
+            .iter()
+            .rev()
+            .find_map(|open_project_item| open_project_item(&project, &path, window, cx))
+        else {
+            return Task::ready(Err(anyhow!("cannot open file {:?}", path.path)));
+        };
+        open_project_item
+    }
+
+    fn build_item<T: project::ProjectItem>(
+        &self,
+        item: Entity<T>,
+        project: Entity<Project>,
+        pane: Option<&Pane>,
+        window: &mut Window,
+        cx: &mut App,
+    ) -> Option<Box<dyn ItemHandle>> {
+        let build = self
+            .build_project_item_fns_by_type
+            .get(&TypeId::of::<T>())?;
+        Some(build(item.into_any(), project, pane, window, cx))
+    }
+}
 
 type WorkspaceItemBuilder =
     Box<dyn FnOnce(&mut Pane, &mut Window, &mut Context<Pane>) -> Box<dyn ItemHandle>>;
 
-impl Global for ProjectItemOpeners {}
+impl Global for ProjectItemRegistry {}
 
 /// Registers a [ProjectItem] for the app. When opening a file, all the registered
 /// items will get a chance to open the file, starting from the project item that
 /// was added last.
 pub fn register_project_item<I: ProjectItem>(cx: &mut App) {
-    let builders = cx.default_global::<ProjectItemOpeners>();
-    builders.push(|project, project_path, window, cx| {
-        let project_item = <I::Item as project::ProjectItem>::try_open(project, project_path, cx)?;
-        let project = project.clone();
-        Some(window.spawn(cx, async move |cx| {
-            let project_item = project_item.await?;
-            let project_entry_id: Option<ProjectEntryId> =
-                project_item.read_with(cx, project::ProjectItem::entry_id)?;
-            let build_workspace_item = Box::new(
-                |pane: &mut Pane, window: &mut Window, cx: &mut Context<Pane>| {
-                    Box::new(
-                        cx.new(|cx| I::for_project_item(project, pane, project_item, window, cx)),
-                    ) as Box<dyn ItemHandle>
-                },
-            ) as Box<_>;
-            Ok((project_entry_id, build_workspace_item))
-        }))
-    });
+    cx.default_global::<ProjectItemRegistry>().register::<I>();
 }
 
 #[derive(Default)]
@@ -666,6 +718,24 @@ pub struct WorkspaceStore {
     _subscriptions: Vec<client::Subscription>,
 }
 
+#[derive(Copy, Clone, Debug, Hash, Eq, PartialEq, PartialOrd, Ord)]
+pub enum CollaboratorId {
+    PeerId(PeerId),
+    Agent,
+}
+
+impl From<PeerId> for CollaboratorId {
+    fn from(peer_id: PeerId) -> Self {
+        CollaboratorId::PeerId(peer_id)
+    }
+}
+
+impl From<&PeerId> for CollaboratorId {
+    fn from(peer_id: &PeerId) -> Self {
+        CollaboratorId::PeerId(*peer_id)
+    }
+}
+
 #[derive(PartialEq, Eq, PartialOrd, Ord, Debug)]
 struct Follower {
     project_id: Option<u64>,
@@ -852,8 +922,8 @@ pub struct Workspace {
     titlebar_item: Option<AnyView>,
     notifications: Notifications,
     project: Entity<Project>,
-    follower_states: HashMap<PeerId, FollowerState>,
-    last_leaders_by_pane: HashMap<WeakEntity<Pane>, PeerId>,
+    follower_states: HashMap<CollaboratorId, FollowerState>,
+    last_leaders_by_pane: HashMap<WeakEntity<Pane>, CollaboratorId>,
     window_edited: bool,
     dirty_items: HashMap<EntityId, Subscription>,
     active_call: Option<(Entity<ActiveCall>, Vec<Subscription>)>,
@@ -883,7 +953,7 @@ impl EventEmitter<Event> for Workspace {}
 
 #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
 pub struct ViewId {
-    pub creator: PeerId,
+    pub creator: CollaboratorId,
     pub id: u64,
 }
 
@@ -984,6 +1054,10 @@ impl Workspace {
                     );
                 }
 
+                project::Event::AgentLocationChanged => {
+                    this.handle_agent_location_changed(window, cx)
+                }
+
                 _ => {}
             }
             cx.notify()
@@ -3083,15 +3157,8 @@ impl Workspace {
         cx: &mut App,
     ) -> Task<Result<(Option<ProjectEntryId>, WorkspaceItemBuilder)>> {
         let project = self.project().clone();
-        let project_item_builders = cx.default_global::<ProjectItemOpeners>().clone();
-        let Some(open_project_item) = project_item_builders
-            .iter()
-            .rev()
-            .find_map(|open_project_item| open_project_item(&project, &path, window, cx))
-        else {
-            return Task::ready(Err(anyhow!("cannot open file {:?}", path.path)));
-        };
-        open_project_item
+        let registry = cx.default_global::<ProjectItemRegistry>().clone();
+        registry.open_path(&project, &path, window, cx)
     }
 
     pub fn find_project_item<T>(
@@ -3152,7 +3219,9 @@ impl Workspace {
         }
 
         let item = pane.update(cx, |pane, cx| {
-            cx.new(|cx| T::for_project_item(self.project().clone(), pane, project_item, window, cx))
+            cx.new(|cx| {
+                T::for_project_item(self.project().clone(), Some(pane), project_item, window, cx)
+            })
         });
         let item_id = item.item_id();
         let mut destination_index = None;
@@ -3605,7 +3674,7 @@ impl Workspace {
         pane: &Entity<Pane>,
         window: &mut Window,
         cx: &mut Context<Workspace>,
-    ) -> Option<PeerId> {
+    ) -> Option<CollaboratorId> {
         let leader_id = self.leader_for_pane(pane)?;
         self.unfollow(leader_id, window, cx);
         Some(leader_id)
@@ -3785,9 +3854,9 @@ impl Workspace {
 
     fn collaborator_left(&mut self, peer_id: PeerId, window: &mut Window, cx: &mut Context<Self>) {
         self.follower_states.retain(|leader_id, state| {
-            if *leader_id == peer_id {
+            if *leader_id == CollaboratorId::PeerId(peer_id) {
                 for item in state.items_by_leader_view_id.values() {
-                    item.view.set_leader_peer_id(None, window, cx);
+                    item.view.set_leader_id(None, window, cx);
                 }
                 false
             } else {
@@ -3799,10 +3868,11 @@ impl Workspace {
 
     pub fn start_following(
         &mut self,
-        leader_id: PeerId,
+        leader_id: impl Into<CollaboratorId>,
         window: &mut Window,
         cx: &mut Context<Self>,
     ) -> Option<Task<Result<()>>> {
+        let leader_id = leader_id.into();
         let pane = self.active_pane().clone();
 
         self.last_leaders_by_pane
@@ -3820,35 +3890,43 @@ impl Workspace {
         );
         cx.notify();
 
-        let room_id = self.active_call()?.read(cx).room()?.read(cx).id();
-        let project_id = self.project.read(cx).remote_id();
-        let request = self.app_state.client.request(proto::Follow {
-            room_id,
-            project_id,
-            leader_id: Some(leader_id),
-        });
-
-        Some(cx.spawn_in(window, async move |this, cx| {
-            let response = request.await?;
-            this.update(cx, |this, _| {
-                let state = this
-                    .follower_states
-                    .get_mut(&leader_id)
-                    .ok_or_else(|| anyhow!("following interrupted"))?;
-                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, &view, cx).await?;
-            }
-            this.update_in(cx, |this, window, cx| {
-                this.leader_updated(leader_id, window, cx)
-            })?;
-            Ok(())
-        }))
+        match leader_id {
+            CollaboratorId::PeerId(leader_peer_id) => {
+                let room_id = self.active_call()?.read(cx).room()?.read(cx).id();
+                let project_id = self.project.read(cx).remote_id();
+                let request = self.app_state.client.request(proto::Follow {
+                    room_id,
+                    project_id,
+                    leader_id: Some(leader_peer_id),
+                });
+
+                Some(cx.spawn_in(window, async move |this, cx| {
+                    let response = request.await?;
+                    this.update(cx, |this, _| {
+                        let state = this
+                            .follower_states
+                            .get_mut(&leader_id)
+                            .ok_or_else(|| anyhow!("following interrupted"))?;
+                        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_peer_id, &view, cx).await?;
+                    }
+                    this.update_in(cx, |this, window, cx| {
+                        this.leader_updated(leader_id, window, cx)
+                    })?;
+                    Ok(())
+                }))
+            }
+            CollaboratorId::Agent => {
+                self.leader_updated(leader_id, window, cx)?;
+                Some(Task::ready(Ok(())))
+            }
+        }
     }
 
     pub fn follow_next_collaborator(
@@ -3861,26 +3939,34 @@ impl Workspace {
         let next_leader_id = if let Some(leader_id) = self.leader_for_pane(&self.active_pane) {
             let mut collaborators = collaborators.keys().copied();
             for peer_id in collaborators.by_ref() {
-                if peer_id == leader_id {
+                if CollaboratorId::PeerId(peer_id) == leader_id {
                     break;
                 }
             }
-            collaborators.next()
+            collaborators.next().map(CollaboratorId::PeerId)
         } else if let Some(last_leader_id) =
             self.last_leaders_by_pane.get(&self.active_pane.downgrade())
         {
-            if collaborators.contains_key(last_leader_id) {
-                Some(*last_leader_id)
-            } else {
-                None
+            match last_leader_id {
+                CollaboratorId::PeerId(peer_id) => {
+                    if collaborators.contains_key(peer_id) {
+                        Some(*last_leader_id)
+                    } else {
+                        None
+                    }
+                }
+                CollaboratorId::Agent => Some(CollaboratorId::Agent),
             }
         } else {
             None
         };
 
         let pane = self.active_pane.clone();
-        let Some(leader_id) = next_leader_id.or_else(|| collaborators.keys().copied().next())
-        else {
+        let Some(leader_id) = next_leader_id.or_else(|| {
+            Some(CollaboratorId::PeerId(
+                collaborators.keys().copied().next()?,
+            ))
+        }) else {
             return;
         };
         if self.unfollow_in_pane(&pane, window, cx) == Some(leader_id) {
@@ -3891,34 +3977,43 @@ impl Workspace {
         }
     }
 
-    pub fn follow(&mut self, leader_id: PeerId, window: &mut Window, cx: &mut Context<Self>) {
-        let Some(room) = ActiveCall::global(cx).read(cx).room() else {
-            return;
-        };
-        let room = room.read(cx);
-        let Some(remote_participant) = room.remote_participant_for_peer_id(leader_id) else {
-            return;
-        };
+    pub fn follow(
+        &mut self,
+        leader_id: impl Into<CollaboratorId>,
+        window: &mut Window,
+        cx: &mut Context<Self>,
+    ) {
+        let leader_id = leader_id.into();
 
-        let project = self.project.read(cx);
+        if let CollaboratorId::PeerId(peer_id) = leader_id {
+            let Some(room) = ActiveCall::global(cx).read(cx).room() else {
+                return;
+            };
+            let room = room.read(cx);
+            let Some(remote_participant) = room.remote_participant_for_peer_id(peer_id) else {
+                return;
+            };
 
-        let other_project_id = match remote_participant.location {
-            call::ParticipantLocation::External => None,
-            call::ParticipantLocation::UnsharedProject => None,
-            call::ParticipantLocation::SharedProject { project_id } => {
-                if Some(project_id) == project.remote_id() {
-                    None
-                } else {
-                    Some(project_id)
+            let project = self.project.read(cx);
+
+            let other_project_id = match remote_participant.location {
+                call::ParticipantLocation::External => None,
+                call::ParticipantLocation::UnsharedProject => None,
+                call::ParticipantLocation::SharedProject { project_id } => {
+                    if Some(project_id) == project.remote_id() {
+                        None
+                    } else {
+                        Some(project_id)
+                    }
                 }
-            }
-        };
+            };
 
-        // if they are active in another project, follow there.
-        if let Some(project_id) = other_project_id {
-            let app_state = self.app_state.clone();
-            crate::join_in_room_project(project_id, remote_participant.user.id, app_state, cx)
-                .detach_and_log_err(cx);
+            // if they are active in another project, follow there.
+            if let Some(project_id) = other_project_id {
+                let app_state = self.app_state.clone();
+                crate::join_in_room_project(project_id, remote_participant.user.id, app_state, cx)
+                    .detach_and_log_err(cx);
+            }
         }
 
         // if you're already following, find the right pane and focus it.
@@ -3936,32 +4031,36 @@ impl Workspace {
 
     pub fn unfollow(
         &mut self,
-        leader_id: PeerId,
+        leader_id: impl Into<CollaboratorId>,
         window: &mut Window,
         cx: &mut Context<Self>,
     ) -> Option<()> {
         cx.notify();
+
+        let leader_id = leader_id.into();
         let state = self.follower_states.remove(&leader_id)?;
         for (_, item) in state.items_by_leader_view_id {
-            item.view.set_leader_peer_id(None, window, cx);
+            item.view.set_leader_id(None, window, cx);
+        }
+
+        if let CollaboratorId::PeerId(leader_peer_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_peer_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();
-
         Some(())
     }
 
-    pub fn is_being_followed(&self, peer_id: PeerId) -> bool {
-        self.follower_states.contains_key(&peer_id)
+    pub fn is_being_followed(&self, id: impl Into<CollaboratorId>) -> bool {
+        self.follower_states.contains_key(&id.into())
     }
 
     fn active_item_path_changed(&mut self, window: &mut Window, cx: &mut Context<Self>) {
@@ -4096,6 +4195,10 @@ impl Workspace {
         let leader_id = self
             .pane_for(&*item)
             .and_then(|pane| self.leader_for_pane(&pane));
+        let leader_peer_id = match leader_id {
+            Some(CollaboratorId::PeerId(peer_id)) => Some(peer_id),
+            Some(CollaboratorId::Agent) | None => None,
+        };
 
         let item_handle = item.to_followable_item_handle(cx)?;
         let id = item_handle.remote_id(&self.app_state.client, window, cx)?;
@@ -4109,8 +4212,8 @@ impl Workspace {
         }
 
         Some(proto::View {
-            id: Some(id.to_proto()),
-            leader_id,
+            id: id.to_proto(),
+            leader_id: leader_peer_id,
             variant: Some(variant),
             panel_id: panel_id.map(|id| id as i32),
         })
@@ -4155,7 +4258,7 @@ impl Workspace {
             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) {
+                    if let Some(state) = this.follower_states.get_mut(&leader_id.into()) {
                         anyhow::Ok(!state.items_by_leader_view_id.contains_key(&view_id))
                     } else {
                         anyhow::Ok(false)
@@ -4168,7 +4271,7 @@ impl Workspace {
             }
             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) {
+                    if let Some(state) = this.follower_states.get_mut(&leader_id.into()) {
                         state.active_view_id = update_active_view
                             .view
                             .as_ref()
@@ -4202,7 +4305,7 @@ impl Workspace {
                 let mut tasks = Vec::new();
                 this.update_in(cx, |this, window, cx| {
                     let project = this.project.clone();
-                    if let Some(state) = this.follower_states.get(&leader_id) {
+                    if let Some(state) = this.follower_states.get(&leader_id.into()) {
                         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(
@@ -4241,7 +4344,7 @@ impl Workspace {
         let pane = this.update(cx, |this, _cx| {
             let state = this
                 .follower_states
-                .get(&leader_id)
+                .get(&leader_id.into())
                 .context("stopped following")?;
             anyhow::Ok(state.pane().clone())
         })??;
@@ -4304,8 +4407,8 @@ impl Workspace {
         };
 
         this.update_in(cx, |this, window, cx| {
-            let state = this.follower_states.get_mut(&leader_id)?;
-            item.set_leader_peer_id(Some(leader_id), window, cx);
+            let state = this.follower_states.get_mut(&leader_id.into())?;
+            item.set_leader_id(Some(leader_id.into()), window, cx);
             state.items_by_leader_view_id.insert(
                 id,
                 FollowerView {
@@ -4320,6 +4423,71 @@ impl Workspace {
         Ok(())
     }
 
+    fn handle_agent_location_changed(&mut self, window: &mut Window, cx: &mut Context<Self>) {
+        let Some(follower_state) = self.follower_states.get_mut(&CollaboratorId::Agent) else {
+            return;
+        };
+
+        if let Some(agent_location) = self.project.read(cx).agent_location() {
+            let buffer_entity_id = agent_location.buffer.entity_id();
+            let view_id = ViewId {
+                creator: CollaboratorId::Agent,
+                id: buffer_entity_id.as_u64(),
+            };
+            follower_state.active_view_id = Some(view_id);
+
+            let item = match follower_state.items_by_leader_view_id.entry(view_id) {
+                hash_map::Entry::Occupied(entry) => Some(entry.into_mut()),
+                hash_map::Entry::Vacant(entry) => {
+                    let existing_view =
+                        follower_state
+                            .center_pane
+                            .read(cx)
+                            .items()
+                            .find_map(|item| {
+                                let item = item.to_followable_item_handle(cx)?;
+                                if item.is_singleton(cx)
+                                    && item.project_item_model_ids(cx).as_slice()
+                                        == [buffer_entity_id]
+                                {
+                                    Some(item)
+                                } else {
+                                    None
+                                }
+                            });
+                    let view = existing_view.or_else(|| {
+                        agent_location.buffer.upgrade().and_then(|buffer| {
+                            cx.update_default_global(|registry: &mut ProjectItemRegistry, cx| {
+                                registry.build_item(buffer, self.project.clone(), None, window, cx)
+                            })?
+                            .to_followable_item_handle(cx)
+                        })
+                    });
+
+                    if let Some(view) = view {
+                        Some(entry.insert(FollowerView {
+                            view,
+                            location: None,
+                        }))
+                    } else {
+                        None
+                    }
+                }
+            };
+
+            if let Some(item) = item {
+                item.view
+                    .set_leader_id(Some(CollaboratorId::Agent), window, cx);
+                item.view
+                    .update_agent_location(agent_location.position, window, cx);
+            }
+        } else {
+            follower_state.active_view_id = None;
+        }
+
+        self.leader_updated(CollaboratorId::Agent, window, cx);
+    }
+
     pub fn update_active_view_for_followers(&mut self, window: &mut Window, cx: &mut App) {
         let mut is_project_item = true;
         let mut update = proto::UpdateActiveView::default();
@@ -4331,6 +4499,10 @@ impl Workspace {
                     let leader_id = self
                         .pane_for(&*item)
                         .and_then(|pane| self.leader_for_pane(&pane));
+                    let leader_peer_id = match leader_id {
+                        Some(CollaboratorId::PeerId(peer_id)) => Some(peer_id),
+                        Some(CollaboratorId::Agent) | None => None,
+                    };
 
                     if let Some(item) = item.to_followable_item_handle(cx) {
                         let id = item
@@ -4340,8 +4512,8 @@ impl Workspace {
                         if let Some(id) = id.clone() {
                             if let Some(variant) = item.to_state_proto(window, cx) {
                                 let view = Some(proto::View {
-                                    id: Some(id.clone()),
-                                    leader_id,
+                                    id: id.clone(),
+                                    leader_id: leader_peer_id,
                                     variant: Some(variant),
                                     panel_id: panel_id.map(|id| id as i32),
                                 });
@@ -4350,8 +4522,8 @@ impl Workspace {
                                 update = proto::UpdateActiveView {
                                     view,
                                     // TODO: Remove after version 0.145.x stabilizes.
-                                    id: Some(id.clone()),
-                                    leader_id,
+                                    id: id.clone(),
+                                    leader_id: leader_peer_id,
                                 };
                             }
                         };
@@ -4420,7 +4592,7 @@ impl Workspace {
         })
     }
 
-    pub fn leader_for_pane(&self, pane: &Entity<Pane>) -> Option<PeerId> {
+    pub fn leader_for_pane(&self, pane: &Entity<Pane>) -> Option<CollaboratorId> {
         self.follower_states.iter().find_map(|(leader_id, state)| {
             if state.center_pane == *pane || state.dock_pane.as_ref() == Some(pane) {
                 Some(*leader_id)
@@ -4432,49 +4604,19 @@ impl Workspace {
 
     fn leader_updated(
         &mut self,
-        leader_id: PeerId,
+        leader_id: impl Into<CollaboratorId>,
         window: &mut Window,
         cx: &mut Context<Self>,
-    ) -> Option<()> {
+    ) -> Option<Box<dyn ItemHandle>> {
         cx.notify();
 
-        let call = self.active_call()?;
-        let room = call.read(cx).room()?.read(cx);
-        let participant = room.remote_participant_for_peer_id(leader_id)?;
-
-        let leader_in_this_app;
-        let leader_in_this_project;
-        match participant.location {
-            call::ParticipantLocation::SharedProject { project_id } => {
-                leader_in_this_app = true;
-                leader_in_this_project = Some(project_id) == self.project.read(cx).remote_id();
-            }
-            call::ParticipantLocation::UnsharedProject => {
-                leader_in_this_app = true;
-                leader_in_this_project = false;
-            }
-            call::ParticipantLocation::External => {
-                leader_in_this_app = false;
-                leader_in_this_project = false;
-            }
+        let leader_id = leader_id.into();
+        let (panel_id, item) = match leader_id {
+            CollaboratorId::PeerId(peer_id) => self.active_item_for_peer(peer_id, window, cx)?,
+            CollaboratorId::Agent => (None, self.active_item_for_agent()?),
         };
 
         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(window, cx) {
-                    item_to_activate = Some((item.location, item.view.boxed_clone()));
-                }
-            }
-        } else if let Some(shared_screen) =
-            self.shared_screen_for_peer(leader_id, &state.center_pane, window, cx)
-        {
-            item_to_activate = Some((None, Box::new(shared_screen)));
-        }
-
-        let (panel_id, item) = item_to_activate?;
-
         let mut transfer_focus = state.center_pane.read(cx).has_focus(window, cx);
         let pane;
         if let Some(panel_id) = panel_id {
@@ -4504,7 +4646,60 @@ impl Workspace {
             }
         });
 
-        None
+        Some(item)
+    }
+
+    fn active_item_for_agent(&self) -> Option<Box<dyn ItemHandle>> {
+        let state = self.follower_states.get(&CollaboratorId::Agent)?;
+        let active_view_id = state.active_view_id?;
+        Some(
+            state
+                .items_by_leader_view_id
+                .get(&active_view_id)?
+                .view
+                .boxed_clone(),
+        )
+    }
+
+    fn active_item_for_peer(
+        &self,
+        peer_id: PeerId,
+        window: &mut Window,
+        cx: &mut Context<Self>,
+    ) -> Option<(Option<PanelId>, Box<dyn ItemHandle>)> {
+        let call = self.active_call()?;
+        let room = call.read(cx).room()?.read(cx);
+        let participant = room.remote_participant_for_peer_id(peer_id)?;
+        let leader_in_this_app;
+        let leader_in_this_project;
+        match participant.location {
+            call::ParticipantLocation::SharedProject { project_id } => {
+                leader_in_this_app = true;
+                leader_in_this_project = Some(project_id) == self.project.read(cx).remote_id();
+            }
+            call::ParticipantLocation::UnsharedProject => {
+                leader_in_this_app = true;
+                leader_in_this_project = false;
+            }
+            call::ParticipantLocation::External => {
+                leader_in_this_app = false;
+                leader_in_this_project = false;
+            }
+        };
+        let state = self.follower_states.get(&peer_id.into())?;
+        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(window, cx) {
+                    item_to_activate = Some((item.location, item.view.boxed_clone()));
+                }
+            }
+        } else if let Some(shared_screen) =
+            self.shared_screen_for_peer(peer_id, &state.center_pane, window, cx)
+        {
+            item_to_activate = Some((None, Box::new(shared_screen)));
+        }
+        item_to_activate
     }
 
     fn shared_screen_for_peer(
@@ -4571,7 +4766,7 @@ impl Workspace {
         match event {
             call::room::Event::ParticipantLocationChanged { participant_id }
             | call::room::Event::RemoteVideoTracksChanged { participant_id } => {
-                self.leader_updated(*participant_id, window, cx);
+                self.leader_updated(participant_id, window, cx);
             }
             _ => {}
         }
@@ -5285,7 +5480,7 @@ impl Workspace {
 }
 
 fn leader_border_for_pane(
-    follower_states: &HashMap<PeerId, FollowerState>,
+    follower_states: &HashMap<CollaboratorId, FollowerState>,
     pane: &Entity<Pane>,
     _: &Window,
     cx: &App,
@@ -5298,14 +5493,18 @@ fn leader_border_for_pane(
         }
     })?;
 
-    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 = match leader_id {
+        CollaboratorId::PeerId(leader_peer_id) => {
+            let room = ActiveCall::try_global(cx)?.read(cx).room()?.read(cx);
+            let leader = room.remote_participant_for_peer_id(leader_peer_id)?;
 
-    let mut leader_color = cx
-        .theme()
-        .players()
-        .color_for_participant(leader.participant_index.0)
-        .cursor;
+            cx.theme()
+                .players()
+                .color_for_participant(leader.participant_index.0)
+                .cursor
+        }
+        CollaboratorId::Agent => cx.theme().players().agent().cursor,
+    };
     leader_color.fade_out(0.3);
     Some(
         div()
@@ -6029,15 +6228,20 @@ impl ViewId {
         Ok(Self {
             creator: message
                 .creator
+                .map(CollaboratorId::PeerId)
                 .ok_or_else(|| anyhow!("creator is missing"))?,
             id: message.id,
         })
     }
 
-    pub(crate) fn to_proto(self) -> proto::ViewId {
-        proto::ViewId {
-            creator: Some(self.creator),
-            id: self.id,
+    pub(crate) fn to_proto(self) -> Option<proto::ViewId> {
+        if let CollaboratorId::PeerId(peer_id) = self.creator {
+            Some(proto::ViewId {
+                creator: Some(peer_id),
+                id: self.id,
+            })
+        } else {
+            None
         }
     }
 }
@@ -9069,7 +9273,7 @@ mod tests {
 
             fn for_project_item(
                 _project: Entity<Project>,
-                _pane: &Pane,
+                _pane: Option<&Pane>,
                 _item: Entity<Self::Item>,
                 _: &mut Window,
                 cx: &mut Context<Self>,
@@ -9144,7 +9348,7 @@ mod tests {
 
             fn for_project_item(
                 _project: Entity<Project>,
-                _pane: &Pane,
+                _pane: Option<&Pane>,
                 _item: Entity<Self::Item>,
                 _: &mut Window,
                 cx: &mut Context<Self>,
@@ -9191,7 +9395,7 @@ mod tests {
 
             fn for_project_item(
                 _project: Entity<Project>,
-                _pane: &Pane,
+                _pane: Option<&Pane>,
                 _item: Entity<Self::Item>,
                 _: &mut Window,
                 cx: &mut Context<Self>,