acp: Remember following state (#36793)

Conrad Irwin created

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

Change summary

crates/acp_thread/src/acp_thread.rs    |  2 
crates/agent_ui/src/acp/thread_view.rs | 73 +++++++++++++++++++++------
2 files changed, 58 insertions(+), 17 deletions(-)

Detailed changes

crates/acp_thread/src/acp_thread.rs 🔗

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

crates/agent_ui/src/acp/thread_view.rs 🔗

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