agent_ui: More refactors (#48186)

Cameron Mcloughlin , Ben Brandt , and Bennet Bo Fenner created

Release Notes:

- N/A *or* Added/Fixed/Improved ...

---------

Co-authored-by: Ben Brandt <benjamin.j.brandt@gmail.com>
Co-authored-by: Bennet Bo Fenner <bennetbo@gmx.de>

Change summary

crates/agent/src/agent.rs                            |  66 
crates/agent/src/tests/mod.rs                        |  61 -
crates/agent/src/thread.rs                           |  17 
crates/agent/src/tools/subagent_tool.rs              |  75 -
crates/agent_ui/src/acp.rs                           |   2 
crates/agent_ui/src/acp/thread_history.rs            |   6 
crates/agent_ui/src/acp/thread_view.rs               | 634 +++++++------
crates/agent_ui/src/acp/thread_view/active_thread.rs | 106 +-
crates/agent_ui/src/agent_panel.rs                   |  55 
crates/agent_ui_v2/src/agent_thread_pane.rs          |   6 
crates/zed/src/visual_test_runner.rs                 |   2 
11 files changed, 474 insertions(+), 556 deletions(-)

Detailed changes

crates/agent/src/agent.rs πŸ”—

@@ -306,6 +306,36 @@ impl NativeAgent {
         }))
     }
 
+    fn new_session(
+        &mut self,
+        project: Entity<Project>,
+        cx: &mut Context<Self>,
+    ) -> Entity<AcpThread> {
+        // Create Thread
+        // Fetch default model from registry settings
+        let registry = LanguageModelRegistry::read_global(cx);
+        // Log available models for debugging
+        let available_count = registry.available_models(cx).count();
+        log::debug!("Total available models: {}", available_count);
+
+        let default_model = registry.default_model().and_then(|default_model| {
+            self.models
+                .model_from_id(&LanguageModels::model_id(&default_model.model))
+        });
+        let thread = cx.new(|cx| {
+            Thread::new(
+                project.clone(),
+                self.project_context.clone(),
+                self.context_server_registry.clone(),
+                self.templates.clone(),
+                default_model,
+                cx,
+            )
+        });
+
+        self.register_session(thread, cx)
+    }
+
     fn register_session(
         &mut self,
         thread_handle: Entity<Thread>,
@@ -1187,38 +1217,10 @@ impl acp_thread::AgentConnection for NativeAgentConnection {
         cwd: &Path,
         cx: &mut App,
     ) -> Task<Result<Entity<acp_thread::AcpThread>>> {
-        let agent = self.0.clone();
-        log::debug!("Creating new thread for project at: {:?}", cwd);
-
-        cx.spawn(async move |cx| {
-            log::debug!("Starting thread creation in async context");
-
-            // Create Thread
-            let thread = agent.update(cx, |agent, cx| {
-                // Fetch default model from registry settings
-                let registry = LanguageModelRegistry::read_global(cx);
-                // Log available models for debugging
-                let available_count = registry.available_models(cx).count();
-                log::debug!("Total available models: {}", available_count);
-
-                let default_model = registry.default_model().and_then(|default_model| {
-                    agent
-                        .models
-                        .model_from_id(&LanguageModels::model_id(&default_model.model))
-                });
-                cx.new(|cx| {
-                    Thread::new(
-                        project.clone(),
-                        agent.project_context.clone(),
-                        agent.context_server_registry.clone(),
-                        agent.templates.clone(),
-                        default_model,
-                        cx,
-                    )
-                })
-            });
-            Ok(agent.update(cx, |agent, cx| agent.register_session(thread, cx)))
-        })
+        log::debug!("Creating new thread for project at: {cwd:?}");
+        Task::ready(Ok(self
+            .0
+            .update(cx, |agent, cx| agent.new_session(project, cx))))
     }
 
     fn supports_load_session(&self, _cx: &App) -> bool {

crates/agent/src/tests/mod.rs πŸ”—

@@ -4293,19 +4293,8 @@ async fn test_subagent_tool_cancellation(cx: &mut TestAppContext) {
         )
     });
 
-    let parent_tools: std::collections::BTreeMap<gpui::SharedString, Arc<dyn crate::AnyAgentTool>> =
-        std::collections::BTreeMap::new();
-
     #[allow(clippy::arc_with_non_send_sync)]
-    let tool = Arc::new(SubagentTool::new(
-        parent.downgrade(),
-        project.clone(),
-        project_context,
-        context_server_registry,
-        Templates::new(),
-        0,
-        parent_tools,
-    ));
+    let tool = Arc::new(SubagentTool::new(parent.downgrade(), 0));
 
     let (event_stream, _rx, mut cancellation_tx) =
         crate::ToolCallEventStream::test_with_cancellation();
@@ -4589,25 +4578,12 @@ async fn test_allowed_tools_rejects_unknown_tool(cx: &mut TestAppContext) {
         thread
     });
 
-    let mut parent_tools: std::collections::BTreeMap<
-        gpui::SharedString,
-        Arc<dyn crate::AnyAgentTool>,
-    > = std::collections::BTreeMap::new();
-    parent_tools.insert("echo".into(), EchoTool.erase());
-
     #[allow(clippy::arc_with_non_send_sync)]
-    let tool = Arc::new(SubagentTool::new(
-        parent.downgrade(),
-        project,
-        project_context,
-        context_server_registry,
-        Templates::new(),
-        0,
-        parent_tools,
-    ));
+    let tool = Arc::new(SubagentTool::new(parent.downgrade(), 0));
 
     let allowed_tools = Some(vec!["nonexistent_tool".to_string()]);
-    let result = tool.validate_allowed_tools(&allowed_tools);
+    let result = cx.read(|cx| tool.validate_allowed_tools(&allowed_tools, cx));
+
     assert!(result.is_err(), "should reject unknown tool");
     let err_msg = result.unwrap_err().to_string();
     assert!(
@@ -4910,19 +4886,8 @@ async fn test_max_parallel_subagents_enforced(cx: &mut TestAppContext) {
         );
     });
 
-    let parent_tools: std::collections::BTreeMap<gpui::SharedString, Arc<dyn crate::AnyAgentTool>> =
-        std::collections::BTreeMap::new();
-
     #[allow(clippy::arc_with_non_send_sync)]
-    let tool = Arc::new(SubagentTool::new(
-        parent.downgrade(),
-        project.clone(),
-        project_context,
-        context_server_registry,
-        Templates::new(),
-        0,
-        parent_tools,
-    ));
+    let tool = Arc::new(SubagentTool::new(parent.downgrade(), 0));
 
     let (event_stream, _rx) = crate::ToolCallEventStream::test();
 
@@ -4983,22 +4948,8 @@ async fn test_subagent_tool_end_to_end(cx: &mut TestAppContext) {
         thread
     });
 
-    let mut parent_tools: std::collections::BTreeMap<
-        gpui::SharedString,
-        Arc<dyn crate::AnyAgentTool>,
-    > = std::collections::BTreeMap::new();
-    parent_tools.insert("echo".into(), EchoTool.erase());
-
     #[allow(clippy::arc_with_non_send_sync)]
-    let tool = Arc::new(SubagentTool::new(
-        parent.downgrade(),
-        project.clone(),
-        project_context,
-        context_server_registry,
-        Templates::new(),
-        0,
-        parent_tools,
-    ));
+    let tool = Arc::new(SubagentTool::new(parent.downgrade(), 0));
 
     let (event_stream, _rx) = crate::ToolCallEventStream::test();
 

crates/agent/src/thread.rs πŸ”—

