From 74dbaeb2ad353426309589b91f158764963fe29c Mon Sep 17 00:00:00 2001 From: Nihal Kumar Date: Mon, 16 Mar 2026 21:59:24 +0530 Subject: [PATCH 1/7] debugger: Fall back to cached adapter binaries when offline (#50928) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Python (debugpy) and Go (delve-shim-dap) debug adapters unconditionally require network access on every debug session start — even when the adapter binary is already cached locally. This makes the debugger completely unusable offline. Python: fetch_debugpy_whl → maybe_fetch_new_wheel hits pypi; failure propagates as a fatal error. Go: install_shim → fetch_latest_adapter_version hits the GitHub API; failure propagates as a fatal error. CodeLLDB and JavaScript adapters already handle this correctly. Fix for Python and Go: Wrap the network fetch in each adapter with a fallback: if the request fails and a previously downloaded adapter exists on disk, log a warning and use the cached version. If no cache exists, the original error propagates unchanged. Logs after the fix: ``` 2026-03-06T16:31:51+05:30 WARN [dap_adapters::python] Failed to fetch latest debugpy, using cached version: getting latest release 2026-03-06T16:31:51+05:30 INFO [dap::transport] Debug adapter has connected to TCP server 127.0.0.1:45533 ``` Limitations: The debugger must be run at least once with internet connectivity to populate the cache on disk. This PR does not change that requirement. Closes #45781 Before you mark this PR as ready for review, make sure that you have: - [ ] Added a solid test coverage and/or screenshots from doing manual testing - No automated tests are included. The existing MockDelegate stubs http_client() and fs() as unimplemented!(), so testing the fallback path would require new mock infrastructure. The fix was verified manually by running the debug build offline with and without cached adapters. The CodeLLDB adapter's equivalent fallback (lines 357-374) also has no dedicated test. - [x] Done a self-review taking into account security and performance aspects Release Notes: - Fixed debugger failing to start when offline if a debug adapter was previously downloaded. --- crates/dap_adapters/src/go.rs | 71 ++++++++++++++++++++++--------- crates/dap_adapters/src/python.rs | 31 +++++++++----- 2 files changed, 73 insertions(+), 29 deletions(-) diff --git a/crates/dap_adapters/src/go.rs b/crates/dap_adapters/src/go.rs index af81f5cca5390d7e72e1805331e25da0a036d9d8..93d0e8a958568cd7899208daca05a9c1dd2f846b 100644 --- a/crates/dap_adapters/src/go.rs +++ b/crates/dap_adapters/src/go.rs @@ -8,6 +8,7 @@ use dap::{ }, }; use fs::Fs; +use futures::StreamExt; use gpui::{AsyncApp, SharedString}; use language::LanguageName; use log::warn; @@ -71,27 +72,59 @@ impl GoDebugAdapter { return Ok(path); } - let asset = Self::fetch_latest_adapter_version(delegate).await?; - let ty = if consts::OS == "windows" { - DownloadedFileType::Zip - } else { - DownloadedFileType::GzipTar - }; - download_adapter_from_github( - "delve-shim-dap".into(), - asset.clone(), - ty, - delegate.as_ref(), - ) - .await?; + let adapter_dir = paths::debug_adapters_dir().join("delve-shim-dap"); + + match Self::fetch_latest_adapter_version(delegate).await { + Ok(asset) => { + let ty = if consts::OS == "windows" { + DownloadedFileType::Zip + } else { + DownloadedFileType::GzipTar + }; + download_adapter_from_github( + "delve-shim-dap".into(), + asset.clone(), + ty, + delegate.as_ref(), + ) + .await?; + + let path = adapter_dir + .join(format!("delve-shim-dap_{}", asset.tag_name)) + .join(format!("delve-shim-dap{}", consts::EXE_SUFFIX)); + self.shim_path.set(path.clone()).ok(); - let path = paths::debug_adapters_dir() - .join("delve-shim-dap") - .join(format!("delve-shim-dap_{}", asset.tag_name)) - .join(format!("delve-shim-dap{}", std::env::consts::EXE_SUFFIX)); - self.shim_path.set(path.clone()).ok(); + Ok(path) + } + Err(error) => { + let binary_name = format!("delve-shim-dap{}", consts::EXE_SUFFIX); + let mut cached = None; + if let Ok(mut entries) = delegate.fs().read_dir(&adapter_dir).await { + while let Some(entry) = entries.next().await { + if let Ok(version_dir) = entry { + let candidate = version_dir.join(&binary_name); + if delegate + .fs() + .metadata(&candidate) + .await + .is_ok_and(|m| m.is_some()) + { + cached = Some(candidate); + break; + } + } + } + } - Ok(path) + if let Some(path) = cached { + warn!("Failed to fetch latest delve-shim-dap, using cached version: {error:#}"); + self.shim_path.set(path.clone()).ok(); + Ok(path) + } else { + Err(error) + } + } + } } } diff --git a/crates/dap_adapters/src/python.rs b/crates/dap_adapters/src/python.rs index 96bdde12672cae471edf1d6e603a06413d1f4b21..111eab5a1d1bf4dea5f99ce83c01ce8fdb9e47e3 100644 --- a/crates/dap_adapters/src/python.rs +++ b/crates/dap_adapters/src/python.rs @@ -224,16 +224,27 @@ impl PythonDebugAdapter { ) -> Result, String> { self.debugpy_whl_base_path .get_or_init(|| async move { - self.maybe_fetch_new_wheel(toolchain, delegate) - .await - .map_err(|e| format!("{e}"))?; - Ok(Arc::from( - debug_adapters_dir() - .join(Self::ADAPTER_NAME) - .join("debugpy") - .join("adapter") - .as_ref(), - )) + let adapter_path = debug_adapters_dir() + .join(Self::ADAPTER_NAME) + .join("debugpy") + .join("adapter"); + + if let Err(error) = self.maybe_fetch_new_wheel(toolchain, delegate).await { + if delegate + .fs() + .metadata(&adapter_path) + .await + .is_ok_and(|m| m.is_some()) + { + log::warn!( + "Failed to fetch latest debugpy, using cached version: {error:#}" + ); + } else { + return Err(format!("{error}")); + } + } + + Ok(Arc::from(adapter_path.as_ref())) }) .await .clone() From 74eb252d7ec38a5a4a5e56a3141b1f5516986595 Mon Sep 17 00:00:00 2001 From: Lukas Wirth Date: Mon, 16 Mar 2026 17:30:27 +0100 Subject: [PATCH 2/7] editor: Fix panic in `refresh_inline_values` when spanning cross excerpts (#51685) Fixes ZED-5VQ Release Notes: - N/A *or* Added/Fixed/Improved ... --- crates/editor/src/editor.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 2512c362f9c06dc94b231a2ea56168df9e13bf7e..204011412ec9b229ffdd49195e907369baa2d97f 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -23664,6 +23664,14 @@ impl Editor { editor.buffer.read(cx).buffer(excerpt.buffer_id()) })?; + if current_execution_position + .text_anchor + .buffer_id + .is_some_and(|id| id != buffer.read(cx).remote_id()) + { + return Some(Task::ready(Ok(Vec::new()))); + } + let range = buffer.read(cx).anchor_before(0)..current_execution_position.text_anchor; From 30b7178f6717301ece536b7653cc1e4ca17471de Mon Sep 17 00:00:00 2001 From: Bennet Bo Fenner Date: Mon, 16 Mar 2026 17:33:12 +0100 Subject: [PATCH 3/7] agent_ui: Rename `ConnectionView` to `ConversationView` (#51684) We now share connections across multiple "ConnectionView"s the naming does not make sense anymore Release Notes: - N/A --- crates/agent_ui/src/agent_panel.rs | 211 ++++--- crates/agent_ui/src/agent_ui.rs | 4 +- ...onnection_view.rs => conversation_view.rs} | 571 ++++++++++-------- .../thread_view.rs | 10 +- crates/agent_ui/src/message_editor.rs | 2 +- crates/agent_ui/src/sidebar.rs | 2 +- crates/agent_ui/src/test_support.rs | 2 +- crates/agent_ui/src/thread_history_view.rs | 20 +- crates/zed/src/visual_test_runner.rs | 4 +- 9 files changed, 454 insertions(+), 372 deletions(-) rename crates/agent_ui/src/{connection_view.rs => conversation_view.rs} (92%) rename crates/agent_ui/src/{connection_view => conversation_view}/thread_view.rs (99%) diff --git a/crates/agent_ui/src/agent_panel.rs b/crates/agent_ui/src/agent_panel.rs index ccf9e481ef48095de76587c915962eef458a77e1..403c11c00b24f9de7c8c1b56a3c8ac02a3bdb77f 100644 --- a/crates/agent_ui/src/agent_panel.rs +++ b/crates/agent_ui/src/agent_panel.rs @@ -31,12 +31,12 @@ use zed_actions::agent::{ use crate::ui::{AcpOnboardingModal, ClaudeCodeOnboardingModal, HoldForDefault}; use crate::{ - AddContextServer, AgentDiffPane, ConnectionView, CopyThreadToClipboard, CycleStartThreadIn, + AddContextServer, AgentDiffPane, ConversationView, CopyThreadToClipboard, CycleStartThreadIn, Follow, InlineAssistant, LoadThreadFromClipboard, NewTextThread, NewThread, OpenActiveThreadAsMarkdown, OpenAgentDiff, OpenHistory, ResetTrialEndUpsell, ResetTrialUpsell, StartThreadIn, ToggleNavigationMenu, ToggleNewThreadMenu, ToggleOptionsMenu, agent_configuration::{AgentConfiguration, AssistantConfigurationEvent}, - connection_view::{AcpThreadViewEvent, ThreadView}, + conversation_view::{AcpThreadViewEvent, ThreadView}, slash_command::SlashCommandCompletionProvider, text_thread_editor::{AgentPanelDelegate, TextThreadEditor, make_lsp_adapter_delegate}, ui::EndTrialUpsell, @@ -275,9 +275,9 @@ pub fn init(cx: &mut App) { .register_action(|workspace, _: &OpenAgentDiff, window, cx| { let thread = workspace .panel::(cx) - .and_then(|panel| panel.read(cx).active_connection_view().cloned()) - .and_then(|thread_view| { - thread_view + .and_then(|panel| panel.read(cx).active_conversation().cloned()) + .and_then(|conversation| { + conversation .read(cx) .active_thread() .map(|r| r.read(cx).thread.clone()) @@ -624,7 +624,7 @@ enum History { enum ActiveView { Uninitialized, AgentThread { - server_view: Entity, + conversation_view: Entity, }, TextThread { text_thread_editor: Entity, @@ -878,7 +878,7 @@ pub struct AgentPanel { focus_handle: FocusHandle, active_view: ActiveView, previous_view: Option, - background_threads: HashMap>, + background_threads: HashMap>, new_thread_menu_handle: PopoverMenuHandle, start_thread_in_menu_handle: PopoverMenuHandle, agent_panel_menu_handle: PopoverMenuHandle, @@ -1334,9 +1334,11 @@ impl AgentPanel { .unwrap_or(false) } - pub fn active_connection_view(&self) -> Option<&Entity> { + pub fn active_conversation(&self) -> Option<&Entity> { match &self.active_view { - ActiveView::AgentThread { server_view, .. } => Some(server_view), + ActiveView::AgentThread { + conversation_view, .. + } => Some(conversation_view), ActiveView::Uninitialized | ActiveView::TextThread { .. } | ActiveView::History { .. } @@ -1556,11 +1558,11 @@ impl AgentPanel { } fn expand_message_editor(&mut self, window: &mut Window, cx: &mut Context) { - let Some(thread_view) = self.active_connection_view() else { + let Some(conversation_view) = self.active_conversation() else { return; }; - let Some(active_thread) = thread_view.read(cx).active_thread().cloned() else { + let Some(active_thread) = conversation_view.read(cx).active_thread().cloned() else { return; }; @@ -1886,8 +1888,8 @@ impl AgentPanel { cx: &mut Context, ) { if let Some(workspace) = self.workspace.upgrade() - && let Some(thread_view) = self.active_connection_view() - && let Some(active_thread) = thread_view.read(cx).active_thread().cloned() + && let Some(conversation_view) = self.active_conversation() + && let Some(active_thread) = conversation_view.read(cx).active_thread().cloned() { active_thread.update(cx, |thread, cx| { thread @@ -2077,21 +2079,23 @@ impl AgentPanel { } } - pub fn as_active_server_view(&self) -> Option<&Entity> { + pub fn active_conversation_view(&self) -> Option<&Entity> { match &self.active_view { - ActiveView::AgentThread { server_view } => Some(server_view), + ActiveView::AgentThread { conversation_view } => Some(conversation_view), _ => None, } } - pub fn as_active_thread_view(&self, cx: &App) -> Option> { - let server_view = self.as_active_server_view()?; + pub fn active_thread_view(&self, cx: &App) -> Option> { + let server_view = self.active_conversation_view()?; server_view.read(cx).active_thread().cloned() } pub fn active_agent_thread(&self, cx: &App) -> Option> { match &self.active_view { - ActiveView::AgentThread { server_view, .. } => server_view + ActiveView::AgentThread { + conversation_view, .. + } => conversation_view .read(cx) .active_thread() .map(|r| r.read(cx).thread.clone()), @@ -2109,7 +2113,7 @@ impl AgentPanel { pub fn parent_threads(&self, cx: &App) -> Vec> { let mut views = Vec::new(); - if let Some(server_view) = self.as_active_server_view() { + if let Some(server_view) = self.active_conversation_view() { if let Some(thread_view) = server_view.read(cx).parent_thread(cx) { views.push(thread_view); } @@ -2125,11 +2129,11 @@ impl AgentPanel { } fn retain_running_thread(&mut self, old_view: ActiveView, cx: &mut Context) { - let ActiveView::AgentThread { server_view } = old_view else { + let ActiveView::AgentThread { conversation_view } = old_view else { return; }; - let Some(thread_view) = server_view.read(cx).parent_thread(cx) else { + let Some(thread_view) = conversation_view.read(cx).parent_thread(cx) else { return; }; @@ -2143,14 +2147,15 @@ impl AgentPanel { return; } - self.background_threads.insert(session_id, server_view); + self.background_threads + .insert(session_id, conversation_view); } pub(crate) fn active_native_agent_thread(&self, cx: &App) -> Option> { match &self.active_view { - ActiveView::AgentThread { server_view, .. } => { - server_view.read(cx).as_native_thread(cx) - } + ActiveView::AgentThread { + conversation_view, .. + } => conversation_view.read(cx).as_native_thread(cx), _ => None, } } @@ -2206,24 +2211,26 @@ impl AgentPanel { // Re-subscribe whenever the ConnectionView changes, since the inner // ThreadView may have been replaced (e.g. navigating between threads). self._active_view_observation = match &self.active_view { - ActiveView::AgentThread { server_view } => { + ActiveView::AgentThread { conversation_view } => { self._thread_view_subscription = - Self::subscribe_to_active_thread_view(server_view, window, cx); - let focus_handle = server_view.focus_handle(cx); + Self::subscribe_to_active_thread_view(conversation_view, window, cx); + let focus_handle = conversation_view.focus_handle(cx); self._active_thread_focus_subscription = Some(cx.on_focus_in(&focus_handle, window, |_this, _window, cx| { cx.emit(AgentPanelEvent::ThreadFocused); cx.notify(); })); - Some( - cx.observe_in(server_view, window, |this, server_view, window, cx| { + Some(cx.observe_in( + conversation_view, + window, + |this, server_view, window, cx| { this._thread_view_subscription = Self::subscribe_to_active_thread_view(&server_view, window, cx); cx.emit(AgentPanelEvent::ActiveViewChanged); this.serialize(cx); cx.notify(); - }), - ) + }, + )) } _ => { self._thread_view_subscription = None; @@ -2347,7 +2354,7 @@ impl AgentPanel { } fn subscribe_to_active_thread_view( - server_view: &Entity, + server_view: &Entity, window: &mut Window, cx: &mut Context, ) -> Option { @@ -2521,13 +2528,18 @@ impl AgentPanel { window: &mut Window, cx: &mut Context, ) { - if let Some(server_view) = self.background_threads.remove(&session_id) { - self.set_active_view(ActiveView::AgentThread { server_view }, focus, window, cx); + if let Some(conversation_view) = self.background_threads.remove(&session_id) { + self.set_active_view( + ActiveView::AgentThread { conversation_view }, + focus, + window, + cx, + ); return; } - if let ActiveView::AgentThread { server_view } = &self.active_view { - if server_view + if let ActiveView::AgentThread { conversation_view } = &self.active_view { + if conversation_view .read(cx) .active_thread() .map(|t| t.read(cx).id.clone()) @@ -2538,8 +2550,8 @@ impl AgentPanel { } } - if let Some(ActiveView::AgentThread { server_view }) = &self.previous_view { - if server_view + if let Some(ActiveView::AgentThread { conversation_view }) = &self.previous_view { + if conversation_view .read(cx) .active_thread() .map(|t| t.read(cx).id.clone()) @@ -2590,8 +2602,8 @@ impl AgentPanel { let connection_store = self.connection_store.clone(); - let server_view = cx.new(|cx| { - crate::ConnectionView::new( + let conversation_view = cx.new(|cx| { + crate::ConversationView::new( server, connection_store, ext_agent, @@ -2608,9 +2620,9 @@ impl AgentPanel { ) }); - cx.observe(&server_view, |this, server_view, cx| { + cx.observe(&conversation_view, |this, server_view, cx| { let is_active = this - .as_active_server_view() + .active_conversation_view() .is_some_and(|active| active.entity_id() == server_view.entity_id()); if is_active { cx.emit(AgentPanelEvent::ActiveViewChanged); @@ -2622,7 +2634,12 @@ impl AgentPanel { }) .detach(); - self.set_active_view(ActiveView::AgentThread { server_view }, focus, window, cx); + self.set_active_view( + ActiveView::AgentThread { conversation_view }, + focus, + window, + cx, + ); } fn active_thread_has_messages(&self, cx: &App) -> bool { @@ -3140,7 +3157,9 @@ impl Focusable for AgentPanel { fn focus_handle(&self, cx: &App) -> FocusHandle { match &self.active_view { ActiveView::Uninitialized => self.focus_handle.clone(), - ActiveView::AgentThread { server_view, .. } => server_view.focus_handle(cx), + ActiveView::AgentThread { + conversation_view, .. + } => conversation_view.focus_handle(cx), ActiveView::History { history: kind } => match kind { History::AgentThreads { view } => view.read(cx).focus_handle(cx), History::TextThreads => self.text_thread_history.focus_handle(cx), @@ -3269,8 +3288,8 @@ impl AgentPanel { const LOADING_SUMMARY_PLACEHOLDER: &str = "Loading Summary…"; let content = match &self.active_view { - ActiveView::AgentThread { server_view } => { - let server_view_ref = server_view.read(cx); + ActiveView::AgentThread { conversation_view } => { + let server_view_ref = conversation_view.read(cx); let is_generating_title = server_view_ref.as_native_thread(cx).is_some() && server_view_ref.parent_thread(cx).map_or(false, |tv| { tv.read(cx).thread.read(cx).has_provisional_title() @@ -3296,18 +3315,18 @@ impl AgentPanel { div() .w_full() .on_action({ - let thread_view = server_view.downgrade(); + let conversation_view = conversation_view.downgrade(); move |_: &menu::Confirm, window, cx| { - if let Some(thread_view) = thread_view.upgrade() { - thread_view.focus_handle(cx).focus(window, cx); + if let Some(conversation_view) = conversation_view.upgrade() { + conversation_view.focus_handle(cx).focus(window, cx); } } }) .on_action({ - let thread_view = server_view.downgrade(); + let conversation_view = conversation_view.downgrade(); move |_: &editor::actions::Cancel, window, cx| { - if let Some(thread_view) = thread_view.upgrade() { - thread_view.focus_handle(cx).focus(window, cx); + if let Some(conversation_view) = conversation_view.upgrade() { + conversation_view.focus_handle(cx).focus(window, cx); } } }) @@ -3315,7 +3334,7 @@ impl AgentPanel { .into_any_element() } } else { - Label::new(server_view.read(cx).title(cx)) + Label::new(conversation_view.read(cx).title(cx)) .color(Color::Muted) .truncate() .into_any_element() @@ -3400,9 +3419,9 @@ impl AgentPanel { .into_any() } - 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) { + fn handle_regenerate_thread_title(conversation_view: Entity, cx: &mut App) { + conversation_view.update(cx, |conversation_view, cx| { + if let Some(thread) = conversation_view.as_native_thread(cx) { thread.update(cx, |thread, cx| { thread.generate_title(cx); }); @@ -3450,18 +3469,20 @@ impl AgentPanel { _ => false, }; - let thread_view = match &self.active_view { - ActiveView::AgentThread { server_view } => Some(server_view.clone()), + let conversation_view = match &self.active_view { + ActiveView::AgentThread { conversation_view } => Some(conversation_view.clone()), _ => None, }; let thread_with_messages = match &self.active_view { - ActiveView::AgentThread { server_view } => { - server_view.read(cx).has_user_submitted_prompt(cx) + ActiveView::AgentThread { conversation_view } => { + conversation_view.read(cx).has_user_submitted_prompt(cx) } _ => false, }; let has_auth_methods = match &self.active_view { - ActiveView::AgentThread { server_view } => server_view.read(cx).has_auth_methods(), + ActiveView::AgentThread { conversation_view } => { + conversation_view.read(cx).has_auth_methods() + } _ => false, }; @@ -3505,13 +3526,13 @@ impl AgentPanel { .separator(); } - if let Some(thread_view) = thread_view.as_ref() { + if let Some(conversation_view) = conversation_view.as_ref() { menu = menu .entry("Regenerate Thread Title", None, { - let thread_view = thread_view.clone(); + let conversation_view = conversation_view.clone(); move |_, cx| { Self::handle_regenerate_thread_title( - thread_view.clone(), + conversation_view.clone(), cx, ); } @@ -3888,7 +3909,9 @@ impl AgentPanel { }; let active_thread = match &self.active_view { - ActiveView::AgentThread { server_view } => server_view.read(cx).as_native_thread(cx), + ActiveView::AgentThread { conversation_view } => { + conversation_view.read(cx).as_native_thread(cx) + } ActiveView::Uninitialized | ActiveView::TextThread { .. } | ActiveView::History { .. } @@ -4208,7 +4231,7 @@ impl AgentPanel { }; let is_thread_loading = self - .active_connection_view() + .active_conversation() .map(|thread| thread.read(cx).is_loading()) .unwrap_or(false); @@ -4533,13 +4556,11 @@ impl AgentPanel { ActiveView::Uninitialized | ActiveView::History { .. } | ActiveView::Configuration => { false } - ActiveView::AgentThread { server_view, .. } - if server_view.read(cx).as_native_thread(cx).is_none() => - { - false - } - ActiveView::AgentThread { server_view } => { - let history_is_empty = server_view + ActiveView::AgentThread { + conversation_view, .. + } if conversation_view.read(cx).as_native_thread(cx).is_none() => false, + ActiveView::AgentThread { conversation_view } => { + let history_is_empty = conversation_view .read(cx) .history() .is_none_or(|h| h.read(cx).is_empty()); @@ -4804,9 +4825,9 @@ impl AgentPanel { cx: &mut Context, ) { match &self.active_view { - ActiveView::AgentThread { server_view } => { - server_view.update(cx, |thread_view, cx| { - thread_view.insert_dragged_files(paths, added_worktrees, window, cx); + ActiveView::AgentThread { conversation_view } => { + conversation_view.update(cx, |conversation_view, cx| { + conversation_view.insert_dragged_files(paths, added_worktrees, window, cx); }); } ActiveView::TextThread { @@ -4905,8 +4926,10 @@ impl Render for AgentPanel { .on_action(cx.listener(Self::reset_font_size)) .on_action(cx.listener(Self::toggle_zoom)) .on_action(cx.listener(|this, _: &ReauthenticateAgent, window, cx| { - if let Some(thread_view) = this.active_connection_view() { - thread_view.update(cx, |thread_view, cx| thread_view.reauthenticate(window, cx)) + if let Some(conversation_view) = this.active_conversation() { + conversation_view.update(cx, |conversation_view, cx| { + conversation_view.reauthenticate(window, cx) + }) } })) .child(self.render_toolbar(window, cx)) @@ -4924,8 +4947,10 @@ impl Render for AgentPanel { match &self.active_view { ActiveView::Uninitialized => parent, - ActiveView::AgentThread { server_view, .. } => parent - .child(server_view.clone()) + ActiveView::AgentThread { + conversation_view, .. + } => parent + .child(conversation_view.clone()) .child(self.render_drag_target(cx)), ActiveView::History { history: kind } => match kind { History::AgentThreads { view } => parent.child(view.clone()), @@ -5130,9 +5155,9 @@ impl AgentPanelDelegate for ConcreteAssistantPanelDelegate { // Wait to create a new context until the workspace is no longer // being updated. cx.defer_in(window, move |panel, window, cx| { - if let Some(thread_view) = panel.active_connection_view() { - thread_view.update(cx, |thread_view, cx| { - thread_view.insert_selections(window, cx); + if let Some(conversation_view) = panel.active_conversation() { + conversation_view.update(cx, |conversation_view, cx| { + conversation_view.insert_selections(window, cx); }); } else if let Some(text_thread_editor) = panel.active_text_thread_editor() { let snapshot = buffer.read(cx).snapshot(cx); @@ -5168,9 +5193,9 @@ impl AgentPanelDelegate for ConcreteAssistantPanelDelegate { // Wait to create a new context until the workspace is no longer // being updated. cx.defer_in(window, move |panel, window, cx| { - if let Some(thread_view) = panel.active_connection_view() { - thread_view.update(cx, |thread_view, cx| { - thread_view.insert_terminal_text(text, window, cx); + if let Some(conversation_view) = panel.active_conversation() { + conversation_view.update(cx, |conversation_view, cx| { + conversation_view.insert_terminal_text(text, window, cx); }); } else if let Some(text_thread_editor) = panel.active_text_thread_editor() { text_thread_editor.update(cx, |text_thread_editor, cx| { @@ -5233,8 +5258,8 @@ 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> { - self.active_connection_view() + pub fn active_thread_view_for_tests(&self) -> Option<&Entity> { + self.active_conversation() } /// Sets the start_thread_in value directly, bypassing validation. @@ -5296,7 +5321,7 @@ impl AgentPanel { #[cfg(test)] mod tests { use super::*; - use crate::connection_view::tests::{StubAgentServer, init_test}; + use crate::conversation_view::tests::{StubAgentServer, init_test}; use crate::test_support::{active_session_id, open_thread_with_connection, send_message}; use acp_thread::{StubAgentConnection, ThreadStatus}; use assistant_text_thread::TextThreadStore; @@ -5419,7 +5444,7 @@ mod tests { "workspace A agent type should be restored" ); assert!( - panel.active_connection_view().is_some(), + panel.active_conversation().is_some(), "workspace A should have its active thread restored" ); }); @@ -5439,7 +5464,7 @@ mod tests { "workspace B agent type should be restored" ); assert!( - panel.active_connection_view().is_none(), + panel.active_conversation().is_none(), "workspace B should have no active thread" ); }); @@ -5856,7 +5881,7 @@ mod tests { send_message(&panel, &mut cx); let weak_view_a = panel.read_with(&cx, |panel, _cx| { - panel.active_connection_view().unwrap().downgrade() + panel.active_conversation().unwrap().downgrade() }); // Thread A should be idle (auto-completed via set_next_prompt_updates). diff --git a/crates/agent_ui/src/agent_ui.rs b/crates/agent_ui/src/agent_ui.rs index e0ab9a707b7e7ab5fed6a0a27d4c253c08445dfa..dde70f15e8084144d9beb1d4fb9563cf12fb942e 100644 --- a/crates/agent_ui/src/agent_ui.rs +++ b/crates/agent_ui/src/agent_ui.rs @@ -8,9 +8,9 @@ mod branch_names; mod buffer_codegen; mod completion_provider; mod config_options; -pub(crate) mod connection_view; mod context; mod context_server_configuration; +pub(crate) mod conversation_view; mod entry_view_state; mod external_source_prompt; mod favorite_models; @@ -71,7 +71,7 @@ pub use crate::agent_panel::{ use crate::agent_registry_ui::AgentRegistryPage; pub use crate::inline_assistant::InlineAssistant; pub use agent_diff::{AgentDiffPane, AgentDiffToolbar}; -pub(crate) use connection_view::ConnectionView; +pub(crate) use conversation_view::ConversationView; pub use external_source_prompt::ExternalSourcePrompt; pub(crate) use mode_selector::ModeSelector; pub(crate) use model_selector::ModelSelector; diff --git a/crates/agent_ui/src/connection_view.rs b/crates/agent_ui/src/conversation_view.rs similarity index 92% rename from crates/agent_ui/src/connection_view.rs rename to crates/agent_ui/src/conversation_view.rs index 42d4fe4e4ef6b69e53d951e0007e564d5cc614a4..c9a6c7334d22cc3159c4fc6dde02fe57c8676ae9 100644 --- a/crates/agent_ui/src/connection_view.rs +++ b/crates/agent_ui/src/conversation_view.rs @@ -306,9 +306,9 @@ pub enum AcpServerViewEvent { ActiveThreadChanged, } -impl EventEmitter for ConnectionView {} +impl EventEmitter for ConversationView {} -pub struct ConnectionView { +pub struct ConversationView { agent: Rc, connection_store: Entity, connection_key: Agent, @@ -325,7 +325,7 @@ pub struct ConnectionView { _subscriptions: Vec, } -impl ConnectionView { +impl ConversationView { pub fn has_auth_methods(&self) -> bool { self.as_connected().map_or(false, |connected| { !connected.connection.auth_methods().is_empty() @@ -478,7 +478,7 @@ impl ConnectedServerState { } } -impl ConnectionView { +impl ConversationView { pub fn new( agent: Rc, connection_store: Entity, @@ -2313,7 +2313,7 @@ impl ConnectionView { fn render_markdown(&self, markdown: Entity, style: MarkdownStyle) -> MarkdownElement { let workspace = self.workspace.clone(); MarkdownElement::new(markdown, style).on_url_click(move |text, window, cx| { - crate::connection_view::thread_view::open_link(text, &workspace, window, cx); + crate::conversation_view::thread_view::open_link(text, &workspace, window, cx); }) } @@ -2643,7 +2643,7 @@ fn placeholder_text(agent_name: &str, has_commands: bool) -> String { } } -impl Focusable for ConnectionView { +impl Focusable for ConversationView { fn focus_handle(&self, cx: &App) -> FocusHandle { match self.active_thread() { Some(thread) => thread.read(cx).focus_handle(cx), @@ -2653,7 +2653,7 @@ impl Focusable for ConnectionView { } #[cfg(any(test, feature = "test-support"))] -impl ConnectionView { +impl ConversationView { /// 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) { @@ -2666,7 +2666,7 @@ impl ConnectionView { } } -impl Render for ConnectionView { +impl Render for ConversationView { fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { self.sync_queued_message_editors(window, cx); let v2_flag = cx.has_flag::(); @@ -2786,9 +2786,10 @@ pub(crate) mod tests { async fn test_drop(cx: &mut TestAppContext) { init_test(cx); - let (thread_view, _cx) = setup_thread_view(StubAgentServer::default_response(), cx).await; - let weak_view = thread_view.downgrade(); - drop(thread_view); + let (conversation_view, _cx) = + setup_conversation_view(StubAgentServer::default_response(), cx).await; + let weak_view = conversation_view.downgrade(); + drop(conversation_view); assert!(!weak_view.is_upgradable()); } @@ -2801,14 +2802,14 @@ pub(crate) mod tests { }; let initial_content = AgentInitialContent::FromExternalSource(prompt); - let (thread_view, cx) = setup_thread_view_with_initial_content( + let (conversation_view, cx) = setup_conversation_view_with_initial_content( StubAgentServer::default_response(), initial_content, cx, ) .await; - active_thread(&thread_view, cx).read_with(cx, |view, cx| { + active_thread(&conversation_view, cx).read_with(cx, |view, cx| { assert!(view.show_external_source_prompt_warning); assert_eq!(view.thread.read(cx).entries().len(), 0); assert_eq!(view.message_editor.read(cx).text(cx), "Write me a script"); @@ -2824,17 +2825,18 @@ pub(crate) mod tests { }; let initial_content = AgentInitialContent::FromExternalSource(prompt); - let (thread_view, cx) = setup_thread_view_with_initial_content( + let (conversation_view, cx) = setup_conversation_view_with_initial_content( StubAgentServer::default_response(), initial_content, cx, ) .await; - active_thread(&thread_view, cx).update_in(cx, |view, window, cx| view.send(window, cx)); + active_thread(&conversation_view, cx) + .update_in(cx, |view, window, cx| view.send(window, cx)); cx.run_until_parked(); - active_thread(&thread_view, cx).read_with(cx, |view, cx| { + active_thread(&conversation_view, cx).read_with(cx, |view, cx| { assert!(!view.show_external_source_prompt_warning); assert_eq!(view.message_editor.read(cx).text(cx), ""); assert_eq!(view.thread.read(cx).entries().len(), 2); @@ -2845,16 +2847,18 @@ pub(crate) mod tests { async fn test_notification_for_stop_event(cx: &mut TestAppContext) { init_test(cx); - let (thread_view, cx) = setup_thread_view(StubAgentServer::default_response(), cx).await; + let (conversation_view, cx) = + setup_conversation_view(StubAgentServer::default_response(), cx).await; - let message_editor = message_editor(&thread_view, cx); + let message_editor = message_editor(&conversation_view, cx); message_editor.update_in(cx, |editor, window, cx| { editor.set_text("Hello", window, cx); }); cx.deactivate_window(); - active_thread(&thread_view, cx).update_in(cx, |view, window, cx| view.send(window, cx)); + active_thread(&conversation_view, cx) + .update_in(cx, |view, window, cx| view.send(window, cx)); cx.run_until_parked(); @@ -2869,17 +2873,18 @@ pub(crate) mod tests { async fn test_notification_for_error(cx: &mut TestAppContext) { init_test(cx); - let (thread_view, cx) = - setup_thread_view(StubAgentServer::new(SaboteurAgentConnection), cx).await; + let (conversation_view, cx) = + setup_conversation_view(StubAgentServer::new(SaboteurAgentConnection), cx).await; - let message_editor = message_editor(&thread_view, cx); + let message_editor = message_editor(&conversation_view, cx); message_editor.update_in(cx, |editor, window, cx| { editor.set_text("Hello", window, cx); }); cx.deactivate_window(); - active_thread(&thread_view, cx).update_in(cx, |view, window, cx| view.send(window, cx)); + active_thread(&conversation_view, cx) + .update_in(cx, |view, window, cx| view.send(window, cx)); cx.run_until_parked(); @@ -2907,9 +2912,9 @@ pub(crate) mod tests { let connection_store = cx.update(|_window, cx| cx.new(|cx| AgentConnectionStore::new(project.clone(), cx))); - let thread_view = cx.update(|window, cx| { + let conversation_view = cx.update(|window, cx| { cx.new(|cx| { - ConnectionView::new( + ConversationView::new( Rc::new(StubAgentServer::default_response()), connection_store, Agent::Custom { id: "Test".into() }, @@ -2931,7 +2936,7 @@ pub(crate) mod tests { cx.run_until_parked(); let history = cx.update(|_window, cx| { - thread_view + conversation_view .read(cx) .history() .expect("Missing history") @@ -2939,7 +2944,7 @@ pub(crate) mod tests { }); // Initially empty because StubAgentConnection.session_list() returns None - active_thread(&thread_view, cx).read_with(cx, |view, _cx| { + active_thread(&conversation_view, cx).read_with(cx, |view, _cx| { assert_eq!(view.recent_history_entries.len(), 0); }); @@ -2951,7 +2956,7 @@ pub(crate) mod tests { }); cx.run_until_parked(); - active_thread(&thread_view, cx).read_with(cx, |view, _cx| { + active_thread(&conversation_view, cx).read_with(cx, |view, _cx| { assert_eq!(view.recent_history_entries.len(), 1); assert_eq!( view.recent_history_entries[0].session_id, @@ -2967,7 +2972,7 @@ pub(crate) mod tests { }); cx.run_until_parked(); - active_thread(&thread_view, cx).read_with(cx, |view, _cx| { + active_thread(&conversation_view, cx).read_with(cx, |view, _cx| { assert_eq!(view.recent_history_entries.len(), 1); assert_eq!( view.recent_history_entries[0].session_id, @@ -2981,7 +2986,7 @@ pub(crate) mod tests { init_test(cx); let session = AgentSessionInfo::new(SessionId::new("history-session")); - let (thread_view, history, cx) = setup_thread_view_with_history( + let (conversation_view, history, cx) = setup_thread_view_with_history( StubAgentServer::new(SessionHistoryConnection::new(vec![session.clone()])), cx, ) @@ -2994,7 +2999,7 @@ pub(crate) mod tests { ); }); - active_thread(&thread_view, cx).read_with(cx, |view, _cx| { + active_thread(&conversation_view, cx).read_with(cx, |view, _cx| { assert_eq!(view.recent_history_entries.len(), 1); assert_eq!( view.recent_history_entries[0].session_id, @@ -3017,9 +3022,9 @@ pub(crate) mod tests { let connection_store = cx.update(|_window, cx| cx.new(|cx| AgentConnectionStore::new(project.clone(), cx))); - let thread_view = cx.update(|window, cx| { + let conversation_view = cx.update(|window, cx| { cx.new(|cx| { - ConnectionView::new( + ConversationView::new( Rc::new(StubAgentServer::new(ResumeOnlyAgentConnection)), connection_store, Agent::Custom { id: "Test".into() }, @@ -3039,7 +3044,7 @@ pub(crate) mod tests { cx.run_until_parked(); - thread_view.read_with(cx, |view, cx| { + conversation_view.read_with(cx, |view, cx| { let state = view.active_thread().unwrap(); assert!(state.read(cx).resumed_without_history); assert_eq!(state.read(cx).list_state.item_count(), 0); @@ -3072,9 +3077,9 @@ pub(crate) mod tests { let connection_store = cx.update(|_window, cx| cx.new(|cx| AgentConnectionStore::new(project.clone(), cx))); - let _thread_view = cx.update(|window, cx| { + let _conversation_view = cx.update(|window, cx| { cx.new(|cx| { - ConnectionView::new( + ConversationView::new( Rc::new(StubAgentServer::new(connection)), connection_store, Agent::Custom { id: "Test".into() }, @@ -3105,20 +3110,21 @@ pub(crate) mod tests { async fn test_refusal_handling(cx: &mut TestAppContext) { init_test(cx); - let (thread_view, cx) = - setup_thread_view(StubAgentServer::new(RefusalAgentConnection), cx).await; + let (conversation_view, cx) = + setup_conversation_view(StubAgentServer::new(RefusalAgentConnection), cx).await; - let message_editor = message_editor(&thread_view, cx); + let message_editor = message_editor(&conversation_view, cx); message_editor.update_in(cx, |editor, window, cx| { editor.set_text("Do something harmful", window, cx); }); - active_thread(&thread_view, cx).update_in(cx, |view, window, cx| view.send(window, cx)); + active_thread(&conversation_view, cx) + .update_in(cx, |view, window, cx| view.send(window, cx)); cx.run_until_parked(); // Check that the refusal error is set - thread_view.read_with(cx, |thread_view, cx| { + conversation_view.read_with(cx, |thread_view, cx| { let state = thread_view.active_thread().unwrap(); assert!( matches!(state.read(cx).thread_error, Some(ThreadError::Refusal)), @@ -3131,9 +3137,9 @@ pub(crate) mod tests { async fn test_connect_failure_transitions_to_load_error(cx: &mut TestAppContext) { init_test(cx); - let (thread_view, cx) = setup_thread_view(FailingAgentServer, cx).await; + let (conversation_view, cx) = setup_conversation_view(FailingAgentServer, cx).await; - thread_view.read_with(cx, |view, cx| { + conversation_view.read_with(cx, |view, cx| { let title = view.title(cx); assert_eq!( title.as_ref(), @@ -3167,11 +3173,12 @@ pub(crate) mod tests { init_test(cx); let connection = AuthGatedAgentConnection::new(); - let (thread_view, cx) = setup_thread_view(StubAgentServer::new(connection), cx).await; + let (conversation_view, cx) = + setup_conversation_view(StubAgentServer::new(connection), cx).await; // When new_session returns AuthRequired, the server should transition // to Connected + Unauthenticated rather than getting stuck in Loading. - thread_view.read_with(cx, |view, _cx| { + conversation_view.read_with(cx, |view, _cx| { let connected = view .as_connected() .expect("Should be in Connected state even though auth is required"); @@ -3189,7 +3196,7 @@ pub(crate) mod tests { ); }); - thread_view.read_with(cx, |view, _cx| { + conversation_view.read_with(cx, |view, _cx| { assert!( view.active_thread().is_none(), "active_thread() should be None when unauthenticated without a session" @@ -3199,7 +3206,7 @@ pub(crate) mod tests { // Authenticate using the real authenticate flow on ConnectionView. // This calls connection.authenticate(), which flips the internal flag, // then on success triggers reset() -> new_session() which now succeeds. - thread_view.update_in(cx, |view, window, cx| { + conversation_view.update_in(cx, |view, window, cx| { view.authenticate( acp::AuthMethodId::new(AuthGatedAgentConnection::AUTH_METHOD_ID), window, @@ -3209,7 +3216,7 @@ pub(crate) mod tests { cx.run_until_parked(); // After auth, the server should have an active thread in the Ok state. - thread_view.read_with(cx, |view, cx| { + conversation_view.read_with(cx, |view, cx| { let connected = view .as_connected() .expect("Should still be in Connected state after auth"); @@ -3254,16 +3261,18 @@ pub(crate) mod tests { connection.set_next_prompt_updates(vec![acp::SessionUpdate::ToolCall(tool_call)]); - let (thread_view, cx) = setup_thread_view(StubAgentServer::new(connection), cx).await; + let (conversation_view, cx) = + setup_conversation_view(StubAgentServer::new(connection), cx).await; - let message_editor = message_editor(&thread_view, cx); + let message_editor = message_editor(&conversation_view, cx); message_editor.update_in(cx, |editor, window, cx| { editor.set_text("Hello", window, cx); }); cx.deactivate_window(); - active_thread(&thread_view, cx).update_in(cx, |view, window, cx| view.send(window, cx)); + active_thread(&conversation_view, cx) + .update_in(cx, |view, window, cx| view.send(window, cx)); cx.run_until_parked(); @@ -3278,11 +3287,12 @@ pub(crate) mod tests { async fn test_notification_when_panel_hidden(cx: &mut TestAppContext) { init_test(cx); - let (thread_view, cx) = setup_thread_view(StubAgentServer::default_response(), cx).await; + let (conversation_view, cx) = + setup_conversation_view(StubAgentServer::default_response(), cx).await; - add_to_workspace(thread_view.clone(), cx); + add_to_workspace(conversation_view.clone(), cx); - let message_editor = message_editor(&thread_view, cx); + let message_editor = message_editor(&conversation_view, cx); message_editor.update_in(cx, |editor, window, cx| { editor.set_text("Hello", window, cx); @@ -3292,7 +3302,8 @@ pub(crate) mod tests { // Note: In the test environment, the panel is not actually added to the dock, // so is_agent_panel_hidden will return true - active_thread(&thread_view, cx).update_in(cx, |view, window, cx| view.send(window, cx)); + active_thread(&conversation_view, cx) + .update_in(cx, |view, window, cx| view.send(window, cx)); cx.run_until_parked(); @@ -3309,9 +3320,10 @@ pub(crate) mod tests { async fn test_notification_still_works_when_window_inactive(cx: &mut TestAppContext) { init_test(cx); - let (thread_view, cx) = setup_thread_view(StubAgentServer::default_response(), cx).await; + let (conversation_view, cx) = + setup_conversation_view(StubAgentServer::default_response(), cx).await; - let message_editor = message_editor(&thread_view, cx); + let message_editor = message_editor(&conversation_view, cx); message_editor.update_in(cx, |editor, window, cx| { editor.set_text("Hello", window, cx); }); @@ -3319,7 +3331,8 @@ pub(crate) mod tests { // Deactivate window - should show notification regardless of setting cx.deactivate_window(); - active_thread(&thread_view, cx).update_in(cx, |view, window, cx| view.send(window, cx)); + active_thread(&conversation_view, cx) + .update_in(cx, |view, window, cx| view.send(window, cx)); cx.run_until_parked(); @@ -3387,9 +3400,9 @@ pub(crate) mod tests { cx.update(|_window, cx| cx.new(|cx| AgentConnectionStore::new(project1.clone(), cx))); let agent = StubAgentServer::default_response(); - let thread_view = cx.update(|window, cx| { + let conversation_view = cx.update(|window, cx| { cx.new(|cx| { - ConnectionView::new( + ConversationView::new( Rc::new(agent), connection_store, Agent::Custom { id: "Test".into() }, @@ -3408,7 +3421,7 @@ pub(crate) mod tests { }); cx.run_until_parked(); - let message_editor = message_editor(&thread_view, cx); + let message_editor = message_editor(&conversation_view, cx); message_editor.update_in(cx, |editor, window, cx| { editor.set_text("Hello", window, cx); }); @@ -3435,7 +3448,8 @@ pub(crate) mod tests { // Window is active, agent panel is visible in workspace1, but workspace1 // is in the background. The notification should show because the user // can't actually see the agent panel. - active_thread(&thread_view, cx).update_in(cx, |view, window, cx| view.send(window, cx)); + active_thread(&conversation_view, cx) + .update_in(cx, |view, window, cx| view.send(window, cx)); cx.run_until_parked(); @@ -3482,16 +3496,18 @@ pub(crate) mod tests { ); }); - let (thread_view, cx) = setup_thread_view(StubAgentServer::default_response(), cx).await; + let (conversation_view, cx) = + setup_conversation_view(StubAgentServer::default_response(), cx).await; - let message_editor = message_editor(&thread_view, cx); + let message_editor = message_editor(&conversation_view, cx); message_editor.update_in(cx, |editor, window, cx| { editor.set_text("Hello", window, cx); }); // Window is active - active_thread(&thread_view, cx).update_in(cx, |view, window, cx| view.send(window, cx)); + active_thread(&conversation_view, cx) + .update_in(cx, |view, window, cx| view.send(window, cx)); cx.run_until_parked(); @@ -3508,18 +3524,20 @@ pub(crate) mod tests { async fn test_notification_closed_when_thread_view_dropped(cx: &mut TestAppContext) { init_test(cx); - let (thread_view, cx) = setup_thread_view(StubAgentServer::default_response(), cx).await; + let (conversation_view, cx) = + setup_conversation_view(StubAgentServer::default_response(), cx).await; - let weak_view = thread_view.downgrade(); + let weak_view = conversation_view.downgrade(); - let message_editor = message_editor(&thread_view, cx); + let message_editor = message_editor(&conversation_view, cx); message_editor.update_in(cx, |editor, window, cx| { editor.set_text("Hello", window, cx); }); cx.deactivate_window(); - active_thread(&thread_view, cx).update_in(cx, |view, window, cx| view.send(window, cx)); + active_thread(&conversation_view, cx) + .update_in(cx, |view, window, cx| view.send(window, cx)); cx.run_until_parked(); @@ -3532,7 +3550,7 @@ pub(crate) mod tests { ); // Drop the thread view (simulating navigation to a new thread) - drop(thread_view); + drop(conversation_view); drop(message_editor); // Trigger an update to flush effects, which will call release_dropped_entities cx.update(|_window, _cx| {}); @@ -3553,45 +3571,49 @@ pub(crate) mod tests { ); } - async fn setup_thread_view( + async fn setup_conversation_view( agent: impl AgentServer + 'static, cx: &mut TestAppContext, - ) -> (Entity, &mut VisualTestContext) { - let (thread_view, _history, cx) = - setup_thread_view_with_history_and_initial_content(agent, None, cx).await; - (thread_view, cx) + ) -> (Entity, &mut VisualTestContext) { + let (conversation_view, _history, cx) = + setup_conversation_view_with_history_and_initial_content(agent, None, cx).await; + (conversation_view, cx) } async fn setup_thread_view_with_history( agent: impl AgentServer + 'static, cx: &mut TestAppContext, ) -> ( - Entity, + Entity, Entity, &mut VisualTestContext, ) { - let (thread_view, history, cx) = - setup_thread_view_with_history_and_initial_content(agent, None, cx).await; - (thread_view, history.expect("Missing history"), cx) + let (conversation_view, history, cx) = + setup_conversation_view_with_history_and_initial_content(agent, None, cx).await; + (conversation_view, history.expect("Missing history"), cx) } - async fn setup_thread_view_with_initial_content( + async fn setup_conversation_view_with_initial_content( agent: impl AgentServer + 'static, initial_content: AgentInitialContent, cx: &mut TestAppContext, - ) -> (Entity, &mut VisualTestContext) { - let (thread_view, _history, cx) = - setup_thread_view_with_history_and_initial_content(agent, Some(initial_content), cx) - .await; - (thread_view, cx) + ) -> (Entity, &mut VisualTestContext) { + let (conversation_view, _history, cx) = + setup_conversation_view_with_history_and_initial_content( + agent, + Some(initial_content), + cx, + ) + .await; + (conversation_view, cx) } - async fn setup_thread_view_with_history_and_initial_content( + async fn setup_conversation_view_with_history_and_initial_content( agent: impl AgentServer + 'static, initial_content: Option, cx: &mut TestAppContext, ) -> ( - Entity, + Entity, Option>, &mut VisualTestContext, ) { @@ -3607,9 +3629,9 @@ pub(crate) mod tests { let agent_key = Agent::Custom { id: "Test".into() }; - let thread_view = cx.update(|window, cx| { + let conversation_view = cx.update(|window, cx| { cx.new(|cx| { - ConnectionView::new( + ConversationView::new( Rc::new(agent), connection_store.clone(), agent_key.clone(), @@ -3635,16 +3657,17 @@ pub(crate) mod tests { .and_then(|e| e.read(cx).history().cloned()) }); - (thread_view, history, cx) + (conversation_view, history, cx) } - fn add_to_workspace(thread_view: Entity, cx: &mut VisualTestContext) { - let workspace = thread_view.read_with(cx, |thread_view, _cx| thread_view.workspace.clone()); + fn add_to_workspace(conversation_view: Entity, cx: &mut VisualTestContext) { + let workspace = + conversation_view.read_with(cx, |thread_view, _cx| thread_view.workspace.clone()); workspace .update_in(cx, |workspace, window, cx| { workspace.add_item_to_active_pane( - Box::new(cx.new(|_| ThreadViewItem(thread_view.clone()))), + Box::new(cx.new(|_| ThreadViewItem(conversation_view.clone()))), None, true, window, @@ -3654,7 +3677,7 @@ pub(crate) mod tests { .unwrap(); } - struct ThreadViewItem(Entity); + struct ThreadViewItem(Entity); impl Item for ThreadViewItem { type Event = (); @@ -4330,11 +4353,11 @@ pub(crate) mod tests { } fn active_thread( - thread_view: &Entity, + conversation_view: &Entity, cx: &TestAppContext, ) -> Entity { cx.read(|cx| { - thread_view + conversation_view .read(cx) .active_thread() .expect("No active thread") @@ -4343,10 +4366,10 @@ pub(crate) mod tests { } fn message_editor( - thread_view: &Entity, + conversation_view: &Entity, cx: &TestAppContext, ) -> Entity { - let thread = active_thread(thread_view, cx); + let thread = active_thread(conversation_view, cx); cx.read(|cx| thread.read(cx).message_editor.clone()) } @@ -4373,9 +4396,9 @@ pub(crate) mod tests { cx.update(|_window, cx| cx.new(|cx| AgentConnectionStore::new(project.clone(), cx))); let connection = Rc::new(StubAgentConnection::new()); - let thread_view = cx.update(|window, cx| { + let conversation_view = cx.update(|window, cx| { cx.new(|cx| { - ConnectionView::new( + ConversationView::new( Rc::new(StubAgentServer::new(connection.as_ref().clone())), connection_store, Agent::Custom { id: "Test".into() }, @@ -4395,7 +4418,7 @@ pub(crate) mod tests { cx.run_until_parked(); - let thread = thread_view + let thread = conversation_view .read_with(cx, |view, cx| { view.active_thread().map(|r| r.read(cx).thread.clone()) }) @@ -4421,7 +4444,7 @@ pub(crate) mod tests { assert_eq!(thread.entries().len(), 2); }); - thread_view.read_with(cx, |view, cx| { + conversation_view.read_with(cx, |view, cx| { let entry_view_state = view .active_thread() .map(|active| active.read(cx).entry_view_state.clone()) @@ -4462,7 +4485,7 @@ pub(crate) mod tests { user_message.id.clone().unwrap() }); - thread_view.read_with(cx, |view, cx| { + conversation_view.read_with(cx, |view, cx| { let entry_view_state = view .active_thread() .unwrap() @@ -4501,7 +4524,7 @@ pub(crate) mod tests { assert_eq!(thread.entries().len(), 2); }); - thread_view.read_with(cx, |view, cx| { + conversation_view.read_with(cx, |view, cx| { let active = view.active_thread().unwrap(); active .read(cx) @@ -4534,10 +4557,10 @@ pub(crate) mod tests { acp::ContentChunk::new("Response 1".into()), )]); - let (thread_view, cx) = - setup_thread_view(StubAgentServer::new(connection.clone()), cx).await; + let (conversation_view, cx) = + setup_conversation_view(StubAgentServer::new(connection.clone()), cx).await; - let thread = thread_view + let thread = conversation_view .read_with(cx, |view, cx| { view.active_thread().map(|r| r.read(cx).thread.clone()) }) @@ -4560,12 +4583,12 @@ pub(crate) mod tests { cx.run_until_parked(); // Move somewhere else first so we're not trivially already on the last user prompt. - active_thread(&thread_view, cx).update(cx, |view, cx| { + active_thread(&conversation_view, cx).update(cx, |view, cx| { view.scroll_to_top(cx); }); cx.run_until_parked(); - active_thread(&thread_view, cx).update(cx, |view, cx| { + active_thread(&conversation_view, cx).update(cx, |view, cx| { view.scroll_to_most_recent_user_prompt(cx); let scroll_top = view.list_state.logical_scroll_top(); // Entries layout is: [User1, Assistant1, User2, Assistant2] @@ -4579,10 +4602,11 @@ pub(crate) mod tests { ) { init_test(cx); - let (thread_view, cx) = setup_thread_view(StubAgentServer::default_response(), cx).await; + let (conversation_view, cx) = + setup_conversation_view(StubAgentServer::default_response(), cx).await; // With no entries, scrolling should be a no-op and must not panic. - active_thread(&thread_view, cx).update(cx, |view, cx| { + active_thread(&conversation_view, cx).update(cx, |view, cx| { view.scroll_to_most_recent_user_prompt(cx); let scroll_top = view.list_state.logical_scroll_top(); assert_eq!(scroll_top.item_ix, 0); @@ -4599,18 +4623,20 @@ pub(crate) mod tests { acp::ContentChunk::new("Response".into()), )]); - let (thread_view, cx) = setup_thread_view(StubAgentServer::new(connection), cx).await; - add_to_workspace(thread_view.clone(), cx); + let (conversation_view, cx) = + setup_conversation_view(StubAgentServer::new(connection), cx).await; + add_to_workspace(conversation_view.clone(), cx); - let message_editor = message_editor(&thread_view, cx); + let message_editor = message_editor(&conversation_view, cx); message_editor.update_in(cx, |editor, window, cx| { editor.set_text("Original message to edit", window, cx); }); - active_thread(&thread_view, cx).update_in(cx, |view, window, cx| view.send(window, cx)); + active_thread(&conversation_view, cx) + .update_in(cx, |view, window, cx| view.send(window, cx)); cx.run_until_parked(); - let user_message_editor = thread_view.read_with(cx, |view, cx| { + let user_message_editor = conversation_view.read_with(cx, |view, cx| { assert_eq!( view.active_thread() .and_then(|active| active.read(cx).editing_message), @@ -4631,7 +4657,7 @@ pub(crate) mod tests { // Focus cx.focus(&user_message_editor); - thread_view.read_with(cx, |view, cx| { + conversation_view.read_with(cx, |view, cx| { assert_eq!( view.active_thread() .and_then(|active| active.read(cx).editing_message), @@ -4649,7 +4675,7 @@ pub(crate) mod tests { window.dispatch_action(Box::new(editor::actions::Cancel), cx); }); - thread_view.read_with(cx, |view, cx| { + conversation_view.read_with(cx, |view, cx| { assert_eq!( view.active_thread() .and_then(|active| active.read(cx).editing_message), @@ -4668,16 +4694,17 @@ pub(crate) mod tests { let connection = StubAgentConnection::new(); - let (thread_view, cx) = setup_thread_view(StubAgentServer::new(connection), cx).await; - add_to_workspace(thread_view.clone(), cx); + let (conversation_view, cx) = + setup_conversation_view(StubAgentServer::new(connection), cx).await; + add_to_workspace(conversation_view.clone(), cx); - let message_editor = message_editor(&thread_view, cx); + let message_editor = message_editor(&conversation_view, cx); message_editor.update_in(cx, |editor, window, cx| { editor.set_text("", window, cx); }); let thread = cx.read(|cx| { - thread_view + conversation_view .read(cx) .active_thread() .unwrap() @@ -4687,7 +4714,7 @@ pub(crate) mod tests { }); let entries_before = cx.read(|cx| thread.read(cx).entries().len()); - active_thread(&thread_view, cx).update_in(cx, |view, window, cx| { + active_thread(&conversation_view, cx).update_in(cx, |view, window, cx| { view.send(window, cx); }); cx.run_until_parked(); @@ -4709,19 +4736,20 @@ pub(crate) mod tests { acp::ContentChunk::new("Response".into()), )]); - let (thread_view, cx) = - setup_thread_view(StubAgentServer::new(connection.clone()), cx).await; - add_to_workspace(thread_view.clone(), cx); + let (conversation_view, cx) = + setup_conversation_view(StubAgentServer::new(connection.clone()), cx).await; + add_to_workspace(conversation_view.clone(), cx); - let message_editor = message_editor(&thread_view, cx); + let message_editor = message_editor(&conversation_view, cx); message_editor.update_in(cx, |editor, window, cx| { editor.set_text("Original message to edit", window, cx); }); - active_thread(&thread_view, cx).update_in(cx, |view, window, cx| view.send(window, cx)); + active_thread(&conversation_view, cx) + .update_in(cx, |view, window, cx| view.send(window, cx)); cx.run_until_parked(); - let user_message_editor = thread_view.read_with(cx, |view, cx| { + let user_message_editor = conversation_view.read_with(cx, |view, cx| { assert_eq!( view.active_thread() .and_then(|active| active.read(cx).editing_message), @@ -4769,7 +4797,7 @@ pub(crate) mod tests { cx.run_until_parked(); - thread_view.read_with(cx, |view, cx| { + conversation_view.read_with(cx, |view, cx| { assert_eq!( view.active_thread() .and_then(|active| active.read(cx).editing_message), @@ -4812,19 +4840,20 @@ pub(crate) mod tests { let connection = StubAgentConnection::new(); - let (thread_view, cx) = - setup_thread_view(StubAgentServer::new(connection.clone()), cx).await; - add_to_workspace(thread_view.clone(), cx); + let (conversation_view, cx) = + setup_conversation_view(StubAgentServer::new(connection.clone()), cx).await; + add_to_workspace(conversation_view.clone(), cx); - let message_editor = message_editor(&thread_view, cx); + let message_editor = message_editor(&conversation_view, cx); message_editor.update_in(cx, |editor, window, cx| { editor.set_text("Original message to edit", window, cx); }); - active_thread(&thread_view, cx).update_in(cx, |view, window, cx| view.send(window, cx)); + active_thread(&conversation_view, cx) + .update_in(cx, |view, window, cx| view.send(window, cx)); cx.run_until_parked(); - let (user_message_editor, session_id) = thread_view.read_with(cx, |view, cx| { + let (user_message_editor, session_id) = conversation_view.read_with(cx, |view, cx| { let thread = view.active_thread().unwrap().read(cx).thread.read(cx); assert_eq!(thread.entries().len(), 1); @@ -4846,7 +4875,7 @@ pub(crate) mod tests { // Focus cx.focus(&user_message_editor); - thread_view.read_with(cx, |view, cx| { + conversation_view.read_with(cx, |view, cx| { assert_eq!( view.active_thread() .and_then(|active| active.read(cx).editing_message), @@ -4859,7 +4888,7 @@ pub(crate) mod tests { editor.set_text("Edited message content", window, cx); }); - thread_view.read_with(cx, |view, cx| { + conversation_view.read_with(cx, |view, cx| { assert_eq!( view.active_thread() .and_then(|active| active.read(cx).editing_message), @@ -4877,7 +4906,7 @@ pub(crate) mod tests { connection.end_turn(session_id, acp::StopReason::EndTurn); }); - thread_view.read_with(cx, |view, cx| { + conversation_view.read_with(cx, |view, cx| { assert_eq!( view.active_thread() .and_then(|active| active.read(cx).editing_message), @@ -4891,7 +4920,7 @@ pub(crate) mod tests { cx.update(|window, cx| { assert!(user_message_editor.focus_handle(cx).is_focused(window)); assert_eq!( - thread_view + conversation_view .read(cx) .active_thread() .and_then(|active| active.read(cx).editing_message), @@ -4905,7 +4934,7 @@ pub(crate) mod tests { } struct GeneratingThreadSetup { - thread_view: Entity, + conversation_view: Entity, thread: Entity, message_editor: Entity, } @@ -4915,17 +4944,18 @@ pub(crate) mod tests { ) -> (GeneratingThreadSetup, &mut VisualTestContext) { let connection = StubAgentConnection::new(); - let (thread_view, cx) = - setup_thread_view(StubAgentServer::new(connection.clone()), cx).await; - add_to_workspace(thread_view.clone(), cx); + let (conversation_view, cx) = + setup_conversation_view(StubAgentServer::new(connection.clone()), cx).await; + add_to_workspace(conversation_view.clone(), cx); - let message_editor = message_editor(&thread_view, cx); + let message_editor = message_editor(&conversation_view, cx); message_editor.update_in(cx, |editor, window, cx| { editor.set_text("Hello", window, cx); }); - active_thread(&thread_view, cx).update_in(cx, |view, window, cx| view.send(window, cx)); + active_thread(&conversation_view, cx) + .update_in(cx, |view, window, cx| view.send(window, cx)); - let (thread, session_id) = thread_view.read_with(cx, |view, cx| { + let (thread, session_id) = conversation_view.read_with(cx, |view, cx| { let thread = view .active_thread() .as_ref() @@ -4956,7 +4986,7 @@ pub(crate) mod tests { ( GeneratingThreadSetup { - thread_view, + conversation_view, thread, message_editor, }, @@ -4971,13 +5001,13 @@ pub(crate) mod tests { let (setup, cx) = setup_generating_thread(cx).await; let focus_handle = setup - .thread_view + .conversation_view .read_with(cx, |view, cx| view.focus_handle(cx)); cx.update(|window, cx| { window.focus(&focus_handle, cx); }); - setup.thread_view.update_in(cx, |_, window, cx| { + setup.conversation_view.update_in(cx, |_, window, cx| { window.dispatch_action(menu::Cancel.boxed_clone(), cx); }); @@ -5016,11 +5046,11 @@ pub(crate) mod tests { async fn test_escape_when_idle_is_noop(cx: &mut TestAppContext) { init_test(cx); - let (thread_view, cx) = - setup_thread_view(StubAgentServer::new(StubAgentConnection::new()), cx).await; - add_to_workspace(thread_view.clone(), cx); + let (conversation_view, cx) = + setup_conversation_view(StubAgentServer::new(StubAgentConnection::new()), cx).await; + add_to_workspace(conversation_view.clone(), cx); - let thread = thread_view.read_with(cx, |view, cx| { + let thread = conversation_view.read_with(cx, |view, cx| { view.active_thread().unwrap().read(cx).thread.clone() }); @@ -5028,12 +5058,12 @@ pub(crate) mod tests { assert_eq!(thread.status(), ThreadStatus::Idle); }); - let focus_handle = thread_view.read_with(cx, |view, _cx| view.focus_handle.clone()); + let focus_handle = conversation_view.read_with(cx, |view, _cx| view.focus_handle.clone()); cx.update(|window, cx| { window.focus(&focus_handle, cx); }); - thread_view.update_in(cx, |_, window, cx| { + conversation_view.update_in(cx, |_, window, cx| { window.dispatch_action(menu::Cancel.boxed_clone(), cx); }); @@ -5050,17 +5080,18 @@ pub(crate) mod tests { let connection = StubAgentConnection::new(); - let (thread_view, cx) = - setup_thread_view(StubAgentServer::new(connection.clone()), cx).await; - add_to_workspace(thread_view.clone(), cx); + let (conversation_view, cx) = + setup_conversation_view(StubAgentServer::new(connection.clone()), cx).await; + add_to_workspace(conversation_view.clone(), cx); - let message_editor = message_editor(&thread_view, cx); + let message_editor = message_editor(&conversation_view, cx); message_editor.update_in(cx, |editor, window, cx| { editor.set_text("Message 1", window, cx); }); - active_thread(&thread_view, cx).update_in(cx, |view, window, cx| view.send(window, cx)); + active_thread(&conversation_view, cx) + .update_in(cx, |view, window, cx| view.send(window, cx)); - let (thread, session_id) = thread_view.read_with(cx, |view, cx| { + let (thread, session_id) = conversation_view.read_with(cx, |view, cx| { let thread = view.active_thread().unwrap().read(cx).thread.clone(); (thread.clone(), thread.read(cx).session_id().clone()) @@ -5099,7 +5130,7 @@ pub(crate) mod tests { message_editor.update_in(cx, |editor, window, cx| { editor.set_text("Message 2", window, cx); }); - active_thread(&thread_view, cx) + active_thread(&conversation_view, cx) .update_in(cx, |view, window, cx| view.interrupt_and_send(window, cx)); cx.update(|_, cx| { @@ -5181,18 +5212,20 @@ pub(crate) mod tests { acp::ContentChunk::new("Response".into()), )]); - let (thread_view, cx) = setup_thread_view(StubAgentServer::new(connection), cx).await; - add_to_workspace(thread_view.clone(), cx); + let (conversation_view, cx) = + setup_conversation_view(StubAgentServer::new(connection), cx).await; + add_to_workspace(conversation_view.clone(), cx); - let message_editor = message_editor(&thread_view, cx); + let message_editor = message_editor(&conversation_view, cx); message_editor.update_in(cx, |editor, window, cx| { editor.set_text("Original message to edit", window, cx) }); - active_thread(&thread_view, cx).update_in(cx, |view, window, cx| view.send(window, cx)); + active_thread(&conversation_view, cx) + .update_in(cx, |view, window, cx| view.send(window, cx)); cx.run_until_parked(); - let user_message_editor = thread_view.read_with(cx, |thread_view, cx| { - thread_view + let user_message_editor = conversation_view.read_with(cx, |conversation_view, cx| { + conversation_view .active_thread() .map(|active| &active.read(cx).entry_view_state) .as_ref() @@ -5206,7 +5239,7 @@ pub(crate) mod tests { }); cx.focus(&user_message_editor); - thread_view.read_with(cx, |view, cx| { + conversation_view.read_with(cx, |view, cx| { assert_eq!( view.active_thread() .and_then(|active| active.read(cx).editing_message), @@ -5222,8 +5255,11 @@ pub(crate) mod tests { // Create a simple buffer with some text so we can create a selection // that will then be added to the message being edited. - let (workspace, project) = thread_view.read_with(cx, |thread_view, _cx| { - (thread_view.workspace.clone(), thread_view.project.clone()) + let (workspace, project) = conversation_view.read_with(cx, |conversation_view, _cx| { + ( + conversation_view.workspace.clone(), + conversation_view.project.clone(), + ) }); let buffer = project.update(cx, |project, cx| { project.create_local_buffer("let a = 10 + 10;", None, false, cx) @@ -5245,7 +5281,7 @@ pub(crate) mod tests { }) .unwrap(); - thread_view.update_in(cx, |view, window, cx| { + conversation_view.update_in(cx, |view, window, cx| { assert_eq!( view.active_thread() .and_then(|active| active.read(cx).editing_message), @@ -5271,18 +5307,22 @@ pub(crate) mod tests { acp::ContentChunk::new("Response".into()), )]); - let (thread_view, cx) = setup_thread_view(StubAgentServer::new(connection), cx).await; - add_to_workspace(thread_view.clone(), cx); + let (conversation_view, cx) = + setup_conversation_view(StubAgentServer::new(connection), cx).await; + add_to_workspace(conversation_view.clone(), cx); - let message_editor = message_editor(&thread_view, cx); + let message_editor = message_editor(&conversation_view, cx); message_editor.update_in(cx, |editor, window, cx| { editor.set_text("Can you review this snippet ", window, cx) }); // Create a simple buffer with some text so we can create a selection // that will then be added to the message being edited. - let (workspace, project) = thread_view.read_with(cx, |thread_view, _cx| { - (thread_view.workspace.clone(), thread_view.project.clone()) + let (workspace, project) = conversation_view.read_with(cx, |conversation_view, _cx| { + ( + conversation_view.workspace.clone(), + conversation_view.project.clone(), + ) }); let buffer = project.update(cx, |project, cx| { project.create_local_buffer("let a = 10 + 10;", None, false, cx) @@ -5304,7 +5344,7 @@ pub(crate) mod tests { }) .unwrap(); - thread_view.update_in(cx, |view, window, cx| { + conversation_view.update_in(cx, |view, window, cx| { assert_eq!( view.active_thread() .and_then(|active| active.read(cx).editing_message), @@ -5343,7 +5383,8 @@ pub(crate) mod tests { connection.set_next_prompt_updates(vec![acp::SessionUpdate::ToolCall(tool_call)]); - let (thread_view, cx) = setup_thread_view(StubAgentServer::new(connection), cx).await; + let (conversation_view, cx) = + setup_conversation_view(StubAgentServer::new(connection), cx).await; // Disable notifications to avoid popup windows cx.update(|_window, cx| { @@ -5356,18 +5397,19 @@ pub(crate) mod tests { ); }); - let message_editor = message_editor(&thread_view, cx); + let message_editor = message_editor(&conversation_view, cx); message_editor.update_in(cx, |editor, window, cx| { editor.set_text("Run cargo build", window, cx); }); - active_thread(&thread_view, cx).update_in(cx, |view, window, cx| view.send(window, cx)); + active_thread(&conversation_view, cx) + .update_in(cx, |view, window, cx| view.send(window, cx)); cx.run_until_parked(); // Verify the tool call is in WaitingForConfirmation state with the expected options - thread_view.read_with(cx, |thread_view, cx| { - let thread = thread_view + conversation_view.read_with(cx, |conversation_view, cx| { + let thread = conversation_view .active_thread() .expect("Thread should exist") .read(cx) @@ -5451,7 +5493,8 @@ pub(crate) mod tests { connection.set_next_prompt_updates(vec![acp::SessionUpdate::ToolCall(tool_call)]); - let (thread_view, cx) = setup_thread_view(StubAgentServer::new(connection), cx).await; + let (conversation_view, cx) = + setup_conversation_view(StubAgentServer::new(connection), cx).await; // Disable notifications cx.update(|_window, cx| { @@ -5464,18 +5507,19 @@ pub(crate) mod tests { ); }); - let message_editor = message_editor(&thread_view, cx); + let message_editor = message_editor(&conversation_view, cx); message_editor.update_in(cx, |editor, window, cx| { editor.set_text("Edit the main file", window, cx); }); - active_thread(&thread_view, cx).update_in(cx, |view, window, cx| view.send(window, cx)); + active_thread(&conversation_view, cx) + .update_in(cx, |view, window, cx| view.send(window, cx)); cx.run_until_parked(); // Verify the options - thread_view.read_with(cx, |thread_view, cx| { - let thread = thread_view + conversation_view.read_with(cx, |conversation_view, cx| { + let thread = conversation_view .active_thread() .expect("Thread should exist") .read(cx) @@ -5539,7 +5583,8 @@ pub(crate) mod tests { connection.set_next_prompt_updates(vec![acp::SessionUpdate::ToolCall(tool_call)]); - let (thread_view, cx) = setup_thread_view(StubAgentServer::new(connection), cx).await; + let (conversation_view, cx) = + setup_conversation_view(StubAgentServer::new(connection), cx).await; // Disable notifications cx.update(|_window, cx| { @@ -5552,18 +5597,19 @@ pub(crate) mod tests { ); }); - let message_editor = message_editor(&thread_view, cx); + let message_editor = message_editor(&conversation_view, cx); message_editor.update_in(cx, |editor, window, cx| { editor.set_text("Fetch the docs", window, cx); }); - active_thread(&thread_view, cx).update_in(cx, |view, window, cx| view.send(window, cx)); + active_thread(&conversation_view, cx) + .update_in(cx, |view, window, cx| view.send(window, cx)); cx.run_until_parked(); // Verify the options - thread_view.read_with(cx, |thread_view, cx| { - let thread = thread_view + conversation_view.read_with(cx, |conversation_view, cx| { + let thread = conversation_view .active_thread() .expect("Thread should exist") .read(cx) @@ -5630,7 +5676,8 @@ pub(crate) mod tests { connection.set_next_prompt_updates(vec![acp::SessionUpdate::ToolCall(tool_call)]); - let (thread_view, cx) = setup_thread_view(StubAgentServer::new(connection), cx).await; + let (conversation_view, cx) = + setup_conversation_view(StubAgentServer::new(connection), cx).await; // Disable notifications cx.update(|_window, cx| { @@ -5643,18 +5690,19 @@ pub(crate) mod tests { ); }); - let message_editor = message_editor(&thread_view, cx); + let message_editor = message_editor(&conversation_view, cx); message_editor.update_in(cx, |editor, window, cx| { editor.set_text("Run the deploy script", window, cx); }); - active_thread(&thread_view, cx).update_in(cx, |view, window, cx| view.send(window, cx)); + active_thread(&conversation_view, cx) + .update_in(cx, |view, window, cx| view.send(window, cx)); cx.run_until_parked(); // Verify only 2 options (no pattern button when command doesn't match pattern) - thread_view.read_with(cx, |thread_view, cx| { - let thread = thread_view + conversation_view.read_with(cx, |conversation_view, cx| { + let thread = conversation_view .active_thread() .expect("Thread should exist") .read(cx) @@ -5729,8 +5777,9 @@ pub(crate) mod tests { connection.set_next_prompt_updates(vec![acp::SessionUpdate::ToolCall(tool_call)]); - let (thread_view, cx) = setup_thread_view(StubAgentServer::new(connection), cx).await; - add_to_workspace(thread_view.clone(), cx); + let (conversation_view, cx) = + setup_conversation_view(StubAgentServer::new(connection), cx).await; + add_to_workspace(conversation_view.clone(), cx); cx.update(|_window, cx| { AgentSettings::override_global( @@ -5742,18 +5791,19 @@ pub(crate) mod tests { ); }); - let message_editor = message_editor(&thread_view, cx); + let message_editor = message_editor(&conversation_view, cx); message_editor.update_in(cx, |editor, window, cx| { editor.set_text("Run tests", window, cx); }); - active_thread(&thread_view, cx).update_in(cx, |view, window, cx| view.send(window, cx)); + active_thread(&conversation_view, cx) + .update_in(cx, |view, window, cx| view.send(window, cx)); cx.run_until_parked(); // Verify tool call is waiting for confirmation - thread_view.read_with(cx, |thread_view, cx| { - let tool_call = thread_view.pending_tool_call(cx); + conversation_view.read_with(cx, |conversation_view, cx| { + let tool_call = conversation_view.pending_tool_call(cx); assert!( tool_call.is_some(), "Expected a tool call waiting for confirmation" @@ -5761,7 +5811,7 @@ pub(crate) mod tests { }); // Dispatch the AuthorizeToolCall action (simulating dropdown menu selection) - thread_view.update_in(cx, |_, window, cx| { + conversation_view.update_in(cx, |_, window, cx| { window.dispatch_action( crate::AuthorizeToolCall { tool_call_id: "action-test-1".to_string(), @@ -5776,8 +5826,8 @@ pub(crate) mod tests { cx.run_until_parked(); // Verify tool call is no longer waiting for confirmation (was authorized) - thread_view.read_with(cx, |thread_view, cx| { - let tool_call = thread_view.pending_tool_call(cx); + conversation_view.read_with(cx, |conversation_view, cx| { + let tool_call = conversation_view.pending_tool_call(cx); assert!( tool_call.is_none(), "Tool call should no longer be waiting for confirmation after AuthorizeToolCall action" @@ -5805,8 +5855,9 @@ pub(crate) mod tests { connection.set_next_prompt_updates(vec![acp::SessionUpdate::ToolCall(tool_call)]); - let (thread_view, cx) = setup_thread_view(StubAgentServer::new(connection), cx).await; - add_to_workspace(thread_view.clone(), cx); + let (conversation_view, cx) = + setup_conversation_view(StubAgentServer::new(connection), cx).await; + add_to_workspace(conversation_view.clone(), cx); cx.update(|_window, cx| { AgentSettings::override_global( @@ -5818,12 +5869,13 @@ pub(crate) mod tests { ); }); - let message_editor = message_editor(&thread_view, cx); + let message_editor = message_editor(&conversation_view, cx); message_editor.update_in(cx, |editor, window, cx| { editor.set_text("Install dependencies", window, cx); }); - active_thread(&thread_view, cx).update_in(cx, |view, window, cx| view.send(window, cx)); + active_thread(&conversation_view, cx) + .update_in(cx, |view, window, cx| view.send(window, cx)); cx.run_until_parked(); @@ -5844,7 +5896,7 @@ pub(crate) mod tests { }; // Dispatch action with the pattern option (simulating "Always allow `npm` commands") - thread_view.update_in(cx, |_, window, cx| { + conversation_view.update_in(cx, |_, window, cx| { window.dispatch_action( crate::AuthorizeToolCall { tool_call_id: "pattern-action-test-1".to_string(), @@ -5859,8 +5911,8 @@ pub(crate) mod tests { cx.run_until_parked(); // Verify tool call was authorized - thread_view.read_with(cx, |thread_view, cx| { - let tool_call = thread_view.pending_tool_call(cx); + conversation_view.read_with(cx, |conversation_view, cx| { + let tool_call = conversation_view.pending_tool_call(cx); assert!( tool_call.is_none(), "Tool call should be authorized after selecting pattern option" @@ -5888,8 +5940,9 @@ pub(crate) mod tests { connection.set_next_prompt_updates(vec![acp::SessionUpdate::ToolCall(tool_call)]); - let (thread_view, cx) = setup_thread_view(StubAgentServer::new(connection), cx).await; - add_to_workspace(thread_view.clone(), cx); + let (conversation_view, cx) = + setup_conversation_view(StubAgentServer::new(connection), cx).await; + add_to_workspace(conversation_view.clone(), cx); cx.update(|_window, cx| { AgentSettings::override_global( @@ -5901,26 +5954,27 @@ pub(crate) mod tests { ); }); - let message_editor = message_editor(&thread_view, cx); + let message_editor = message_editor(&conversation_view, cx); message_editor.update_in(cx, |editor, window, cx| { editor.set_text("Push changes", window, cx); }); - active_thread(&thread_view, cx).update_in(cx, |view, window, cx| view.send(window, cx)); + active_thread(&conversation_view, cx) + .update_in(cx, |view, window, cx| view.send(window, cx)); cx.run_until_parked(); // Use default granularity (last option = "Only this time") // Simulate clicking the Deny button - active_thread(&thread_view, cx).update_in(cx, |view, window, cx| { + active_thread(&conversation_view, cx).update_in(cx, |view, window, cx| { view.reject_once(&RejectOnce, window, cx) }); cx.run_until_parked(); // Verify tool call was rejected (no longer waiting for confirmation) - thread_view.read_with(cx, |thread_view, cx| { - let tool_call = thread_view.pending_tool_call(cx); + conversation_view.read_with(cx, |conversation_view, cx| { + let tool_call = conversation_view.pending_tool_call(cx); assert!( tool_call.is_none(), "Tool call should be rejected after Deny" @@ -5986,10 +6040,11 @@ pub(crate) mod tests { async fn test_manually_editing_title_updates_acp_thread_title(cx: &mut TestAppContext) { init_test(cx); - let (thread_view, cx) = setup_thread_view(StubAgentServer::default_response(), cx).await; - add_to_workspace(thread_view.clone(), cx); + let (conversation_view, cx) = + setup_conversation_view(StubAgentServer::default_response(), cx).await; + add_to_workspace(conversation_view.clone(), cx); - let active = active_thread(&thread_view, cx); + let active = active_thread(&conversation_view, cx); let title_editor = cx.read(|cx| active.read(cx).title_editor.clone()); let thread = cx.read(|cx| active.read(cx).thread.clone()); @@ -5997,7 +6052,7 @@ pub(crate) mod tests { assert!(!editor.read_only(cx)); }); - cx.focus(&thread_view); + cx.focus(&conversation_view); cx.focus(&title_editor); cx.dispatch_action(editor::actions::DeleteLine); @@ -6017,10 +6072,10 @@ pub(crate) mod tests { async fn test_title_editor_is_read_only_when_set_title_unsupported(cx: &mut TestAppContext) { init_test(cx); - let (thread_view, cx) = - setup_thread_view(StubAgentServer::new(ResumeOnlyAgentConnection), cx).await; + let (conversation_view, cx) = + setup_conversation_view(StubAgentServer::new(ResumeOnlyAgentConnection), cx).await; - let active = active_thread(&thread_view, cx); + let active = active_thread(&conversation_view, cx); let title_editor = cx.read(|cx| active.read(cx).title_editor.clone()); title_editor.read_with(cx, |editor, cx| { @@ -6037,16 +6092,17 @@ pub(crate) mod tests { let connection = StubAgentConnection::new(); - let (thread_view, cx) = - setup_thread_view(StubAgentServer::new(connection.clone()), cx).await; + let (conversation_view, cx) = + setup_conversation_view(StubAgentServer::new(connection.clone()), cx).await; - let message_editor = message_editor(&thread_view, cx); + let message_editor = message_editor(&conversation_view, cx); message_editor.update_in(cx, |editor, window, cx| { editor.set_text("Some prompt", window, cx); }); - active_thread(&thread_view, cx).update_in(cx, |view, window, cx| view.send(window, cx)); + active_thread(&conversation_view, cx) + .update_in(cx, |view, window, cx| view.send(window, cx)); - let session_id = thread_view.read_with(cx, |view, cx| { + let session_id = conversation_view.read_with(cx, |view, cx| { view.active_thread() .unwrap() .read(cx) @@ -6064,8 +6120,8 @@ pub(crate) mod tests { cx.run_until_parked(); - thread_view.read_with(cx, |thread_view, cx| { - let state = thread_view.active_thread().unwrap(); + conversation_view.read_with(cx, |conversation_view, cx| { + let state = conversation_view.active_thread().unwrap(); let error = &state.read(cx).thread_error; match error { Some(ThreadError::Other { message, .. }) => { @@ -6356,11 +6412,11 @@ pub(crate) mod tests { async fn test_move_queued_message_to_empty_main_editor(cx: &mut TestAppContext) { init_test(cx); - let (connection_view, cx) = - setup_thread_view(StubAgentServer::default_response(), cx).await; + let (conversation_view, cx) = + setup_conversation_view(StubAgentServer::default_response(), cx).await; // Add a plain-text message to the queue directly. - active_thread(&connection_view, cx).update_in(cx, |thread, window, cx| { + active_thread(&conversation_view, cx).update_in(cx, |thread, window, cx| { thread.add_to_queue( vec![acp::ContentBlock::Text(acp::TextContent::new( "queued message".to_string(), @@ -6377,12 +6433,12 @@ pub(crate) mod tests { cx.run_until_parked(); // Queue should now be empty. - let queue_len = active_thread(&connection_view, cx) + let queue_len = active_thread(&conversation_view, cx) .read_with(cx, |thread, _cx| thread.local_queued_messages.len()); assert_eq!(queue_len, 0, "Queue should be empty after move"); // Main editor should contain the queued message text. - let text = message_editor(&connection_view, cx).update(cx, |editor, cx| editor.text(cx)); + let text = message_editor(&conversation_view, cx).update(cx, |editor, cx| editor.text(cx)); assert_eq!( text, "queued message", "Main editor should contain the moved queued message" @@ -6393,11 +6449,11 @@ pub(crate) mod tests { async fn test_move_queued_message_to_non_empty_main_editor(cx: &mut TestAppContext) { init_test(cx); - let (connection_view, cx) = - setup_thread_view(StubAgentServer::default_response(), cx).await; + let (conversation_view, cx) = + setup_conversation_view(StubAgentServer::default_response(), cx).await; // Seed the main editor with existing content. - message_editor(&connection_view, cx).update_in(cx, |editor, window, cx| { + message_editor(&conversation_view, cx).update_in(cx, |editor, window, cx| { editor.set_message( vec![acp::ContentBlock::Text(acp::TextContent::new( "existing content".to_string(), @@ -6408,7 +6464,7 @@ pub(crate) mod tests { }); // Add a plain-text message to the queue. - active_thread(&connection_view, cx).update_in(cx, |thread, window, cx| { + active_thread(&conversation_view, cx).update_in(cx, |thread, window, cx| { thread.add_to_queue( vec![acp::ContentBlock::Text(acp::TextContent::new( "queued message".to_string(), @@ -6422,12 +6478,12 @@ pub(crate) mod tests { cx.run_until_parked(); // Queue should now be empty. - let queue_len = active_thread(&connection_view, cx) + let queue_len = active_thread(&conversation_view, cx) .read_with(cx, |thread, _cx| thread.local_queued_messages.len()); assert_eq!(queue_len, 0, "Queue should be empty after move"); // Main editor should contain existing content + separator + queued content. - let text = message_editor(&connection_view, cx).update(cx, |editor, cx| editor.text(cx)); + let text = message_editor(&conversation_view, cx).update(cx, |editor, cx| editor.text(cx)); assert_eq!( text, "existing content\n\nqueued message", "Main editor should have existing content and queued message separated by two newlines" @@ -6449,9 +6505,9 @@ pub(crate) mod tests { cx.update(|_window, cx| cx.new(|cx| AgentConnectionStore::new(project.clone(), cx))); // StubAgentConnection defaults to supports_close_session() -> false - let thread_view = cx.update(|window, cx| { + let conversation_view = cx.update(|window, cx| { cx.new(|cx| { - ConnectionView::new( + ConversationView::new( Rc::new(StubAgentServer::default_response()), connection_store, Agent::Custom { id: "Test".into() }, @@ -6471,7 +6527,7 @@ pub(crate) mod tests { cx.run_until_parked(); - thread_view.read_with(cx, |view, _cx| { + conversation_view.read_with(cx, |view, _cx| { let connected = view.as_connected().expect("Should be connected"); assert!( !connected.threads.is_empty(), @@ -6483,7 +6539,7 @@ pub(crate) mod tests { ); }); - thread_view + conversation_view .update(cx, |view, cx| { view.as_connected() .expect("Should be connected") @@ -6496,12 +6552,12 @@ pub(crate) mod tests { async fn test_close_all_sessions_calls_close_when_supported(cx: &mut TestAppContext) { init_test(cx); - let (thread_view, cx) = - setup_thread_view(StubAgentServer::new(CloseCapableConnection::new()), cx).await; + let (conversation_view, cx) = + setup_conversation_view(StubAgentServer::new(CloseCapableConnection::new()), cx).await; cx.run_until_parked(); - let close_capable = thread_view.read_with(cx, |view, _cx| { + let close_capable = conversation_view.read_with(cx, |view, _cx| { let connected = view.as_connected().expect("Should be connected"); assert!( !connected.threads.is_empty(), @@ -6519,7 +6575,7 @@ pub(crate) mod tests { .expect("Should be CloseCapableConnection") }); - thread_view + conversation_view .update(cx, |view, cx| { view.as_connected() .expect("Should be connected") @@ -6538,11 +6594,12 @@ pub(crate) mod tests { async fn test_close_session_returns_error_when_unsupported(cx: &mut TestAppContext) { init_test(cx); - let (thread_view, cx) = setup_thread_view(StubAgentServer::default_response(), cx).await; + let (conversation_view, cx) = + setup_conversation_view(StubAgentServer::default_response(), cx).await; cx.run_until_parked(); - let result = thread_view + let result = conversation_view .update(cx, |view, cx| { let connected = view.as_connected().expect("Should be connected"); assert!( diff --git a/crates/agent_ui/src/connection_view/thread_view.rs b/crates/agent_ui/src/conversation_view/thread_view.rs similarity index 99% rename from crates/agent_ui/src/connection_view/thread_view.rs rename to crates/agent_ui/src/conversation_view/thread_view.rs index ed2a062c0aed13c60c8ea15193bb598764a31806..3bee1499d983da06e345cae23c3deba5973b22e6 100644 --- a/crates/agent_ui/src/connection_view/thread_view.rs +++ b/crates/agent_ui/src/conversation_view/thread_view.rs @@ -167,7 +167,7 @@ pub struct ThreadView { pub parent_id: Option, pub thread: Entity, pub(crate) conversation: Entity, - pub server_view: WeakEntity, + pub server_view: WeakEntity, pub agent_icon: IconName, pub agent_icon_from_external_svg: Option, pub agent_id: AgentId, @@ -256,7 +256,7 @@ impl ThreadView { parent_id: Option, thread: Entity, conversation: Entity, - server_view: WeakEntity, + server_view: WeakEntity, agent_icon: IconName, agent_icon_from_external_svg: Option, agent_id: AgentId, @@ -882,7 +882,7 @@ impl ThreadView { let agent_id = self.agent_id.clone(); let server_view = self.server_view.clone(); move |window, cx| { - ConnectionView::handle_auth_required( + ConversationView::handle_auth_required( server_view.clone(), AuthRequired::new(), agent_id, @@ -1755,7 +1755,7 @@ impl ThreadView { pub fn sync_thread( &mut self, project: Entity, - server_view: Entity, + server_view: Entity, window: &mut Window, cx: &mut Context, ) { @@ -7318,7 +7318,7 @@ impl ThreadView { } let connection = this.thread.read(cx).connection().clone(); window.defer(cx, |window, cx| { - ConnectionView::handle_auth_required( + ConversationView::handle_auth_required( server_view, AuthRequired::new(), agent_name, diff --git a/crates/agent_ui/src/message_editor.rs b/crates/agent_ui/src/message_editor.rs index 6c62bd2f81ded87ae3b2aec4ac23473bc3324b8c..fd625db07b0c34cdf90a9913f574d38df32e97f8 100644 --- a/crates/agent_ui/src/message_editor.rs +++ b/crates/agent_ui/src/message_editor.rs @@ -1680,7 +1680,7 @@ mod tests { use crate::completion_provider::{PromptCompletionProviderDelegate, PromptContextType}; use crate::{ - connection_view::tests::init_test, + conversation_view::tests::init_test, message_editor::{Mention, MessageEditor, parse_mention_links}, }; diff --git a/crates/agent_ui/src/sidebar.rs b/crates/agent_ui/src/sidebar.rs index b2634a807fe0a0588536e822f2ec06c6d2099c09..87f4efeb0cf145f635b9af9d6923ec53a8e38ff5 100644 --- a/crates/agent_ui/src/sidebar.rs +++ b/crates/agent_ui/src/sidebar.rs @@ -486,7 +486,7 @@ impl Sidebar { self.focused_thread = active_workspace .as_ref() .and_then(|ws| ws.read(cx).panel::(cx)) - .and_then(|panel| panel.read(cx).active_connection_view().cloned()) + .and_then(|panel| panel.read(cx).active_conversation().cloned()) .and_then(|cv| cv.read(cx).parent_id(cx)); let mut threads_by_paths: HashMap> = HashMap::new(); diff --git a/crates/agent_ui/src/test_support.rs b/crates/agent_ui/src/test_support.rs index 7b986d045dddbf25fbe940a3b783c4f145781e8b..66c8c447a827e7f36c3098b4835026836ef8ccd8 100644 --- a/crates/agent_ui/src/test_support.rs +++ b/crates/agent_ui/src/test_support.rs @@ -82,7 +82,7 @@ pub fn open_thread_with_connection( } pub fn send_message(panel: &Entity, cx: &mut VisualTestContext) { - let thread_view = panel.read_with(cx, |panel, cx| panel.as_active_thread_view(cx).unwrap()); + let thread_view = panel.read_with(cx, |panel, cx| panel.active_thread_view(cx).unwrap()); let message_editor = thread_view.read_with(cx, |view, _cx| view.message_editor.clone()); message_editor.update_in(cx, |editor, window, cx| { editor.set_text("Hello", window, cx); diff --git a/crates/agent_ui/src/thread_history_view.rs b/crates/agent_ui/src/thread_history_view.rs index 6961f78884d2fb5fb95830d91dad940ca9dc48e9..bfb01d74d534934cbe731bd80403d04f4e454457 100644 --- a/crates/agent_ui/src/thread_history_view.rs +++ b/crates/agent_ui/src/thread_history_view.rs @@ -1,5 +1,5 @@ use crate::thread_history::ThreadHistory; -use crate::{AgentPanel, ConnectionView, RemoveHistory, RemoveSelectedThread}; +use crate::{AgentPanel, ConversationView, RemoveHistory, RemoveSelectedThread}; use acp_thread::AgentSessionInfo; use chrono::{Datelike as _, Local, NaiveDate, TimeDelta, Utc}; use editor::{Editor, EditorEvent}; @@ -640,7 +640,7 @@ impl Render for ThreadHistoryView { #[derive(IntoElement)] pub struct HistoryEntryElement { entry: AgentSessionInfo, - thread_view: WeakEntity, + conversation_view: WeakEntity, selected: bool, hovered: bool, supports_delete: bool, @@ -648,10 +648,10 @@ pub struct HistoryEntryElement { } impl HistoryEntryElement { - pub fn new(entry: AgentSessionInfo, thread_view: WeakEntity) -> Self { + pub fn new(entry: AgentSessionInfo, conversation_view: WeakEntity) -> Self { Self { entry, - thread_view, + conversation_view, selected: false, hovered: false, supports_delete: false, @@ -725,13 +725,13 @@ impl RenderOnce for HistoryEntryElement { Tooltip::for_action("Delete", &RemoveSelectedThread, cx) }) .on_click({ - let thread_view = self.thread_view.clone(); + let conversation_view = self.conversation_view.clone(); let session_id = self.entry.session_id.clone(); move |_event, _window, cx| { - if let Some(thread_view) = thread_view.upgrade() { - thread_view.update(cx, |thread_view, cx| { - thread_view.delete_history_entry(&session_id, cx); + if let Some(conversation_view) = conversation_view.upgrade() { + conversation_view.update(cx, |conversation_view, cx| { + conversation_view.delete_history_entry(&session_id, cx); }); } } @@ -741,11 +741,11 @@ impl RenderOnce for HistoryEntryElement { None }) .on_click({ - let thread_view = self.thread_view.clone(); + let conversation_view = self.conversation_view.clone(); let entry = self.entry; move |_event, window, cx| { - if let Some(workspace) = thread_view + if let Some(workspace) = conversation_view .upgrade() .and_then(|view| view.read(cx).workspace().upgrade()) { diff --git a/crates/zed/src/visual_test_runner.rs b/crates/zed/src/visual_test_runner.rs index 310632648053036162491a54d346eb5c98f13994..197a7c6003737c486bc6adfb7190a1f23dbcf94b 100644 --- a/crates/zed/src/visual_test_runner.rs +++ b/crates/zed/src/visual_test_runner.rs @@ -3459,7 +3459,7 @@ edition = "2021" // Insert a message into the active thread's message editor and submit. let thread_view = cx - .read(|cx| panel.read(cx).as_active_thread_view(cx)) + .read(|cx| panel.read(cx).active_thread_view(cx)) .ok_or_else(|| anyhow::anyhow!("No active thread view"))?; cx.update_window(workspace_window.into(), |_, window, cx| { @@ -3528,7 +3528,7 @@ edition = "2021" new_workspace.read(cx).panel::(cx) })?; if let Some(new_panel) = new_panel { - let new_thread_view = cx.read(|cx| new_panel.read(cx).as_active_thread_view(cx)); + let new_thread_view = cx.read(|cx| new_panel.read(cx).active_thread_view(cx)); if let Some(new_thread_view) = new_thread_view { cx.update_window(workspace_window.into(), |_, window, cx| { let message_editor = new_thread_view.read(cx).message_editor.clone(); From 9b5d170ecab57e38ecf3ab32a4826053158bd1bc Mon Sep 17 00:00:00 2001 From: Karthik Nishanth <7759435+nishanthkarthik@users.noreply.github.com> Date: Mon, 16 Mar 2026 10:01:28 -0700 Subject: [PATCH 4/7] editor: Go to previous and next symbol actions (#50777) Closes discussion #34890 This is similar to the vim prev/next method/section motion, but more flexible because this follows the items in editor's outline (Tree sitter or LSP provided). Before you mark this PR as ready for review, make sure that you have: - [x] Added a solid test coverage and/or screenshots from doing manual testing - [x] Done a self-review taking into account security and performance aspects - [x] Aligned any UI changes with the [UI checklist](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist) Release Notes: - Added actions `editor::GoToPreviousSymbol` and `editor::GoToNextSymbol` actions to go to the previous and next outline symbol. This is either the tree sitter outline, or the LSP provided outline depending on the configuration. --- crates/editor/src/actions.rs | 4 + crates/editor/src/editor.rs | 102 ++++++++++++ crates/editor/src/editor_tests.rs | 254 ++++++++++++++++++++++++++++++ crates/editor/src/element.rs | 2 + 4 files changed, 362 insertions(+) diff --git a/crates/editor/src/actions.rs b/crates/editor/src/actions.rs index 7cc41752f34d719c27f5954c41f26fa9febfde94..7451aaced9072d3f60483a3d1091caa38f92294b 100644 --- a/crates/editor/src/actions.rs +++ b/crates/editor/src/actions.rs @@ -568,6 +568,10 @@ actions!( GoToParentModule, /// Goes to the previous change in the file. GoToPreviousChange, + /// Goes to the next symbol. + GoToNextSymbol, + /// Goes to the previous symbol. + GoToPreviousSymbol, /// Goes to the next reference to the symbol under the cursor. GoToNextReference, /// Goes to the previous reference to the symbol under the cursor. diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 204011412ec9b229ffdd49195e907369baa2d97f..da99dc45001548627317c3d2133859860a971a47 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -18125,6 +18125,108 @@ impl Editor { }; } + fn go_to_symbol_by_offset( + &mut self, + window: &mut Window, + cx: &mut Context, + offset: i8, + ) -> Task> { + let editor_snapshot = self.snapshot(window, cx); + + // We don't care about multi-buffer symbols + let Some((excerpt_id, _, _)) = editor_snapshot.as_singleton() else { + return Task::ready(Ok(())); + }; + + let cursor_offset = self + .selections + .newest::(&editor_snapshot.display_snapshot) + .head(); + + cx.spawn_in(window, async move |editor, wcx| -> Result<()> { + let Ok(Some(remote_id)) = editor.update(wcx, |ed, cx| { + let buffer = ed.buffer.read(cx).as_singleton()?; + Some(buffer.read(cx).remote_id()) + }) else { + return Ok(()); + }; + + let task = editor.update(wcx, |ed, cx| ed.buffer_outline_items(remote_id, cx))?; + let outline_items: Vec> = task.await; + + let multi_snapshot = editor_snapshot.buffer(); + let buffer_range = |range: &Range<_>| { + Anchor::range_in_buffer(excerpt_id, range.clone()).to_offset(multi_snapshot) + }; + + wcx.update_window(wcx.window_handle(), |_, window, acx| { + let current_idx = outline_items + .iter() + .enumerate() + .filter_map(|(idx, item)| { + // Find the closest outline item by distance between outline text and cursor location + let source_range = buffer_range(&item.source_range_for_text); + let distance_to_closest_endpoint = cmp::min( + (source_range.start.0 as isize - cursor_offset.0 as isize).abs(), + (source_range.end.0 as isize - cursor_offset.0 as isize).abs(), + ); + + let item_towards_offset = + (source_range.start.0 as isize - cursor_offset.0 as isize).signum() + == (offset as isize).signum(); + + let source_range_contains_cursor = source_range.contains(&cursor_offset); + + // To pick the next outline to jump to, we should jump in the direction of the offset, and + // we should not already be within the outline's source range. We then pick the closest outline + // item. + (item_towards_offset && !source_range_contains_cursor) + .then_some((distance_to_closest_endpoint, idx)) + }) + .min() + .map(|(_, idx)| idx); + + let Some(idx) = current_idx else { + return; + }; + + let range = buffer_range(&outline_items[idx].source_range_for_text); + let selection = [range.start..range.start]; + + let _ = editor + .update(acx, |editor, ecx| { + editor.change_selections( + SelectionEffects::scroll(Autoscroll::newest()), + window, + ecx, + |s| s.select_ranges(selection), + ); + }) + .ok(); + })?; + + Ok(()) + }) + } + + fn go_to_next_symbol( + &mut self, + _: &GoToNextSymbol, + window: &mut Window, + cx: &mut Context, + ) { + self.go_to_symbol_by_offset(window, cx, 1).detach(); + } + + fn go_to_previous_symbol( + &mut self, + _: &GoToPreviousSymbol, + window: &mut Window, + cx: &mut Context, + ) { + self.go_to_symbol_by_offset(window, cx, -1).detach(); + } + pub fn go_to_reference_before_or_after_position( &mut self, direction: Direction, diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index 683995e8ff0817e9f11c276fba1e85eef29eee7a..04d3babf20f5c866dd3f3447f0909f11becf724d 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -19328,6 +19328,260 @@ fn test_split_words_for_snippet_prefix() { assert_eq!(split("a.s"), &["s", ".s", "a.s"]); } +#[gpui::test] +async fn test_move_to_syntax_node_relative_jumps(tcx: &mut TestAppContext) { + init_test(tcx, |_| {}); + + let mut cx = EditorLspTestContext::new( + Arc::into_inner(markdown_lang()).unwrap(), + Default::default(), + tcx, + ) + .await; + + async fn assert(offset: i8, before: &str, after: &str, cx: &mut EditorLspTestContext) { + let _state_context = cx.set_state(before); + cx.run_until_parked(); + cx.update_editor(|editor, window, cx| editor.go_to_symbol_by_offset(window, cx, offset)) + .await + .unwrap(); + cx.run_until_parked(); + cx.assert_editor_state(after); + } + + const ABOVE: i8 = -1; + const BELOW: i8 = 1; + + assert( + ABOVE, + indoc! {" + # Foo + + ˇFoo foo foo + + # Bar + + Bar bar bar + "}, + indoc! {" + ˇ# Foo + + Foo foo foo + + # Bar + + Bar bar bar + "}, + &mut cx, + ) + .await; + + assert( + ABOVE, + indoc! {" + ˇ# Foo + + Foo foo foo + + # Bar + + Bar bar bar + "}, + indoc! {" + ˇ# Foo + + Foo foo foo + + # Bar + + Bar bar bar + "}, + &mut cx, + ) + .await; + + assert( + BELOW, + indoc! {" + ˇ# Foo + + Foo foo foo + + # Bar + + Bar bar bar + "}, + indoc! {" + # Foo + + Foo foo foo + + ˇ# Bar + + Bar bar bar + "}, + &mut cx, + ) + .await; + + assert( + BELOW, + indoc! {" + # Foo + + ˇFoo foo foo + + # Bar + + Bar bar bar + "}, + indoc! {" + # Foo + + Foo foo foo + + ˇ# Bar + + Bar bar bar + "}, + &mut cx, + ) + .await; + + assert( + BELOW, + indoc! {" + # Foo + + Foo foo foo + + ˇ# Bar + + Bar bar bar + "}, + indoc! {" + # Foo + + Foo foo foo + + ˇ# Bar + + Bar bar bar + "}, + &mut cx, + ) + .await; + + assert( + BELOW, + indoc! {" + # Foo + + Foo foo foo + + # Bar + ˇ + Bar bar bar + "}, + indoc! {" + # Foo + + Foo foo foo + + # Bar + ˇ + Bar bar bar + "}, + &mut cx, + ) + .await; +} + +#[gpui::test] +async fn test_move_to_syntax_node_relative_dead_zone(tcx: &mut TestAppContext) { + init_test(tcx, |_| {}); + + let mut cx = EditorLspTestContext::new( + Arc::into_inner(rust_lang()).unwrap(), + Default::default(), + tcx, + ) + .await; + + async fn assert(offset: i8, before: &str, after: &str, cx: &mut EditorLspTestContext) { + let _state_context = cx.set_state(before); + cx.run_until_parked(); + cx.update_editor(|editor, window, cx| editor.go_to_symbol_by_offset(window, cx, offset)) + .await + .unwrap(); + cx.run_until_parked(); + cx.assert_editor_state(after); + } + + const ABOVE: i8 = -1; + const BELOW: i8 = 1; + + assert( + ABOVE, + indoc! {" + fn foo() { + // foo fn + } + + ˇ// this zone is not inside any top level outline node + + fn bar() { + // bar fn + let _ = 2; + } + "}, + indoc! {" + ˇfn foo() { + // foo fn + } + + // this zone is not inside any top level outline node + + fn bar() { + // bar fn + let _ = 2; + } + "}, + &mut cx, + ) + .await; + + assert( + BELOW, + indoc! {" + fn foo() { + // foo fn + } + + ˇ// this zone is not inside any top level outline node + + fn bar() { + // bar fn + let _ = 2; + } + "}, + indoc! {" + fn foo() { + // foo fn + } + + // this zone is not inside any top level outline node + + ˇfn bar() { + // bar fn + let _ = 2; + } + "}, + &mut cx, + ) + .await; +} + #[gpui::test] async fn test_move_to_enclosing_bracket(cx: &mut TestAppContext) { init_test(cx, |_| {}); diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index ab00de0df25ca209604c7052367f0ac6ce2142ae..7128c60b7f45147f99b6f46d3bd85b9428d358ef 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -540,6 +540,8 @@ impl EditorElement { register_action(editor, window, Editor::go_to_next_change); register_action(editor, window, Editor::go_to_prev_reference); register_action(editor, window, Editor::go_to_next_reference); + register_action(editor, window, Editor::go_to_previous_symbol); + register_action(editor, window, Editor::go_to_next_symbol); register_action(editor, window, |editor, action, window, cx| { if let Some(task) = editor.format(action, window, cx) { From 44c73a8753dbb4ffa38c90a4c17d592b7be1d122 Mon Sep 17 00:00:00 2001 From: Lukas Wirth Date: Mon, 16 Mar 2026 18:10:31 +0100 Subject: [PATCH 5/7] gpui: Fix hash collision panics in ProfilingCollector (#51683) Release Notes: - N/A *or* Added/Fixed/Improved ... --- crates/gpui/src/profiler.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/crates/gpui/src/profiler.rs b/crates/gpui/src/profiler.rs index ccbc86e3fe35a095b2de9de159286250a24d7a05..dc6e9a6600f5c172050fd30cfed181ac7ed81ec4 100644 --- a/crates/gpui/src/profiler.rs +++ b/crates/gpui/src/profiler.rs @@ -169,7 +169,7 @@ pub struct ThreadTimingsDelta { #[doc(hidden)] pub struct ProfilingCollector { startup_time: Instant, - cursors: HashMap, + cursors: HashMap, } impl ProfilingCollector { @@ -195,7 +195,7 @@ impl ProfilingCollector { thread.thread_id.hash(&mut hasher); let hashed_id = hasher.finish(); - let prev_cursor = self.cursors.get(&hashed_id).copied().unwrap_or(0); + let prev_cursor = self.cursors.get(&thread.thread_id).copied().unwrap_or(0); let buffer_len = thread.timings.len() as u64; let buffer_start = thread.total_pushed.saturating_sub(buffer_len); @@ -205,7 +205,7 @@ impl ProfilingCollector { thread.timings.as_slice() } else { let skip = (prev_cursor - buffer_start) as usize; - &thread.timings[skip..] + &thread.timings[skip.min(thread.timings.len())..] }; // Don't emit the last entry if it's still in-progress (end: None). @@ -215,12 +215,12 @@ impl ProfilingCollector { } let cursor_advance = if incomplete_at_end { - thread.total_pushed - 1 + thread.total_pushed.saturating_sub(1) } else { thread.total_pushed }; - self.cursors.insert(hashed_id, cursor_advance); + self.cursors.insert(thread.thread_id, cursor_advance); if slice.is_empty() { continue; From 9386e20505636b938f6bdbc0038e755882d1e702 Mon Sep 17 00:00:00 2001 From: morgankrey Date: Mon, 16 Mar 2026 13:34:20 -0500 Subject: [PATCH 7/7] docs: Update Claude Sonnet 4.6 and Opus 4.6 context windows to 1M (#51695) ## Summary - Update the context window table in `models.md` to reflect that Claude Sonnet 4.6 and Opus 4.6 now support 1M token context windows (previously listed as 200k) - Remove Sonnet 4.5/4.6 from the "may increase in future releases" callout since the 4.6 models are now at 1M Release Notes: - N/A --- docs/src/ai/models.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/src/ai/models.md b/docs/src/ai/models.md index bbf41cf66cc4d93b38123c12fadd7a60c119dfef..a6bb25e929116faf6b75fae14e55f293c85829a5 100644 --- a/docs/src/ai/models.md +++ b/docs/src/ai/models.md @@ -83,9 +83,9 @@ A context window is the maximum span of text and code an LLM can consider at onc | Model | Provider | Zed-Hosted Context Window | | ----------------- | --------- | ------------------------- | | Claude Opus 4.5 | Anthropic | 200k | -| Claude Opus 4.6 | Anthropic | 200k | +| Claude Opus 4.6 | Anthropic | 1M | | Claude Sonnet 4.5 | Anthropic | 200k | -| Claude Sonnet 4.6 | Anthropic | 200k | +| Claude Sonnet 4.6 | Anthropic | 1M | | Claude Haiku 4.5 | Anthropic | 200k | | GPT-5.2 | OpenAI | 400k | | GPT-5.2 Codex | OpenAI | 400k | @@ -94,7 +94,7 @@ A context window is the maximum span of text and code an LLM can consider at onc | Gemini 3.1 Pro | Google | 200k | | Gemini 3 Flash | Google | 200k | -> Context window limits for hosted Sonnet 4.5/4.6 and Gemini 3.1 Pro/3 Pro/Flash may increase in future releases. +> Context window limits for hosted Gemini 3.1 Pro/3 Pro/Flash may increase in future releases. Each Agent thread and text thread in Zed maintains its own context window. The more prompts, attached files, and responses included in a session, the larger the context window grows.