acp: Remember following state (#36793)

A beta user reported that following was "lost" when asking for
confirmation, I
suspect they moved their cursor in the agent file while reviewing the
change.
Now we will resume following when the agent starts up again.

Release Notes:

- N/A
This commit is contained in:
Conrad Irwin 2025-08-25 09:34:30 -06:00 committed by GitHub
parent 2fe3dbed31
commit 65fb17e2c9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 58 additions and 17 deletions

View file

@ -774,7 +774,7 @@ pub enum AcpThreadEvent {
impl EventEmitter<AcpThreadEvent> for AcpThread {} impl EventEmitter<AcpThreadEvent> for AcpThread {}
#[derive(PartialEq, Eq)] #[derive(PartialEq, Eq, Debug)]
pub enum ThreadStatus { pub enum ThreadStatus {
Idle, Idle,
WaitingForToolConfirmation, WaitingForToolConfirmation,

View file

@ -274,6 +274,7 @@ pub struct AcpThreadView {
edits_expanded: bool, edits_expanded: bool,
plan_expanded: bool, plan_expanded: bool,
editor_expanded: bool, editor_expanded: bool,
should_be_following: bool,
editing_message: Option<usize>, editing_message: Option<usize>,
prompt_capabilities: Rc<Cell<PromptCapabilities>>, prompt_capabilities: Rc<Cell<PromptCapabilities>>,
is_loading_contents: bool, is_loading_contents: bool,
@ -385,6 +386,7 @@ impl AcpThreadView {
edits_expanded: false, edits_expanded: false,
plan_expanded: false, plan_expanded: false,
editor_expanded: false, editor_expanded: false,
should_be_following: false,
history_store, history_store,
hovered_recent_history_item: None, hovered_recent_history_item: None,
prompt_capabilities, prompt_capabilities,
@ -897,6 +899,13 @@ impl AcpThreadView {
let Some(thread) = self.thread().cloned() else { let Some(thread) = self.thread().cloned() else {
return; return;
}; };
if self.should_be_following {
self.workspace
.update(cx, |workspace, cx| {
workspace.follow(CollaboratorId::Agent, window, cx);
})
.ok();
}
self.is_loading_contents = true; self.is_loading_contents = true;
let guard = cx.new(|_| ()); let guard = cx.new(|_| ());
@ -938,6 +947,16 @@ impl AcpThreadView {
this.handle_thread_error(err, cx); this.handle_thread_error(err, cx);
}) })
.ok(); .ok();
} else {
this.update(cx, |this, cx| {
this.should_be_following = this
.workspace
.update(cx, |workspace, _| {
workspace.is_being_followed(CollaboratorId::Agent)
})
.unwrap_or_default();
})
.ok();
} }
}) })
.detach(); .detach();
@ -1254,6 +1273,7 @@ impl AcpThreadView {
tool_call_id: acp::ToolCallId, tool_call_id: acp::ToolCallId,
option_id: acp::PermissionOptionId, option_id: acp::PermissionOptionId,
option_kind: acp::PermissionOptionKind, option_kind: acp::PermissionOptionKind,
window: &mut Window,
cx: &mut Context<Self>, cx: &mut Context<Self>,
) { ) {
let Some(thread) = self.thread() else { let Some(thread) = self.thread() else {
@ -1262,6 +1282,13 @@ impl AcpThreadView {
thread.update(cx, |thread, cx| { thread.update(cx, |thread, cx| {
thread.authorize_tool_call(tool_call_id, option_id, option_kind, cx); thread.authorize_tool_call(tool_call_id, option_id, option_kind, cx);
}); });
if self.should_be_following {
self.workspace
.update(cx, |workspace, cx| {
workspace.follow(CollaboratorId::Agent, window, cx);
})
.ok();
}
cx.notify(); cx.notify();
} }
@ -2095,11 +2122,12 @@ impl AcpThreadView {
let tool_call_id = tool_call_id.clone(); let tool_call_id = tool_call_id.clone();
let option_id = option.id.clone(); let option_id = option.id.clone();
let option_kind = option.kind; let option_kind = option.kind;
move |this, _, _, cx| { move |this, _, window, cx| {
this.authorize_tool_call( this.authorize_tool_call(
tool_call_id.clone(), tool_call_id.clone(),
option_id.clone(), option_id.clone(),
option_kind, option_kind,
window,
cx, cx,
); );
} }
@ -3652,13 +3680,34 @@ impl AcpThreadView {
} }
} }
fn render_follow_toggle(&self, cx: &mut Context<Self>) -> impl IntoElement { fn is_following(&self, cx: &App) -> bool {
let following = self match self.thread().map(|thread| thread.read(cx).status()) {
.workspace Some(ThreadStatus::Generating) => self
.read_with(cx, |workspace, _| { .workspace
workspace.is_being_followed(CollaboratorId::Agent) .read_with(cx, |workspace, _| {
workspace.is_being_followed(CollaboratorId::Agent)
})
.unwrap_or(false),
_ => self.should_be_following,
}
}
fn toggle_following(&mut self, window: &mut Window, cx: &mut Context<Self>) {
let following = self.is_following(cx);
self.should_be_following = !following;
self.workspace
.update(cx, |workspace, cx| {
if following {
workspace.unfollow(CollaboratorId::Agent, window, cx);
} else {
workspace.follow(CollaboratorId::Agent, window, cx);
}
}) })
.unwrap_or(false); .ok();
}
fn render_follow_toggle(&self, cx: &mut Context<Self>) -> impl IntoElement {
let following = self.is_following(cx);
IconButton::new("follow-agent", IconName::Crosshair) IconButton::new("follow-agent", IconName::Crosshair)
.icon_size(IconSize::Small) .icon_size(IconSize::Small)
@ -3679,15 +3728,7 @@ impl AcpThreadView {
} }
}) })
.on_click(cx.listener(move |this, _, window, cx| { .on_click(cx.listener(move |this, _, window, cx| {
this.workspace this.toggle_following(window, cx);
.update(cx, |workspace, cx| {
if following {
workspace.unfollow(CollaboratorId::Agent, window, cx);
} else {
workspace.follow(CollaboratorId::Agent, window, cx);
}
})
.ok();
})) }))
} }