diff --git a/Cargo.lock b/Cargo.lock
index 6a76f96cd9..d3cbc2afb0 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -18092,7 +18092,6 @@ dependencies = [
"component",
"dap",
"db",
- "derive_more",
"env_logger 0.11.8",
"fs",
"futures 0.3.31",
diff --git a/assets/icons/crosshair.svg b/assets/icons/crosshair.svg
new file mode 100644
index 0000000000..cd8c40ed03
--- /dev/null
+++ b/assets/icons/crosshair.svg
@@ -0,0 +1 @@
+
diff --git a/crates/agent/src/assistant.rs b/crates/agent/src/assistant.rs
index 8f45ca042b..d454f04343 100644
--- a/crates/agent/src/assistant.rs
+++ b/crates/agent/src/assistant.rs
@@ -77,7 +77,8 @@ actions!(
Keep,
Reject,
RejectAll,
- KeepAll
+ KeepAll,
+ Follow
]
);
diff --git a/crates/agent/src/assistant_model_selector.rs b/crates/agent/src/assistant_model_selector.rs
index fc8d035293..47bb62be9c 100644
--- a/crates/agent/src/assistant_model_selector.rs
+++ b/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)
diff --git a/crates/agent/src/assistant_panel.rs b/crates/agent/src/assistant_panel.rs
index 2d93f76b85..0e8836ebaa 100644
--- a/crates/agent/src/assistant_panel.rs
+++ b/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::(cx) {
workspace.focus_panel::(window, cx);
diff --git a/crates/agent/src/context_strip.rs b/crates/agent/src/context_strip.rs
index e8184d9055..b06446483c 100644
--- a/crates/agent/src/context_strip.rs
+++ b/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) -> impl IntoElement {
+ fn render(&mut self, _window: &mut Window, cx: &mut Context) -> 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()
diff --git a/crates/agent/src/message_editor.rs b/crates/agent/src/message_editor.rs
index 30e3ada988..2ac2d9f92d 100644
--- a/crates/agent/src/message_editor.rs
+++ b/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) -> 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()
- };
+ .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);
- })),
- )
- }),
+ 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()
diff --git a/crates/agent/src/profile_selector.rs b/crates/agent/src/profile_selector.rs
index fa4032d28c..db08e93577 100644
--- a/crates/agent/src/profile_selector.rs
+++ b/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,
thread_store: WeakEntity,
- focus_handle: FocusHandle,
menu_handle: PopoverMenuHandle,
_subscriptions: Vec,
}
@@ -28,7 +26,6 @@ impl ProfileSelector {
pub fn new(
fs: Arc,
thread_store: WeakEntity,
- focus_handle: FocusHandle,
cx: &mut Context,
) -> Self {
let settings_subscription = cx.observe_global::(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) -> impl IntoElement {
+ fn render(&mut self, _window: &mut Window, cx: &mut Context) -> 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")
diff --git a/crates/agent/src/thread.rs b/crates/agent/src/thread.rs
index 4a90916888..02fee57a47 100644
--- a/crates/agent/src/thread.rs
+++ b/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::() {
cx.emit(ThreadEvent::ShowError(ThreadError::PaymentRequired));
} else if error.is::() {
diff --git a/crates/assistant_context_editor/src/context_editor.rs b/crates/assistant_context_editor/src/context_editor.rs
index b2a9b884b7..894da1b00b 100644
--- a/crates/assistant_context_editor/src/context_editor.rs
+++ b/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,
+ leader_id: Option,
window: &mut Window,
cx: &mut Context,
) {
- 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 {
diff --git a/crates/assistant_tool/src/action_log.rs b/crates/assistant_tool/src/action_log.rs
index 61fc983b9e..de6263b5a7 100644
--- a/crates/assistant_tool/src/action_log.rs
+++ b/crates/assistant_tool/src/action_log.rs
@@ -29,6 +29,10 @@ impl ActionLog {
}
}
+ pub fn project(&self) -> &Entity {
+ &self.project
+ }
+
/// Notifies a diagnostics check
pub fn checked_project_diagnostics(&mut self) {
self.edited_since_project_diagnostics_check = false;
diff --git a/crates/assistant_tools/src/edit_agent.rs b/crates/assistant_tools/src/edit_agent.rs
index 8925f9b7fc..6495bbe1fc 100644
--- a/crates/assistant_tools/src/edit_agent.rs
+++ b/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,
action_log: Entity,
+ project: Entity,
templates: Arc,
}
impl EditAgent {
pub fn new(
model: Arc,
+ project: Entity,
action_log: Entity,
templates: Arc,
) -> Self {
EditAgent {
model,
+ project,
action_log,
templates,
}
@@ -118,39 +122,74 @@ 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))?;
-
- cx.update(|cx| {
- buffer.update(cx, |buffer, cx| buffer.set_text("", cx));
- this.action_log
- .update(cx, |log, cx| log.buffer_edited(buffer.clone(), cx));
- })?;
-
- 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(),
- })
+ 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,
+ edit_chunks: impl 'static + Send + Stream- >,
+ output_events_tx: mpsc::UnboundedSender,
+ cx: &mut AsyncApp,
+ ) -> Result {
+ 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.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();
+ }
+
+ Ok(EditAgentOutput {
+ _raw_edits: raw_edits,
+ _parser_metrics: EditParserMetrics::default(),
+ })
+ }
+
pub fn edit(
&self,
buffer: Entity,
@@ -161,6 +200,18 @@ impl EditAgent {
Task>,
mpsc::UnboundedReceiver,
) {
+ 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,
cx: &mut AsyncApp,
) -> Result {
- // 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::(
+ 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").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("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("").unwrap();
chunks_tx.unbounded_send("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").unwrap();
chunks_tx.unbounded_send("").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").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("gh").unwrap();
chunks_tx.unbounded_send("i").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").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,
- ) -> Vec {
- 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,
+ ) -> Vec {
+ let mut events = Vec::new();
+ while let Ok(Some(event)) = stream.try_next() {
+ events.push(event);
+ }
+ events
}
}
diff --git a/crates/assistant_tools/src/edit_agent/evals.rs b/crates/assistant_tools/src/edit_agent/evals.rs
index dfdc60fd2d..76099c8fa8 100644
--- a/crates/assistant_tools/src/edit_agent/evals.rs
+++ b/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,
}
diff --git a/crates/assistant_tools/src/edit_file_tool.rs b/crates/assistant_tools/src/edit_file_tool.rs
index 76d42d3812..e22502ef9b 100644
--- a/crates/assistant_tools/src/edit_file_tool.rs
+++ b/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::(&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
})?;
diff --git a/crates/assistant_tools/src/read_file_tool.rs b/crates/assistant_tools/src/read_file_tool.rs
index a7f4f2ef2c..942bffd41b 100644
--- a/crates/assistant_tools/src/read_file_tool.rs
+++ b/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,
+ pub start_line: Option,
/// Optional line number to end reading on (1-based index, inclusive)
#[serde(default)]
- pub end_line: Option,
+ pub end_line: Option,
}
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()
}
}
diff --git a/crates/assistant_tools/src/streaming_edit_file_tool.rs b/crates/assistant_tools/src/streaming_edit_file_tool.rs
index f99ea60072..356899bad5 100644
--- a/crates/assistant_tools/src/streaming_edit_file_tool.rs
+++ b/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| {
diff --git a/crates/clock/src/clock.rs b/crates/clock/src/clock.rs
index acbde90dc1..b4f57116d2 100644
--- a/crates/clock/src/clock.rs
+++ b/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;
diff --git a/crates/collab/src/tests/channel_buffer_tests.rs b/crates/collab/src/tests/channel_buffer_tests.rs
index 4ed2745aa5..4069f61f90 100644
--- a/crates/collab/src/tests/channel_buffer_tests.rs
+++ b/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,
) {
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::>();
assert_eq!(
diff --git a/crates/collab/src/tests/following_tests.rs b/crates/collab/src/tests/following_tests.rs
index 57494bd42b..0f5bdd5326 100644
--- a/crates/collab/src/tests/following_tests.rs
+++ b/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, 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)
diff --git a/crates/collab_ui/src/channel_view.rs b/crates/collab_ui/src/channel_view.rs
index acbb80c36a..ff1247f5b3 100644
--- a/crates/collab_ui/src/channel_view.rs
+++ b/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,
+ leader_id: Option,
window: &mut Window,
cx: &mut Context,
) {
- 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 {
diff --git a/crates/debugger_ui/src/session.rs b/crates/debugger_ui/src/session.rs
index d34d4d3c44..aa06ea1961 100644
--- a/crates/debugger_ui/src/session.rs
+++ b/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,
+ _leader_id: Option,
_window: &mut Window,
_cx: &mut Context,
) {
diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs
index 05c71d98aa..c49da98be4 100644
--- a/crates/editor/src/editor.rs
+++ b/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,
+ leader_id: Option,
remote_id: Option,
pub hover_state: HoverState,
pending_mouse_down: Option>>>,
@@ -1059,10 +1059,10 @@ pub struct RemoteSelection {
pub replica_id: ReplicaId,
pub selection: Selection,
pub cursor_shape: CursorShape,
- pub peer_id: PeerId,
+ pub collaborator_id: CollaboratorId,
pub line_mode: bool,
- pub participant_index: Option,
pub user_name: Option,
+ 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 {
- self.leader_peer_id
+ pub fn leader_id(&self) -> Option {
+ self.leader_id
}
pub fn buffer(&self) -> &Entity {
@@ -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()
+ },
+ })
+ }
})
}
diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs
index 1baf7c39cb..676233227b 100644
--- a/crates/editor/src/editor_tests.rs
+++ b/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,
diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs
index 7c8848d841..b77023ffce 100644
--- a/crates/editor/src/element.rs
+++ b/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, 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,
diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs
index ca8cdbd788..0a482f5366 100644
--- a/crates/editor/src/items.rs
+++ b/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,
+ leader_id: Option,
window: &mut Window,
cx: &mut Context,
) {
- 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,
+ ) {
+ 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,
- pane: &Pane,
+ pane: Option<&Pane>,
buffer: Entity,
window: &mut Window,
cx: &mut Context,
@@ -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::())
.and_then(|data| {
let file = project::File::from_dyn(buffer.read(cx).file())?;
diff --git a/crates/icons/src/icons.rs b/crates/icons/src/icons.rs
index fc0d9fed3c..8d9d1ea8f0 100644
--- a/crates/icons/src/icons.rs
+++ b/crates/icons/src/icons.rs
@@ -72,6 +72,7 @@ pub enum IconName {
CopilotInit,
Copy,
CountdownTimer,
+ Crosshair,
CursorIBeam,
Dash,
DatabaseZap,
diff --git a/crates/image_viewer/src/image_viewer.rs b/crates/image_viewer/src/image_viewer.rs
index ba735b356f..9ac8358a17 100644
--- a/crates/image_viewer/src/image_viewer.rs
+++ b/crates/image_viewer/src/image_viewer.rs
@@ -375,7 +375,7 @@ impl ProjectItem for ImageView {
fn for_project_item(
project: Entity,
- _: &Pane,
+ _: Option<&Pane>,
item: Entity,
window: &mut Window,
cx: &mut Context,
diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs
index 4636e19b12..98b577ea39 100644
--- a/crates/language/src/buffer.rs
+++ b/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]>,
+ line_mode: bool,
+ cursor_shape: CursorShape,
+ cx: &mut Context,
+ ) {
+ 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.set_agent_selections(Arc::default(), false, Default::default(), cx);
+ }
+
/// Replaces the buffer's entire text.
pub fn set_text(&mut self, text: T, cx: &mut Context) -> Option
where
diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs
index dc4aa48991..7b4be1ea26 100644
--- a/crates/project/src/project.rs
+++ b/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,
path: &ProjectPath,
@@ -197,6 +197,13 @@ pub struct Project {
environment: Entity,
settings_observer: Entity,
toolchain_store: Option>,
+ agent_location: Option,
+}
+
+#[derive(Clone, Debug, PartialEq, Eq)]
+pub struct AgentLocation {
+ pub buffer: WeakEntity,
+ 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