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

@ -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::*;