@@ -784,16 +784,16 @@ pub struct Thread {
     /// Used to signal that the turn should end at the next message boundary.
     has_queued_message: bool,
     pending_message: Option<AgentMessage>,
-    tools: BTreeMap<SharedString, Arc<dyn AnyAgentTool>>,
+    pub(crate) tools: BTreeMap<SharedString, Arc<dyn AnyAgentTool>>,
     request_token_usage: HashMap<UserMessageId, language_model::TokenUsage>,
     #[allow(unused)]
     cumulative_token_usage: TokenUsage,
     #[allow(unused)]
     initial_project_snapshot: Shared<Task<Option<Arc<ProjectSnapshot>>>>,
-    context_server_registry: Entity<ContextServerRegistry>,
+    pub(crate) context_server_registry: Entity<ContextServerRegistry>,
     profile_id: AgentProfileId,
     project_context: Entity<ProjectContext>,
-    templates: Arc<Templates>,
+    pub(crate) templates: Arc<Templates>,
     model: Option<Arc<dyn LanguageModel>>,
     summarization_model: Option<Arc<dyn LanguageModel>>,
     thinking_enabled: bool,
@@ -1280,16 +1280,7 @@ impl Thread {
         self.add_tool(WebSearchTool);
 
         if cx.has_flag::<SubagentsFeatureFlag>() && self.depth() < MAX_SUBAGENT_DEPTH {
-            let parent_tools = self.tools.clone();
-            self.add_tool(SubagentTool::new(
-                cx.weak_entity(),
-                self.project.clone(),
-                self.project_context.clone(),
-                self.context_server_registry.clone(),
-                self.templates.clone(),
-                self.depth(),
-                parent_tools,
-            ));
+            self.add_tool(SubagentTool::new(cx.weak_entity(), self.depth()));
         }
     }
 

crates/agent/src/tools/subagent_tool.rs πŸ”—

@@ -7,7 +7,6 @@ use futures::{FutureExt, channel::mpsc};
 use gpui::{App, AppContext, AsyncApp, Entity, SharedString, Task, WeakEntity};
 use language_model::LanguageModelToolUseId;
 use project::Project;
-use prompt_store::ProjectContext;
 use schemars::JsonSchema;
 use serde::{Deserialize, Serialize};
 use smol::stream::StreamExt;
@@ -20,8 +19,8 @@ use util::ResultExt;
 use watch;
 
 use crate::{
-    AgentTool, AnyAgentTool, ContextServerRegistry, MAX_PARALLEL_SUBAGENTS, MAX_SUBAGENT_DEPTH,
-    SubagentContext, Templates, Thread, ThreadEvent, ToolCallAuthorization, ToolCallEventStream,
+    AgentTool, AnyAgentTool, MAX_PARALLEL_SUBAGENTS, MAX_SUBAGENT_DEPTH, SubagentContext, Thread,
+    ThreadEvent, ToolCallAuthorization, ToolCallEventStream,
 };
 
 /// When a subagent's remaining context window falls below this fraction (25%),
@@ -87,57 +86,38 @@ pub struct SubagentToolInput {
 /// Tool that spawns a subagent thread to work on a task.
 pub struct SubagentTool {
     parent_thread: WeakEntity<Thread>,
-    project: Entity<Project>,
-    project_context: Entity<ProjectContext>,
-    context_server_registry: Entity<ContextServerRegistry>,
-    templates: Arc<Templates>,
     current_depth: u8,
-    /// The tools available to the parent thread, captured before SubagentTool was added.
-    /// Subagents inherit from this set (or a subset via `allowed_tools` in the config).
-    /// This is captured early so subagents don't get the subagent tool themselves.
-    parent_tools: BTreeMap<SharedString, Arc<dyn AnyAgentTool>>,
 }
 
 impl SubagentTool {
-    pub fn new(
-        parent_thread: WeakEntity<Thread>,
-        project: Entity<Project>,
-        project_context: Entity<ProjectContext>,
-        context_server_registry: Entity<ContextServerRegistry>,
-        templates: Arc<Templates>,
-        current_depth: u8,
-        parent_tools: BTreeMap<SharedString, Arc<dyn AnyAgentTool>>,
-    ) -> Self {
+    pub fn new(parent_thread: WeakEntity<Thread>, current_depth: u8) -> Self {
         Self {
             parent_thread,
-            project,
-            project_context,
-            context_server_registry,
-            templates,
             current_depth,
-            parent_tools,
         }
     }
 
-    pub fn validate_allowed_tools(&self, allowed_tools: &Option<Vec<String>>) -> Result<()> {
-        let Some(tools) = allowed_tools else {
+    pub fn validate_allowed_tools(
+        &self,
+        allowed_tools: &Option<Vec<String>>,
+        cx: &App,
+    ) -> Result<()> {
+        let Some(allowed_tools) = allowed_tools else {
             return Ok(());
         };
 
-        let invalid_tools: Vec<&str> = tools
-            .iter()
-            .filter(|tool| !self.parent_tools.contains_key(tool.as_str()))
-            .map(|s| s.as_str())
-            .collect();
+        let invalid_tools: Vec<_> = self.parent_thread.read_with(cx, |thread, _cx| {
+            allowed_tools
+                .iter()
+                .filter(|tool| !thread.tools.contains_key(tool.as_str()))
+                .map(|s| format!("'{s}'"))
+                .collect()
+        })?;
 
         if !invalid_tools.is_empty() {
             return Err(anyhow!(
                 "The following tools do not exist: {}",
-                invalid_tools
-                    .iter()
-                    .map(|t| format!("'{}'", t))
-                    .collect::<Vec<_>>()
-                    .join(", ")
+                invalid_tools.join(", ")
             ));
         }
 
@@ -180,18 +160,19 @@ impl AgentTool for SubagentTool {
             )));
         }
 
-        if let Err(e) = self.validate_allowed_tools(&input.allowed_tools) {
+        if let Err(e) = self.validate_allowed_tools(&input.allowed_tools, cx) {
             return Task::ready(Err(e));
         }
 
-        let Some(parent_thread) = self.parent_thread.upgrade() else {
+        let Some(parent_thread_entity) = self.parent_thread.upgrade() else {
             return Task::ready(Err(anyhow!(
                 "Parent thread no longer exists (subagent depth={})",
                 self.current_depth + 1
             )));
         };
+        let parent_thread = parent_thread_entity.read(cx);
 
-        let running_count = parent_thread.read(cx).running_subagent_count();
+        let running_count = parent_thread.running_subagent_count();
         if running_count >= MAX_PARALLEL_SUBAGENTS {
             return Task::ready(Err(anyhow!(
                 "Maximum parallel subagents ({}) reached. Wait for existing subagents to complete.",
@@ -199,17 +180,17 @@ impl AgentTool for SubagentTool {
             )));
         }
 
-        let parent_model = parent_thread.read(cx).model().cloned();
+        let parent_model = parent_thread.model().cloned();
         let Some(model) = parent_model else {
             return Task::ready(Err(anyhow!("No model configured")));
         };
 
-        let parent_thread_id = parent_thread.read(cx).id().clone();
-        let project = self.project.clone();
-        let project_context = self.project_context.clone();
-        let context_server_registry = self.context_server_registry.clone();
-        let templates = self.templates.clone();
-        let parent_tools = self.parent_tools.clone();
+        let parent_thread_id = parent_thread.id().clone();
+        let project = parent_thread.project.clone();
+        let project_context = parent_thread.project_context().clone();
+        let context_server_registry = parent_thread.context_server_registry.clone();
+        let templates = parent_thread.templates.clone();
+        let parent_tools = parent_thread.tools.clone();
         let current_depth = self.current_depth;
         let parent_thread_weak = self.parent_thread.clone();
 

crates/agent_ui/src/acp.rs πŸ”—

@@ -11,4 +11,4 @@ pub use mode_selector::ModeSelector;
 pub use model_selector::AcpModelSelector;
 pub use model_selector_popover::AcpModelSelectorPopover;
 pub use thread_history::*;
-pub use thread_view::AcpThreadView;
+pub use thread_view::AcpServerView;

crates/agent_ui/src/acp/thread_history.rs πŸ”—

@@ -1,4 +1,4 @@
-use crate::acp::AcpThreadView;
+use crate::acp::AcpServerView;
 use crate::{AgentPanel, RemoveHistory, RemoveSelectedThread};
 use acp_thread::{AgentSessionInfo, AgentSessionList, AgentSessionListRequest, SessionListUpdate};
 use agent_client_protocol as acp;
@@ -843,7 +843,7 @@ impl Render for AcpThreadHistory {
 #[derive(IntoElement)]
 pub struct AcpHistoryEntryElement {
     entry: AgentSessionInfo,
-    thread_view: WeakEntity<AcpThreadView>,
+    thread_view: WeakEntity<AcpServerView>,
     selected: bool,
     hovered: bool,
     supports_delete: bool,
@@ -851,7 +851,7 @@ pub struct AcpHistoryEntryElement {
 }
 
 impl AcpHistoryEntryElement {
-    pub fn new(entry: AgentSessionInfo, thread_view: WeakEntity<AcpThreadView>) -> Self {
+    pub fn new(entry: AgentSessionInfo, thread_view: WeakEntity<AcpServerView>) -> Self {
         Self {
             entry,
             thread_view,

crates/agent_ui/src/acp/thread_view.rs πŸ”—

@@ -324,14 +324,14 @@ impl DiffStats {
     }
 }
 
-pub struct AcpThreadView {
+pub struct AcpServerView {
     agent: Rc<dyn AgentServer>,
     agent_server_store: Entity<AgentServerStore>,
     workspace: WeakEntity<Workspace>,
     project: Entity<Project>,
     thread_store: Option<Entity<ThreadStore>>,
     prompt_store: Option<Entity<PromptStore>>,
-    thread_state: ThreadState,
+    server_state: ServerState,
     login: Option<task::SpawnInTerminal>, // is some <=> Active | Unauthenticated
     recent_history_entries: Vec<AgentSessionInfo>,
     history: Entity<AcpThreadHistory>,
@@ -343,34 +343,59 @@ pub struct AcpThreadView {
     notification_subscriptions: HashMap<WindowHandle<AgentNotification>, Vec<Subscription>>,
     slash_command_registry: Option<Entity<SlashCommandRegistry>>,
     auth_task: Option<Task<()>>,
-    _subscriptions: [Subscription; 4],
+    _subscriptions: Vec<Subscription>,
     show_codex_windows_warning: bool,
     in_flight_prompt: Option<Vec<acp::ContentBlock>>,
     add_context_menu_handle: PopoverMenuHandle<ContextMenu>,
 }
 
-impl AcpThreadView {
-    pub fn as_active_thread(&self) -> Option<&ActiveThreadState> {
-        match &self.thread_state {
-            ThreadState::Active(active) => Some(active),
+impl AcpServerView {
+    pub fn as_active_thread(&self) -> Option<&AcpThreadView> {
+        match &self.server_state {
+            ServerState::Connected(connected) => Some(&connected.current),
             _ => None,
         }
     }
 
-    pub fn as_active_thread_mut(&mut self) -> Option<&mut ActiveThreadState> {
-        match &mut self.thread_state {
-            ThreadState::Active(active) => Some(active),
+    pub fn as_active_thread_mut(&mut self) -> Option<&mut AcpThreadView> {
+        match &mut self.server_state {
+            ServerState::Connected(connected) => Some(&mut connected.current),
+            _ => None,
+        }
+    }
+
+    pub fn as_connected(&self) -> Option<&ConnectedServerState> {
+        match &self.server_state {
+            ServerState::Connected(connected) => Some(connected),
+            _ => None,
+        }
+    }
+
+    pub fn as_connected_mut(&mut self) -> Option<&mut ConnectedServerState> {
+        match &mut self.server_state {
+            ServerState::Connected(connected) => Some(connected),
             _ => None,
         }
     }
 }
 
-enum ThreadState {
+enum ServerState {
     Loading(Entity<LoadingView>),
-    Active(ActiveThreadState),
     LoadError(LoadError),
+    Connected(ConnectedServerState),
+}
+
+// current -> Entity
+// hashmap of threads, current becomes session_id
+pub struct ConnectedServerState {
+    auth_state: AuthState,
+    current: AcpThreadView,
+    connection: Rc<dyn AgentConnection>,
+}
+
+enum AuthState {
+    Ok,
     Unauthenticated {
-        connection: Rc<dyn AgentConnection>,
         description: Option<Entity<Markdown>>,
         configuration_view: Option<AnyView>,
         pending_auth_method: Option<acp::AuthMethodId>,
@@ -378,13 +403,25 @@ enum ThreadState {
     },
 }
 
+impl AuthState {
+    pub fn is_ok(&self) -> bool {
+        matches!(self, Self::Ok)
+    }
+}
+
 struct LoadingView {
     title: SharedString,
     _load_task: Task<()>,
     _update_title_task: Task<anyhow::Result<()>>,
 }
 
-impl AcpThreadView {
+impl ConnectedServerState {
+    pub fn has_thread_error(&self) -> bool {
+        self.current.thread_error.is_some()
+    }
+}
+
+impl AcpServerView {
     pub fn new(
         agent: Rc<dyn AgentServer>,
         resume_thread: Option<AgentSessionInfo>,
@@ -447,7 +484,7 @@ impl AcpThreadView {
             editor
         });
 
-        let subscriptions = [
+        let subscriptions = vec![
             cx.observe_global_in::<SettingsStore>(window, Self::agent_ui_font_size_changed),
             cx.observe_global_in::<AgentFontSize>(window, Self::agent_ui_font_size_changed),
             cx.subscribe_in(&message_editor, window, Self::handle_message_editor_event),
@@ -524,7 +561,7 @@ impl AcpThreadView {
             project: project.clone(),
             thread_store,
             prompt_store,
-            thread_state: Self::initial_state(
+            server_state: Self::initial_state(
                 agent.clone(),
                 resume_thread,
                 workspace.clone(),
@@ -560,15 +597,9 @@ impl AcpThreadView {
         let cached_user_commands = Rc::new(RefCell::new(collections::HashMap::default()));
         let cached_user_command_errors = Rc::new(RefCell::new(Vec::new()));
 
-        let resume_thread_metadata = if let ThreadState::Active(ActiveThreadState {
-            resume_thread_metadata,
-            ..
-        }) = &self.thread_state
-        {
-            resume_thread_metadata.clone()
-        } else {
-            None
-        };
+        let resume_thread_metadata = self
+            .as_active_thread()
+            .and_then(|thread| thread.resume_thread_metadata.clone());
 
         self.message_editor.update(cx, |editor, cx| {
             editor.set_command_state(
@@ -580,7 +611,7 @@ impl AcpThreadView {
             );
         });
 
-        self.thread_state = Self::initial_state(
+        self.server_state = Self::initial_state(
             self.agent.clone(),
             resume_thread_metadata,
             self.workspace.clone(),
@@ -608,11 +639,11 @@ impl AcpThreadView {
         cached_user_command_errors: Rc<RefCell<Vec<CommandLoadError>>>,
         window: &mut Window,
         cx: &mut Context<Self>,
-    ) -> ThreadState {
+    ) -> ServerState {
         if project.read(cx).is_via_collab()
             && agent.clone().downcast::<NativeAgentServer>().is_none()
         {
-            return ThreadState::LoadError(LoadError::Other(
+            return ServerState::LoadError(LoadError::Other(
                 "External agents are not yet supported in shared projects.".into(),
             ));
         }
@@ -715,7 +746,7 @@ impl AcpThreadView {
                 Err(e) => match e.downcast::<acp_thread::AuthRequired>() {
                     Ok(err) => {
                         cx.update(|window, cx| {
-                            Self::handle_auth_required(this, err, agent, connection, window, cx)
+                            Self::handle_auth_required(this, err, agent.name(), window, cx)
                         })
                         .log_err();
                         return;
@@ -869,24 +900,29 @@ impl AcpThreadView {
                                 })
                             });
 
-                        this.thread_state = ThreadState::Active(ActiveThreadState::new(
-                            thread,
-                            workspace.clone(),
-                            entry_view_state,
-                            title_editor,
-                            config_options_view,
-                            mode_selector,
-                            model_selector,
-                            profile_selector,
-                            list_state,
-                            prompt_capabilities,
-                            available_commands,
-                            cached_user_commands,
-                            cached_user_command_errors,
-                            resumed_without_history,
-                            resume_thread.clone(),
-                            subscriptions,
-                        ));
+                        this.server_state = ServerState::Connected(ConnectedServerState {
+                            connection,
+                            auth_state: AuthState::Ok,
+                            current: AcpThreadView::new(
+                                thread,
+                                workspace.clone(),
+                                entry_view_state,
+                                title_editor,
+                                config_options_view,
+                                mode_selector,
+                                model_selector,
+                                profile_selector,
+                                list_state,
+                                prompt_capabilities,
+                                available_commands,
+                                cached_user_commands,
+                                cached_user_command_errors,
+                                resumed_without_history,
+                                resume_thread.clone(),
+                                subscriptions,
+                                cx,
+                            ),
+                        });
 
                         if this.focus_handle.contains_focused(window, cx) {
                             this.message_editor.focus_handle(cx).focus(window, cx);
@@ -906,12 +942,8 @@ impl AcpThreadView {
             while let Ok(new_version) = new_version_available_rx.recv().await {
                 if let Some(new_version) = new_version {
                     this.update(cx, |this, cx| {
-                        if let ThreadState::Active(ActiveThreadState {
-                            new_server_version_available,
-                            ..
-                        }) = &mut this.thread_state
-                        {
-                            *new_server_version_available = Some(new_version.into());
+                        if let Some(thread) = this.as_active_thread_mut() {
+                            thread.new_server_version_available = Some(new_version.into());
                         }
                         cx.notify();
                     })
@@ -939,18 +971,16 @@ impl AcpThreadView {
             }
         });
 
-        ThreadState::Loading(loading_view)
+        ServerState::Loading(loading_view)
     }
 
     fn handle_auth_required(
         this: WeakEntity<Self>,
         err: AuthRequired,
-        agent: Rc<dyn AgentServer>,
-        connection: Rc<dyn AgentConnection>,
+        agent_name: SharedString,
         window: &mut Window,
         cx: &mut App,
     ) {
-        let agent_name = agent.name();
         let (configuration_view, subscription) = if let Some(provider_id) = &err.provider_id {
             let registry = LanguageModelRegistry::global(cx);
 
@@ -975,7 +1005,7 @@ impl AcpThreadView {
 
             let view = registry.read(cx).provider(&provider_id).map(|provider| {
                 provider.configuration_view(
-                    language_model::ConfigurationViewTargetAgent::Other(agent_name.clone()),
+                    language_model::ConfigurationViewTargetAgent::Other(agent_name),
                     window,
                     cx,
                 )
@@ -987,15 +1017,18 @@ impl AcpThreadView {
         };
 
         this.update(cx, |this, cx| {
-            this.thread_state = ThreadState::Unauthenticated {
-                pending_auth_method: None,
-                connection,
-                configuration_view,
-                description: err
+            if let Some(connected) = this.as_connected_mut() {
+                let description = err
                     .description
-                    .map(|desc| cx.new(|cx| Markdown::new(desc.into(), None, None, cx))),
-                _subscription: subscription,
-            };
+                    .map(|desc| cx.new(|cx| Markdown::new(desc.into(), None, None, cx)));
+
+                connected.auth_state = AuthState::Unauthenticated {
+                    pending_auth_method: None,
+                    configuration_view,
+                    description,
+                    _subscription: subscription,
+                };
+            }
             if this.message_editor.focus_handle(cx).is_focused(window) {
                 this.focus_handle.focus(window, cx)
             }
@@ -1011,10 +1044,10 @@ impl AcpThreadView {
         cx: &mut Context<Self>,
     ) {
         if let Some(load_err) = err.downcast_ref::<LoadError>() {
-            self.thread_state = ThreadState::LoadError(load_err.clone());
+            self.server_state = ServerState::LoadError(load_err.clone());
         } else {
-            self.thread_state =
-                ThreadState::LoadError(LoadError::Other(format!("{:#}", err).into()))
+            self.server_state =
+                ServerState::LoadError(LoadError::Other(format!("{:#}", err).into()))
         }
         if self.message_editor.focus_handle(cx).is_focused(window) {
             self.focus_handle.focus(window, cx)
@@ -1032,13 +1065,12 @@ impl AcpThreadView {
         // If we're in a LoadError state OR have a thread_error set (which can happen
         // when agent.connect() fails during loading), retry loading the thread.
         // This handles the case where a thread is restored before authentication completes.
-        let should_retry = match &self.thread_state {
-            ThreadState::LoadError(_)
-            | ThreadState::Active(ActiveThreadState {
-                thread_error: Some(_),
-                ..
-            }) => true,
-            _ => false,
+        let should_retry = match &self.server_state {
+            ServerState::Loading(_) => false,
+            ServerState::LoadError(_) => true,
+            ServerState::Connected(connected) => {
+                connected.auth_state.is_ok() && connected.has_thread_error()
+            }
         };
 
         if should_retry {
@@ -1055,12 +1087,10 @@ impl AcpThreadView {
     }
 
     pub fn title(&self, cx: &App) -> SharedString {
-        match &self.thread_state {
-            ThreadState::Active(ActiveThreadState { .. }) | ThreadState::Unauthenticated { .. } => {
-                "New Thread".into()
-            }
-            ThreadState::Loading(loading_view) => loading_view.read(cx).title.clone(),
-            ThreadState::LoadError(error) => match error {
+        match &self.server_state {
+            ServerState::Connected(_) => "New Thread".into(),
+            ServerState::Loading(loading_view) => loading_view.read(cx).title.clone(),
+            ServerState::LoadError(error) => match error {
                 LoadError::Unsupported { .. } => format!("Upgrade {}", self.agent.name()).into(),
                 LoadError::FailedToInstall(_) => {
                     format!("Failed to Install {}", self.agent.name()).into()
@@ -1320,7 +1350,7 @@ impl AcpThreadView {
     }
 
     pub fn is_loading(&self) -> bool {
-        matches!(self.thread_state, ThreadState::Loading { .. })
+        matches!(self.server_state, ServerState::Loading { .. })
     }
 
     fn retry_generation(&mut self, cx: &mut Context<Self>) {
@@ -1331,11 +1361,11 @@ impl AcpThreadView {
 
     fn send(&mut self, window: &mut Window, cx: &mut Context<Self>) {
         let message_editor = self.message_editor.clone();
-        let agent = self.agent.clone();
         let login = self.login.clone();
+        let agent_name = self.agent.name();
 
         if let Some(active) = self.as_active_thread_mut() {
-            active.send(message_editor, agent, login, window, cx);
+            active.send(message_editor, agent_name, login, window, cx);
         }
     }
 
@@ -1390,28 +1420,16 @@ impl AcpThreadView {
             )
         });
 
-        if let ThreadState::Active(ActiveThreadState {
-            thread_error,
-            thread_feedback,
-            editing_message,
-            ..
-        }) = &mut self.thread_state
-        {
-            thread_error.take();
-            thread_feedback.clear();
-            editing_message.take();
-        }
+        if let Some(thread) = self.as_active_thread_mut() {
+            thread.thread_error.take();
+            thread.thread_feedback.clear();
+            thread.editing_message.take();
 
-        if let ThreadState::Active(ActiveThreadState {
-            should_be_following: true,
-            ..
-        }) = &self.thread_state
-        {
-            self.workspace
-                .update(cx, |workspace, cx| {
+            if thread.should_be_following {
+                let _ = self.workspace.update(cx, |workspace, cx| {
                     workspace.follow(CollaboratorId::Agent, window, cx);
-                })
-                .ok();
+                });
+            }
         }
 
         let contents_task = cx.spawn_in(window, async move |this, cx| {
@@ -1495,9 +1513,8 @@ impl AcpThreadView {
     fn handle_thread_error(&mut self, error: anyhow::Error, cx: &mut Context<Self>) {
         let error = ThreadError::from_err(error, &self.agent);
         self.emit_thread_error_telemetry(&error, cx);
-        if let ThreadState::Active(ActiveThreadState { thread_error, .. }) = &mut self.thread_state
-        {
-            *thread_error = Some(error);
+        if let Some(thread) = self.as_active_thread_mut() {
+            thread.thread_error = Some(error);
         }
         cx.notify();
     }
@@ -1673,7 +1690,7 @@ impl AcpThreadView {
                 );
             }
             AcpThreadEvent::LoadError(error) => {
-                self.thread_state = ThreadState::LoadError(error.clone());
+                self.server_state = ServerState::LoadError(error.clone());
                 if self.message_editor.focus_handle(cx).is_focused(window) {
                     self.focus_handle.focus(window, cx)
                 }
@@ -1752,126 +1769,133 @@ impl AcpThreadView {
         window: &mut Window,
         cx: &mut Context<Self>,
     ) {
-        let ThreadState::Unauthenticated {
-            connection,
-            pending_auth_method,
+        let Some(connected) = self.as_connected_mut() else {
+            return;
+        };
+        let connection = connected.connection.clone();
+
+        let AuthState::Unauthenticated {
             configuration_view,
+            pending_auth_method,
             ..
-        } = &mut self.thread_state
+        } = &mut connected.auth_state
         else {
             return;
         };
+
         let agent_telemetry_id = connection.telemetry_id();
 
         // Check for the experimental "terminal-auth" _meta field
         let auth_method = connection.auth_methods().iter().find(|m| m.id == method);
 
-        if let Some(auth_method) = auth_method {
-            if let Some(meta) = &auth_method.meta {
-                if let Some(terminal_auth) = meta.get("terminal-auth") {
-                    // Extract terminal auth details from meta
-                    if let (Some(command), Some(label)) = (
-                        terminal_auth.get("command").and_then(|v| v.as_str()),
-                        terminal_auth.get("label").and_then(|v| v.as_str()),
-                    ) {
-                        let args = terminal_auth
-                            .get("args")
-                            .and_then(|v| v.as_array())
-                            .map(|arr| {
-                                arr.iter()
-                                    .filter_map(|v| v.as_str().map(String::from))
-                                    .collect()
-                            })
-                            .unwrap_or_default();
-
-                        let env = terminal_auth
-                            .get("env")
-                            .and_then(|v| v.as_object())
-                            .map(|obj| {
-                                obj.iter()
-                                    .filter_map(|(k, v)| {
-                                        v.as_str().map(|val| (k.clone(), val.to_string()))
-                                    })
-                                    .collect::<HashMap<String, String>>()
-                            })
-                            .unwrap_or_default();
-
-                        // Run SpawnInTerminal in the same dir as the ACP server
-                        let cwd = connection
-                            .clone()
-                            .downcast::<agent_servers::AcpConnection>()
-                            .map(|acp_conn| acp_conn.root_dir().to_path_buf());
-
-                        // Build SpawnInTerminal from _meta
-                        let login = task::SpawnInTerminal {
-                            id: task::TaskId(format!("external-agent-{}-login", label)),
-                            full_label: label.to_string(),
-                            label: label.to_string(),
-                            command: Some(command.to_string()),
-                            args,
-                            command_label: label.to_string(),
-                            cwd,
-                            env,
-                            use_new_terminal: true,
-                            allow_concurrent_runs: true,
-                            hide: task::HideStrategy::Always,
-                            ..Default::default()
-                        };
+        if let Some(terminal_auth) = auth_method
+            .and_then(|a| a.meta.as_ref())
+            .and_then(|m| m.get("terminal-auth"))
+        {
+            // Extract terminal auth details from meta
+            if let (Some(command), Some(label)) = (
+                terminal_auth.get("command").and_then(|v| v.as_str()),
+                terminal_auth.get("label").and_then(|v| v.as_str()),
+            ) {
+                let args = terminal_auth
+                    .get("args")
+                    .and_then(|v| v.as_array())
+                    .map(|arr| {
+                        arr.iter()
+                            .filter_map(|v| v.as_str().map(String::from))
+                            .collect()
+                    })
+                    .unwrap_or_default();
+
+                let env = terminal_auth
+                    .get("env")
+                    .and_then(|v| v.as_object())
+                    .map(|obj| {
+                        obj.iter()
+                            .filter_map(|(k, v)| v.as_str().map(|val| (k.clone(), val.to_string())))
+                            .collect::<HashMap<String, String>>()
+                    })
+                    .unwrap_or_default();
+
+                // Run SpawnInTerminal in the same dir as the ACP server
+                let cwd = connected
+                    .connection
+                    .clone()
+                    .downcast::<agent_servers::AcpConnection>()
+                    .map(|acp_conn| acp_conn.root_dir().to_path_buf());
+
+                // Build SpawnInTerminal from _meta
+                let login = task::SpawnInTerminal {
+                    id: task::TaskId(format!("external-agent-{}-login", label)),
+                    full_label: label.to_string(),
+                    label: label.to_string(),
+                    command: Some(command.to_string()),
+                    args,
+                    command_label: label.to_string(),
+                    cwd,
+                    env,
+                    use_new_terminal: true,
+                    allow_concurrent_runs: true,
+                    hide: task::HideStrategy::Always,
+                    ..Default::default()
+                };
 
-                        configuration_view.take();
-                        pending_auth_method.replace(method.clone());
-
-                        if let Some(workspace) = self.workspace.upgrade() {
-                            let project = self.project.clone();
-                            let authenticate = Self::spawn_external_agent_login(
-                                login,
-                                workspace,
-                                project,
-                                method.clone(),
-                                false,
-                                window,
-                                cx,
-                            );
-                            cx.notify();
-                            self.auth_task = Some(cx.spawn_in(window, {
-                                async move |this, cx| {
-                                    let result = authenticate.await;
-
-                                    match &result {
-                                        Ok(_) => telemetry::event!(
-                                            "Authenticate Agent Succeeded",
-                                            agent = agent_telemetry_id
-                                        ),
-                                        Err(_) => {
-                                            telemetry::event!(
-                                                "Authenticate Agent Failed",
-                                                agent = agent_telemetry_id,
-                                            )
-                                        }
-                                    }
+                configuration_view.take();
+                pending_auth_method.replace(method.clone());
+
+                if let Some(workspace) = self.workspace.upgrade() {
+                    let project = self.project.clone();
+                    let authenticate = Self::spawn_external_agent_login(
+                        login,
+                        workspace,
+                        project,
+                        method.clone(),
+                        false,
+                        window,
+                        cx,
+                    );
+                    cx.notify();
+                    self.auth_task = Some(cx.spawn_in(window, {
+                        async move |this, cx| {
+                            let result = authenticate.await;
+
+                            match &result {
+                                Ok(_) => telemetry::event!(
+                                    "Authenticate Agent Succeeded",
+                                    agent = agent_telemetry_id
+                                ),
+                                Err(_) => {
+                                    telemetry::event!(
+                                        "Authenticate Agent Failed",
+                                        agent = agent_telemetry_id,
+                                    )
+                                }
+                            }
 
-                                    this.update_in(cx, |this, window, cx| {
-                                        if let Err(err) = result {
-                                            if let ThreadState::Unauthenticated {
+                            this.update_in(cx, |this, window, cx| {
+                                if let Err(err) = result {
+                                    if let Some(ConnectedServerState {
+                                        auth_state:
+                                            AuthState::Unauthenticated {
                                                 pending_auth_method,
                                                 ..
-                                            } = &mut this.thread_state
-                                            {
-                                                pending_auth_method.take();
-                                            }
-                                            this.handle_thread_error(err, cx);
-                                        } else {
-                                            this.reset(window, cx);
-                                        }
-                                        this.auth_task.take()
-                                    })
-                                    .ok();
+                                            },
+                                        ..
+                                    }) = this.as_connected_mut()
+                                    {
+                                        pending_auth_method.take();
+                                    }
+                                    this.handle_thread_error(err, cx);
+                                } else {
+                                    this.reset(window, cx);
                                 }
-                            }));
+                                this.auth_task.take()
+                            })
+                            .ok();
                         }
-                        return;
-                    }
+                    }));
                 }
+                return;
             }
         }
 
@@ -1883,8 +1907,7 @@ impl AcpThreadView {
                 .unwrap();
             if !provider.is_authenticated(cx) {
                 let this = cx.weak_entity();
-                let agent = self.agent.clone();
-                let connection = connection.clone();
+                let agent_name = self.agent.name();
                 window.defer(cx, |window, cx| {
                     Self::handle_auth_required(
                         this,
@@ -1892,8 +1915,7 @@ impl AcpThreadView {
                             description: Some("GEMINI_API_KEY must be set".to_owned()),
                             provider_id: Some(language_model::GOOGLE_PROVIDER_ID),
                         },
-                        agent,
-                        connection,
+                        agent_name,
                         window,
                         cx,
                     );
@@ -1906,8 +1928,7 @@ impl AcpThreadView {
                 || (std::env::var("GOOGLE_CLOUD_PROJECT").is_err()))
         {
             let this = cx.weak_entity();
-            let agent = self.agent.clone();
-            let connection = connection.clone();
+            let agent_name = self.agent.name();
 
             window.defer(cx, |window, cx| {
                     Self::handle_auth_required(
@@ -1919,8 +1940,7 @@ impl AcpThreadView {
                             ),
                             provider_id: None,
                         },
-                        agent,
-                        connection,
+                        agent_name,
                         window,
                         cx,
                     )
@@ -1965,10 +1985,14 @@ impl AcpThreadView {
 
                 this.update_in(cx, |this, window, cx| {
                     if let Err(err) = result {
-                        if let ThreadState::Unauthenticated {
-                            pending_auth_method,
+                        if let Some(ConnectedServerState {
+                            auth_state:
+                                AuthState::Unauthenticated {
+                                    pending_auth_method,
+                                    ..
+                                },
                             ..
-                        } = &mut this.thread_state
+                        }) = this.as_connected_mut()
                         {
                             pending_auth_method.take();
                         }
@@ -2282,7 +2306,7 @@ impl AcpThreadView {
                                     .bg(cx.theme().colors().editor_background)
                                     .overflow_hidden();
 
-                                let is_loading_contents = matches!(&self.thread_state, ThreadState::Active(ActiveThreadState { is_loading_contents: true, .. }));
+                                let is_loading_contents = matches!(&self.server_state, ServerState::Connected(ConnectedServerState { current: AcpThreadView { is_loading_contents: true, .. }, ..}));
                                 if message.id.is_some() {
                                     this.child(
                                         base_container
@@ -2653,7 +2677,7 @@ impl AcpThreadView {
 
         let key = (entry_ix, chunk_ix);
 
-        let is_open = matches!(&self.thread_state, ThreadState::Active(ActiveThreadState { expanded_thinking_blocks, .. }) if expanded_thinking_blocks.contains(&key));
+        let is_open = matches!(&self.server_state, ServerState::Connected(ConnectedServerState {current: AcpThreadView { expanded_thinking_blocks, .. }, ..}) if expanded_thinking_blocks.contains(&key));
 
         let scroll_handle = self
             .as_active_thread()
@@ -2798,8 +2822,14 @@ impl AcpThreadView {
 
         let has_image_content = tool_call.content.iter().any(|c| c.image().is_some());
         let is_collapsible = !tool_call.content.is_empty() && !needs_confirmation;
-        let is_open = needs_confirmation
-            || matches!(&self.thread_state, ThreadState::Active(ActiveThreadState { expanded_tool_calls, .. }) if expanded_tool_calls.contains(&tool_call.id));
+        let mut is_open = match &self.server_state {
+            ServerState::Connected(ConnectedServerState { current, .. }) => {
+                current.expanded_tool_calls.contains(&tool_call.id)
+            }
+            _ => false,
+        };
+
+        is_open |= needs_confirmation;
 
         let should_show_raw_input = !is_terminal_tool && !is_edit && !has_image_content;
 
@@ -2837,7 +2867,7 @@ impl AcpThreadView {
                     )
                     .when(should_show_raw_input, |this| {
                         let is_raw_input_expanded =
-                            matches!(&self.thread_state, ThreadState::Active(ActiveThreadState { expanded_tool_call_raw_inputs, .. }) if expanded_tool_call_raw_inputs.contains(&tool_call.id));
+                            matches!(&self.server_state, ServerState::Connected(ConnectedServerState {current: AcpThreadView { expanded_tool_call_raw_inputs, .. }, ..}) if expanded_tool_call_raw_inputs.contains(&tool_call.id));
 
                         let input_header = if is_raw_input_expanded {
                             "Raw Input:"
@@ -3076,7 +3106,7 @@ impl AcpThreadView {
                                         })
                                         .when_some(diff_for_discard, |this, diff| {
                                             let tool_call_id = tool_call.id.clone();
-                                            let is_discarded = matches!(&self.thread_state, ThreadState::Active(ActiveThreadState { discarded_partial_edits, .. }) if discarded_partial_edits.contains(&tool_call_id));
+                                            let is_discarded = matches!(&self.server_state, ServerState::Connected(ConnectedServerState{current: AcpThreadView { discarded_partial_edits, .. }, ..}) if discarded_partial_edits.contains(&tool_call_id));
                                             this.when(!is_discarded, |this| {
                                                 this.child(
                                                     IconButton::new(
@@ -3577,19 +3607,17 @@ impl AcpThreadView {
             }
         });
 
-        let scroll_handle = if let ThreadState::Active(ActiveThreadState {
-            subagent_scroll_handles,
-            ..
-        }) = &self.thread_state
-        {
-            subagent_scroll_handles
-                .borrow_mut()
-                .entry(session_id.clone())
-                .or_default()
-                .clone()
-        } else {
-            ScrollHandle::default()
-        };
+        let scroll_handle = self
+            .as_active_thread()
+            .map(|state| {
+                state
+                    .subagent_scroll_handles
+                    .borrow_mut()
+                    .entry(session_id.clone())
+                    .or_default()
+                    .clone()
+            })
+            .unwrap_or_default();
 
         scroll_handle.scroll_to_bottom();
         let editor_bg = cx.theme().colors().editor_background;
@@ -3979,12 +4007,9 @@ impl AcpThreadView {
             .map(|(i, choice)| (i, choice.label()))
             .collect();
 
-        let permission_dropdown_handle = match &self.thread_state {
-            ThreadState::Active(ActiveThreadState {
-                permission_dropdown_handle,
-                ..
-            }) => permission_dropdown_handle.clone(),
-            _ => return div().into_any_element(),
+        let permission_dropdown_handle = match self.as_active_thread() {
+            Some(thread) => thread.permission_dropdown_handle.clone(),
+            None => return div().into_any_element(),
         };
 
         PopoverMenu::new(("permission-granularity", entry_ix))
@@ -4408,11 +4433,8 @@ impl AcpThreadView {
             .map(|(i, choice)| (i, choice.label()))
             .collect();
 
-        let permission_dropdown_handle = match &self.thread_state {
-            ThreadState::Active(ActiveThreadState {
-                permission_dropdown_handle,
-                ..
-            }) => permission_dropdown_handle.clone(),
+        let permission_dropdown_handle = match self.as_active_thread() {
+            Some(thread) => thread.permission_dropdown_handle.clone(),
             _ => return div().into_any_element(),
         };
 
@@ -4671,7 +4693,9 @@ impl AcpThreadView {
         let command_element =
             self.render_collapsible_command(false, command_content, &tool_call.id, cx);
 
-        let is_expanded = matches!(&self.thread_state, ThreadState::Active(ActiveThreadState { expanded_tool_calls, .. }) if expanded_tool_calls.contains(&tool_call.id));
+        let is_expanded = self
+            .as_connected()
+            .is_some_and(|c| c.current.expanded_tool_calls.contains(&tool_call.id));
 
         let header = h_flex()
             .id(header_id)
@@ -5896,10 +5920,12 @@ impl AcpThreadView {
                                         .label_size(LabelSize::XSmall),
                                     )
                                     .when(
-                                        !matches!(
-                                            &self.thread_state,
-                                            ThreadState::Active(ActiveThreadState { hovered_edited_file_buttons: Some(i), .. }) if *i == index
-                                        ),
+                                        match self.as_active_thread() {
+                                            Some(thread) => {
+                                                thread.hovered_edited_file_buttons == Some(index)
+                                            }
+                                            None => false,
+                                        },
                                         |this| {
                                             let full_path = full_path.clone();
                                             this.hover(|s| s.bg(cx.theme().colors().element_hover))
@@ -5991,13 +6017,10 @@ impl AcpThreadView {
         let message_editor = self.message_editor.read(cx);
         let focus_handle = message_editor.focus_handle(cx);
 
-        let queued_message_editors = match &self.thread_state {
-            ThreadState::Active(ActiveThreadState {
-                queued_message_editors,
-                ..
-            }) => queued_message_editors.as_slice(),
-            _ => &[],
-        };
+        let queued_message_editors = self
+            .as_connected()
+            .map(|c| c.current.queued_message_editors.as_slice())
+            .unwrap_or(&[]);
 
         let queue_len = queued_message_editors.len();
         let can_fast_track = if let Some(active) = self.as_active_thread() {
@@ -6222,12 +6245,9 @@ impl AcpThreadView {
             .opacity(0.8)
             .block_mouse_except_scroll();
 
-        let enable_editor = match self.thread_state {
-            ThreadState::Active(ActiveThreadState { .. }) => true,
-            ThreadState::Loading { .. }
-            | ThreadState::Unauthenticated { .. }
-            | ThreadState::LoadError(..) => false,
-        };
+        let enable_editor = self
+            .as_connected()
+            .is_some_and(|conn| conn.auth_state.is_ok());
 
         v_flex()
             .on_action(cx.listener(Self::expand_message_editor))
@@ -6326,13 +6346,9 @@ impl AcpThreadView {
     }
 
     fn queued_messages_len(&self) -> usize {
-        match &self.thread_state {
-            ThreadState::Active(ActiveThreadState {
-                local_queued_messages,
-                ..
-            }) => local_queued_messages.len(),
-            _ => 0,
-        }
+        self.as_active_thread()
+            .map(|thread| thread.local_queued_messages.len())
+            .unwrap_or_default()
     }
 
     fn has_queued_messages(&self) -> bool {
@@ -6376,18 +6392,15 @@ impl AcpThreadView {
         tracked_buffers: Vec<Entity<Buffer>>,
         _cx: &mut Context<Self>,
     ) -> bool {
-        match &mut self.thread_state {
-            ThreadState::Active(ActiveThreadState {
-                local_queued_messages,
-                ..
-            }) if index < local_queued_messages.len() => {
-                local_queued_messages[index] = QueuedMessage {
+        match self.as_active_thread_mut() {
+            Some(thread) if index < thread.local_queued_messages.len() => {
+                thread.local_queued_messages[index] = QueuedMessage {
                     content,
                     tracked_buffers,
                 };
                 true
             }
-            _ => false,
+            Some(_) | None => false,
         }
     }
 
@@ -6399,25 +6412,20 @@ impl AcpThreadView {
     }
 
     fn queued_message_contents(&self) -> Vec<Vec<acp::ContentBlock>> {
-        match &self.thread_state {
-            ThreadState::Active(ActiveThreadState {
-                local_queued_messages,
-                ..
-            }) => local_queued_messages
+        match self.as_active_thread() {
+            None => Vec::new(),
+            Some(thread) => thread
+                .local_queued_messages
                 .iter()
                 .map(|q| q.content.clone())
                 .collect(),
-            _ => Vec::new(),
         }
     }
 
     fn save_queued_message_at_index(&mut self, index: usize, cx: &mut Context<Self>) {
-        let editor = match &self.thread_state {
-            ThreadState::Active(ActiveThreadState {
-                queued_message_editors,
-                ..
-            }) => queued_message_editors.get(index).cloned(),
-            _ => None,
+        let editor = match self.as_active_thread() {
+            Some(thread) => thread.queued_message_editors.get(index).cloned(),
+            None => None,
         };
         let Some(editor) = editor else {
             return;
@@ -6444,31 +6452,31 @@ impl AcpThreadView {
         let needed_count = self.queued_messages_len();
         let queued_messages = self.queued_message_contents();
 
-        let ThreadState::Active(ActiveThreadState {
-            queued_message_editors,
-            queued_message_editor_subscriptions,
-            last_synced_queue_length,
-            prompt_capabilities,
-            available_commands,
-            ..
-        }) = &mut self.thread_state
-        else {
+        let agent_name = self.agent.name();
+        let workspace = self.workspace.clone();
+        let project = self.project.downgrade();
+        let history = self.history.downgrade();
+        let message_editor = self.message_editor.clone();
+
+        let Some(thread) = self.as_active_thread_mut() else {
             return;
         };
-        let prompt_capabilities = prompt_capabilities.clone();
-        let available_commands = available_commands.clone();
+        let prompt_capabilities = thread.prompt_capabilities.clone();
+        let available_commands = thread.available_commands.clone();
 
-        let current_count = queued_message_editors.len();
+        let current_count = thread.queued_message_editors.len();
 
-        if current_count == needed_count && needed_count == *last_synced_queue_length {
+        if current_count == needed_count && needed_count == thread.last_synced_queue_length {
             return;
         }
 
         if current_count > needed_count {
-            queued_message_editors.truncate(needed_count);
-            queued_message_editor_subscriptions.truncate(needed_count);
+            thread.queued_message_editors.truncate(needed_count);
+            thread
+                .queued_message_editor_subscriptions
+                .truncate(needed_count);
 
-            for (index, editor) in queued_message_editors.iter().enumerate() {
+            for (index, editor) in thread.queued_message_editors.iter().enumerate() {
                 if let Some(content) = queued_messages.get(index) {
                     editor.update(cx, |editor, cx| {
                         editor.set_message(content.clone(), window, cx);

crates/agent_ui/src/acp/thread_view/active_thread.rs πŸ”—

@@ -1,6 +1,7 @@
 use super::*;
 
-pub struct ActiveThreadState {
+pub struct AcpThreadView {
+    pub id: acp::SessionId,
     pub thread: Entity<AcpThread>,
     pub workspace: WeakEntity<Workspace>,
     pub entry_view_state: Entity<EntryViewState>,
@@ -66,7 +67,7 @@ pub struct TurnFields {
     pub turn_tokens: Option<u64>,
 }
 
-impl ActiveThreadState {
+impl AcpThreadView {
     pub fn new(
         thread: Entity<AcpThread>,
         workspace: WeakEntity<Workspace>,
@@ -84,8 +85,11 @@ impl ActiveThreadState {
         resumed_without_history: bool,
         resume_thread_metadata: Option<AgentSessionInfo>,
         subscriptions: Vec<Subscription>,
+        cx: &App,
     ) -> Self {
+        let id = thread.read(cx).session_id().clone();
         Self {
+            id,
             thread,
             workspace,
             entry_view_state,
@@ -179,7 +183,7 @@ impl ActiveThreadState {
 
     // turns
 
-    pub fn start_turn(&mut self, cx: &mut Context<AcpThreadView>) -> usize {
+    pub fn start_turn(&mut self, cx: &mut Context<AcpServerView>) -> usize {
         self.turn_fields.turn_generation += 1;
         let generation = self.turn_fields.turn_generation;
         self.turn_fields.turn_started_at = Some(Instant::now());
@@ -223,10 +227,10 @@ impl ActiveThreadState {
     pub fn send(
         &mut self,
         message_editor: Entity<MessageEditor>,
-        agent: Rc<dyn AgentServer>,
+        agent_name: SharedString,
         login: Option<task::SpawnInTerminal>,
         window: &mut Window,
-        cx: &mut Context<AcpThreadView>,
+        cx: &mut Context<AcpServerView>,
     ) {
         let thread = &self.thread;
 
@@ -269,13 +273,11 @@ impl ActiveThreadState {
                 message_editor.update(cx, |editor, cx| editor.clear(window, cx));
 
                 let this = cx.weak_entity();
-                let agent = agent.clone();
                 window.defer(cx, |window, cx| {
-                    AcpThreadView::handle_auth_required(
+                    AcpServerView::handle_auth_required(
                         this,
                         AuthRequired::new(),
-                        agent,
-                        connection,
+                        agent_name,
                         window,
                         cx,
                     );
@@ -292,7 +294,7 @@ impl ActiveThreadState {
         &mut self,
         message_editor: Entity<MessageEditor>,
         window: &mut Window,
-        cx: &mut Context<AcpThreadView>,
+        cx: &mut Context<AcpServerView>,
     ) {
         let full_mention_content = self.as_native_thread(cx).is_some_and(|thread| {
             // Include full contents when using minimal profile
@@ -349,7 +351,7 @@ impl ActiveThreadState {
         &mut self,
         contents_task: Task<anyhow::Result<Option<(Vec<acp::ContentBlock>, Vec<Entity<Buffer>>)>>>,
         window: &mut Window,
-        cx: &mut Context<AcpThreadView>,
+        cx: &mut Context<AcpServerView>,
     ) {
         let session_id = self.thread.read(cx).session_id().clone();
         let agent_telemetry_id = self.thread.read(cx).connection().telemetry_id();
@@ -361,12 +363,8 @@ impl ActiveThreadState {
         let mode_id = self.current_mode_id(cx);
         let guard = cx.new(|_| ());
         cx.observe_release(&guard, |this, _guard, cx| {
-            if let ThreadState::Active(ActiveThreadState {
-                is_loading_contents,
-                ..
-            }) = &mut this.thread_state
-            {
-                *is_loading_contents = false;
+            if let Some(thread) = this.as_active_thread_mut() {
+                thread.is_loading_contents = false;
             }
             cx.notify();
         })
@@ -444,17 +442,14 @@ impl ActiveThreadState {
                 .ok();
             } else {
                 this.update(cx, |this, cx| {
-                    if let ThreadState::Active(ActiveThreadState {
-                        should_be_following,
-                        ..
-                    }) = &mut this.thread_state
-                    {
-                        *should_be_following = this
-                            .workspace
-                            .update(cx, |workspace, _| {
-                                workspace.is_being_followed(CollaboratorId::Agent)
-                            })
-                            .unwrap_or_default();
+                    let should_be_following = this
+                        .workspace
+                        .update(cx, |workspace, _| {
+                            workspace.is_being_followed(CollaboratorId::Agent)
+                        })
+                        .unwrap_or_default();
+                    if let Some(thread) = this.as_active_thread_mut() {
+                        thread.should_be_following = should_be_following;
                     }
                 })
                 .ok();
@@ -467,7 +462,7 @@ impl ActiveThreadState {
         &mut self,
         message_editor: Entity<MessageEditor>,
         window: &mut Window,
-        cx: &mut Context<AcpThreadView>,
+        cx: &mut Context<AcpServerView>,
     ) {
         let thread = &self.thread;
 
@@ -486,7 +481,7 @@ impl ActiveThreadState {
     pub fn stop_current_and_send_new_message(
         &mut self,
         window: &mut Window,
-        cx: &mut Context<AcpThreadView>,
+        cx: &mut Context<AcpServerView>,
     ) {
         let thread = self.thread.clone();
         self.skip_queue_processing_count = 0;
@@ -507,14 +502,14 @@ impl ActiveThreadState {
 
     // generation
 
-    pub fn cancel_generation(&mut self, cx: &mut Context<AcpThreadView>) {
+    pub fn cancel_generation(&mut self, cx: &mut Context<AcpServerView>) {
         self.thread_retry_status.take();
         self.thread_error.take();
         self.user_interrupted_generation = true;
         self._cancel_task = Some(self.thread.update(cx, |thread, cx| thread.cancel(cx)));
     }
 
-    pub fn retry_generation(&mut self, cx: &mut Context<AcpThreadView>) {
+    pub fn retry_generation(&mut self, cx: &mut Context<AcpServerView>) {
         self.thread_error.take();
 
         let thread = &self.thread;
@@ -540,7 +535,7 @@ impl ActiveThreadState {
         entry_ix: usize,
         message_editor: Entity<MessageEditor>,
         window: &mut Window,
-        cx: &mut Context<AcpThreadView>,
+        cx: &mut Context<AcpServerView>,
     ) {
         if self.is_loading_contents {
             return;
@@ -594,7 +589,7 @@ impl ActiveThreadState {
         &mut self,
         message_editor: Entity<MessageEditor>,
         window: &mut Window,
-        cx: &mut Context<AcpThreadView>,
+        cx: &mut Context<AcpServerView>,
     ) {
         let is_idle = self.thread.read(cx).status() == acp_thread::ThreadStatus::Idle;
 
@@ -646,7 +641,7 @@ impl ActiveThreadState {
     pub fn remove_from_queue(
         &mut self,
         index: usize,
-        cx: &mut Context<AcpThreadView>,
+        cx: &mut Context<AcpServerView>,
     ) -> Option<QueuedMessage> {
         if index < self.local_queued_messages.len() {
             let removed = self.local_queued_messages.remove(index);
@@ -657,7 +652,7 @@ impl ActiveThreadState {
         }
     }
 
-    pub fn sync_queue_flag_to_native_thread(&self, cx: &mut Context<AcpThreadView>) {
+    pub fn sync_queue_flag_to_native_thread(&self, cx: &mut Context<AcpServerView>) {
         if let Some(native_thread) = self.as_native_thread(cx) {
             let has_queued = self.has_queued_messages();
             native_thread.update(cx, |thread, _| {
@@ -671,7 +666,7 @@ impl ActiveThreadState {
         index: usize,
         is_send_now: bool,
         window: &mut Window,
-        cx: &mut Context<AcpThreadView>,
+        cx: &mut Context<AcpServerView>,
     ) {
         let Some(queued) = self.remove_from_queue(index, cx) else {
             return;
@@ -715,7 +710,7 @@ impl ActiveThreadState {
     pub fn expand_message_editor(
         &mut self,
         message_editor: Entity<MessageEditor>,
-        cx: &mut Context<AcpThreadView>,
+        cx: &mut Context<AcpServerView>,
     ) {
         self.set_editor_is_expanded(!self.editor_expanded, message_editor, cx);
         cx.stop_propagation();
@@ -726,7 +721,7 @@ impl ActiveThreadState {
         &mut self,
         is_expanded: bool,
         message_editor: Entity<MessageEditor>,
-        cx: &mut Context<AcpThreadView>,
+        cx: &mut Context<AcpServerView>,
     ) {
         self.editor_expanded = is_expanded;
         message_editor.update(cx, |editor, cx| {
@@ -758,7 +753,7 @@ impl ActiveThreadState {
         title_editor: &Entity<Editor>,
         event: &EditorEvent,
         window: &mut Window,
-        cx: &mut Context<AcpThreadView>,
+        cx: &mut Context<AcpServerView>,
     ) {
         let thread = &self.thread;
 
@@ -786,7 +781,7 @@ impl ActiveThreadState {
         &mut self,
         focus_handle: FocusHandle,
         window: &mut Window,
-        cx: &mut Context<AcpThreadView>,
+        cx: &mut Context<AcpServerView>,
     ) {
         if let Some(index) = self.editing_message.take()
             && let Some(editor) = &self
@@ -820,7 +815,7 @@ impl ActiveThreadState {
         option_id: acp::PermissionOptionId,
         option_kind: acp::PermissionOptionKind,
         window: &mut Window,
-        cx: &mut Context<AcpThreadView>,
+        cx: &mut Context<AcpServerView>,
     ) {
         let thread = &self.thread;
         let agent_telemetry_id = thread.read(cx).connection().telemetry_id();
@@ -849,7 +844,7 @@ impl ActiveThreadState {
         &mut self,
         kind: acp::PermissionOptionKind,
         window: &mut Window,
-        cx: &mut Context<AcpThreadView>,
+        cx: &mut Context<AcpServerView>,
     ) -> Option<()> {
         let thread = self.thread.read(cx);
         let tool_call = thread.first_tool_awaiting_confirmation()?;
@@ -872,7 +867,7 @@ impl ActiveThreadState {
     pub fn handle_select_permission_granularity(
         &mut self,
         action: &SelectPermissionGranularity,
-        cx: &mut Context<AcpThreadView>,
+        cx: &mut Context<AcpServerView>,
     ) {
         let tool_call_id = acp::ToolCallId::new(action.tool_call_id.clone());
         self.selected_permission_granularity
@@ -883,7 +878,7 @@ impl ActiveThreadState {
 
     // edits
 
-    pub fn keep_all(&mut self, cx: &mut Context<AcpThreadView>) {
+    pub fn keep_all(&mut self, cx: &mut Context<AcpServerView>) {
         let thread = &self.thread;
         let telemetry = ActionLogTelemetry::from(thread.read(cx));
         let action_log = thread.read(cx).action_log().clone();
@@ -892,7 +887,7 @@ impl ActiveThreadState {
         });
     }
 
-    pub fn reject_all(&mut self, cx: &mut Context<AcpThreadView>) {
+    pub fn reject_all(&mut self, cx: &mut Context<AcpServerView>) {
         let thread = &self.thread;
         let telemetry = ActionLogTelemetry::from(thread.read(cx));
         let action_log = thread.read(cx).action_log().clone();
@@ -907,7 +902,7 @@ impl ActiveThreadState {
         &mut self,
         buffer: &Entity<Buffer>,
         window: &mut Window,
-        cx: &mut Context<AcpThreadView>,
+        cx: &mut Context<AcpServerView>,
     ) {
         let thread = &self.thread;
 
@@ -928,7 +923,7 @@ impl ActiveThreadState {
         &mut self,
         project: Entity<Project>,
         window: &mut Window,
-        cx: &mut Context<AcpThreadView>,
+        cx: &mut Context<AcpServerView>,
     ) {
         if !self.is_imported_thread(cx) {
             return;
@@ -972,11 +967,8 @@ impl ActiveThreadState {
             };
 
             this.update_in(cx, |this, window, cx| {
-                if let ThreadState::Active(ActiveThreadState {
-                    resume_thread_metadata,
-                    ..
-                }) = &mut this.thread_state
-                {
+                if let Some(thread) = this.as_active_thread_mut() {
+                    let resume_thread_metadata = &mut thread.resume_thread_metadata;
                     *resume_thread_metadata = Some(thread_metadata);
                 }
                 this.reset(window, cx);
@@ -1006,7 +998,7 @@ impl ActiveThreadState {
     pub fn restore_checkpoint(
         &mut self,
         message_id: &UserMessageId,
-        cx: &mut Context<AcpThreadView>,
+        cx: &mut Context<AcpServerView>,
     ) {
         self.thread
             .update(cx, |thread, cx| {
@@ -1015,7 +1007,7 @@ impl ActiveThreadState {
             .detach_and_log_err(cx);
     }
 
-    pub fn clear_thread_error(&mut self, cx: &mut Context<AcpThreadView>) {
+    pub fn clear_thread_error(&mut self, cx: &mut Context<AcpServerView>) {
         self.thread_error = None;
         self.thread_error_markdown = None;
         self.token_limit_callout_dismissed = true;
@@ -1051,7 +1043,7 @@ impl ActiveThreadState {
 
     pub fn render_command_load_errors(
         &self,
-        cx: &mut Context<AcpThreadView>,
+        cx: &mut Context<AcpServerView>,
     ) -> Option<impl IntoElement> {
         let errors = self.cached_user_command_errors.borrow();
 
@@ -1164,7 +1156,7 @@ impl ActiveThreadState {
         )
     }
 
-    pub fn handle_open_rules(&mut self, window: &mut Window, cx: &mut Context<AcpThreadView>) {
+    pub fn handle_open_rules(&mut self, window: &mut Window, cx: &mut Context<AcpServerView>) {
         let Some(thread) = self.as_native_thread(cx) else {
             return;
         };

crates/agent_ui/src/agent_panel.rs πŸ”—

@@ -21,7 +21,7 @@ use crate::{
     LoadThreadFromClipboard, NewTextThread, NewThread, OpenActiveThreadAsMarkdown, OpenAgentDiff,
     OpenHistory, ResetTrialEndUpsell, ResetTrialUpsell, ToggleNavigationMenu, ToggleNewThreadMenu,
     ToggleOptionsMenu,
-    acp::AcpThreadView,
+    acp::AcpServerView,
     agent_configuration::{AgentConfiguration, AssistantConfigurationEvent},
     slash_command::SlashCommandCompletionProvider,
     text_thread_editor::{AgentPanelDelegate, TextThreadEditor, make_lsp_adapter_delegate},
@@ -243,8 +243,8 @@ enum HistoryKind {
 
 enum ActiveView {
     Uninitialized,
-    ExternalAgentThread {
-        thread_view: Entity<AcpThreadView>,
+    AgentThread {
+        thread_view: Entity<AcpServerView>,
     },
     TextThread {
         text_thread_editor: Entity<TextThreadEditor>,
@@ -316,7 +316,7 @@ impl ActiveView {
     pub fn which_font_size_used(&self) -> WhichFontSize {
         match self {
             ActiveView::Uninitialized
-            | ActiveView::ExternalAgentThread { .. }
+            | ActiveView::AgentThread { .. }
             | ActiveView::History { .. } => WhichFontSize::AgentFont,
             ActiveView::TextThread { .. } => WhichFontSize::BufferFont,
             ActiveView::Configuration => WhichFontSize::None,
@@ -731,9 +731,9 @@ impl AgentPanel {
             .unwrap_or(true)
     }
 
-    pub(crate) fn active_thread_view(&self) -> Option<&Entity<AcpThreadView>> {
+    pub(crate) fn active_thread_view(&self) -> Option<&Entity<AcpServerView>> {
         match &self.active_view {
-            ActiveView::ExternalAgentThread { thread_view, .. } => Some(thread_view),
+            ActiveView::AgentThread { thread_view, .. } => Some(thread_view),
             ActiveView::Uninitialized
             | ActiveView::TextThread { .. }
             | ActiveView::History { .. }
@@ -1017,7 +1017,7 @@ impl AgentPanel {
                     self.active_view = previous_view;
 
                     match &self.active_view {
-                        ActiveView::ExternalAgentThread { thread_view } => {
+                        ActiveView::AgentThread { thread_view } => {
                             thread_view.focus_handle(cx).focus(window, cx);
                         }
                         ActiveView::TextThread {
@@ -1190,7 +1190,7 @@ impl AgentPanel {
         };
 
         match &self.active_view {
-            ActiveView::ExternalAgentThread { thread_view } => {
+            ActiveView::AgentThread { thread_view } => {
                 thread_view
                     .update(cx, |thread_view, cx| {
                         thread_view.open_thread_as_markdown(workspace, window, cx)
@@ -1418,7 +1418,7 @@ impl AgentPanel {
 
     pub(crate) fn active_agent_thread(&self, cx: &App) -> Option<Entity<AcpThread>> {
         match &self.active_view {
-            ActiveView::ExternalAgentThread { thread_view, .. } => thread_view
+            ActiveView::AgentThread { thread_view, .. } => thread_view
                 .read(cx)
                 .as_active_thread()
                 .map(|r| r.thread.clone()),
@@ -1428,7 +1428,7 @@ impl AgentPanel {
 
     pub(crate) fn active_native_agent_thread(&self, cx: &App) -> Option<Entity<agent::Thread>> {
         match &self.active_view {
-            ActiveView::ExternalAgentThread { thread_view, .. } => {
+            ActiveView::AgentThread { thread_view, .. } => {
                 thread_view.read(cx).as_native_thread(cx)
             }
             _ => None,
@@ -1702,7 +1702,7 @@ impl AgentPanel {
             .then(|| self.thread_store.clone());
 
         let thread_view = cx.new(|cx| {
-            crate::acp::AcpThreadView::new(
+            crate::acp::AcpServerView::new(
                 server,
                 resume_thread,
                 initial_content,
@@ -1716,12 +1716,7 @@ impl AgentPanel {
             )
         });
 
-        self.set_active_view(
-            ActiveView::ExternalAgentThread { thread_view },
-            true,
-            window,
-            cx,
-        );
+        self.set_active_view(ActiveView::AgentThread { thread_view }, true, window, cx);
     }
 }
 
@@ -1729,7 +1724,7 @@ impl Focusable for AgentPanel {
     fn focus_handle(&self, cx: &App) -> FocusHandle {
         match &self.active_view {
             ActiveView::Uninitialized => self.focus_handle.clone(),
-            ActiveView::ExternalAgentThread { thread_view, .. } => thread_view.focus_handle(cx),
+            ActiveView::AgentThread { thread_view, .. } => thread_view.focus_handle(cx),
             ActiveView::History { kind } => match kind {
                 HistoryKind::AgentThreads => self.acp_history.focus_handle(cx),
                 HistoryKind::TextThreads => self.text_thread_history.focus_handle(cx),
@@ -1845,7 +1840,7 @@ impl AgentPanel {
         const LOADING_SUMMARY_PLACEHOLDER: &str = "Loading Summary…";
 
         let content = match &self.active_view {
-            ActiveView::ExternalAgentThread { thread_view } => {
+            ActiveView::AgentThread { thread_view } => {
                 let is_generating_title = thread_view
                     .read(cx)
                     .as_native_thread(cx)
@@ -1975,7 +1970,7 @@ impl AgentPanel {
             .into_any()
     }
 
-    fn handle_regenerate_thread_title(thread_view: Entity<AcpThreadView>, cx: &mut App) {
+    fn handle_regenerate_thread_title(thread_view: Entity<AcpServerView>, cx: &mut App) {
         thread_view.update(cx, |thread_view, cx| {
             if let Some(thread) = thread_view.as_native_thread(cx) {
                 thread.update(cx, |thread, cx| {
@@ -2028,11 +2023,11 @@ impl AgentPanel {
         };
 
         let thread_view = match &self.active_view {
-            ActiveView::ExternalAgentThread { thread_view } => Some(thread_view.clone()),
+            ActiveView::AgentThread { thread_view } => Some(thread_view.clone()),
             _ => None,
         };
         let thread_with_messages = match &self.active_view {
-            ActiveView::ExternalAgentThread { thread_view } => {
+            ActiveView::AgentThread { thread_view } => {
                 thread_view.read(cx).has_user_submitted_prompt(cx)
             }
             _ => false,
@@ -2196,9 +2191,7 @@ impl AgentPanel {
             };
 
         let active_thread = match &self.active_view {
-            ActiveView::ExternalAgentThread { thread_view } => {
-                thread_view.read(cx).as_native_thread(cx)
-            }
+            ActiveView::AgentThread { thread_view } => thread_view.read(cx).as_native_thread(cx),
             ActiveView::Uninitialized
             | ActiveView::TextThread { .. }
             | ActiveView::History { .. }
@@ -2600,7 +2593,7 @@ impl AgentPanel {
                 }
             }
             ActiveView::Uninitialized
-            | ActiveView::ExternalAgentThread { .. }
+            | ActiveView::AgentThread { .. }
             | ActiveView::History { .. }
             | ActiveView::Configuration => return false,
         }
@@ -2632,7 +2625,7 @@ impl AgentPanel {
             ActiveView::Uninitialized | ActiveView::History { .. } | ActiveView::Configuration => {
                 false
             }
-            ActiveView::ExternalAgentThread { thread_view, .. }
+            ActiveView::AgentThread { thread_view, .. }
                 if thread_view.read(cx).as_native_thread(cx).is_none() =>
             {
                 false
@@ -2878,7 +2871,7 @@ impl AgentPanel {
         cx: &mut Context<Self>,
     ) {
         match &self.active_view {
-            ActiveView::ExternalAgentThread { thread_view } => {
+            ActiveView::AgentThread { thread_view } => {
                 thread_view.update(cx, |thread_view, cx| {
                     thread_view.insert_dragged_files(paths, added_worktrees, window, cx);
                 });
@@ -2936,7 +2929,7 @@ impl AgentPanel {
         let mut key_context = KeyContext::new_with_defaults();
         key_context.add("AgentPanel");
         match &self.active_view {
-            ActiveView::ExternalAgentThread { .. } => key_context.add("acp_thread"),
+            ActiveView::AgentThread { .. } => key_context.add("acp_thread"),
             ActiveView::TextThread { .. } => key_context.add("text_thread"),
             ActiveView::Uninitialized | ActiveView::History { .. } | ActiveView::Configuration => {}
         }
@@ -2988,7 +2981,7 @@ impl Render for AgentPanel {
             .children(self.render_onboarding(window, cx))
             .map(|parent| match &self.active_view {
                 ActiveView::Uninitialized => parent,
-                ActiveView::ExternalAgentThread { thread_view, .. } => parent
+                ActiveView::AgentThread { thread_view, .. } => parent
                     .child(thread_view.clone())
                     .child(self.render_drag_target(cx)),
                 ActiveView::History { kind } => match kind {
@@ -3246,7 +3239,7 @@ impl AgentPanel {
     ///
     /// This is a test-only accessor that exposes the private `active_thread_view()`
     /// method for test assertions. Not compiled into production builds.
-    pub fn active_thread_view_for_tests(&self) -> Option<&Entity<AcpThreadView>> {
+    pub fn active_thread_view_for_tests(&self) -> Option<&Entity<AcpServerView>> {
         self.active_thread_view()
     }
 }

crates/agent_ui_v2/src/agent_thread_pane.rs πŸ”—

@@ -3,7 +3,7 @@ use agent::{NativeAgentServer, ThreadStore};
 use agent_client_protocol as acp;
 use agent_servers::AgentServer;
 use agent_settings::AgentSettings;
-use agent_ui::acp::{AcpThreadHistory, AcpThreadView};
+use agent_ui::acp::{AcpServerView, AcpThreadHistory};
 use fs::Fs;
 use gpui::{
     Entity, EventEmitter, Focusable, Pixels, SharedString, Subscription, WeakEntity, prelude::*,
@@ -51,7 +51,7 @@ impl EventEmitter<MinimizePane> for AgentThreadPane {}
 impl EventEmitter<ClosePane> for AgentThreadPane {}
 
 struct ActiveThreadView {
-    view: Entity<AcpThreadView>,
+    view: Entity<AcpServerView>,
     thread_id: acp::SessionId,
     _notify: Subscription,
 }
@@ -112,7 +112,7 @@ impl AgentThreadPane {
 
         let history = self.history.clone();
         let thread_view = cx.new(|cx| {
-            AcpThreadView::new(
+            AcpServerView::new(
                 agent,
                 resume_thread,
                 None,

crates/zed/src/visual_test_runner.rs πŸ”—

@@ -1951,7 +1951,7 @@ fn run_subagent_visual_tests(
     )?;
 
     // Expand the first subagent
-    thread_view.update(cx, |view: &mut agent_ui::acp::AcpThreadView, cx| {
+    thread_view.update(cx, |view: &mut agent_ui::acp::AcpServerView, cx| {
         view.expand_subagent(acp::SessionId::new("subagent-1"), cx);
     });