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

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>
This commit is contained in:
Antonio Scandurra 2025-05-04 10:28:39 +02:00 committed by GitHub
parent 425f32e068
commit 545ae27079
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
37 changed files with 1255 additions and 567 deletions

View file

@ -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!(

View file

@ -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)