From be8842b94171838c72aa99ce3a4a74921c5c95df Mon Sep 17 00:00:00 2001 From: Cameron Mcloughlin Date: Mon, 2 Feb 2026 13:14:45 +0000 Subject: [PATCH] agent_ui: More refactors (#48186) Release Notes: - N/A *or* Added/Fixed/Improved ... --------- Co-authored-by: Ben Brandt Co-authored-by: Bennet Bo Fenner --- 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 | 828 +++++++++--------- .../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, 547 insertions(+), 677 deletions(-) diff --git a/crates/agent/src/agent.rs b/crates/agent/src/agent.rs index 43555e30dc4fe8830150a44bd0793f551835845a..ab8283f65d509dd20311551282fe490e7ef0a080 100644 --- a/crates/agent/src/agent.rs +++ b/crates/agent/src/agent.rs @@ -306,6 +306,36 @@ impl NativeAgent { })) } + fn new_session( + &mut self, + project: Entity, + cx: &mut Context, + ) -> Entity { + // 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, @@ -1187,38 +1217,10 @@ impl acp_thread::AgentConnection for NativeAgentConnection { cwd: &Path, cx: &mut App, ) -> Task>> { - 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 { diff --git a/crates/agent/src/tests/mod.rs b/crates/agent/src/tests/mod.rs index 2cd807b1cdc6c1715a17e28df51f26b0b78d3f60..948ec877148b0ee3e872e1b5beaa70b2a58ba093 100644 --- a/crates/agent/src/tests/mod.rs +++ b/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> = - 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, - > = 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> = - 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, - > = 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(); diff --git a/crates/agent/src/thread.rs b/crates/agent/src/thread.rs index 29f59720e7dd1da0a48cb3d8c69686ca19a55c0e..fd301dda5773578a9bb7d52ea5e54bffb237a41f 100644 --- a/crates/agent/src/thread.rs +++ b/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, - tools: BTreeMap>, + pub(crate) tools: BTreeMap>, request_token_usage: HashMap, #[allow(unused)] cumulative_token_usage: TokenUsage, #[allow(unused)] initial_project_snapshot: Shared>>>, - context_server_registry: Entity, + pub(crate) context_server_registry: Entity, profile_id: AgentProfileId, project_context: Entity, - templates: Arc, + pub(crate) templates: Arc, model: Option>, summarization_model: Option>, thinking_enabled: bool, @@ -1280,16 +1280,7 @@ impl Thread { self.add_tool(WebSearchTool); if cx.has_flag::() && 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())); } } diff --git a/crates/agent/src/tools/subagent_tool.rs b/crates/agent/src/tools/subagent_tool.rs index 8176a2bca2b2c4dd109835f5c53ef53f6b16c95d..17de5284caa14ab37c0ac12e90717662b7e0e243 100644 --- a/crates/agent/src/tools/subagent_tool.rs +++ b/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, - project: Entity, - project_context: Entity, - context_server_registry: Entity, - templates: Arc, 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>, } impl SubagentTool { - pub fn new( - parent_thread: WeakEntity, - project: Entity, - project_context: Entity, - context_server_registry: Entity, - templates: Arc, - current_depth: u8, - parent_tools: BTreeMap>, - ) -> Self { + pub fn new(parent_thread: WeakEntity, 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>) -> Result<()> { - let Some(tools) = allowed_tools else { + pub fn validate_allowed_tools( + &self, + allowed_tools: &Option>, + 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::>() - .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(); diff --git a/crates/agent_ui/src/acp.rs b/crates/agent_ui/src/acp.rs index 5c892f00f56b057e9272a84a776e1fa771768efe..904c9a6c7b7e383d09b54f58115be2303ef8754a 100644 --- a/crates/agent_ui/src/acp.rs +++ b/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; diff --git a/crates/agent_ui/src/acp/thread_history.rs b/crates/agent_ui/src/acp/thread_history.rs index 8f0e7a0410a9b680396bb8cc5290b41279ee4b1d..16108e599e31f8d56f1b04e9f2e74bebff9ecee1 100644 --- a/crates/agent_ui/src/acp/thread_history.rs +++ b/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, + thread_view: WeakEntity, selected: bool, hovered: bool, supports_delete: bool, @@ -851,7 +851,7 @@ pub struct AcpHistoryEntryElement { } impl AcpHistoryEntryElement { - pub fn new(entry: AgentSessionInfo, thread_view: WeakEntity) -> Self { + pub fn new(entry: AgentSessionInfo, thread_view: WeakEntity) -> Self { Self { entry, thread_view, diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index d23119477d8e930cc40d274c6a28de433813c977..fcf9f756240323307a1dc9302b9cae5169624377 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -324,14 +324,14 @@ impl DiffStats { } } -pub struct AcpThreadView { +pub struct AcpServerView { agent: Rc, agent_server_store: Entity, workspace: WeakEntity, project: Entity, thread_store: Option>, prompt_store: Option>, - thread_state: ThreadState, + server_state: ServerState, login: Option, // is some <=> Active | Unauthenticated recent_history_entries: Vec, history: Entity, @@ -343,34 +343,59 @@ pub struct AcpThreadView { notification_subscriptions: HashMap, Vec>, slash_command_registry: Option>, auth_task: Option>, - _subscriptions: [Subscription; 4], + _subscriptions: Vec, show_codex_windows_warning: bool, in_flight_prompt: Option>, add_context_menu_handle: PopoverMenuHandle, } -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), - 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, +} + +enum AuthState { + Ok, Unauthenticated { - connection: Rc, description: Option>, configuration_view: Option, pending_auth_method: Option, @@ -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>, } -impl AcpThreadView { +impl ConnectedServerState { + pub fn has_thread_error(&self) -> bool { + self.current.thread_error.is_some() + } +} + +impl AcpServerView { pub fn new( agent: Rc, resume_thread: Option, @@ -447,7 +484,7 @@ impl AcpThreadView { editor }); - let subscriptions = [ + let subscriptions = vec![ cx.observe_global_in::(window, Self::agent_ui_font_size_changed), cx.observe_global_in::(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>>, window: &mut Window, cx: &mut Context, - ) -> ThreadState { + ) -> ServerState { if project.read(cx).is_via_collab() && agent.clone().downcast::().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::() { 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, err: AuthRequired, - agent: Rc, - connection: Rc, + 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, ) { if let Some(load_err) = err.downcast_ref::() { - 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) { @@ -1331,11 +1361,11 @@ impl AcpThreadView { fn send(&mut self, window: &mut Window, cx: &mut Context) { 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) { 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, ) { - 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::>() - }) - .unwrap_or_default(); - - // Run SpawnInTerminal in the same dir as the ACP server - let cwd = connection - .clone() - .downcast::() - .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::>() + }) + .unwrap_or_default(); + + // Run SpawnInTerminal in the same dir as the ACP server + let cwd = connected + .connection + .clone() + .downcast::() + .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>, _cx: &mut Context, ) -> 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> { - 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) { - 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); @@ -6477,17 +6485,16 @@ impl AcpThreadView { } } - while queued_message_editors.len() < needed_count { - let agent_name = self.agent.name(); - let index = queued_message_editors.len(); + while thread.queued_message_editors.len() < needed_count { + let index = thread.queued_message_editors.len(); let content = queued_messages.get(index).cloned().unwrap_or_default(); let editor = cx.new(|cx| { let mut editor = MessageEditor::new( - self.workspace.clone(), - self.project.downgrade(), + workspace.clone(), + project.clone(), None, - self.history.downgrade(), + history.clone(), None, prompt_capabilities.clone(), available_commands.clone(), @@ -6504,7 +6511,7 @@ impl AcpThreadView { editor }); - let main_editor = self.message_editor.clone(); + let message_editor = message_editor.clone(); let subscription = cx.subscribe_in( &editor, window, @@ -6513,10 +6520,10 @@ impl AcpThreadView { this.save_queued_message_at_index(index, cx); } MessageEditorEvent::Cancel => { - window.focus(&main_editor.focus_handle(cx), cx); + window.focus(&message_editor.focus_handle(cx), cx); } MessageEditorEvent::Send => { - window.focus(&main_editor.focus_handle(cx), cx); + window.focus(&message_editor.focus_handle(cx), cx); } MessageEditorEvent::SendImmediately => { this.send_queued_message_at_index(index, true, window, cx); @@ -6525,8 +6532,10 @@ impl AcpThreadView { }, ); - queued_message_editors.push(editor); - queued_message_editor_subscriptions.push(subscription); + thread.queued_message_editors.push(editor); + thread + .queued_message_editor_subscriptions + .push(subscription); } if let Some(active) = self.as_active_thread_mut() { @@ -6834,13 +6843,10 @@ impl AcpThreadView { .as_active_thread() .is_some_and(|active| active.thread.read(cx).status() != ThreadStatus::Idle); - if matches!( - &self.thread_state, - ThreadState::Active(ActiveThreadState { - is_loading_contents: true, - .. - }) - ) { + if self + .as_active_thread() + .is_some_and(|thread| thread.is_loading_contents) + { div() .id("loading-message-content") .px_1() @@ -6920,13 +6926,9 @@ impl AcpThreadView { workspace.is_being_followed(CollaboratorId::Agent) }) .unwrap_or(false), - _ => matches!( - &self.thread_state, - ThreadState::Active(ActiveThreadState { - should_be_following: true, - .. - }) - ), + _ => self + .as_active_thread() + .is_some_and(|thread| thread.should_be_following), } } @@ -8380,12 +8382,7 @@ impl AcpThreadView { .style(ButtonStyle::Filled) .on_click(cx.listener({ move |this, _, window, cx| { - let agent = this.agent.clone(); - let Some(thread_state) = this.as_active_thread() else { - return; - }; - - let connection = thread_state.thread.read(cx).connection().clone(); + let agent_name = this.agent.name(); this.clear_thread_error(cx); if let Some(message) = this.in_flight_prompt.take() { this.message_editor.update(cx, |editor, cx| { @@ -8397,8 +8394,7 @@ impl AcpThreadView { Self::handle_auth_required( this, AuthRequired::new(), - agent, - connection, + agent_name, window, cx, ); @@ -8408,16 +8404,11 @@ impl AcpThreadView { } pub(crate) fn reauthenticate(&mut self, window: &mut Window, cx: &mut Context) { - let agent = self.agent.clone(); - let Some(thread_state) = self.as_active_thread() else { - return; - }; - - let connection = thread_state.thread.read(cx).connection().clone(); + let agent_name = self.agent.name(); self.clear_thread_error(cx); let this = cx.weak_entity(); window.defer(cx, |window, cx| { - Self::handle_auth_required(this, AuthRequired::new(), agent, connection, window, cx); + Self::handle_auth_required(this, AuthRequired::new(), agent_name, window, cx); }) } @@ -8533,21 +8524,17 @@ fn placeholder_text(agent_name: &str, has_commands: bool) -> String { } } -impl Focusable for AcpThreadView { +impl Focusable for AcpServerView { fn focus_handle(&self, cx: &App) -> FocusHandle { - match self.thread_state { - ThreadState::Active(ActiveThreadState { .. }) => { - self.active_editor(cx).focus_handle(cx) - } - ThreadState::Loading { .. } - | ThreadState::LoadError(_) - | ThreadState::Unauthenticated { .. } => self.focus_handle.clone(), + match self.as_active_thread() { + Some(_) => self.active_editor(cx).focus_handle(cx), + None => self.focus_handle.clone(), } } } #[cfg(any(test, feature = "test-support"))] -impl AcpThreadView { +impl AcpServerView { /// Expands a tool call so its content is visible. /// This is primarily useful for visual testing. pub fn expand_tool_call(&mut self, tool_call_id: acp::ToolCallId, cx: &mut Context) { @@ -8567,7 +8554,7 @@ impl AcpThreadView { } } -impl Render for AcpThreadView { +impl Render for AcpServerView { fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { self.sync_queued_message_editors(window, cx); @@ -8733,14 +8720,29 @@ impl Render for AcpThreadView { })) .track_focus(&self.focus_handle) .bg(cx.theme().colors().panel_background) - .child(match &self.thread_state { - ThreadState::Unauthenticated { + .child(match &self.server_state { + ServerState::Loading { .. } => v_flex() + .flex_1() + .child(self.render_recent_history(cx)) + .into_any(), + ServerState::LoadError(e) => v_flex() + .flex_1() + .size_full() + .items_center() + .justify_end() + .child(self.render_load_error(e, window, cx)) + .into_any(), + ServerState::Connected(ConnectedServerState { connection, - description, - configuration_view, - pending_auth_method, + auth_state: + AuthState::Unauthenticated { + description, + configuration_view, + pending_auth_method, + _subscription, + }, .. - } => v_flex() + }) => v_flex() .flex_1() .size_full() .justify_end() @@ -8753,25 +8755,14 @@ impl Render for AcpThreadView { cx, )) .into_any_element(), - ThreadState::Loading { .. } => v_flex() - .flex_1() - .child(self.render_recent_history(cx)) - .into_any(), - ThreadState::LoadError(e) => v_flex() - .flex_1() - .size_full() - .items_center() - .justify_end() - .child(self.render_load_error(e, window, cx)) - .into_any(), - ThreadState::Active(thread_state) => v_flex().flex_1().map(|this| { - let this = this.when(thread_state.resumed_without_history, |this| { + ServerState::Connected(connected) => v_flex().flex_1().map(|this| { + let this = this.when(connected.current.resumed_without_history, |this| { this.child(self.render_resume_notice(cx)) }); if has_messages { this.child( list( - thread_state.list_state.clone(), + connected.current.list_state.clone(), cx.processor(|this, index: usize, window, cx| { let Some((entry, len)) = this.as_active_thread().and_then(|active| { @@ -8788,7 +8779,7 @@ impl Render for AcpThreadView { .flex_grow() .into_any(), ) - .vertical_scrollbar_for(&thread_state.list_state, window, cx) + .vertical_scrollbar_for(&connected.current.list_state, window, cx) .into_any() } else { this.child(self.render_recent_history(cx)).into_any() @@ -8798,10 +8789,8 @@ impl Render for AcpThreadView { // The activity bar is intentionally rendered outside of the ThreadState::Active match // above so that the scrollbar doesn't render behind it. The current setup allows // the scrollbar to stop exactly at the activity bar start. - .when(has_messages, |this| match &self.thread_state { - ThreadState::Active(ActiveThreadState { thread, .. }) => { - this.children(self.render_activity_bar(thread, window, cx)) - } + .when(has_messages, |this| match self.as_active_thread() { + Some(thread) => this.children(self.render_activity_bar(&thread.thread, window, cx)), _ => this, }) .when(self.show_codex_windows_warning, |this| { @@ -8952,7 +8941,7 @@ pub(crate) mod tests { let thread_view = cx.update(|window, cx| { cx.new(|cx| { - AcpThreadView::new( + AcpServerView::new( Rc::new(StubAgentServer::default_response()), None, None, @@ -9023,7 +9012,7 @@ pub(crate) mod tests { let thread_view = cx.update(|window, cx| { cx.new(|cx| { - AcpThreadView::new( + AcpServerView::new( Rc::new(StubAgentServer::new(ResumeOnlyAgentConnection)), Some(session), None, @@ -9041,20 +9030,9 @@ pub(crate) mod tests { cx.run_until_parked(); thread_view.read_with(cx, |view, _cx| { - let ThreadState::Active(ActiveThreadState { - resumed_without_history, - .. - }) = &view.thread_state - else { - panic!("Expected Active state"); - }; - assert!(*resumed_without_history); - assert_eq!( - view.as_active_thread() - .map(|active| &active.list_state) - .map_or(0, |s| s.item_count()), - 0 - ); + let state = view.as_active_thread().unwrap(); + assert!(state.resumed_without_history); + assert_eq!(state.list_state.item_count(), 0); }); } @@ -9078,13 +9056,9 @@ pub(crate) mod tests { // Check that the refusal error is set thread_view.read_with(cx, |thread_view, _cx| { - let ThreadState::Active(ActiveThreadState { thread_error, .. }) = - &thread_view.thread_state - else { - panic!("Expected Active state"); - }; + let state = thread_view.as_active_thread().unwrap(); assert!( - matches!(thread_error, Some(ThreadError::Refusal)), + matches!(state.thread_error, Some(ThreadError::Refusal)), "Expected refusal error to be set" ); }); @@ -9287,7 +9261,7 @@ pub(crate) mod tests { async fn setup_thread_view( agent: impl AgentServer + 'static, cx: &mut TestAppContext, - ) -> (Entity, &mut VisualTestContext) { + ) -> (Entity, &mut VisualTestContext) { let fs = FakeFs::new(cx.executor()); let project = Project::test(fs, [], cx).await; let (workspace, cx) = @@ -9298,7 +9272,7 @@ pub(crate) mod tests { let thread_view = cx.update(|window, cx| { cx.new(|cx| { - AcpThreadView::new( + AcpServerView::new( Rc::new(agent), None, None, @@ -9316,7 +9290,7 @@ pub(crate) mod tests { (thread_view, cx) } - fn add_to_workspace(thread_view: Entity, cx: &mut VisualTestContext) { + fn add_to_workspace(thread_view: Entity, cx: &mut VisualTestContext) { let workspace = thread_view.read_with(cx, |thread_view, _cx| thread_view.workspace.clone()); workspace @@ -9332,7 +9306,7 @@ pub(crate) mod tests { .unwrap(); } - struct ThreadViewItem(Entity); + struct ThreadViewItem(Entity); impl Item for ThreadViewItem { type Event = (); @@ -9684,7 +9658,7 @@ pub(crate) mod tests { let connection = Rc::new(StubAgentConnection::new()); let thread_view = cx.update(|window, cx| { cx.new(|cx| { - AcpThreadView::new( + AcpServerView::new( Rc::new(StubAgentServer::new(connection.as_ref().clone())), None, None, @@ -10211,7 +10185,7 @@ pub(crate) mod tests { } struct GeneratingThreadSetup { - thread_view: Entity, + thread_view: Entity, thread: Entity, message_editor: Entity, } @@ -11230,15 +11204,8 @@ pub(crate) mod tests { // Verify default granularity is the last option (index 2 = "Only this time") thread_view.read_with(cx, |thread_view, _cx| { - let selected = if let ThreadState::Active(ActiveThreadState { - selected_permission_granularity, - .. - }) = &thread_view.thread_state - { - selected_permission_granularity.get(&tool_call_id) - } else { - None - }; + let state = thread_view.as_active_thread().unwrap(); + let selected = state.selected_permission_granularity.get(&tool_call_id); assert!( selected.is_none(), "Should have no selection initially (defaults to last)" @@ -11261,15 +11228,8 @@ pub(crate) mod tests { // Verify the selection was updated thread_view.read_with(cx, |thread_view, _cx| { - let selected = if let ThreadState::Active(ActiveThreadState { - selected_permission_granularity, - .. - }) = &thread_view.thread_state - { - selected_permission_granularity.get(&tool_call_id) - } else { - None - }; + let state = thread_view.as_active_thread().unwrap(); + let selected = state.selected_permission_granularity.get(&tool_call_id); assert_eq!(selected, Some(&0), "Should have selected index 0"); }); } diff --git a/crates/agent_ui/src/acp/thread_view/active_thread.rs b/crates/agent_ui/src/acp/thread_view/active_thread.rs index 0abcfa3fcd53cf93798021e6b49a2a475ee44b34..bd902f28ccf7e53abf6fc2abd7e62581edf3bf88 100644 --- a/crates/agent_ui/src/acp/thread_view/active_thread.rs +++ b/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, pub workspace: WeakEntity, pub entry_view_state: Entity, @@ -66,7 +67,7 @@ pub struct TurnFields { pub turn_tokens: Option, } -impl ActiveThreadState { +impl AcpThreadView { pub fn new( thread: Entity, workspace: WeakEntity, @@ -84,8 +85,11 @@ impl ActiveThreadState { resumed_without_history: bool, resume_thread_metadata: Option, subscriptions: Vec, + 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) -> usize { + pub fn start_turn(&mut self, cx: &mut Context) -> 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, - agent: Rc, + agent_name: SharedString, login: Option, window: &mut Window, - cx: &mut Context, + cx: &mut Context, ) { 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, window: &mut Window, - cx: &mut Context, + cx: &mut Context, ) { 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, Vec>)>>>, window: &mut Window, - cx: &mut Context, + cx: &mut Context, ) { 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, window: &mut Window, - cx: &mut Context, + cx: &mut Context, ) { 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, + cx: &mut Context, ) { 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) { + pub fn cancel_generation(&mut self, cx: &mut Context) { 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) { + pub fn retry_generation(&mut self, cx: &mut Context) { self.thread_error.take(); let thread = &self.thread; @@ -540,7 +535,7 @@ impl ActiveThreadState { entry_ix: usize, message_editor: Entity, window: &mut Window, - cx: &mut Context, + cx: &mut Context, ) { if self.is_loading_contents { return; @@ -594,7 +589,7 @@ impl ActiveThreadState { &mut self, message_editor: Entity, window: &mut Window, - cx: &mut Context, + cx: &mut Context, ) { 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, + cx: &mut Context, ) -> Option { 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) { + pub fn sync_queue_flag_to_native_thread(&self, cx: &mut Context) { 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, + cx: &mut Context, ) { 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, - cx: &mut Context, + cx: &mut Context, ) { 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, - cx: &mut Context, + cx: &mut Context, ) { self.editor_expanded = is_expanded; message_editor.update(cx, |editor, cx| { @@ -758,7 +753,7 @@ impl ActiveThreadState { title_editor: &Entity, event: &EditorEvent, window: &mut Window, - cx: &mut Context, + cx: &mut Context, ) { let thread = &self.thread; @@ -786,7 +781,7 @@ impl ActiveThreadState { &mut self, focus_handle: FocusHandle, window: &mut Window, - cx: &mut Context, + cx: &mut Context, ) { 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, + cx: &mut Context, ) { 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, + cx: &mut Context, ) -> 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, + cx: &mut Context, ) { 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) { + pub fn keep_all(&mut self, cx: &mut Context) { 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) { + pub fn reject_all(&mut self, cx: &mut Context) { 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, window: &mut Window, - cx: &mut Context, + cx: &mut Context, ) { let thread = &self.thread; @@ -928,7 +923,7 @@ impl ActiveThreadState { &mut self, project: Entity, window: &mut Window, - cx: &mut Context, + cx: &mut Context, ) { 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, + cx: &mut Context, ) { 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) { + pub fn clear_thread_error(&mut self, cx: &mut Context) { 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, + cx: &mut Context, ) -> Option { 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) { + pub fn handle_open_rules(&mut self, window: &mut Window, cx: &mut Context) { let Some(thread) = self.as_native_thread(cx) else { return; }; diff --git a/crates/agent_ui/src/agent_panel.rs b/crates/agent_ui/src/agent_panel.rs index 15009c3d929bb8926499aafc7dbcd0601135152c..f176d0e2c4fb854a7822ec68ddbefbf7fed51e38 100644 --- a/crates/agent_ui/src/agent_panel.rs +++ b/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, + AgentThread { + thread_view: Entity, }, TextThread { text_thread_editor: Entity, @@ -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> { + pub(crate) fn active_thread_view(&self) -> Option<&Entity> { 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> { 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> { 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, cx: &mut App) { + fn handle_regenerate_thread_title(thread_view: Entity, 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, ) { 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> { + pub fn active_thread_view_for_tests(&self) -> Option<&Entity> { self.active_thread_view() } } diff --git a/crates/agent_ui_v2/src/agent_thread_pane.rs b/crates/agent_ui_v2/src/agent_thread_pane.rs index 535c28c65717820d79ad1314225b81d78c10b081..d04d636b775bb981cc90c7611b20ec062235bad6 100644 --- a/crates/agent_ui_v2/src/agent_thread_pane.rs +++ b/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 for AgentThreadPane {} impl EventEmitter for AgentThreadPane {} struct ActiveThreadView { - view: Entity, + view: Entity, 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, diff --git a/crates/zed/src/visual_test_runner.rs b/crates/zed/src/visual_test_runner.rs index 97e105deb7efe9fd847e42078019a395cdd4c647..2d9c11ccc54c83c7fb322951d7513123861acbcb 100644 --- a/crates/zed/src/visual_test_runner.rs +++ b/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); });