Merge branch 'main' into agent-drawer

Max Brunsfeld created

Change summary

.github/workflows/extension_bump.yml                        |    2 
Cargo.lock                                                  |    4 
assets/settings/default.json                                |    8 
crates/acp_thread/src/acp_thread.rs                         |  119 
crates/acp_thread/src/connection.rs                         |   32 
crates/acp_tools/src/acp_tools.rs                           |   14 
crates/agent/src/agent.rs                                   |   64 
crates/agent/src/db.rs                                      |    7 
crates/agent/src/native_agent_server.rs                     |    7 
crates/agent/src/tests/mod.rs                               |   16 
crates/agent/src/thread_store.rs                            |   64 
crates/agent_servers/src/acp.rs                             |   62 
crates/agent_servers/src/agent_servers.rs                   |    6 
crates/agent_servers/src/custom.rs                          |  116 
crates/agent_servers/src/e2e_tests.rs                       |    9 
crates/agent_ui/src/agent_configuration.rs                  |    4 
crates/agent_ui/src/agent_connection_store.rs               |    5 
crates/agent_ui/src/agent_diff.rs                           |   18 
crates/agent_ui/src/agent_panel.rs                          |  136 
crates/agent_ui/src/agent_ui.rs                             |   35 
crates/agent_ui/src/connection_view.rs                      |  354 -
crates/agent_ui/src/connection_view/thread_view.rs          |   58 
crates/agent_ui/src/entry_view_state.rs                     |   22 
crates/agent_ui/src/message_editor.rs                       |   15 
crates/agent_ui/src/test_support.rs                         |    5 
crates/agent_ui/src/thread_history.rs                       |   14 
crates/agent_ui/src/thread_history_view.rs                  |    2 
crates/agent_ui/src/thread_metadata_store.rs                |  528 +++
crates/agent_ui/src/threads_archive_view.rs                 |   22 
crates/agent_ui/src/threads_panel.rs                        |  574 +-
crates/agent_ui/src/ui/acp_onboarding_modal.rs              |    4 
crates/agent_ui/src/ui/claude_agent_onboarding_modal.rs     |    4 
crates/anthropic/src/anthropic.rs                           |  104 
crates/audio/src/audio.rs                                   |  437 --
crates/audio/src/audio_pipeline.rs                          |  355 ++
crates/audio/src/audio_pipeline/echo_canceller.rs           |   54 
crates/audio/src/audio_pipeline/replays.rs                  |    3 
crates/audio/src/audio_pipeline/rodio_ext.rs                |    0 
crates/cloud_llm_client/src/cloud_llm_client.rs             |    4 
crates/collab_ui/src/notification_panel.rs                  |   12 
crates/collab_ui/src/panel_settings.rs                      |    2 
crates/edit_prediction/src/edit_prediction.rs               |   14 
crates/edit_prediction/src/edit_prediction_tests.rs         |   18 
crates/edit_prediction/src/fim.rs                           |   11 
crates/edit_prediction/src/mercury.rs                       |   12 
crates/edit_prediction/src/prediction.rs                    |   23 
crates/edit_prediction/src/sweep_ai.rs                      |   18 
crates/edit_prediction/src/zeta.rs                          |   17 
crates/edit_prediction_ui/src/rate_prediction_modal.rs      |   55 
crates/eval_cli/src/main.rs                                 |    7 
crates/file_finder/Cargo.toml                               |    1 
crates/file_finder/src/file_finder.rs                       |    8 
crates/file_finder/src/file_finder_tests.rs                 |   85 
crates/git_ui/src/conflict_view.rs                          |   17 
crates/git_ui/src/git_panel.rs                              |    8 
crates/git_ui/src/git_panel_settings.rs                     |    2 
crates/go_to_line/Cargo.toml                                |    1 
crates/go_to_line/src/go_to_line.rs                         |   36 
crates/gpui/src/styled.rs                                   |   51 
crates/language_selector/src/language_selector.rs           |  180 
crates/livekit_client/src/livekit_client/playback.rs        |   19 
crates/livekit_client/src/livekit_client/playback/source.rs |    5 
crates/multi_buffer/src/multi_buffer.rs                     |    2 
crates/multi_buffer/src/multi_buffer_tests.rs               |   24 
crates/project/src/agent_registry_store.rs                  |   12 
crates/project/src/agent_server_store.rs                    |   76 
crates/project/src/project.rs                               |    4 
crates/project/tests/integration/ext_agent_tests.rs         |    8 
crates/project/tests/integration/extension_agent_tests.rs   |   18 
crates/recent_projects/src/remote_connections.rs            |    8 
crates/recent_projects/src/remote_servers.rs                |   15 
crates/settings/src/vscode_import.rs                        |    1 
crates/settings_content/src/settings_content.rs             |    9 
crates/settings_content/src/terminal.rs                     |    4 
crates/settings_ui/src/page_data.rs                         |   72 
crates/settings_ui/src/pages/audio_test_window.rs           |    2 
crates/terminal/src/terminal_settings.rs                    |    2 
crates/terminal_view/src/terminal_panel.rs                  |   17 
crates/text/src/tests.rs                                    |   18 
crates/text/src/text.rs                                     |   31 
crates/ui/src/components.rs                                 |    2 
crates/ui/src/components/count_badge.rs                     |   93 
crates/util/src/path_list.rs                                |   17 
crates/workspace/src/dock.rs                                |   23 
crates/workspace/src/notifications.rs                       |    8 
crates/workspace/src/workspace.rs                           |    6 
crates/zed/src/visual_test_runner.rs                        |    7 
docs/acp-threads-in-sidebar-plan.md                         |  580 +++
extensions/glsl/Cargo.toml                                  |    2 
extensions/glsl/extension.toml                              |    2 
extensions/glsl/languages/glsl/highlights.scm               |    2 
output.txt                                                  | 1195 +++++++
plan.md                                                     |    2 
tooling/xtask/src/tasks/workflows/extension_bump.rs         |    2 
94 files changed, 4,471 insertions(+), 1,682 deletions(-)

Detailed changes

.github/workflows/extension_bump.yml 🔗

@@ -230,7 +230,7 @@ jobs:
 
         echo "extension_id=${EXTENSION_ID}" >> "$GITHUB_OUTPUT"
     - name: extension_bump::release_action
-      uses: zed-extensions/update-action@543925fc45da8866b0d017218a656c8a3296ed3f
+      uses: zed-extensions/update-action@1ef53b23be40fe2549be0baffaa98e9f51838fef
       with:
         extension-name: ${{ steps.get-extension-id.outputs.extension_id }}
         push-to: zed-industries/extensions

Cargo.lock 🔗

@@ -6296,7 +6296,6 @@ dependencies = [
  "serde",
  "serde_json",
  "settings",
- "text",
  "theme",
  "ui",
  "util",
@@ -7521,6 +7520,7 @@ dependencies = [
  "indoc",
  "language",
  "menu",
+ "multi_buffer",
  "project",
  "rope",
  "serde",
@@ -22123,7 +22123,7 @@ dependencies = [
 
 [[package]]
 name = "zed_glsl"
-version = "0.2.1"
+version = "0.2.2"
 dependencies = [
  "zed_extension_api 0.1.0",
 ]

assets/settings/default.json 🔗

@@ -922,6 +922,10 @@
     ///
     /// Default: false
     "tree_view": false,
+    // Whether to show a badge on the git panel icon with the count of uncommitted changes.
+    //
+    // Default: false
+    "show_count_badge": false,
     "scrollbar": {
       // When to show the scrollbar in the git panel.
       //
@@ -946,6 +950,8 @@
     "dock": "right",
     // Default width of the notification panel.
     "default_width": 380,
+    // Whether to show a badge on the notification panel icon with the count of unread notifications.
+    "show_count_badge": false,
   },
   "agent": {
     // Whether the inline assistant should use streaming tools, when available
@@ -1867,6 +1873,8 @@
     // Timeout for hover and Cmd-click path hyperlink discovery in milliseconds. Specifying a
     // timeout of `0` will disable path hyperlinking in terminal.
     "path_hyperlink_timeout_ms": 1,
+    // Whether to show a badge on the terminal panel icon with the count of open terminals.
+    "show_count_badge": false,
   },
   "code_actions_on_format": {},
   // Settings related to running tasks.

crates/acp_thread/src/acp_thread.rs 🔗

@@ -31,6 +31,7 @@ use task::{Shell, ShellBuilder};
 pub use terminal::*;
 use text::Bias;
 use ui::App;
+use util::path_list::PathList;
 use util::{ResultExt, get_default_system_shell_preferring_bash, paths::PathStyle};
 use uuid::Uuid;
 
@@ -953,7 +954,7 @@ struct RunningTurn {
 
 pub struct AcpThread {
     session_id: acp::SessionId,
-    cwd: Option<PathBuf>,
+    work_dirs: Option<PathList>,
     parent_session_id: Option<acp::SessionId>,
     title: SharedString,
     provisional_title: Option<SharedString>,
@@ -1119,7 +1120,7 @@ impl AcpThread {
     pub fn new(
         parent_session_id: Option<acp::SessionId>,
         title: impl Into<SharedString>,
-        cwd: Option<PathBuf>,
+        work_dirs: Option<PathList>,
         connection: Rc<dyn AgentConnection>,
         project: Entity<Project>,
         action_log: Entity<ActionLog>,
@@ -1140,7 +1141,7 @@ impl AcpThread {
 
         Self {
             parent_session_id,
-            cwd,
+            work_dirs,
             action_log,
             shared_buffers: Default::default(),
             entries: Default::default(),
@@ -1219,8 +1220,8 @@ impl AcpThread {
         &self.session_id
     }
 
-    pub fn cwd(&self) -> Option<&PathBuf> {
-        self.cwd.as_ref()
+    pub fn work_dirs(&self) -> Option<&PathList> {
+        self.work_dirs.as_ref()
     }
 
     pub fn status(&self) -> ThreadStatus {
@@ -2858,7 +2859,7 @@ mod tests {
     use futures::{channel::mpsc, future::LocalBoxFuture, select};
     use gpui::{App, AsyncApp, TestAppContext, WeakEntity};
     use indoc::indoc;
-    use project::{FakeFs, Fs};
+    use project::{AgentId, FakeFs, Fs};
     use rand::{distr, prelude::*};
     use serde_json::json;
     use settings::SettingsStore;
@@ -2871,7 +2872,7 @@ mod tests {
         sync::atomic::{AtomicBool, AtomicUsize, Ordering::SeqCst},
         time::Duration,
     };
-    use util::path;
+    use util::{path, path_list::PathList};
 
     fn init_test(cx: &mut TestAppContext) {
         env_logger::try_init().ok();
@@ -2889,7 +2890,13 @@ mod tests {
         let project = Project::test(fs, [], cx).await;
         let connection = Rc::new(FakeAgentConnection::new());
         let thread = cx
-            .update(|cx| connection.new_session(project, std::path::Path::new(path!("/test")), cx))
+            .update(|cx| {
+                connection.new_session(
+                    project,
+                    PathList::new(&[std::path::Path::new(path!("/test"))]),
+                    cx,
+                )
+            })
             .await
             .unwrap();
 
@@ -2953,7 +2960,13 @@ mod tests {
         let project = Project::test(fs, [], cx).await;
         let connection = Rc::new(FakeAgentConnection::new());
         let thread = cx
-            .update(|cx| connection.new_session(project, std::path::Path::new(path!("/test")), cx))
+            .update(|cx| {
+                connection.new_session(
+                    project,
+                    PathList::new(&[std::path::Path::new(path!("/test"))]),
+                    cx,
+                )
+            })
             .await
             .unwrap();
 
@@ -3041,7 +3054,13 @@ mod tests {
         let project = Project::test(fs, [], cx).await;
         let connection = Rc::new(FakeAgentConnection::new());
         let thread = cx
-            .update(|cx| connection.new_session(project.clone(), Path::new(path!("/test")), cx))
+            .update(|cx| {
+                connection.new_session(
+                    project.clone(),
+                    PathList::new(&[Path::new(path!("/test"))]),
+                    cx,
+                )
+            })
             .await
             .unwrap();
 
@@ -3152,7 +3171,9 @@ mod tests {
         let project = Project::test(fs, [], cx).await;
         let connection = Rc::new(FakeAgentConnection::new());
         let thread = cx
-            .update(|cx| connection.new_session(project, Path::new(path!("/test")), cx))
+            .update(|cx| {
+                connection.new_session(project, PathList::new(&[Path::new(path!("/test"))]), cx)
+            })
             .await
             .unwrap();
 
@@ -3246,7 +3267,9 @@ mod tests {
         ));
 
         let thread = cx
-            .update(|cx| connection.new_session(project, Path::new(path!("/test")), cx))
+            .update(|cx| {
+                connection.new_session(project, PathList::new(&[Path::new(path!("/test"))]), cx)
+            })
             .await
             .unwrap();
 
@@ -3327,7 +3350,9 @@ mod tests {
             .unwrap();
 
         let thread = cx
-            .update(|cx| connection.new_session(project, Path::new(path!("/tmp")), cx))
+            .update(|cx| {
+                connection.new_session(project, PathList::new(&[Path::new(path!("/tmp"))]), cx)
+            })
             .await
             .unwrap();
 
@@ -3368,7 +3393,9 @@ mod tests {
         let connection = Rc::new(FakeAgentConnection::new());
 
         let thread = cx
-            .update(|cx| connection.new_session(project, Path::new(path!("/tmp")), cx))
+            .update(|cx| {
+                connection.new_session(project, PathList::new(&[Path::new(path!("/tmp"))]), cx)
+            })
             .await
             .unwrap();
 
@@ -3443,7 +3470,9 @@ mod tests {
         let connection = Rc::new(FakeAgentConnection::new());
 
         let thread = cx
-            .update(|cx| connection.new_session(project, Path::new(path!("/tmp")), cx))
+            .update(|cx| {
+                connection.new_session(project, PathList::new(&[Path::new(path!("/tmp"))]), cx)
+            })
             .await
             .unwrap();
 
@@ -3517,7 +3546,9 @@ mod tests {
         let connection = Rc::new(FakeAgentConnection::new());
 
         let thread = cx
-            .update(|cx| connection.new_session(project, Path::new(path!("/tmp")), cx))
+            .update(|cx| {
+                connection.new_session(project, PathList::new(&[Path::new(path!("/tmp"))]), cx)
+            })
             .await
             .unwrap();
 
@@ -3565,7 +3596,9 @@ mod tests {
         }));
 
         let thread = cx
-            .update(|cx| connection.new_session(project, Path::new(path!("/test")), cx))
+            .update(|cx| {
+                connection.new_session(project, PathList::new(&[Path::new(path!("/test"))]), cx)
+            })
             .await
             .unwrap();
 
@@ -3656,7 +3689,9 @@ mod tests {
         }));
 
         let thread = cx
-            .update(|cx| connection.new_session(project, Path::new(path!("/test")), cx))
+            .update(|cx| {
+                connection.new_session(project, PathList::new(&[Path::new(path!("/test"))]), cx)
+            })
             .await
             .unwrap();
 
@@ -3715,7 +3750,9 @@ mod tests {
             }
         }));
         let thread = cx
-            .update(|cx| connection.new_session(project, Path::new(path!("/test")), cx))
+            .update(|cx| {
+                connection.new_session(project, PathList::new(&[Path::new(path!("/test"))]), cx)
+            })
             .await
             .unwrap();
 
@@ -3888,7 +3925,9 @@ mod tests {
         }));
 
         let thread = cx
-            .update(|cx| connection.new_session(project, Path::new(path!("/test")), cx))
+            .update(|cx| {
+                connection.new_session(project, PathList::new(&[Path::new(path!("/test"))]), cx)
+            })
             .await
             .unwrap();
 
@@ -3964,7 +4003,9 @@ mod tests {
         }));
 
         let thread = cx
-            .update(|cx| connection.new_session(project, Path::new(path!("/test")), cx))
+            .update(|cx| {
+                connection.new_session(project, PathList::new(&[Path::new(path!("/test"))]), cx)
+            })
             .await
             .unwrap();
 
@@ -4037,7 +4078,9 @@ mod tests {
             }
         }));
         let thread = cx
-            .update(|cx| connection.new_session(project, Path::new(path!("/test")), cx))
+            .update(|cx| {
+                connection.new_session(project, PathList::new(&[Path::new(path!("/test"))]), cx)
+            })
             .await
             .unwrap();
 
@@ -4158,6 +4201,10 @@ mod tests {
     }
 
     impl AgentConnection for FakeAgentConnection {
+        fn agent_id(&self) -> AgentId {
+            AgentId::new("fake")
+        }
+
         fn telemetry_id(&self) -> SharedString {
             "fake".into()
         }
@@ -4169,7 +4216,7 @@ mod tests {
         fn new_session(
             self: Rc<Self>,
             project: Entity<Project>,
-            cwd: &Path,
+            work_dirs: PathList,
             cx: &mut App,
         ) -> Task<gpui::Result<Entity<AcpThread>>> {
             let session_id = acp::SessionId::new(
@@ -4184,7 +4231,7 @@ mod tests {
                 AcpThread::new(
                     None,
                     "Test",
-                    Some(cwd.to_path_buf()),
+                    Some(work_dirs),
                     self.clone(),
                     project,
                     action_log,
@@ -4283,7 +4330,9 @@ mod tests {
         let project = Project::test(fs, [], cx).await;
         let connection = Rc::new(FakeAgentConnection::new());
         let thread = cx
-            .update(|cx| connection.new_session(project, Path::new(path!("/test")), cx))
+            .update(|cx| {
+                connection.new_session(project, PathList::new(&[Path::new(path!("/test"))]), cx)
+            })
             .await
             .unwrap();
 
@@ -4349,7 +4398,9 @@ mod tests {
         let project = Project::test(fs, [], cx).await;
         let connection = Rc::new(FakeAgentConnection::new());
         let thread = cx
-            .update(|cx| connection.new_session(project, Path::new(path!("/test")), cx))
+            .update(|cx| {
+                connection.new_session(project, PathList::new(&[Path::new(path!("/test"))]), cx)
+            })
             .await
             .unwrap();
 
@@ -4662,7 +4713,9 @@ mod tests {
         ));
 
         let thread = cx
-            .update(|cx| connection.new_session(project, Path::new(path!("/test")), cx))
+            .update(|cx| {
+                connection.new_session(project, PathList::new(&[Path::new(path!("/test"))]), cx)
+            })
             .await
             .unwrap();
 
@@ -4736,7 +4789,9 @@ mod tests {
         }));
 
         let thread = cx
-            .update(|cx| connection.new_session(project, Path::new(path!("/test")), cx))
+            .update(|cx| {
+                connection.new_session(project, PathList::new(&[Path::new(path!("/test"))]), cx)
+            })
             .await
             .unwrap();
 
@@ -4819,7 +4874,9 @@ mod tests {
         ));
 
         let thread = cx
-            .update(|cx| connection.new_session(project, Path::new(path!("/test")), cx))
+            .update(|cx| {
+                connection.new_session(project, PathList::new(&[Path::new(path!("/test"))]), cx)
+            })
             .await
             .unwrap();
 
@@ -4867,7 +4924,9 @@ mod tests {
         let set_title_calls = connection.set_title_calls.clone();
 
         let thread = cx
-            .update(|cx| connection.new_session(project, Path::new(path!("/test")), cx))
+            .update(|cx| {
+                connection.new_session(project, PathList::new(&[Path::new(path!("/test"))]), cx)
+            })
             .await
             .unwrap();
 

crates/acp_thread/src/connection.rs 🔗

@@ -5,17 +5,11 @@ use chrono::{DateTime, Utc};
 use collections::IndexMap;
 use gpui::{Entity, SharedString, Task};
 use language_model::LanguageModelProviderId;
-use project::Project;
+use project::{AgentId, Project};
 use serde::{Deserialize, Serialize};
-use std::{
-    any::Any,
-    error::Error,
-    fmt,
-    path::{Path, PathBuf},
-    rc::Rc,
-    sync::Arc,
-};
+use std::{any::Any, error::Error, fmt, path::PathBuf, rc::Rc, sync::Arc};
 use ui::{App, IconName};
+use util::path_list::PathList;
 use uuid::Uuid;
 
 #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Hash)]
@@ -28,12 +22,14 @@ impl UserMessageId {
 }
 
 pub trait AgentConnection {
+    fn agent_id(&self) -> AgentId;
+
     fn telemetry_id(&self) -> SharedString;
 
     fn new_session(
         self: Rc<Self>,
         project: Entity<Project>,
-        cwd: &Path,
+        _work_dirs: PathList,
         cx: &mut App,
     ) -> Task<Result<Entity<AcpThread>>>;
 
@@ -47,7 +43,7 @@ pub trait AgentConnection {
         self: Rc<Self>,
         _session_id: acp::SessionId,
         _project: Entity<Project>,
-        _cwd: &Path,
+        _work_dirs: PathList,
         _title: Option<SharedString>,
         _cx: &mut App,
     ) -> Task<Result<Entity<AcpThread>>> {
@@ -78,7 +74,7 @@ pub trait AgentConnection {
         self: Rc<Self>,
         _session_id: acp::SessionId,
         _project: Entity<Project>,
-        _cwd: &Path,
+        _work_dirs: PathList,
         _title: Option<SharedString>,
         _cx: &mut App,
     ) -> Task<Result<Entity<AcpThread>>> {
@@ -243,7 +239,7 @@ impl AgentSessionListResponse {
 #[derive(Debug, Clone, PartialEq)]
 pub struct AgentSessionInfo {
     pub session_id: acp::SessionId,
-    pub cwd: Option<PathBuf>,
+    pub work_dirs: Option<PathList>,
     pub title: Option<SharedString>,
     pub updated_at: Option<DateTime<Utc>>,
     pub created_at: Option<DateTime<Utc>>,
@@ -254,7 +250,7 @@ impl AgentSessionInfo {
     pub fn new(session_id: impl Into<acp::SessionId>) -> Self {
         Self {
             session_id: session_id.into(),
-            cwd: None,
+            work_dirs: None,
             title: None,
             updated_at: None,
             created_at: None,
@@ -609,6 +605,10 @@ mod test_support {
     }
 
     impl AgentConnection for StubAgentConnection {
+        fn agent_id(&self) -> AgentId {
+            AgentId::new("stub")
+        }
+
         fn telemetry_id(&self) -> SharedString {
             "stub".into()
         }
@@ -627,7 +627,7 @@ mod test_support {
         fn new_session(
             self: Rc<Self>,
             project: Entity<Project>,
-            cwd: &Path,
+            work_dirs: PathList,
             cx: &mut gpui::App,
         ) -> Task<gpui::Result<Entity<AcpThread>>> {
             static NEXT_SESSION_ID: AtomicUsize = AtomicUsize::new(0);
@@ -638,7 +638,7 @@ mod test_support {
                 AcpThread::new(
                     None,
                     "Test",
-                    Some(cwd.to_path_buf()),
+                    Some(work_dirs),
                     self.clone(),
                     project,
                     action_log,

crates/acp_tools/src/acp_tools.rs 🔗

@@ -14,7 +14,7 @@ use gpui::{
 };
 use language::LanguageRegistry;
 use markdown::{CodeBlockRenderer, Markdown, MarkdownElement, MarkdownStyle};
-use project::Project;
+use project::{AgentId, Project};
 use settings::Settings;
 use theme::ThemeSettings;
 use ui::{CopyButton, Tooltip, WithScrollbar, prelude::*};
@@ -48,7 +48,7 @@ pub struct AcpConnectionRegistry {
 }
 
 struct ActiveConnection {
-    server_name: SharedString,
+    agent_id: AgentId,
     connection: Weak<acp::ClientSideConnection>,
 }
 
@@ -65,12 +65,12 @@ impl AcpConnectionRegistry {
 
     pub fn set_active_connection(
         &self,
-        server_name: impl Into<SharedString>,
+        agent_id: AgentId,
         connection: &Rc<acp::ClientSideConnection>,
         cx: &mut Context<Self>,
     ) {
         self.active_connection.replace(Some(ActiveConnection {
-            server_name: server_name.into(),
+            agent_id,
             connection: Rc::downgrade(connection),
         }));
         cx.notify();
@@ -87,7 +87,7 @@ struct AcpTools {
 }
 
 struct WatchedConnection {
-    server_name: SharedString,
+    agent_id: AgentId,
     messages: Vec<WatchedConnectionMessage>,
     list_state: ListState,
     connection: Weak<acp::ClientSideConnection>,
@@ -144,7 +144,7 @@ impl AcpTools {
             });
 
             self.watched_connection = Some(WatchedConnection {
-                server_name: active_connection.server_name.clone(),
+                agent_id: active_connection.agent_id.clone(),
                 messages: vec![],
                 list_state: ListState::new(0, ListAlignment::Bottom, px(2048.)),
                 connection: active_connection.connection.clone(),
@@ -483,7 +483,7 @@ impl Item for AcpTools {
             "ACP: {}",
             self.watched_connection
                 .as_ref()
-                .map_or("Disconnected", |connection| &connection.server_name)
+                .map_or("Disconnected", |connection| connection.agent_id.0.as_ref())
         )
         .into()
     }

crates/agent/src/agent.rs 🔗

@@ -41,7 +41,7 @@ use gpui::{
     WeakEntity,
 };
 use language_model::{IconOrSvg, LanguageModel, LanguageModelProvider, LanguageModelRegistry};
-use project::{Project, ProjectItem, ProjectPath, Worktree};
+use project::{AgentId, Project, ProjectItem, ProjectPath, Worktree};
 use prompt_store::{
     ProjectContext, PromptStore, RULES_FILE_NAMES, RulesFileContext, UserRulesContext,
     WorktreeContext,
@@ -49,9 +49,9 @@ use prompt_store::{
 use serde::{Deserialize, Serialize};
 use settings::{LanguageModelSelection, update_settings_file};
 use std::any::Any;
-use std::path::{Path, PathBuf};
+use std::path::PathBuf;
 use std::rc::Rc;
-use std::sync::Arc;
+use std::sync::{Arc, LazyLock};
 use util::ResultExt;
 use util::path_list::PathList;
 use util::rel_path::RelPath;
@@ -1381,7 +1381,13 @@ impl acp_thread::AgentModelSelector for NativeAgentModelSelector {
     }
 }
 
+pub static ZED_AGENT_ID: LazyLock<AgentId> = LazyLock::new(|| AgentId::new("Zed Agent"));
+
 impl acp_thread::AgentConnection for NativeAgentConnection {
+    fn agent_id(&self) -> AgentId {
+        ZED_AGENT_ID.clone()
+    }
+
     fn telemetry_id(&self) -> SharedString {
         "zed".into()
     }
@@ -1389,10 +1395,10 @@ impl acp_thread::AgentConnection for NativeAgentConnection {
     fn new_session(
         self: Rc<Self>,
         project: Entity<Project>,
-        cwd: &Path,
+        work_dirs: PathList,
         cx: &mut App,
     ) -> Task<Result<Entity<acp_thread::AcpThread>>> {
-        log::debug!("Creating new thread for project at: {cwd:?}");
+        log::debug!("Creating new thread for project at: {work_dirs:?}");
         Task::ready(Ok(self
             .0
             .update(cx, |agent, cx| agent.new_session(project, cx))))
@@ -1406,7 +1412,7 @@ impl acp_thread::AgentConnection for NativeAgentConnection {
         self: Rc<Self>,
         session_id: acp::SessionId,
         project: Entity<Project>,
-        _cwd: &Path,
+        _work_dirs: PathList,
         _title: Option<SharedString>,
         cx: &mut App,
     ) -> Task<Result<Entity<acp_thread::AcpThread>>> {
@@ -2079,6 +2085,8 @@ impl TerminalHandle for AcpTerminalHandle {
 
 #[cfg(test)]
 mod internal_tests {
+    use std::path::Path;
+
     use super::*;
     use acp_thread::{AgentConnection, AgentModelGroupName, AgentModelInfo, MentionUri};
     use fs::FakeFs;
@@ -2111,7 +2119,13 @@ mod internal_tests {
         // Creating a session registers the project and triggers context building.
         let connection = NativeAgentConnection(agent.clone());
         let _acp_thread = cx
-            .update(|cx| Rc::new(connection).new_session(project.clone(), Path::new("/"), cx))
+            .update(|cx| {
+                Rc::new(connection).new_session(
+                    project.clone(),
+                    PathList::new(&[Path::new("/")]),
+                    cx,
+                )
+            })
             .await
             .unwrap();
         cx.run_until_parked();
@@ -2180,7 +2194,11 @@ mod internal_tests {
         // Create a thread/session
         let acp_thread = cx
             .update(|cx| {
-                Rc::new(connection.clone()).new_session(project.clone(), Path::new("/a"), cx)
+                Rc::new(connection.clone()).new_session(
+                    project.clone(),
+                    PathList::new(&[Path::new("/a")]),
+                    cx,
+                )
             })
             .await
             .unwrap();
@@ -2251,7 +2269,11 @@ mod internal_tests {
         // Create a thread/session
         let acp_thread = cx
             .update(|cx| {
-                Rc::new(connection.clone()).new_session(project.clone(), Path::new("/a"), cx)
+                Rc::new(connection.clone()).new_session(
+                    project.clone(),
+                    PathList::new(&[Path::new("/a")]),
+                    cx,
+                )
             })
             .await
             .unwrap();
@@ -2343,7 +2365,11 @@ mod internal_tests {
 
         let acp_thread = cx
             .update(|cx| {
-                Rc::new(connection.clone()).new_session(project.clone(), Path::new("/a"), cx)
+                Rc::new(connection.clone()).new_session(
+                    project.clone(),
+                    PathList::new(&[Path::new("/a")]),
+                    cx,
+                )
             })
             .await
             .unwrap();
@@ -2450,9 +2476,11 @@ mod internal_tests {
         // Create a thread and select the thinking model.
         let acp_thread = cx
             .update(|cx| {
-                connection
-                    .clone()
-                    .new_session(project.clone(), Path::new("/a"), cx)
+                connection.clone().new_session(
+                    project.clone(),
+                    PathList::new(&[Path::new("/a")]),
+                    cx,
+                )
             })
             .await
             .unwrap();
@@ -2552,9 +2580,11 @@ mod internal_tests {
         // Create a thread and select the model.
         let acp_thread = cx
             .update(|cx| {
-                connection
-                    .clone()
-                    .new_session(project.clone(), Path::new("/a"), cx)
+                connection.clone().new_session(
+                    project.clone(),
+                    PathList::new(&[Path::new("/a")]),
+                    cx,
+                )
             })
             .await
             .unwrap();
@@ -2645,7 +2675,7 @@ mod internal_tests {
             .update(|cx| {
                 connection
                     .clone()
-                    .new_session(project.clone(), Path::new(""), cx)
+                    .new_session(project.clone(), PathList::new(&[Path::new("")]), cx)
             })
             .await
             .unwrap();

crates/agent/src/db.rs 🔗

@@ -25,11 +25,10 @@ pub type DbMessage = crate::Message;
 pub type DbSummary = crate::legacy_thread::DetailedSummaryState;
 pub type DbLanguageModel = crate::legacy_thread::SerializedLanguageModel;
 
-#[derive(Debug, Clone, Serialize, Deserialize)]
+#[derive(Debug, Clone)]
 pub struct DbThreadMetadata {
     pub id: acp::SessionId,
     pub parent_session_id: Option<acp::SessionId>,
-    #[serde(alias = "summary")]
     pub title: SharedString,
     pub updated_at: DateTime<Utc>,
     pub created_at: Option<DateTime<Utc>>,
@@ -42,7 +41,7 @@ impl From<&DbThreadMetadata> for acp_thread::AgentSessionInfo {
     fn from(meta: &DbThreadMetadata) -> Self {
         Self {
             session_id: meta.id.clone(),
-            cwd: None,
+            work_dirs: Some(meta.folder_paths.clone()),
             title: Some(meta.title.clone()),
             updated_at: Some(meta.updated_at),
             created_at: meta.created_at,
@@ -881,7 +880,6 @@ mod tests {
 
         let threads = database.list_threads().await.unwrap();
         assert_eq!(threads.len(), 1);
-        assert_eq!(threads[0].folder_paths, folder_paths);
     }
 
     #[gpui::test]
@@ -901,7 +899,6 @@ mod tests {
 
         let threads = database.list_threads().await.unwrap();
         assert_eq!(threads.len(), 1);
-        assert!(threads[0].folder_paths.is_empty());
     }
 
     #[test]

crates/agent/src/native_agent_server.rs 🔗

@@ -6,7 +6,8 @@ use agent_settings::AgentSettings;
 use anyhow::Result;
 use collections::HashSet;
 use fs::Fs;
-use gpui::{App, Entity, SharedString, Task};
+use gpui::{App, Entity, Task};
+use project::AgentId;
 use prompt_store::PromptStore;
 use settings::{LanguageModelSelection, Settings as _, update_settings_file};
 
@@ -25,8 +26,8 @@ impl NativeAgentServer {
 }
 
 impl AgentServer for NativeAgentServer {
-    fn name(&self) -> SharedString {
-        "Zed Agent".into()
+    fn agent_id(&self) -> AgentId {
+        crate::ZED_AGENT_ID.clone()
     }
 
     fn logo(&self) -> ui::IconName {

crates/agent/src/tests/mod.rs 🔗

@@ -3177,7 +3177,7 @@ async fn test_agent_connection(cx: &mut TestAppContext) {
     let fake_fs = cx.update(|cx| fs::FakeFs::new(cx.background_executor().clone()));
     fake_fs.insert_tree(path!("/test"), json!({})).await;
     let project = Project::test(fake_fs.clone(), [Path::new("/test")], cx).await;
-    let cwd = Path::new("/test");
+    let cwd = PathList::new(&[Path::new("/test")]);
     let thread_store = cx.new(|cx| ThreadStore::new(cx));
 
     // Create agent and connection
@@ -4389,7 +4389,7 @@ async fn test_subagent_tool_call_end_to_end(cx: &mut TestAppContext) {
         .update(|cx| {
             connection
                 .clone()
-                .new_session(project.clone(), Path::new(""), cx)
+                .new_session(project.clone(), PathList::new(&[Path::new("")]), cx)
         })
         .await
         .unwrap();
@@ -4524,7 +4524,7 @@ async fn test_subagent_tool_output_does_not_include_thinking(cx: &mut TestAppCon
         .update(|cx| {
             connection
                 .clone()
-                .new_session(project.clone(), Path::new(""), cx)
+                .new_session(project.clone(), PathList::new(&[Path::new("")]), cx)
         })
         .await
         .unwrap();
@@ -4672,7 +4672,7 @@ async fn test_subagent_tool_call_cancellation_during_task_prompt(cx: &mut TestAp
         .update(|cx| {
             connection
                 .clone()
-                .new_session(project.clone(), Path::new(""), cx)
+                .new_session(project.clone(), PathList::new(&[Path::new("")]), cx)
         })
         .await
         .unwrap();
@@ -4802,7 +4802,7 @@ async fn test_subagent_tool_resume_session(cx: &mut TestAppContext) {
         .update(|cx| {
             connection
                 .clone()
-                .new_session(project.clone(), Path::new(""), cx)
+                .new_session(project.clone(), PathList::new(&[Path::new("")]), cx)
         })
         .await
         .unwrap();
@@ -5174,7 +5174,7 @@ async fn test_subagent_context_window_warning(cx: &mut TestAppContext) {
         .update(|cx| {
             connection
                 .clone()
-                .new_session(project.clone(), Path::new(""), cx)
+                .new_session(project.clone(), PathList::new(&[Path::new("")]), cx)
         })
         .await
         .unwrap();
@@ -5300,7 +5300,7 @@ async fn test_subagent_no_context_window_warning_when_already_at_warning(cx: &mu
         .update(|cx| {
             connection
                 .clone()
-                .new_session(project.clone(), Path::new(""), cx)
+                .new_session(project.clone(), PathList::new(&[Path::new("")]), cx)
         })
         .await
         .unwrap();
@@ -5474,7 +5474,7 @@ async fn test_subagent_error_propagation(cx: &mut TestAppContext) {
         .update(|cx| {
             connection
                 .clone()
-                .new_session(project.clone(), Path::new(""), cx)
+                .new_session(project.clone(), PathList::new(&[Path::new("")]), cx)
         })
         .await
         .unwrap();

crates/agent/src/thread_store.rs 🔗

@@ -2,7 +2,6 @@ use crate::{DbThread, DbThreadMetadata, ThreadsDatabase};
 use agent_client_protocol as acp;
 use anyhow::{Result, anyhow};
 use gpui::{App, Context, Entity, Global, Task, prelude::*};
-use std::collections::HashMap;
 use util::path_list::PathList;
 
 struct GlobalThreadStore(Entity<ThreadStore>);
@@ -11,7 +10,6 @@ impl Global for GlobalThreadStore {}
 
 pub struct ThreadStore {
     threads: Vec<DbThreadMetadata>,
-    threads_by_paths: HashMap<PathList, Vec<usize>>,
 }
 
 impl ThreadStore {
@@ -31,7 +29,6 @@ impl ThreadStore {
     pub fn new(cx: &mut Context<Self>) -> Self {
         let this = Self {
             threads: Vec::new(),
-            threads_by_paths: HashMap::default(),
         };
         this.reload(cx);
         this
@@ -97,16 +94,10 @@ impl ThreadStore {
             let all_threads = database.list_threads().await?;
             this.update(cx, |this, cx| {
                 this.threads.clear();
-                this.threads_by_paths.clear();
                 for thread in all_threads {
                     if thread.parent_session_id.is_some() {
                         continue;
                     }
-                    let index = this.threads.len();
-                    this.threads_by_paths
-                        .entry(thread.folder_paths.clone())
-                        .or_default()
-                        .push(index);
                     this.threads.push(thread);
                 }
                 cx.notify();
@@ -122,15 +113,6 @@ impl ThreadStore {
     pub fn entries(&self) -> impl Iterator<Item = DbThreadMetadata> + '_ {
         self.threads.iter().cloned()
     }
-
-    /// Returns threads whose folder_paths match the given paths exactly.
-    /// Uses a cached index for O(1) lookup per path list.
-    pub fn threads_for_paths(&self, paths: &PathList) -> impl Iterator<Item = &DbThreadMetadata> {
-        self.threads_by_paths
-            .get(paths)
-            .into_iter()
-            .flat_map(|indices| indices.iter().map(|&index| &self.threads[index]))
-    }
 }
 
 #[cfg(test)]
@@ -306,50 +288,4 @@ mod tests {
         assert_eq!(entries[0].id, first_id);
         assert_eq!(entries[1].id, second_id);
     }
-
-    #[gpui::test]
-    async fn test_threads_for_paths_filters_correctly(cx: &mut TestAppContext) {
-        let thread_store = cx.new(|cx| ThreadStore::new(cx));
-        cx.run_until_parked();
-
-        let project_a_paths = PathList::new(&[std::path::PathBuf::from("/home/user/project-a")]);
-        let project_b_paths = PathList::new(&[std::path::PathBuf::from("/home/user/project-b")]);
-
-        let thread_a = make_thread(
-            "Thread in A",
-            Utc.with_ymd_and_hms(2024, 1, 1, 0, 0, 0).unwrap(),
-        );
-        let thread_b = make_thread(
-            "Thread in B",
-            Utc.with_ymd_and_hms(2024, 1, 2, 0, 0, 0).unwrap(),
-        );
-        let thread_a_id = session_id("thread-a");
-        let thread_b_id = session_id("thread-b");
-
-        let save_a = thread_store.update(cx, |store, cx| {
-            store.save_thread(thread_a_id.clone(), thread_a, project_a_paths.clone(), cx)
-        });
-        save_a.await.unwrap();
-
-        let save_b = thread_store.update(cx, |store, cx| {
-            store.save_thread(thread_b_id.clone(), thread_b, project_b_paths.clone(), cx)
-        });
-        save_b.await.unwrap();
-
-        cx.run_until_parked();
-
-        thread_store.read_with(cx, |store, _cx| {
-            let a_threads: Vec<_> = store.threads_for_paths(&project_a_paths).collect();
-            assert_eq!(a_threads.len(), 1);
-            assert_eq!(a_threads[0].id, thread_a_id);
-
-            let b_threads: Vec<_> = store.threads_for_paths(&project_b_paths).collect();
-            assert_eq!(b_threads.len(), 1);
-            assert_eq!(b_threads[0].id, thread_b_id);
-
-            let nonexistent = PathList::new(&[std::path::PathBuf::from("/nonexistent")]);
-            let no_threads: Vec<_> = store.threads_for_paths(&nonexistent).collect();
-            assert!(no_threads.is_empty());
-        });
-    }
 }

crates/agent_servers/src/acp.rs 🔗

@@ -9,18 +9,19 @@ use anyhow::anyhow;
 use collections::HashMap;
 use futures::AsyncBufReadExt as _;
 use futures::io::BufReader;
-use project::Project;
-use project::agent_server_store::{AgentServerCommand, GEMINI_NAME};
+use project::agent_server_store::{AgentServerCommand, GEMINI_ID};
+use project::{AgentId, Project};
 use serde::Deserialize;
 use settings::Settings as _;
 use task::ShellBuilder;
 use util::ResultExt as _;
+use util::path_list::PathList;
 use util::process::Child;
 
 use std::path::PathBuf;
 use std::process::Stdio;
+use std::rc::Rc;
 use std::{any::Any, cell::RefCell};
-use std::{path::Path, rc::Rc};
 use thiserror::Error;
 
 use anyhow::{Context as _, Result};
@@ -35,7 +36,7 @@ use terminal::terminal_settings::{AlternateScroll, CursorShape, TerminalSettings
 pub struct UnsupportedVersion;
 
 pub struct AcpConnection {
-    server_name: SharedString,
+    id: AgentId,
     display_name: SharedString,
     telemetry_id: SharedString,
     connection: Rc<acp::ClientSideConnection>,
@@ -124,7 +125,7 @@ impl AgentSessionList for AcpSessionList {
                     .into_iter()
                     .map(|s| AgentSessionInfo {
                         session_id: s.session_id,
-                        cwd: Some(s.cwd),
+                        work_dirs: Some(PathList::new(&[s.cwd])),
                         title: s.title.map(Into::into),
                         updated_at: s.updated_at.and_then(|date_str| {
                             chrono::DateTime::parse_from_rfc3339(&date_str)
@@ -158,7 +159,7 @@ impl AgentSessionList for AcpSessionList {
 }
 
 pub async fn connect(
-    server_name: SharedString,
+    agent_id: AgentId,
     display_name: SharedString,
     command: AgentServerCommand,
     default_mode: Option<acp::SessionModeId>,
@@ -167,7 +168,7 @@ pub async fn connect(
     cx: &mut AsyncApp,
 ) -> Result<Rc<dyn AgentConnection>> {
     let conn = AcpConnection::stdio(
-        server_name,
+        agent_id,
         display_name,
         command.clone(),
         default_mode,
@@ -183,7 +184,7 @@ const MINIMUM_SUPPORTED_VERSION: acp::ProtocolVersion = acp::ProtocolVersion::V1
 
 impl AcpConnection {
     pub async fn stdio(
-        server_name: SharedString,
+        agent_id: AgentId,
         display_name: SharedString,
         command: AgentServerCommand,
         default_mode: Option<acp::SessionModeId>,
@@ -270,7 +271,7 @@ impl AcpConnection {
 
         cx.update(|cx| {
             AcpConnectionRegistry::default_global(cx).update(cx, |registry, cx| {
-                registry.set_active_connection(server_name.clone(), &connection, cx)
+                registry.set_active_connection(agent_id.clone(), &connection, cx)
             });
         });
 
@@ -305,7 +306,7 @@ impl AcpConnection {
             // Use the one the agent provides if we have one
             .map(|info| info.name.into())
             // Otherwise, just use the name
-            .unwrap_or_else(|| server_name.clone());
+            .unwrap_or_else(|| agent_id.0.to_string().into());
 
         let session_list = if response
             .agent_capabilities
@@ -321,7 +322,7 @@ impl AcpConnection {
         };
 
         // TODO: Remove this override once Google team releases their official auth methods
-        let auth_methods = if server_name == GEMINI_NAME {
+        let auth_methods = if agent_id.0.as_ref() == GEMINI_ID {
             let mut args = command.args.clone();
             args.retain(|a| a != "--experimental-acp");
             let value = serde_json::json!({
@@ -340,9 +341,9 @@ impl AcpConnection {
             response.auth_methods
         };
         Ok(Self {
+            id: agent_id,
             auth_methods,
             connection,
-            server_name,
             display_name,
             telemetry_id,
             sessions,
@@ -368,7 +369,7 @@ impl AcpConnection {
         config_options: &Rc<RefCell<Vec<acp::SessionConfigOption>>>,
         cx: &mut AsyncApp,
     ) {
-        let name = self.server_name.clone();
+        let id = self.id.clone();
         let defaults_to_apply: Vec<_> = {
             let config_opts_ref = config_options.borrow();
             config_opts_ref
@@ -410,7 +411,7 @@ impl AcpConnection {
                             "`{}` is not a valid value for config option `{}` in {}",
                             default_value,
                             config_option.id.0,
-                            name
+                            id
                         );
                         None
                     }
@@ -466,6 +467,10 @@ impl Drop for AcpConnection {
 }
 
 impl AgentConnection for AcpConnection {
+    fn agent_id(&self) -> AgentId {
+        self.id.clone()
+    }
+
     fn telemetry_id(&self) -> SharedString {
         self.telemetry_id.clone()
     }
@@ -473,11 +478,14 @@ impl AgentConnection for AcpConnection {
     fn new_session(
         self: Rc<Self>,
         project: Entity<Project>,
-        cwd: &Path,
+        work_dirs: PathList,
         cx: &mut App,
     ) -> Task<Result<Entity<AcpThread>>> {
-        let name = self.server_name.clone();
-        let cwd = cwd.to_path_buf();
+        // TODO: remove this once ACP supports multiple working directories
+        let Some(cwd) = work_dirs.ordered_paths().next().cloned() else {
+            return Task::ready(Err(anyhow!("Working directory cannot be empty")));
+        };
+        let name = self.id.0.clone();
         let mcp_servers = mcp_servers_for_project(&project, cx);
 
         cx.spawn(async move |cx| {
@@ -575,7 +583,7 @@ impl AgentConnection for AcpConnection {
                 AcpThread::new(
                     None,
                     self.display_name.clone(),
-                    Some(cwd),
+                    Some(work_dirs),
                     self.clone(),
                     project,
                     action_log,
@@ -616,7 +624,7 @@ impl AgentConnection for AcpConnection {
         self: Rc<Self>,
         session_id: acp::SessionId,
         project: Entity<Project>,
-        cwd: &Path,
+        work_dirs: PathList,
         title: Option<SharedString>,
         cx: &mut App,
     ) -> Task<Result<Entity<AcpThread>>> {
@@ -625,8 +633,11 @@ impl AgentConnection for AcpConnection {
                 "Loading sessions is not supported by this agent.".into()
             ))));
         }
+        // TODO: remove this once ACP supports multiple working directories
+        let Some(cwd) = work_dirs.ordered_paths().next().cloned() else {
+            return Task::ready(Err(anyhow!("Working directory cannot be empty")));
+        };
 
-        let cwd = cwd.to_path_buf();
         let mcp_servers = mcp_servers_for_project(&project, cx);
         let action_log = cx.new(|_| ActionLog::new(project.clone()));
         let title = title.unwrap_or_else(|| self.display_name.clone());
@@ -634,7 +645,7 @@ impl AgentConnection for AcpConnection {
             AcpThread::new(
                 None,
                 title,
-                Some(cwd.clone()),
+                Some(work_dirs.clone()),
                 self.clone(),
                 project,
                 action_log,
@@ -691,7 +702,7 @@ impl AgentConnection for AcpConnection {
         self: Rc<Self>,
         session_id: acp::SessionId,
         project: Entity<Project>,
-        cwd: &Path,
+        work_dirs: PathList,
         title: Option<SharedString>,
         cx: &mut App,
     ) -> Task<Result<Entity<AcpThread>>> {
@@ -705,8 +716,11 @@ impl AgentConnection for AcpConnection {
                 "Resuming sessions is not supported by this agent.".into()
             ))));
         }
+        // TODO: remove this once ACP supports multiple working directories
+        let Some(cwd) = work_dirs.ordered_paths().next().cloned() else {
+            return Task::ready(Err(anyhow!("Working directory cannot be empty")));
+        };
 
-        let cwd = cwd.to_path_buf();
         let mcp_servers = mcp_servers_for_project(&project, cx);
         let action_log = cx.new(|_| ActionLog::new(project.clone()));
         let title = title.unwrap_or_else(|| self.display_name.clone());
@@ -714,7 +728,7 @@ impl AgentConnection for AcpConnection {
             AcpThread::new(
                 None,
                 title,
-                Some(cwd.clone()),
+                Some(work_dirs),
                 self.clone(),
                 project,
                 action_log,

crates/agent_servers/src/agent_servers.rs 🔗

@@ -9,11 +9,11 @@ use collections::{HashMap, HashSet};
 pub use custom::*;
 use fs::Fs;
 use http_client::read_no_proxy_from_env;
-use project::agent_server_store::AgentServerStore;
+use project::{AgentId, agent_server_store::AgentServerStore};
 
 use acp_thread::AgentConnection;
 use anyhow::Result;
-use gpui::{App, AppContext, Entity, SharedString, Task};
+use gpui::{App, AppContext, Entity, Task};
 use settings::SettingsStore;
 use std::{any::Any, rc::Rc, sync::Arc};
 
@@ -38,7 +38,7 @@ impl AgentServerDelegate {
 
 pub trait AgentServer: Send {
     fn logo(&self) -> ui::IconName;
-    fn name(&self) -> SharedString;
+    fn agent_id(&self) -> AgentId;
     fn connect(
         &self,
         delegate: AgentServerDelegate,

crates/agent_servers/src/custom.rs 🔗

@@ -5,10 +5,10 @@ use anyhow::{Context as _, Result};
 use collections::HashSet;
 use credentials_provider::CredentialsProvider;
 use fs::Fs;
-use gpui::{App, AppContext as _, SharedString, Task};
+use gpui::{App, AppContext as _, Task};
 use language_model::{ApiKey, EnvVar};
 use project::agent_server_store::{
-    AllAgentServersSettings, CLAUDE_AGENT_NAME, CODEX_NAME, ExternalAgentServerName, GEMINI_NAME,
+    AgentId, AllAgentServersSettings, CLAUDE_AGENT_ID, CODEX_ID, GEMINI_ID,
 };
 use settings::{SettingsStore, update_settings_file};
 use std::{rc::Rc, sync::Arc};
@@ -16,18 +16,18 @@ use ui::IconName;
 
 /// A generic agent server implementation for custom user-defined agents
 pub struct CustomAgentServer {
-    name: SharedString,
+    agent_id: AgentId,
 }
 
 impl CustomAgentServer {
-    pub fn new(name: SharedString) -> Self {
-        Self { name }
+    pub fn new(agent_id: AgentId) -> Self {
+        Self { agent_id }
     }
 }
 
 impl AgentServer for CustomAgentServer {
-    fn name(&self) -> SharedString {
-        self.name.clone()
+    fn agent_id(&self) -> AgentId {
+        self.agent_id.clone()
     }
 
     fn logo(&self) -> IconName {
@@ -38,7 +38,7 @@ impl AgentServer for CustomAgentServer {
         let settings = cx.read_global(|settings: &SettingsStore, _| {
             settings
                 .get::<AllAgentServersSettings>(None)
-                .get(self.name().as_ref())
+                .get(self.agent_id().0.as_ref())
                 .cloned()
         });
 
@@ -55,7 +55,7 @@ impl AgentServer for CustomAgentServer {
         let settings = cx.read_global(|settings: &SettingsStore, _| {
             settings
                 .get::<AllAgentServersSettings>(None)
-                .get(self.name().as_ref())
+                .get(self.agent_id().0.as_ref())
                 .cloned()
         });
 
@@ -80,7 +80,7 @@ impl AgentServer for CustomAgentServer {
         fs: Arc<dyn Fs>,
         cx: &App,
     ) {
-        let name = self.name();
+        let agent_id = self.agent_id();
         let config_id = config_id.to_string();
         let value_id = value_id.to_string();
 
@@ -88,8 +88,8 @@ impl AgentServer for CustomAgentServer {
             let settings = settings
                 .agent_servers
                 .get_or_insert_default()
-                .entry(name.to_string())
-                .or_insert_with(|| default_settings_for_agent(&name, cx));
+                .entry(agent_id.0.to_string())
+                .or_insert_with(|| default_settings_for_agent(agent_id, cx));
 
             match settings {
                 settings::CustomAgentServerSettings::Custom {
@@ -124,13 +124,13 @@ impl AgentServer for CustomAgentServer {
     }
 
     fn set_default_mode(&self, mode_id: Option<acp::SessionModeId>, fs: Arc<dyn Fs>, cx: &mut App) {
-        let name = self.name();
+        let agent_id = self.agent_id();
         update_settings_file(fs, cx, move |settings, cx| {
             let settings = settings
                 .agent_servers
                 .get_or_insert_default()
-                .entry(name.to_string())
-                .or_insert_with(|| default_settings_for_agent(&name, cx));
+                .entry(agent_id.0.to_string())
+                .or_insert_with(|| default_settings_for_agent(agent_id, cx));
 
             match settings {
                 settings::CustomAgentServerSettings::Custom { default_mode, .. }
@@ -146,7 +146,7 @@ impl AgentServer for CustomAgentServer {
         let settings = cx.read_global(|settings: &SettingsStore, _| {
             settings
                 .get::<AllAgentServersSettings>(None)
-                .get(self.name().as_ref())
+                .get(self.agent_id().as_ref())
                 .cloned()
         });
 
@@ -156,13 +156,13 @@ impl AgentServer for CustomAgentServer {
     }
 
     fn set_default_model(&self, model_id: Option<acp::ModelId>, fs: Arc<dyn Fs>, cx: &mut App) {
-        let name = self.name();
+        let agent_id = self.agent_id();
         update_settings_file(fs, cx, move |settings, cx| {
             let settings = settings
                 .agent_servers
                 .get_or_insert_default()
-                .entry(name.to_string())
-                .or_insert_with(|| default_settings_for_agent(&name, cx));
+                .entry(agent_id.0.to_string())
+                .or_insert_with(|| default_settings_for_agent(agent_id, cx));
 
             match settings {
                 settings::CustomAgentServerSettings::Custom { default_model, .. }
@@ -178,7 +178,7 @@ impl AgentServer for CustomAgentServer {
         let settings = cx.read_global(|settings: &SettingsStore, _| {
             settings
                 .get::<AllAgentServersSettings>(None)
-                .get(self.name().as_ref())
+                .get(self.agent_id().as_ref())
                 .cloned()
         });
 
@@ -200,13 +200,13 @@ impl AgentServer for CustomAgentServer {
         fs: Arc<dyn Fs>,
         cx: &App,
     ) {
-        let name = self.name();
+        let agent_id = self.agent_id();
         update_settings_file(fs, cx, move |settings, cx| {
             let settings = settings
                 .agent_servers
                 .get_or_insert_default()
-                .entry(name.to_string())
-                .or_insert_with(|| default_settings_for_agent(&name, cx));
+                .entry(agent_id.0.to_string())
+                .or_insert_with(|| default_settings_for_agent(agent_id, cx));
 
             let favorite_models = match settings {
                 settings::CustomAgentServerSettings::Custom {
@@ -235,7 +235,7 @@ impl AgentServer for CustomAgentServer {
         let settings = cx.read_global(|settings: &SettingsStore, _| {
             settings
                 .get::<AllAgentServersSettings>(None)
-                .get(self.name().as_ref())
+                .get(self.agent_id().as_ref())
                 .cloned()
         });
 
@@ -251,15 +251,15 @@ impl AgentServer for CustomAgentServer {
         fs: Arc<dyn Fs>,
         cx: &mut App,
     ) {
-        let name = self.name();
+        let agent_id = self.agent_id();
         let config_id = config_id.to_string();
         let value_id = value_id.map(|s| s.to_string());
         update_settings_file(fs, cx, move |settings, cx| {
             let settings = settings
                 .agent_servers
                 .get_or_insert_default()
-                .entry(name.to_string())
-                .or_insert_with(|| default_settings_for_agent(&name, cx));
+                .entry(agent_id.0.to_string())
+                .or_insert_with(|| default_settings_for_agent(agent_id, cx));
 
             match settings {
                 settings::CustomAgentServerSettings::Custom {
@@ -289,19 +289,19 @@ impl AgentServer for CustomAgentServer {
         delegate: AgentServerDelegate,
         cx: &mut App,
     ) -> Task<Result<Rc<dyn AgentConnection>>> {
-        let name = self.name();
+        let agent_id = self.agent_id();
         let display_name = delegate
             .store
             .read(cx)
-            .agent_display_name(&ExternalAgentServerName(name.clone()))
-            .unwrap_or_else(|| name.clone());
+            .agent_display_name(&agent_id)
+            .unwrap_or_else(|| agent_id.0.clone());
         let default_mode = self.default_mode(cx);
         let default_model = self.default_model(cx);
-        let is_registry_agent = is_registry_agent(&name, cx);
+        let is_registry_agent = is_registry_agent(agent_id.clone(), cx);
         let default_config_options = cx.read_global(|settings: &SettingsStore, _| {
             settings
                 .get::<AllAgentServersSettings>(None)
-                .get(self.name().as_ref())
+                .get(self.agent_id().as_ref())
                 .map(|s| match s {
                     project::agent_server_store::CustomAgentServerSettings::Custom {
                         default_config_options,
@@ -330,11 +330,11 @@ impl AgentServer for CustomAgentServer {
             extra_env.insert("NO_BROWSER".to_owned(), "1".to_owned());
         }
         if is_registry_agent {
-            match name.as_ref() {
-                CLAUDE_AGENT_NAME => {
+            match agent_id.as_ref() {
+                CLAUDE_AGENT_ID => {
                     extra_env.insert("ANTHROPIC_API_KEY".into(), "".into());
                 }
-                CODEX_NAME => {
+                CODEX_ID => {
                     if let Ok(api_key) = std::env::var("CODEX_API_KEY") {
                         extra_env.insert("CODEX_API_KEY".into(), api_key);
                     }
@@ -342,7 +342,7 @@ impl AgentServer for CustomAgentServer {
                         extra_env.insert("OPEN_AI_API_KEY".into(), api_key);
                     }
                 }
-                GEMINI_NAME => {
+                GEMINI_ID => {
                     extra_env.insert("SURFACE".to_owned(), "zed".to_owned());
                 }
                 _ => {}
@@ -350,18 +350,16 @@ impl AgentServer for CustomAgentServer {
         }
         let store = delegate.store.downgrade();
         cx.spawn(async move |cx| {
-            if is_registry_agent && name.as_ref() == GEMINI_NAME {
+            if is_registry_agent && agent_id.as_ref() == GEMINI_ID {
                 if let Some(api_key) = cx.update(api_key_for_gemini_cli).await.ok() {
                     extra_env.insert("GEMINI_API_KEY".into(), api_key);
                 }
             }
             let command = store
                 .update(cx, |store, cx| {
-                    let agent = store
-                        .get_external_agent(&ExternalAgentServerName(name.clone()))
-                        .with_context(|| {
-                            format!("Custom agent server `{}` is not registered", name)
-                        })?;
+                    let agent = store.get_external_agent(&agent_id).with_context(|| {
+                        format!("Custom agent server `{}` is not registered", agent_id)
+                    })?;
                     anyhow::Ok(agent.get_command(
                         extra_env,
                         delegate.new_version_available,
@@ -370,7 +368,7 @@ impl AgentServer for CustomAgentServer {
                 })??
                 .await?;
             let connection = crate::acp::connect(
-                name,
+                agent_id,
                 display_name,
                 command,
                 default_mode,
@@ -405,15 +403,17 @@ fn api_key_for_gemini_cli(cx: &mut App) -> Task<Result<String>> {
     })
 }
 
-fn is_registry_agent(name: &str, cx: &App) -> bool {
-    let is_previous_built_in = matches!(name, CLAUDE_AGENT_NAME | CODEX_NAME | GEMINI_NAME);
+fn is_registry_agent(agent_id: impl Into<AgentId>, cx: &App) -> bool {
+    let agent_id = agent_id.into();
+    let is_previous_built_in =
+        matches!(agent_id.0.as_ref(), CLAUDE_AGENT_ID | CODEX_ID | GEMINI_ID);
     let is_in_registry = project::AgentRegistryStore::try_global(cx)
-        .map(|store| store.read(cx).agent(name).is_some())
+        .map(|store| store.read(cx).agent(&agent_id).is_some())
         .unwrap_or(false);
     let is_settings_registry = cx.read_global(|settings: &SettingsStore, _| {
         settings
             .get::<AllAgentServersSettings>(None)
-            .get(name)
+            .get(agent_id.as_ref())
             .is_some_and(|s| {
                 matches!(
                     s,
@@ -424,8 +424,11 @@ fn is_registry_agent(name: &str, cx: &App) -> bool {
     is_previous_built_in || is_in_registry || is_settings_registry
 }
 
-fn default_settings_for_agent(name: &str, cx: &App) -> settings::CustomAgentServerSettings {
-    if is_registry_agent(name, cx) {
+fn default_settings_for_agent(
+    agent_id: impl Into<AgentId>,
+    cx: &App,
+) -> settings::CustomAgentServerSettings {
+    if is_registry_agent(agent_id, cx) {
         settings::CustomAgentServerSettings::Registry {
             default_model: None,
             default_mode: None,
@@ -455,6 +458,7 @@ mod tests {
         AgentRegistryStore, RegistryAgent, RegistryAgentMetadata, RegistryNpxAgent,
     };
     use settings::Settings as _;
+    use ui::SharedString;
 
     fn init_test(cx: &mut TestAppContext) {
         cx.update(|cx| {
@@ -470,7 +474,7 @@ mod tests {
                 let id = SharedString::from(id.to_string());
                 RegistryAgent::Npx(RegistryNpxAgent {
                     metadata: RegistryAgentMetadata {
-                        id: id.clone(),
+                        id: AgentId::new(id.clone()),
                         name: id.clone(),
                         description: SharedString::from(""),
                         version: SharedString::from("1.0.0"),
@@ -509,9 +513,9 @@ mod tests {
     fn test_previous_builtins_are_registry(cx: &mut TestAppContext) {
         init_test(cx);
         cx.update(|cx| {
-            assert!(is_registry_agent(CLAUDE_AGENT_NAME, cx));
-            assert!(is_registry_agent(CODEX_NAME, cx));
-            assert!(is_registry_agent(GEMINI_NAME, cx));
+            assert!(is_registry_agent(CLAUDE_AGENT_ID, cx));
+            assert!(is_registry_agent(CODEX_ID, cx));
+            assert!(is_registry_agent(GEMINI_ID, cx));
         });
     }
 
@@ -582,15 +586,15 @@ mod tests {
         init_test(cx);
         cx.update(|cx| {
             assert!(matches!(
-                default_settings_for_agent(CODEX_NAME, cx),
+                default_settings_for_agent(CODEX_ID, cx),
                 settings::CustomAgentServerSettings::Registry { .. }
             ));
             assert!(matches!(
-                default_settings_for_agent(CLAUDE_AGENT_NAME, cx),
+                default_settings_for_agent(CLAUDE_AGENT_ID, cx),
                 settings::CustomAgentServerSettings::Registry { .. }
             ));
             assert!(matches!(
-                default_settings_for_agent(GEMINI_NAME, cx),
+                default_settings_for_agent(GEMINI_ID, cx),
                 settings::CustomAgentServerSettings::Registry { .. }
             ));
         });

crates/agent_servers/src/e2e_tests.rs 🔗

@@ -14,6 +14,7 @@ use std::{
     time::Duration,
 };
 use util::path;
+use util::path_list::PathList;
 
 pub async fn test_basic<T, F>(server: F, cx: &mut TestAppContext)
 where
@@ -435,9 +436,11 @@ pub async fn new_test_thread(
 
     let connection = cx.update(|cx| server.connect(delegate, cx)).await.unwrap();
 
-    cx.update(|cx| connection.new_session(project.clone(), current_dir.as_ref(), cx))
-        .await
-        .unwrap()
+    cx.update(|cx| {
+        connection.new_session(project.clone(), PathList::new(&[current_dir.as_ref()]), cx)
+    })
+    .await
+    .unwrap()
 }
 
 pub async fn run_until_first_tool_call(

crates/agent_ui/src/agent_configuration.rs 🔗

@@ -28,7 +28,7 @@ use language_model::{
 use language_models::AllLanguageModelSettings;
 use notifications::status_toast::{StatusToast, ToastIcon};
 use project::{
-    agent_server_store::{AgentServerStore, ExternalAgentServerName, ExternalAgentSource},
+    agent_server_store::{AgentId, AgentServerStore, ExternalAgentSource},
     context_server_store::{ContextServerConfiguration, ContextServerStatus, ContextServerStore},
 };
 use settings::{Settings, SettingsStore, update_settings_file};
@@ -1103,7 +1103,7 @@ impl AgentConfiguration {
             ExternalAgentSource::Custom => None,
         };
 
-        let agent_server_name = ExternalAgentServerName(id.clone());
+        let agent_server_name = AgentId(id.clone());
 
         let uninstall_button = match source {
             ExternalAgentSource::Extension => Some(

crates/agent_ui/src/agent_connection_store.rs 🔗

@@ -10,7 +10,6 @@ use project::{AgentServerStore, AgentServersUpdated, Project};
 use watch::Receiver;
 
 use crate::{Agent, ThreadHistory};
-use project::ExternalAgentServerName;
 
 pub enum AgentConnectionEntry {
     Connecting {
@@ -143,9 +142,7 @@ impl AgentConnectionStore {
         let store = store.read(cx);
         self.entries.retain(|key, _| match key {
             Agent::NativeAgent => true,
-            Agent::Custom { name } => store
-                .external_agents
-                .contains_key(&ExternalAgentServerName(name.clone())),
+            Agent::Custom { id } => store.external_agents.contains_key(id),
         });
         cx.notify();
     }

crates/agent_ui/src/agent_diff.rs 🔗

@@ -1805,7 +1805,7 @@ mod tests {
     use settings::SettingsStore;
     use std::{path::Path, rc::Rc};
     use util::path;
-    use workspace::MultiWorkspace;
+    use workspace::{MultiWorkspace, PathList};
 
     #[gpui::test]
     async fn test_multibuffer_agent_diff(cx: &mut TestAppContext) {
@@ -1833,9 +1833,11 @@ mod tests {
         let connection = Rc::new(acp_thread::StubAgentConnection::new());
         let thread = cx
             .update(|cx| {
-                connection
-                    .clone()
-                    .new_session(project.clone(), Path::new(path!("/test")), cx)
+                connection.clone().new_session(
+                    project.clone(),
+                    PathList::new(&[Path::new(path!("/test"))]),
+                    cx,
+                )
             })
             .await
             .unwrap();
@@ -2024,9 +2026,11 @@ mod tests {
         let connection = Rc::new(acp_thread::StubAgentConnection::new());
         let thread = cx
             .update(|_, cx| {
-                connection
-                    .clone()
-                    .new_session(project.clone(), Path::new(path!("/test")), cx)
+                connection.clone().new_session(
+                    project.clone(),
+                    PathList::new(&[Path::new(path!("/test"))]),
+                    cx,
+                )
             })
             .await
             .unwrap();

crates/agent_ui/src/agent_panel.rs 🔗

@@ -17,8 +17,8 @@ use collections::HashSet;
 use db::kvp::{Dismissable, KEY_VALUE_STORE};
 use itertools::Itertools;
 use project::{
-    ExternalAgentServerName,
-    agent_server_store::{CLAUDE_AGENT_NAME, CODEX_NAME, GEMINI_NAME},
+    AgentId,
+    agent_server_store::{CLAUDE_AGENT_ID, CODEX_ID, GEMINI_ID},
 };
 use serde::{Deserialize, Serialize};
 use settings::{LanguageModelProviderSetting, LanguageModelSelection};
@@ -84,8 +84,8 @@ use ui::{
 };
 use util::{ResultExt as _, debug_panic};
 use workspace::{
-    CollaboratorId, DraggedSelection, DraggedTab, OpenResult, ToggleZoom, ToolbarItemView,
-    Workspace, WorkspaceId,
+    CollaboratorId, DraggedSelection, DraggedTab, OpenResult, PathList, SerializedPathList,
+    ToggleZoom, ToolbarItemView, Workspace, WorkspaceId,
     dock::{DockPosition, PanelEvent},
 };
 use zed_actions::{
@@ -128,7 +128,7 @@ fn read_legacy_serialized_panel() -> Option<SerializedAgentPanel> {
         .and_then(|json| serde_json::from_str::<SerializedAgentPanel>(&json).log_err())
 }
 
-#[derive(Serialize, Deserialize, Debug, Clone)]
+#[derive(Serialize, Deserialize, Debug)]
 struct SerializedAgentPanel {
     width: Option<Pixels>,
     selected_agent: Option<AgentType>,
@@ -138,12 +138,12 @@ struct SerializedAgentPanel {
     start_thread_in: Option<StartThreadIn>,
 }
 
-#[derive(Serialize, Deserialize, Debug, Clone)]
+#[derive(Serialize, Deserialize, Debug)]
 struct SerializedActiveThread {
     session_id: String,
     agent_type: AgentType,
     title: Option<String>,
-    cwd: Option<std::path::PathBuf>,
+    work_dirs: Option<SerializedPathList>,
 }
 
 pub fn init(cx: &mut App) {
@@ -589,7 +589,8 @@ pub enum AgentType {
     NativeAgent,
     TextThread,
     Custom {
-        name: SharedString,
+        #[serde(rename = "name")]
+        id: AgentId,
     },
 }
 
@@ -609,13 +610,13 @@ impl<'de> Deserialize<'de> for AgentType {
                 "NativeAgent" => Ok(Self::NativeAgent),
                 "TextThread" => Ok(Self::TextThread),
                 "ClaudeAgent" | "ClaudeCode" => Ok(Self::Custom {
-                    name: CLAUDE_AGENT_NAME.into(),
+                    id: CLAUDE_AGENT_ID.into(),
                 }),
                 "Codex" => Ok(Self::Custom {
-                    name: CODEX_NAME.into(),
+                    id: CODEX_ID.into(),
                 }),
                 "Gemini" => Ok(Self::Custom {
-                    name: GEMINI_NAME.into(),
+                    id: GEMINI_ID.into(),
                 }),
                 other => Err(serde::de::Error::unknown_variant(
                     other,
@@ -640,7 +641,9 @@ impl<'de> Deserialize<'de> for AgentType {
                 }
                 let fields: CustomFields =
                     serde_json::from_value(inner.clone()).map_err(serde::de::Error::custom)?;
-                return Ok(Self::Custom { name: fields.name });
+                return Ok(Self::Custom {
+                    id: AgentId::new(fields.name),
+                });
             }
         }
 
@@ -658,7 +661,7 @@ impl AgentType {
     fn label(&self) -> SharedString {
         match self {
             Self::NativeAgent | Self::TextThread => "Zed Agent".into(),
-            Self::Custom { name, .. } => name.into(),
+            Self::Custom { id, .. } => id.0.clone(),
         }
     }
 
@@ -673,7 +676,7 @@ impl AgentType {
 impl From<Agent> for AgentType {
     fn from(value: Agent) -> Self {
         match value {
-            Agent::Custom { name } => Self::Custom { name },
+            Agent::Custom { id } => Self::Custom { id },
             Agent::NativeAgent => Self::NativeAgent,
         }
     }
@@ -853,6 +856,7 @@ impl AgentPanel {
         let last_active_thread = self.active_agent_thread(cx).map(|thread| {
             let thread = thread.read(cx);
             let title = thread.title();
+            let work_dirs = thread.work_dirs().cloned();
             SerializedActiveThread {
                 session_id: thread.session_id().0.to_string(),
                 agent_type: self.selected_agent_type.clone(),
@@ -861,7 +865,7 @@ impl AgentPanel {
                 } else {
                     None
                 },
-                cwd: None,
+                work_dirs: work_dirs.map(|dirs| dirs.serialize()),
             }
         });
 
@@ -919,7 +923,7 @@ impl AgentPanel {
 
             let last_active_thread = if let Some(thread_info) = serialized_panel
                 .as_ref()
-                .and_then(|p| p.last_active_thread.clone())
+                .and_then(|p| p.last_active_thread.as_ref())
             {
                 if thread_info.agent_type.is_native() {
                     let session_id = acp::SessionId::new(thread_info.session_id.clone());
@@ -988,9 +992,9 @@ impl AgentPanel {
                         if let Some(agent) = panel.selected_agent() {
                             panel.load_agent_thread(
                                 agent,
-                                thread_info.session_id.into(),
-                                thread_info.cwd,
-                                thread_info.title.map(SharedString::from),
+                                thread_info.session_id.clone().into(),
+                                thread_info.work_dirs.as_ref().map(|dirs| PathList::deserialize(dirs)),
+                                thread_info.title.as_ref().map(|t| t.clone().into()),
                                 false,
                                 window,
                                 cx,
@@ -1225,7 +1229,7 @@ impl AgentPanel {
     pub fn open_thread(
         &mut self,
         session_id: acp::SessionId,
-        cwd: Option<PathBuf>,
+        work_dirs: Option<PathList>,
         title: Option<SharedString>,
         window: &mut Window,
         cx: &mut Context<Self>,
@@ -1233,7 +1237,7 @@ impl AgentPanel {
         self.external_thread(
             Some(crate::Agent::NativeAgent),
             Some(session_id),
-            cwd,
+            work_dirs,
             title,
             None,
             true,
@@ -1355,7 +1359,7 @@ impl AgentPanel {
         &mut self,
         agent_choice: Option<crate::Agent>,
         resume_session_id: Option<acp::SessionId>,
-        cwd: Option<PathBuf>,
+        work_dirs: Option<PathList>,
         title: Option<SharedString>,
         initial_content: Option<AgentInitialContent>,
         focus: bool,
@@ -1396,7 +1400,7 @@ impl AgentPanel {
             self.create_agent_thread(
                 server,
                 resume_session_id,
-                cwd,
+                work_dirs,
                 title,
                 initial_content,
                 workspace,
@@ -1429,7 +1433,7 @@ impl AgentPanel {
                     agent_panel.create_agent_thread(
                         server,
                         resume_session_id,
-                        cwd,
+                        work_dirs,
                         title,
                         initial_content,
                         workspace,
@@ -1489,8 +1493,8 @@ impl AgentPanel {
     fn has_history_for_selected_agent(&self, cx: &App) -> bool {
         match &self.selected_agent_type {
             AgentType::TextThread | AgentType::NativeAgent => true,
-            AgentType::Custom { name } => {
-                let agent = Agent::Custom { name: name.clone() };
+            AgentType::Custom { id } => {
+                let agent = Agent::Custom { id: id.clone() };
                 self.connection_store
                     .read(cx)
                     .entry(&agent)
@@ -1519,8 +1523,8 @@ impl AgentPanel {
                     view: self.create_thread_history_view(Agent::NativeAgent, history, window, cx),
                 })
             }
-            AgentType::Custom { name } => {
-                let agent = Agent::Custom { name: name.clone() };
+            AgentType::Custom { id, .. } => {
+                let agent = Agent::Custom { id: id.clone() };
                 let history = self
                     .connection_store
                     .read(cx)
@@ -1555,7 +1559,7 @@ impl AgentPanel {
                     this.load_agent_thread(
                         agent.clone(),
                         thread.session_id.clone(),
-                        thread.cwd.clone(),
+                        thread.work_dirs.clone(),
                         thread.title.clone(),
                         true,
                         window,
@@ -2206,7 +2210,7 @@ impl AgentPanel {
                                         this.load_agent_thread(
                                             agent,
                                             entry.session_id.clone(),
-                                            entry.cwd.clone(),
+                                            entry.work_dirs.clone(),
                                             entry.title.clone(),
                                             true,
                                             window,
@@ -2335,7 +2339,7 @@ impl AgentPanel {
     pub(crate) fn selected_agent(&self) -> Option<Agent> {
         match &self.selected_agent_type {
             AgentType::NativeAgent => Some(Agent::NativeAgent),
-            AgentType::Custom { name } => Some(Agent::Custom { name: name.clone() }),
+            AgentType::Custom { id } => Some(Agent::Custom { id: id.clone() }),
             AgentType::TextThread => None,
         }
     }
@@ -2414,8 +2418,8 @@ impl AgentPanel {
                 window,
                 cx,
             ),
-            AgentType::Custom { name } => self.external_thread(
-                Some(crate::Agent::Custom { name }),
+            AgentType::Custom { id } => self.external_thread(
+                Some(crate::Agent::Custom { id }),
                 None,
                 None,
                 None,
@@ -2431,7 +2435,7 @@ impl AgentPanel {
         &mut self,
         agent: Agent,
         session_id: acp::SessionId,
-        cwd: Option<PathBuf>,
+        work_dirs: Option<PathList>,
         title: Option<SharedString>,
         focus: bool,
         window: &mut Window,
@@ -2470,7 +2474,7 @@ impl AgentPanel {
         self.external_thread(
             Some(agent),
             Some(session_id),
-            cwd,
+            work_dirs,
             title,
             None,
             focus,
@@ -2483,7 +2487,7 @@ impl AgentPanel {
         &mut self,
         server: Rc<dyn AgentServer>,
         resume_session_id: Option<acp::SessionId>,
-        cwd: Option<PathBuf>,
+        work_dirs: Option<PathList>,
         title: Option<SharedString>,
         initial_content: Option<AgentInitialContent>,
         workspace: WeakEntity<Workspace>,
@@ -2512,7 +2516,7 @@ impl AgentPanel {
                 connection_store,
                 ext_agent,
                 resume_session_id,
-                cwd,
+                work_dirs,
                 title,
                 initial_content,
                 workspace.clone(),
@@ -3582,12 +3586,12 @@ impl AgentPanel {
         let docked_right = agent_panel_dock_position(cx) == DockPosition::Right;
 
         let (selected_agent_custom_icon, selected_agent_label) =
-            if let AgentType::Custom { name, .. } = &self.selected_agent_type {
+            if let AgentType::Custom { id, .. } = &self.selected_agent_type {
                 let store = agent_server_store.read(cx);
-                let icon = store.agent_icon(&ExternalAgentServerName(name.clone()));
+                let icon = store.agent_icon(&id);
 
                 let label = store
-                    .agent_display_name(&ExternalAgentServerName(name.clone()))
+                    .agent_display_name(&id)
                     .unwrap_or_else(|| self.selected_agent_type.label());
                 (icon, label)
             } else {
@@ -3716,24 +3720,24 @@ impl AgentPanel {
                                 registry_store.as_ref().map(|s| s.read(cx));
 
                             struct AgentMenuItem {
-                                id: ExternalAgentServerName,
+                                id: AgentId,
                                 display_name: SharedString,
                             }
 
                             let agent_items = agent_server_store
                                 .external_agents()
-                                .map(|name| {
+                                .map(|agent_id| {
                                     let display_name = agent_server_store
-                                        .agent_display_name(name)
+                                        .agent_display_name(agent_id)
                                         .or_else(|| {
                                             registry_store_ref
                                                 .as_ref()
-                                                .and_then(|store| store.agent(name.0.as_ref()))
+                                                .and_then(|store| store.agent(agent_id))
                                                 .map(|a| a.name().clone())
                                         })
-                                        .unwrap_or_else(|| name.0.clone());
+                                        .unwrap_or_else(|| agent_id.0.clone());
                                     AgentMenuItem {
-                                        id: name.clone(),
+                                        id: agent_id.clone(),
                                         display_name,
                                     }
                                 })
@@ -3749,7 +3753,7 @@ impl AgentPanel {
                                     .or_else(|| {
                                         registry_store_ref
                                             .as_ref()
-                                            .and_then(|store| store.agent(item.id.0.as_str()))
+                                            .and_then(|store| store.agent(&item.id))
                                             .and_then(|a| a.icon_path().cloned())
                                     });
 
@@ -3762,7 +3766,7 @@ impl AgentPanel {
                                 entry = entry
                                     .when(
                                         is_agent_selected(AgentType::Custom {
-                                            name: item.id.0.clone(),
+                                            id: item.id.clone(),
                                         }),
                                         |this| {
                                             this.action(Box::new(
@@ -3784,7 +3788,7 @@ impl AgentPanel {
                                                         panel.update(cx, |panel, cx| {
                                                             panel.new_agent_thread(
                                                                 AgentType::Custom {
-                                                                    name: agent_id.0.clone(),
+                                                                    id: agent_id.clone(),
                                                                 },
                                                                 window,
                                                                 cx,
@@ -3809,20 +3813,20 @@ impl AgentPanel {
                             let registry_store_ref =
                                 registry_store.as_ref().map(|s| s.read(cx));
 
-                            let previous_built_in_ids: &[ExternalAgentServerName] =
-                                &[CLAUDE_AGENT_NAME.into(), CODEX_NAME.into(), GEMINI_NAME.into()];
+                            let previous_built_in_ids: &[AgentId] =
+                                &[CLAUDE_AGENT_ID.into(), CODEX_ID.into(), GEMINI_ID.into()];
 
                             let promoted_items = previous_built_in_ids
                                 .iter()
                                 .filter(|id| {
                                     !agent_server_store.external_agents.contains_key(*id)
                                 })
-                                .filter_map(|name| {
+                                .filter_map(|id| {
                                     let display_name = registry_store_ref
                                         .as_ref()
-                                        .and_then(|store| store.agent(name.0.as_ref()))
+                                        .and_then(|store| store.agent(&id))
                                         .map(|a| a.name().clone())?;
-                                    Some((name.clone(), display_name))
+                                    Some((id.clone(), display_name))
                                 })
                                 .sorted_unstable_by_key(|(_, display_name)| display_name.to_lowercase())
                                 .collect::<Vec<_>>();
@@ -3833,7 +3837,7 @@ impl AgentPanel {
 
                                 let icon_path = registry_store_ref
                                     .as_ref()
-                                    .and_then(|store| store.agent(agent_id.0.as_str()))
+                                    .and_then(|store| store.agent(agent_id))
                                     .and_then(|a| a.icon_path().cloned());
 
                                 if let Some(icon_path) = icon_path {
@@ -3880,7 +3884,7 @@ impl AgentPanel {
                                                         panel.update(cx, |panel, cx| {
                                                             panel.new_agent_thread(
                                                                 AgentType::Custom {
-                                                                    name: agent_id.0.clone(),
+                                                                    id: agent_id.clone(),
                                                                 },
                                                                 window,
                                                                 cx,
@@ -4880,7 +4884,7 @@ impl AgentPanel {
         let project = self.project.clone();
 
         let ext_agent = Agent::Custom {
-            name: server.name(),
+            id: server.agent_id(),
         };
 
         self.create_agent_thread(
@@ -5042,7 +5046,7 @@ mod tests {
         panel_b.update(cx, |panel, _cx| {
             panel.width = Some(px(400.0));
             panel.selected_agent_type = AgentType::Custom {
-                name: "claude-acp".into(),
+                id: "claude-acp".into(),
             };
         });
 
@@ -5093,7 +5097,7 @@ mod tests {
             assert_eq!(
                 panel.selected_agent_type,
                 AgentType::Custom {
-                    name: "claude-acp".into()
+                    id: "claude-acp".into()
                 },
                 "workspace B agent type should be restored"
             );
@@ -5892,25 +5896,25 @@ mod tests {
         assert_eq!(
             serde_json::from_str::<AgentType>(r#""ClaudeAgent""#).unwrap(),
             AgentType::Custom {
-                name: CLAUDE_AGENT_NAME.into(),
+                id: CLAUDE_AGENT_ID.into(),
             },
         );
         assert_eq!(
             serde_json::from_str::<AgentType>(r#""ClaudeCode""#).unwrap(),
             AgentType::Custom {
-                name: CLAUDE_AGENT_NAME.into(),
+                id: CLAUDE_AGENT_ID.into(),
             },
         );
         assert_eq!(
             serde_json::from_str::<AgentType>(r#""Codex""#).unwrap(),
             AgentType::Custom {
-                name: CODEX_NAME.into(),
+                id: CODEX_ID.into(),
             },
         );
         assert_eq!(
             serde_json::from_str::<AgentType>(r#""Gemini""#).unwrap(),
             AgentType::Custom {
-                name: GEMINI_NAME.into(),
+                id: GEMINI_ID.into(),
             },
         );
     }
@@ -5928,7 +5932,7 @@ mod tests {
         assert_eq!(
             serde_json::from_str::<AgentType>(r#"{"Custom":{"name":"my-agent"}}"#).unwrap(),
             AgentType::Custom {
-                name: "my-agent".into(),
+                id: "my-agent".into(),
             },
         );
     }
@@ -5948,14 +5952,14 @@ mod tests {
         assert_eq!(
             panel.selected_agent,
             Some(AgentType::Custom {
-                name: CLAUDE_AGENT_NAME.into(),
+                id: CLAUDE_AGENT_ID.into(),
             }),
         );
         let thread = panel.last_active_thread.unwrap();
         assert_eq!(
             thread.agent_type,
             AgentType::Custom {
-                name: CODEX_NAME.into(),
+                id: CODEX_ID.into(),
             },
         );
     }

crates/agent_ui/src/agent_ui.rs 🔗

@@ -33,6 +33,7 @@ mod text_thread_editor;
 mod text_thread_history;
 mod thread_history;
 mod thread_history_view;
+mod thread_metadata_store;
 mod threads_archive_view;
 mod threads_panel;
 mod ui;
@@ -55,7 +56,7 @@ use language::{
 use language_model::{
     ConfiguredModel, LanguageModelId, LanguageModelProviderId, LanguageModelRegistry,
 };
-use project::DisableAiSettings;
+use project::{AgentId, DisableAiSettings};
 use prompt_store::PromptBuilder;
 use schemars::JsonSchema;
 use serde::{Deserialize, Serialize};
@@ -222,7 +223,10 @@ pub struct NewNativeAgentThreadFromSummary {
 #[serde(rename_all = "snake_case")]
 pub enum Agent {
     NativeAgent,
-    Custom { name: SharedString },
+    Custom {
+        #[serde(rename = "name")]
+        id: AgentId,
+    },
 }
 
 // Custom impl handles legacy variant names from before the built-in agents were moved to
@@ -234,7 +238,7 @@ impl<'de> serde::Deserialize<'de> for Agent {
     where
         D: serde::Deserializer<'de>,
     {
-        use project::agent_server_store::{CLAUDE_AGENT_NAME, CODEX_NAME, GEMINI_NAME};
+        use project::agent_server_store::{CLAUDE_AGENT_ID, CODEX_ID, GEMINI_ID};
 
         let value = serde_json::Value::deserialize(deserializer)?;
 
@@ -242,13 +246,13 @@ impl<'de> serde::Deserialize<'de> for Agent {
             return match s {
                 "native_agent" => Ok(Self::NativeAgent),
                 "claude_code" | "claude_agent" => Ok(Self::Custom {
-                    name: CLAUDE_AGENT_NAME.into(),
+                    id: CLAUDE_AGENT_ID.into(),
                 }),
                 "codex" => Ok(Self::Custom {
-                    name: CODEX_NAME.into(),
+                    id: CODEX_ID.into(),
                 }),
                 "gemini" => Ok(Self::Custom {
-                    name: GEMINI_NAME.into(),
+                    id: GEMINI_ID.into(),
                 }),
                 other => Err(serde::de::Error::unknown_variant(
                     other,
@@ -272,7 +276,9 @@ impl<'de> serde::Deserialize<'de> for Agent {
                 }
                 let fields: CustomFields =
                     serde_json::from_value(inner.clone()).map_err(serde::de::Error::custom)?;
-                return Ok(Self::Custom { name: fields.name });
+                return Ok(Self::Custom {
+                    id: AgentId::new(fields.name),
+                });
             }
         }
 
@@ -290,7 +296,9 @@ impl Agent {
     ) -> Rc<dyn agent_servers::AgentServer> {
         match self {
             Self::NativeAgent => Rc::new(agent::NativeAgentServer::new(fs, thread_store)),
-            Self::Custom { name } => Rc::new(agent_servers::CustomAgentServer::new(name.clone())),
+            Self::Custom { id: name } => {
+                Rc::new(agent_servers::CustomAgentServer::new(name.clone()))
+            }
         }
     }
 }
@@ -379,6 +387,7 @@ pub fn init(
     agent_panel::init(cx);
     context_server_configuration::init(language_registry.clone(), fs.clone(), cx);
     TextThreadEditor::init(cx);
+    thread_metadata_store::init(cx);
 
     register_slash_commands(cx);
     inline_assistant::init(fs.clone(), prompt_builder.clone(), cx);
@@ -752,24 +761,24 @@ mod tests {
 
     #[test]
     fn test_deserialize_legacy_external_agent_variants() {
-        use project::agent_server_store::{CLAUDE_AGENT_NAME, CODEX_NAME, GEMINI_NAME};
+        use project::agent_server_store::{CLAUDE_AGENT_ID, CODEX_ID, GEMINI_ID};
 
         assert_eq!(
             serde_json::from_str::<Agent>(r#""claude_code""#).unwrap(),
             Agent::Custom {
-                name: CLAUDE_AGENT_NAME.into(),
+                id: CLAUDE_AGENT_ID.into(),
             },
         );
         assert_eq!(
             serde_json::from_str::<Agent>(r#""codex""#).unwrap(),
             Agent::Custom {
-                name: CODEX_NAME.into(),
+                id: CODEX_ID.into(),
             },
         );
         assert_eq!(
             serde_json::from_str::<Agent>(r#""gemini""#).unwrap(),
             Agent::Custom {
-                name: GEMINI_NAME.into(),
+                id: GEMINI_ID.into(),
             },
         );
     }
@@ -783,7 +792,7 @@ mod tests {
         assert_eq!(
             serde_json::from_str::<Agent>(r#"{"custom":{"name":"my-agent"}}"#).unwrap(),
             Agent::Custom {
-                name: "my-agent".into(),
+                id: "my-agent".into(),
             },
         );
     }

crates/agent_ui/src/connection_view.rs 🔗

@@ -36,12 +36,12 @@ use gpui::{
 use language::Buffer;
 use language_model::LanguageModelRegistry;
 use markdown::{Markdown, MarkdownElement, MarkdownFont, MarkdownStyle};
-use project::{AgentServerStore, ExternalAgentServerName, Project, ProjectEntryId};
+use project::{AgentId, AgentServerStore, Project, ProjectEntryId};
 use prompt_store::{PromptId, PromptStore};
 use rope::Point;
 use settings::{NotifyWhenAgentWaiting, Settings as _, SettingsStore};
 use std::cell::RefCell;
-use std::path::{Path, PathBuf};
+use std::path::Path;
 use std::sync::Arc;
 use std::time::Instant;
 use std::{collections::BTreeMap, rc::Rc, time::Duration};
@@ -56,6 +56,7 @@ use ui::{
 };
 use util::{ResultExt, size::format_file_size, time::duration_alt_display};
 use util::{debug_panic, defer};
+use workspace::PathList;
 use workspace::{
     CollaboratorId, MultiWorkspace, NewTerminal, Toast, Workspace, notifications::NotificationId,
 };
@@ -74,6 +75,7 @@ use crate::agent_diff::AgentDiff;
 use crate::entry_view_state::{EntryViewEvent, ViewEvent};
 use crate::message_editor::{MessageEditor, MessageEditorEvent};
 use crate::profile_selector::{ProfileProvider, ProfileSelector};
+use crate::thread_metadata_store::ThreadMetadataStore;
 use crate::ui::{AgentNotification, AgentNotificationEvent};
 use crate::{
     Agent, AgentDiffPane, AgentInitialContent, AgentPanel, AllowAlways, AllowOnce,
@@ -482,7 +484,7 @@ impl ConnectionView {
         connection_store: Entity<AgentConnectionStore>,
         connection_key: Agent,
         resume_session_id: Option<acp::SessionId>,
-        cwd: Option<PathBuf>,
+        work_dirs: Option<PathList>,
         title: Option<SharedString>,
         initial_content: Option<AgentInitialContent>,
         workspace: WeakEntity<Workspace>,
@@ -531,7 +533,7 @@ impl ConnectionView {
                 connection_store,
                 connection_key,
                 resume_session_id,
-                cwd,
+                work_dirs,
                 title,
                 project,
                 initial_content,
@@ -563,7 +565,7 @@ impl ConnectionView {
                 let thread = thread_view.read(cx).thread.read(cx);
                 (
                     Some(thread.session_id().clone()),
-                    thread.cwd().cloned(),
+                    thread.work_dirs().cloned(),
                     Some(thread.title()),
                 )
             })
@@ -602,7 +604,7 @@ impl ConnectionView {
         connection_store: Entity<AgentConnectionStore>,
         connection_key: Agent,
         resume_session_id: Option<acp::SessionId>,
-        cwd: Option<PathBuf>,
+        work_dirs: Option<PathList>,
         title: Option<SharedString>,
         project: Entity<Project>,
         initial_content: Option<AgentInitialContent>,
@@ -638,24 +640,13 @@ impl ConnectionView {
                 }
             })
             .collect();
-        let session_cwd = cwd
-            .filter(|cwd| {
-                // Validate with the normalized path (rejects `..` traversals),
-                // but return the original cwd to preserve its path separators.
-                // On Windows, `normalize_lexically` rebuilds the path with
-                // backslashes via `PathBuf::push`, which would corrupt
-                // forward-slash Linux paths used by WSL agents.
-                util::paths::normalize_lexically(cwd)
-                    .ok()
-                    .is_some_and(|normalized| {
-                        worktree_roots
-                            .iter()
-                            .any(|root| normalized.starts_with(root.as_ref()))
-                    })
-            })
-            .map(|path| path.into())
-            .or_else(|| worktree_roots.first().cloned())
-            .unwrap_or_else(|| paths::home_dir().as_path().into());
+        let session_work_dirs = work_dirs.unwrap_or_else(|| {
+            if worktree_roots.is_empty() {
+                PathList::new(&[paths::home_dir().as_path()])
+            } else {
+                PathList::new(&worktree_roots)
+            }
+        });
 
         let connection_entry = connection_store.update(cx, |store, cx| {
             store.request_connection(connection_key, agent.clone(), cx)
@@ -701,7 +692,7 @@ impl ConnectionView {
                         connection.clone().load_session(
                             session_id,
                             project.clone(),
-                            &session_cwd,
+                            session_work_dirs,
                             title,
                             cx,
                         )
@@ -710,7 +701,7 @@ impl ConnectionView {
                         connection.clone().resume_session(
                             session_id,
                             project.clone(),
-                            &session_cwd,
+                            session_work_dirs,
                             title,
                             cx,
                         )
@@ -725,7 +716,7 @@ impl ConnectionView {
                 cx.update(|_, cx| {
                     connection
                         .clone()
-                        .new_session(project.clone(), session_cwd.as_ref(), cx)
+                        .new_session(project.clone(), session_work_dirs, cx)
                 })
                 .log_err()
             };
@@ -741,7 +732,7 @@ impl ConnectionView {
                             Self::handle_auth_required(
                                 this,
                                 err,
-                                agent.name(),
+                                agent.agent_id(),
                                 connection,
                                 window,
                                 cx,
@@ -829,7 +820,7 @@ impl ConnectionView {
         window: &mut Window,
         cx: &mut Context<Self>,
     ) -> Entity<ThreadView> {
-        let agent_name = self.agent.name();
+        let agent_id = self.agent.agent_id();
         let prompt_capabilities = Rc::new(RefCell::new(acp::PromptCapabilities::default()));
         let available_commands = Rc::new(RefCell::new(vec![]));
 
@@ -846,7 +837,7 @@ impl ConnectionView {
                 self.prompt_store.clone(),
                 prompt_capabilities.clone(),
                 available_commands.clone(),
-                self.agent.name(),
+                self.agent.agent_id(),
             )
         });
 
@@ -969,19 +960,19 @@ impl ConnectionView {
         let agent_display_name = self
             .agent_server_store
             .read(cx)
-            .agent_display_name(&ExternalAgentServerName(agent_name.clone()))
-            .unwrap_or_else(|| agent_name.clone());
+            .agent_display_name(&agent_id.clone())
+            .unwrap_or_else(|| agent_id.0.clone());
 
         let agent_icon = self.agent.logo();
         let agent_icon_from_external_svg = self
             .agent_server_store
             .read(cx)
-            .agent_icon(&ExternalAgentServerName(self.agent.name()))
+            .agent_icon(&self.agent.agent_id())
             .or_else(|| {
                 project::AgentRegistryStore::try_global(cx).and_then(|store| {
                     store
                         .read(cx)
-                        .agent(self.agent.name().as_ref())
+                        .agent(&self.agent.agent_id())
                         .and_then(|a| a.icon_path().cloned())
                 })
             });
@@ -995,7 +986,7 @@ impl ConnectionView {
                 weak,
                 agent_icon,
                 agent_icon_from_external_svg,
-                agent_name,
+                agent_id,
                 agent_display_name,
                 self.workspace.clone(),
                 entry_view_state,
@@ -1022,7 +1013,7 @@ impl ConnectionView {
     fn handle_auth_required(
         this: WeakEntity<Self>,
         err: AuthRequired,
-        agent_name: SharedString,
+        agent_id: AgentId,
         connection: Rc<dyn AgentConnection>,
         window: &mut Window,
         cx: &mut App,
@@ -1051,7 +1042,7 @@ impl ConnectionView {
 
             let view = registry.read(cx).provider(&provider_id).map(|provider| {
                 provider.configuration_view(
-                    language_model::ConfigurationViewTargetAgent::Other(agent_name),
+                    language_model::ConfigurationViewTargetAgent::Other(agent_id.0),
                     window,
                     cx,
                 )
@@ -1166,12 +1157,14 @@ impl ConnectionView {
             ServerState::Connected(_) => "New Thread".into(),
             ServerState::Loading(_) => "Loading…".into(),
             ServerState::LoadError { error, .. } => match error {
-                LoadError::Unsupported { .. } => format!("Upgrade {}", self.agent.name()).into(),
+                LoadError::Unsupported { .. } => {
+                    format!("Upgrade {}", self.agent.agent_id()).into()
+                }
                 LoadError::FailedToInstall(_) => {
-                    format!("Failed to Install {}", self.agent.name()).into()
+                    format!("Failed to Install {}", self.agent.agent_id()).into()
                 }
-                LoadError::Exited { .. } => format!("{} Exited", self.agent.name()).into(),
-                LoadError::Other(_) => format!("Error Loading {}", self.agent.name()).into(),
+                LoadError::Exited { .. } => format!("{} Exited", self.agent.agent_id()).into(),
+                LoadError::Other(_) => format!("Error Loading {}", self.agent.agent_id()).into(),
             },
         }
     }
@@ -1451,8 +1444,8 @@ impl ConnectionView {
                 let agent_display_name = self
                     .agent_server_store
                     .read(cx)
-                    .agent_display_name(&ExternalAgentServerName(self.agent.name()))
-                    .unwrap_or_else(|| self.agent.name());
+                    .agent_display_name(&self.agent.agent_id())
+                    .unwrap_or_else(|| self.agent.agent_id().0.to_string().into());
 
                 if let Some(active) = self.active_thread() {
                     let new_placeholder =
@@ -1673,24 +1666,21 @@ impl ConnectionView {
         {
             return;
         }
-        let root_dir = self
-            .project
+        let Some(parent_thread) = connected.threads.get(&parent_id) else {
+            return;
+        };
+        let work_dirs = parent_thread
             .read(cx)
-            .worktrees(cx)
-            .filter_map(|worktree| {
-                if worktree.read(cx).is_single_file() {
-                    Some(worktree.read(cx).abs_path().parent()?.into())
-                } else {
-                    Some(worktree.read(cx).abs_path())
-                }
-            })
-            .next();
-        let cwd = root_dir.unwrap_or_else(|| paths::home_dir().as_path().into());
+            .thread
+            .read(cx)
+            .work_dirs()
+            .cloned()
+            .unwrap_or_else(|| PathList::new(&[paths::home_dir().as_path()]));
 
         let subagent_thread_task = connected.connection.clone().load_session(
             subagent_id.clone(),
             self.project.clone(),
-            &cwd,
+            work_dirs,
             None,
             cx,
         );
@@ -1876,8 +1866,8 @@ impl ConnectionView {
         let agent_display_name = self
             .agent_server_store
             .read(cx)
-            .agent_display_name(&ExternalAgentServerName(self.agent.name()))
-            .unwrap_or_else(|| self.agent.name());
+            .agent_display_name(&self.agent.agent_id())
+            .unwrap_or_else(|| self.agent.agent_id().0);
 
         let show_fallback_description = auth_methods.len() > 1
             && configuration_view.is_none()
@@ -2038,7 +2028,7 @@ impl ConnectionView {
             LoadError::Other(_) => "other",
         };
 
-        let agent_name = self.agent.name();
+        let agent_name = self.agent.agent_id();
 
         telemetry::event!(
             "Agent Panel Error Shown",
@@ -2097,7 +2087,7 @@ impl ConnectionView {
         cx: &mut Context<Self>,
     ) -> AnyElement {
         let (heading_label, description_label) = (
-            format!("Upgrade {} to work with Zed", self.agent.name()),
+            format!("Upgrade {} to work with Zed", self.agent.agent_id()),
             if version.is_empty() {
                 format!(
                     "Currently using {}, which does not report a valid --version",
@@ -2217,7 +2207,7 @@ impl ConnectionView {
         let needed_count = self.queued_messages_len(cx);
         let queued_messages = self.queued_message_contents(cx);
 
-        let agent_name = self.agent.name();
+        let agent_name = self.agent.agent_id();
         let workspace = self.workspace.clone();
         let project = self.project.downgrade();
         let Some(connected) = self.as_connected() else {
@@ -2395,7 +2385,7 @@ impl ConnectionView {
         }
 
         // TODO: Change this once we have title summarization for external agents.
-        let title = self.agent.name();
+        let title = self.agent.agent_id().0;
 
         match settings.notify_when_agent_waiting {
             NotifyWhenAgentWaiting::PrimaryScreen => {
@@ -2584,7 +2574,7 @@ impl ConnectionView {
                 .unwrap_or_else(|| SharedString::from("The model"))
         } else {
             // ACP agent - use the agent name (e.g., "Claude Agent", "Gemini CLI")
-            self.agent.name()
+            self.agent.agent_id().0
         }
     }
 
@@ -2595,7 +2585,7 @@ impl ConnectionView {
     }
 
     pub(crate) fn reauthenticate(&mut self, window: &mut Window, cx: &mut Context<Self>) {
-        let agent_name = self.agent.name();
+        let agent_id = self.agent.agent_id();
         if let Some(active) = self.active_thread() {
             active.update(cx, |active, cx| active.clear_thread_error(cx));
         }
@@ -2605,14 +2595,7 @@ impl ConnectionView {
             return;
         };
         window.defer(cx, |window, cx| {
-            Self::handle_auth_required(
-                this,
-                AuthRequired::new(),
-                agent_name,
-                connection,
-                window,
-                cx,
-            );
+            Self::handle_auth_required(this, AuthRequired::new(), agent_id, connection, window, cx);
         })
     }
 
@@ -2629,6 +2612,12 @@ impl ConnectionView {
             .history
             .update(cx, |history, cx| history.delete_session(&session_id, cx));
         task.detach_and_log_err(cx);
+
+        if let Some(store) = ThreadMetadataStore::try_global(cx) {
+            store
+                .update(cx, |store, cx| store.delete(session_id.clone(), cx))
+                .detach_and_log_err(cx);
+        }
     }
 }
 
@@ -2641,7 +2630,7 @@ fn loading_contents_spinner(size: IconSize) -> AnyElement {
 }
 
 fn placeholder_text(agent_name: &str, has_commands: bool) -> String {
-    if agent_name == "Zed Agent" {
+    if agent_name == agent::ZED_AGENT_ID.as_ref() {
         format!("Message the {} — @ to include context", agent_name)
     } else if has_commands {
         format!(
@@ -2922,9 +2911,7 @@ pub(crate) mod tests {
                 ConnectionView::new(
                     Rc::new(StubAgentServer::default_response()),
                     connection_store,
-                    Agent::Custom {
-                        name: "Test".into(),
-                    },
+                    Agent::Custom { id: "Test".into() },
                     None,
                     None,
                     None,
@@ -3034,9 +3021,7 @@ pub(crate) mod tests {
                 ConnectionView::new(
                     Rc::new(StubAgentServer::new(ResumeOnlyAgentConnection)),
                     connection_store,
-                    Agent::Custom {
-                        name: "Test".into(),
-                    },
+                    Agent::Custom { id: "Test".into() },
                     Some(SessionId::new("resume-session")),
                     None,
                     None,
@@ -3080,7 +3065,7 @@ pub(crate) mod tests {
         let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
 
         let connection = CwdCapturingConnection::new();
-        let captured_cwd = connection.captured_cwd.clone();
+        let captured_cwd = connection.captured_work_dirs.clone();
 
         let thread_store = cx.update(|_window, cx| cx.new(|cx| ThreadStore::new(cx)));
         let connection_store =
@@ -3091,11 +3076,9 @@ pub(crate) mod tests {
                 ConnectionView::new(
                     Rc::new(StubAgentServer::new(connection)),
                     connection_store,
-                    Agent::Custom {
-                        name: "Test".into(),
-                    },
+                    Agent::Custom { id: "Test".into() },
                     Some(SessionId::new("session-1")),
-                    Some(PathBuf::from("/project/subdir")),
+                    Some(PathList::new(&[PathBuf::from("/project/subdir")])),
                     None,
                     None,
                     workspace.downgrade(),
@@ -3111,122 +3094,12 @@ pub(crate) mod tests {
         cx.run_until_parked();
 
         assert_eq!(
-            captured_cwd.lock().as_deref(),
-            Some(Path::new("/project/subdir")),
+            captured_cwd.lock().as_ref().unwrap(),
+            &PathList::new(&[Path::new("/project/subdir")]),
             "Should use session cwd when it's inside the project"
         );
     }
 
-    #[gpui::test]
-    async fn test_resume_thread_uses_fallback_cwd_when_outside_project(cx: &mut TestAppContext) {
-        init_test(cx);
-
-        let fs = FakeFs::new(cx.executor());
-        fs.insert_tree(
-            "/project",
-            json!({
-                "file.txt": "hello"
-            }),
-        )
-        .await;
-        let project = Project::test(fs, [Path::new("/project")], cx).await;
-        let (multi_workspace, cx) =
-            cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
-        let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
-
-        let connection = CwdCapturingConnection::new();
-        let captured_cwd = connection.captured_cwd.clone();
-
-        let thread_store = cx.update(|_window, cx| cx.new(|cx| ThreadStore::new(cx)));
-        let connection_store =
-            cx.update(|_window, cx| cx.new(|cx| AgentConnectionStore::new(project.clone(), cx)));
-
-        let _thread_view = cx.update(|window, cx| {
-            cx.new(|cx| {
-                ConnectionView::new(
-                    Rc::new(StubAgentServer::new(connection)),
-                    connection_store,
-                    Agent::Custom {
-                        name: "Test".into(),
-                    },
-                    Some(SessionId::new("session-1")),
-                    Some(PathBuf::from("/some/other/path")),
-                    None,
-                    None,
-                    workspace.downgrade(),
-                    project,
-                    Some(thread_store),
-                    None,
-                    window,
-                    cx,
-                )
-            })
-        });
-
-        cx.run_until_parked();
-
-        assert_eq!(
-            captured_cwd.lock().as_deref(),
-            Some(Path::new("/project")),
-            "Should use fallback project cwd when session cwd is outside the project"
-        );
-    }
-
-    #[gpui::test]
-    async fn test_resume_thread_rejects_unnormalized_cwd_outside_project(cx: &mut TestAppContext) {
-        init_test(cx);
-
-        let fs = FakeFs::new(cx.executor());
-        fs.insert_tree(
-            "/project",
-            json!({
-                "file.txt": "hello"
-            }),
-        )
-        .await;
-        let project = Project::test(fs, [Path::new("/project")], cx).await;
-        let (multi_workspace, cx) =
-            cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
-        let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
-
-        let connection = CwdCapturingConnection::new();
-        let captured_cwd = connection.captured_cwd.clone();
-
-        let thread_store = cx.update(|_window, cx| cx.new(|cx| ThreadStore::new(cx)));
-        let connection_store =
-            cx.update(|_window, cx| cx.new(|cx| AgentConnectionStore::new(project.clone(), cx)));
-
-        let _thread_view = cx.update(|window, cx| {
-            cx.new(|cx| {
-                ConnectionView::new(
-                    Rc::new(StubAgentServer::new(connection)),
-                    connection_store,
-                    Agent::Custom {
-                        name: "Test".into(),
-                    },
-                    Some(SessionId::new("session-1")),
-                    Some(PathBuf::from("/project/../outside")),
-                    None,
-                    None,
-                    workspace.downgrade(),
-                    project,
-                    Some(thread_store),
-                    None,
-                    window,
-                    cx,
-                )
-            })
-        });
-
-        cx.run_until_parked();
-
-        assert_eq!(
-            captured_cwd.lock().as_deref(),
-            Some(Path::new("/project")),
-            "Should reject unnormalized cwd that resolves outside the project and use fallback cwd"
-        );
-    }
-
     #[gpui::test]
     async fn test_refusal_handling(cx: &mut TestAppContext) {
         init_test(cx);
@@ -3518,9 +3391,7 @@ pub(crate) mod tests {
                 ConnectionView::new(
                     Rc::new(agent),
                     connection_store,
-                    Agent::Custom {
-                        name: "Test".into(),
-                    },
+                    Agent::Custom { id: "Test".into() },
                     None,
                     None,
                     None,
@@ -3733,9 +3604,7 @@ pub(crate) mod tests {
         let connection_store =
             cx.update(|_window, cx| cx.new(|cx| AgentConnectionStore::new(project.clone(), cx)));
 
-        let agent_key = Agent::Custom {
-            name: "Test".into(),
-        };
+        let agent_key = Agent::Custom { id: "Test".into() };
 
         let thread_view = cx.update(|window, cx| {
             cx.new(|cx| {
@@ -3848,7 +3717,7 @@ pub(crate) mod tests {
             ui::IconName::Ai
         }
 
-        fn name(&self) -> SharedString {
+        fn agent_id(&self) -> AgentId {
             "Test".into()
         }
 
@@ -3872,8 +3741,8 @@ pub(crate) mod tests {
             ui::IconName::AiOpenAi
         }
 
-        fn name(&self) -> SharedString {
-            "Codex CLI".into()
+        fn agent_id(&self) -> AgentId {
+            AgentId::new("Codex CLI")
         }
 
         fn connect(
@@ -3959,6 +3828,10 @@ pub(crate) mod tests {
     }
 
     impl AgentConnection for SessionHistoryConnection {
+        fn agent_id(&self) -> AgentId {
+            AgentId::new("history-connection")
+        }
+
         fn telemetry_id(&self) -> SharedString {
             "history-connection".into()
         }
@@ -3966,7 +3839,7 @@ pub(crate) mod tests {
         fn new_session(
             self: Rc<Self>,
             project: Entity<Project>,
-            _cwd: &Path,
+            _work_dirs: PathList,
             cx: &mut App,
         ) -> Task<anyhow::Result<Entity<AcpThread>>> {
             let thread = build_test_thread(
@@ -4019,6 +3892,10 @@ pub(crate) mod tests {
     struct ResumeOnlyAgentConnection;
 
     impl AgentConnection for ResumeOnlyAgentConnection {
+        fn agent_id(&self) -> AgentId {
+            AgentId::new("resume-only")
+        }
+
         fn telemetry_id(&self) -> SharedString {
             "resume-only".into()
         }
@@ -4026,7 +3903,7 @@ pub(crate) mod tests {
         fn new_session(
             self: Rc<Self>,
             project: Entity<Project>,
-            _cwd: &Path,
+            _work_dirs: PathList,
             cx: &mut gpui::App,
         ) -> Task<gpui::Result<Entity<AcpThread>>> {
             let thread = build_test_thread(
@@ -4047,7 +3924,7 @@ pub(crate) mod tests {
             self: Rc<Self>,
             session_id: acp::SessionId,
             project: Entity<Project>,
-            _cwd: &Path,
+            _work_dirs: PathList,
             _title: Option<SharedString>,
             cx: &mut App,
         ) -> Task<gpui::Result<Entity<AcpThread>>> {
@@ -4108,6 +3985,10 @@ pub(crate) mod tests {
     }
 
     impl AgentConnection for AuthGatedAgentConnection {
+        fn agent_id(&self) -> AgentId {
+            AgentId::new("auth-gated")
+        }
+
         fn telemetry_id(&self) -> SharedString {
             "auth-gated".into()
         }
@@ -4115,7 +3996,7 @@ pub(crate) mod tests {
         fn new_session(
             self: Rc<Self>,
             project: Entity<Project>,
-            cwd: &Path,
+            work_dirs: PathList,
             cx: &mut gpui::App,
         ) -> Task<gpui::Result<Entity<AcpThread>>> {
             if !*self.authenticated.lock() {
@@ -4130,7 +4011,7 @@ pub(crate) mod tests {
                 AcpThread::new(
                     None,
                     "AuthGatedAgent",
-                    Some(cwd.to_path_buf()),
+                    Some(work_dirs),
                     self,
                     project,
                     action_log,
@@ -4185,6 +4066,10 @@ pub(crate) mod tests {
     struct SaboteurAgentConnection;
 
     impl AgentConnection for SaboteurAgentConnection {
+        fn agent_id(&self) -> AgentId {
+            AgentId::new("saboteur")
+        }
+
         fn telemetry_id(&self) -> SharedString {
             "saboteur".into()
         }
@@ -4192,7 +4077,7 @@ pub(crate) mod tests {
         fn new_session(
             self: Rc<Self>,
             project: Entity<Project>,
-            cwd: &Path,
+            work_dirs: PathList,
             cx: &mut gpui::App,
         ) -> Task<gpui::Result<Entity<AcpThread>>> {
             Task::ready(Ok(cx.new(|cx| {
@@ -4200,7 +4085,7 @@ pub(crate) mod tests {
                 AcpThread::new(
                     None,
                     "SaboteurAgentConnection",
-                    Some(cwd.to_path_buf()),
+                    Some(work_dirs),
                     self,
                     project,
                     action_log,
@@ -4251,6 +4136,10 @@ pub(crate) mod tests {
     struct RefusalAgentConnection;
 
     impl AgentConnection for RefusalAgentConnection {
+        fn agent_id(&self) -> AgentId {
+            AgentId::new("refusal")
+        }
+
         fn telemetry_id(&self) -> SharedString {
             "refusal".into()
         }
@@ -4258,7 +4147,7 @@ pub(crate) mod tests {
         fn new_session(
             self: Rc<Self>,
             project: Entity<Project>,
-            cwd: &Path,
+            work_dirs: PathList,
             cx: &mut gpui::App,
         ) -> Task<gpui::Result<Entity<AcpThread>>> {
             Task::ready(Ok(cx.new(|cx| {
@@ -4266,7 +4155,7 @@ pub(crate) mod tests {
                 AcpThread::new(
                     None,
                     "RefusalAgentConnection",
-                    Some(cwd.to_path_buf()),
+                    Some(work_dirs),
                     self,
                     project,
                     action_log,
@@ -4314,18 +4203,22 @@ pub(crate) mod tests {
 
     #[derive(Clone)]
     struct CwdCapturingConnection {
-        captured_cwd: Arc<Mutex<Option<PathBuf>>>,
+        captured_work_dirs: Arc<Mutex<Option<PathList>>>,
     }
 
     impl CwdCapturingConnection {
         fn new() -> Self {
             Self {
-                captured_cwd: Arc::new(Mutex::new(None)),
+                captured_work_dirs: Arc::new(Mutex::new(None)),
             }
         }
     }
 
     impl AgentConnection for CwdCapturingConnection {
+        fn agent_id(&self) -> AgentId {
+            AgentId::new("cwd-capturing")
+        }
+
         fn telemetry_id(&self) -> SharedString {
             "cwd-capturing".into()
         }
@@ -4333,16 +4226,16 @@ pub(crate) mod tests {
         fn new_session(
             self: Rc<Self>,
             project: Entity<Project>,
-            cwd: &Path,
+            work_dirs: PathList,
             cx: &mut gpui::App,
         ) -> Task<gpui::Result<Entity<AcpThread>>> {
-            *self.captured_cwd.lock() = Some(cwd.to_path_buf());
+            *self.captured_work_dirs.lock() = Some(work_dirs.clone());
             let action_log = cx.new(|_| ActionLog::new(project.clone()));
             let thread = cx.new(|cx| {
                 AcpThread::new(
                     None,
                     "CwdCapturingConnection",
-                    Some(cwd.to_path_buf()),
+                    Some(work_dirs),
                     self.clone(),
                     project,
                     action_log,
@@ -4367,17 +4260,17 @@ pub(crate) mod tests {
             self: Rc<Self>,
             session_id: acp::SessionId,
             project: Entity<Project>,
-            cwd: &Path,
+            work_dirs: PathList,
             _title: Option<SharedString>,
             cx: &mut App,
         ) -> Task<gpui::Result<Entity<AcpThread>>> {
-            *self.captured_cwd.lock() = Some(cwd.to_path_buf());
+            *self.captured_work_dirs.lock() = Some(work_dirs.clone());
             let action_log = cx.new(|_| ActionLog::new(project.clone()));
             let thread = cx.new(|cx| {
                 AcpThread::new(
                     None,
                     "CwdCapturingConnection",
-                    Some(cwd.to_path_buf()),
+                    Some(work_dirs),
                     self.clone(),
                     project,
                     action_log,
@@ -4426,6 +4319,7 @@ pub(crate) mod tests {
         cx.update(|cx| {
             let settings_store = SettingsStore::test(cx);
             cx.set_global(settings_store);
+            ThreadMetadataStore::init_global(cx);
             theme::init(theme::LoadThemes::JustBase, cx);
             editor::init(cx);
             agent_panel::init(cx);
@@ -4483,9 +4377,7 @@ pub(crate) mod tests {
                 ConnectionView::new(
                     Rc::new(StubAgentServer::new(connection.as_ref().clone())),
                     connection_store,
-                    Agent::Custom {
-                        name: "Test".into(),
-                    },
+                    Agent::Custom { id: "Test".into() },
                     None,
                     None,
                     None,
@@ -6561,9 +6453,7 @@ pub(crate) mod tests {
                 ConnectionView::new(
                     Rc::new(StubAgentServer::default_response()),
                     connection_store,
-                    Agent::Custom {
-                        name: "Test".into(),
-                    },
+                    Agent::Custom { id: "Test".into() },
                     None,
                     None,
                     None,
@@ -6692,6 +6582,10 @@ pub(crate) mod tests {
     }
 
     impl AgentConnection for CloseCapableConnection {
+        fn agent_id(&self) -> AgentId {
+            AgentId::new("close-capable")
+        }
+
         fn telemetry_id(&self) -> SharedString {
             "close-capable".into()
         }
@@ -6699,7 +6593,7 @@ pub(crate) mod tests {
         fn new_session(
             self: Rc<Self>,
             project: Entity<Project>,
-            cwd: &Path,
+            work_dirs: PathList,
             cx: &mut gpui::App,
         ) -> Task<gpui::Result<Entity<AcpThread>>> {
             let action_log = cx.new(|_| ActionLog::new(project.clone()));
@@ -6707,7 +6601,7 @@ pub(crate) mod tests {
                 AcpThread::new(
                     None,
                     "CloseCapableConnection",
-                    Some(cwd.to_path_buf()),
+                    Some(work_dirs),
                     self,
                     project,
                     action_log,

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

@@ -170,7 +170,7 @@ pub struct ThreadView {
     pub server_view: WeakEntity<ConnectionView>,
     pub agent_icon: IconName,
     pub agent_icon_from_external_svg: Option<SharedString>,
-    pub agent_name: SharedString,
+    pub agent_id: AgentId,
     pub focus_handle: FocusHandle,
     pub workspace: WeakEntity<Workspace>,
     pub entry_view_state: Entity<EntryViewState>,
@@ -259,7 +259,7 @@ impl ThreadView {
         server_view: WeakEntity<ConnectionView>,
         agent_icon: IconName,
         agent_icon_from_external_svg: Option<SharedString>,
-        agent_name: SharedString,
+        agent_id: AgentId,
         agent_display_name: SharedString,
         workspace: WeakEntity<Workspace>,
         entry_view_state: Entity<EntryViewState>,
@@ -300,7 +300,7 @@ impl ThreadView {
                 prompt_store,
                 prompt_capabilities.clone(),
                 available_commands.clone(),
-                agent_name.clone(),
+                agent_id.clone(),
                 &placeholder,
                 editor::EditorMode::AutoHeight {
                     min_lines: AgentSettings::get_global(cx).message_editor_min_lines,
@@ -342,7 +342,7 @@ impl ThreadView {
 
         let show_codex_windows_warning = cfg!(windows)
             && project.upgrade().is_some_and(|p| p.read(cx).is_local())
-            && agent_name == "Codex";
+            && agent_id.as_ref() == "Codex";
 
         let title_editor = {
             let can_edit = thread.update(cx, |thread, cx| thread.can_set_title(cx));
@@ -403,7 +403,7 @@ impl ThreadView {
             server_view,
             agent_icon,
             agent_icon_from_external_svg,
-            agent_name,
+            agent_id,
             workspace,
             entry_view_state,
             title_editor,
@@ -739,10 +739,13 @@ impl ThreadView {
                 }
             }
         }));
+        if self.parent_id.is_none() {
+            self.suppress_merge_conflict_notification(cx);
+        }
         generation
     }
 
-    pub fn stop_turn(&mut self, generation: usize) {
+    pub fn stop_turn(&mut self, generation: usize, cx: &mut Context<Self>) {
         if self.turn_fields.turn_generation != generation {
             return;
         }
@@ -753,6 +756,25 @@ impl ThreadView {
             .map(|started| started.elapsed());
         self.turn_fields.last_turn_tokens = self.turn_fields.turn_tokens.take();
         self.turn_fields._turn_timer_task = None;
+        if self.parent_id.is_none() {
+            self.unsuppress_merge_conflict_notification(cx);
+        }
+    }
+
+    fn suppress_merge_conflict_notification(&self, cx: &mut Context<Self>) {
+        self.workspace
+            .update(cx, |workspace, cx| {
+                workspace.suppress_notification(&workspace::merge_conflict_notification_id(), cx);
+            })
+            .ok();
+    }
+
+    fn unsuppress_merge_conflict_notification(&self, cx: &mut Context<Self>) {
+        self.workspace
+            .update(cx, |workspace, _cx| {
+                workspace.unsuppress(workspace::merge_conflict_notification_id());
+            })
+            .ok();
     }
 
     pub fn update_turn_tokens(&mut self, cx: &App) {
@@ -857,13 +879,13 @@ impl ThreadView {
 
                 let connection = self.thread.read(cx).connection().clone();
                 window.defer(cx, {
-                    let agent_name = self.agent_name.clone();
+                    let agent_id = self.agent_id.clone();
                     let server_view = self.server_view.clone();
                     move |window, cx| {
                         ConnectionView::handle_auth_required(
                             server_view.clone(),
                             AuthRequired::new(),
-                            agent_name,
+                            agent_id,
                             connection,
                             window,
                             cx,
@@ -962,7 +984,7 @@ impl ThreadView {
                 let mut cx = cx.clone();
                 move || {
                     this.update(&mut cx, |this, cx| {
-                        this.stop_turn(generation);
+                        this.stop_turn(generation, cx);
                         cx.notify();
                     })
                     .ok();
@@ -3700,16 +3722,16 @@ impl ThreadView {
         let following = self.is_following(cx);
 
         let tooltip_label = if following {
-            if self.agent_name == "Zed Agent" {
-                format!("Stop Following the {}", self.agent_name)
+            if self.agent_id.as_ref() == agent::ZED_AGENT_ID.as_ref() {
+                format!("Stop Following the {}", self.agent_id)
             } else {
-                format!("Stop Following {}", self.agent_name)
+                format!("Stop Following {}", self.agent_id)
             }
         } else {
-            if self.agent_name == "Zed Agent" {
-                format!("Follow the {}", self.agent_name)
+            if self.agent_id.as_ref() == agent::ZED_AGENT_ID.as_ref() {
+                format!("Follow the {}", self.agent_id)
             } else {
-                format!("Follow {}", self.agent_name)
+                format!("Follow {}", self.agent_id)
             }
         };
 
@@ -3801,7 +3823,7 @@ impl ThreadView {
                 let agent_name = if is_subagent {
                     "subagents".into()
                 } else {
-                    self.agent_name.clone()
+                    self.agent_id.clone()
                 };
 
                 v_flex()
@@ -7286,7 +7308,7 @@ impl ThreadView {
             .on_click(cx.listener({
                 move |this, _, window, cx| {
                     let server_view = this.server_view.clone();
-                    let agent_name = this.agent_name.clone();
+                    let agent_name = this.agent_id.clone();
 
                     this.clear_thread_error(cx);
                     if let Some(message) = this.in_flight_prompt.take() {
@@ -7321,7 +7343,7 @@ impl ThreadView {
                 .unwrap_or_else(|| SharedString::from("The model"))
         } else {
             // ACP agent - use the agent name (e.g., "Claude Agent", "Gemini CLI")
-            self.agent_name.clone()
+            self.agent_id.0.clone()
         }
     }
 

crates/agent_ui/src/entry_view_state.rs 🔗

@@ -8,10 +8,10 @@ use collections::HashMap;
 use editor::{Editor, EditorEvent, EditorMode, MinimapVisibility, SizingBehavior};
 use gpui::{
     AnyEntity, App, AppContext as _, Entity, EntityId, EventEmitter, FocusHandle, Focusable,
-    ScrollHandle, SharedString, TextStyleRefinement, WeakEntity, Window,
+    ScrollHandle, TextStyleRefinement, WeakEntity, Window,
 };
 use language::language_settings::SoftWrap;
-use project::Project;
+use project::{AgentId, Project};
 use prompt_store::PromptStore;
 use rope::Point;
 use settings::Settings as _;
@@ -31,7 +31,7 @@ pub struct EntryViewState {
     entries: Vec<Entry>,
     prompt_capabilities: Rc<RefCell<acp::PromptCapabilities>>,
     available_commands: Rc<RefCell<Vec<acp::AvailableCommand>>>,
-    agent_name: SharedString,
+    agent_id: AgentId,
 }
 
 impl EntryViewState {
@@ -43,7 +43,7 @@ impl EntryViewState {
         prompt_store: Option<Entity<PromptStore>>,
         prompt_capabilities: Rc<RefCell<acp::PromptCapabilities>>,
         available_commands: Rc<RefCell<Vec<acp::AvailableCommand>>>,
-        agent_name: SharedString,
+        agent_id: AgentId,
     ) -> Self {
         Self {
             workspace,
@@ -54,7 +54,7 @@ impl EntryViewState {
             entries: Vec::new(),
             prompt_capabilities,
             available_commands,
-            agent_name,
+            agent_id,
         }
     }
 
@@ -96,7 +96,7 @@ impl EntryViewState {
                             self.prompt_store.clone(),
                             self.prompt_capabilities.clone(),
                             self.available_commands.clone(),
-                            self.agent_name.clone(),
+                            self.agent_id.clone(),
                             "Edit message - @ to include context",
                             editor::EditorMode::AutoHeight {
                                 min_lines: 1,
@@ -468,7 +468,7 @@ mod tests {
     use serde_json::json;
     use settings::SettingsStore;
     use util::path;
-    use workspace::MultiWorkspace;
+    use workspace::{MultiWorkspace, PathList};
 
     #[gpui::test]
     async fn test_diff_sync(cx: &mut TestAppContext) {
@@ -495,9 +495,11 @@ mod tests {
         let connection = Rc::new(StubAgentConnection::new());
         let thread = cx
             .update(|_, cx| {
-                connection
-                    .clone()
-                    .new_session(project.clone(), Path::new(path!("/project")), cx)
+                connection.clone().new_session(
+                    project.clone(),
+                    PathList::new(&[Path::new(path!("/project"))]),
+                    cx,
+                )
             })
             .await
             .unwrap();

crates/agent_ui/src/message_editor.rs 🔗

@@ -27,6 +27,7 @@ use gpui::{
     KeyContext, SharedString, Subscription, Task, TextStyle, WeakEntity,
 };
 use language::{Buffer, Language, language_settings::InlayHintKind};
+use project::AgentId;
 use project::{CompletionIntent, InlayHint, InlayHintLabel, InlayId, Project, Worktree};
 use prompt_store::PromptStore;
 use rope::Point;
@@ -45,7 +46,7 @@ pub struct MessageEditor {
     workspace: WeakEntity<Workspace>,
     prompt_capabilities: Rc<RefCell<acp::PromptCapabilities>>,
     available_commands: Rc<RefCell<Vec<acp::AvailableCommand>>>,
-    agent_name: SharedString,
+    agent_id: AgentId,
     thread_store: Option<Entity<ThreadStore>>,
     _subscriptions: Vec<Subscription>,
     _parse_slash_command_task: Task<()>,
@@ -113,7 +114,7 @@ impl MessageEditor {
         prompt_store: Option<Entity<PromptStore>>,
         prompt_capabilities: Rc<RefCell<acp::PromptCapabilities>>,
         available_commands: Rc<RefCell<Vec<acp::AvailableCommand>>>,
-        agent_name: SharedString,
+        agent_id: AgentId,
         placeholder: &str,
         mode: EditorMode,
         window: &mut Window,
@@ -236,7 +237,7 @@ impl MessageEditor {
             workspace,
             prompt_capabilities,
             available_commands,
-            agent_name,
+            agent_id,
             thread_store,
             _subscriptions: subscriptions,
             _parse_slash_command_task: Task::ready(()),
@@ -379,7 +380,7 @@ impl MessageEditor {
     fn validate_slash_commands(
         text: &str,
         available_commands: &[acp::AvailableCommand],
-        agent_name: &str,
+        agent_id: &AgentId,
     ) -> Result<()> {
         if let Some(parsed_command) = SlashCommandCompletion::try_parse(text, 0) {
             if let Some(command_name) = parsed_command.command {
@@ -392,7 +393,7 @@ impl MessageEditor {
                     return Err(anyhow!(
                         "The /{} command is not supported by {}.\n\nAvailable commands: {}",
                         command_name,
-                        agent_name,
+                        agent_id,
                         if available_commands.is_empty() {
                             "none".to_string()
                         } else {
@@ -416,11 +417,11 @@ impl MessageEditor {
     ) -> Task<Result<(Vec<acp::ContentBlock>, Vec<Entity<Buffer>>)>> {
         let text = self.editor.read(cx).text(cx);
         let available_commands = self.available_commands.borrow().clone();
-        let agent_name = self.agent_name.clone();
+        let agent_id = self.agent_id.clone();
         let build_task = self.build_content_blocks(full_mention_content, cx);
 
         cx.spawn(async move |_, _cx| {
-            Self::validate_slash_commands(&text, &available_commands, &agent_name)?;
+            Self::validate_slash_commands(&text, &available_commands, &agent_id)?;
             build_task.await
         })
     }

crates/agent_ui/src/test_support.rs 🔗

@@ -1,7 +1,8 @@
 use acp_thread::{AgentConnection, StubAgentConnection};
 use agent_client_protocol as acp;
 use agent_servers::{AgentServer, AgentServerDelegate};
-use gpui::{Entity, SharedString, Task, TestAppContext, VisualTestContext};
+use gpui::{Entity, Task, TestAppContext, VisualTestContext};
+use project::AgentId;
 use settings::SettingsStore;
 use std::any::Any;
 use std::rc::Rc;
@@ -37,7 +38,7 @@ where
         ui::IconName::Ai
     }
 
-    fn name(&self) -> SharedString {
+    fn agent_id(&self) -> AgentId {
         "Test".into()
     }
 

crates/agent_ui/src/thread_history.rs 🔗

@@ -408,7 +408,7 @@ mod tests {
     fn test_session(session_id: &str, title: &str) -> AgentSessionInfo {
         AgentSessionInfo {
             session_id: acp::SessionId::new(session_id),
-            cwd: None,
+            work_dirs: None,
             title: Some(title.to_string().into()),
             updated_at: None,
             created_at: None,
@@ -608,7 +608,7 @@ mod tests {
         let session_id = acp::SessionId::new("test-session");
         let sessions = vec![AgentSessionInfo {
             session_id: session_id.clone(),
-            cwd: None,
+            work_dirs: None,
             title: Some("Original Title".into()),
             updated_at: None,
             created_at: None,
@@ -641,7 +641,7 @@ mod tests {
         let session_id = acp::SessionId::new("test-session");
         let sessions = vec![AgentSessionInfo {
             session_id: session_id.clone(),
-            cwd: None,
+            work_dirs: None,
             title: Some("Original Title".into()),
             updated_at: None,
             created_at: None,
@@ -671,7 +671,7 @@ mod tests {
         let session_id = acp::SessionId::new("test-session");
         let sessions = vec![AgentSessionInfo {
             session_id: session_id.clone(),
-            cwd: None,
+            work_dirs: None,
             title: Some("Original Title".into()),
             updated_at: None,
             created_at: None,
@@ -704,7 +704,7 @@ mod tests {
         let session_id = acp::SessionId::new("test-session");
         let sessions = vec![AgentSessionInfo {
             session_id: session_id.clone(),
-            cwd: None,
+            work_dirs: None,
             title: None,
             updated_at: None,
             created_at: None,
@@ -741,7 +741,7 @@ mod tests {
         let session_id = acp::SessionId::new("test-session");
         let sessions = vec![AgentSessionInfo {
             session_id: session_id.clone(),
-            cwd: None,
+            work_dirs: None,
             title: Some("Server Title".into()),
             updated_at: None,
             created_at: None,
@@ -775,7 +775,7 @@ mod tests {
         let session_id = acp::SessionId::new("known-session");
         let sessions = vec![AgentSessionInfo {
             session_id,
-            cwd: None,
+            work_dirs: None,
             title: Some("Original".into()),
             updated_at: None,
             created_at: None,

crates/agent_ui/src/thread_history_view.rs 🔗

@@ -755,7 +755,7 @@ impl RenderOnce for HistoryEntryElement {
                                     panel.load_agent_thread(
                                         agent,
                                         entry.session_id.clone(),
-                                        entry.cwd.clone(),
+                                        entry.work_dirs.clone(),
                                         entry.title.clone(),
                                         true,
                                         window,

crates/agent_ui/src/thread_metadata_store.rs 🔗

@@ -0,0 +1,528 @@
+use std::{path::Path, sync::Arc};
+
+use agent::{ThreadStore, ZED_AGENT_ID};
+use agent_client_protocol as acp;
+use anyhow::Result;
+use chrono::{DateTime, Utc};
+use collections::HashMap;
+use db::{
+    sqlez::{
+        bindable::Column, domain::Domain, statement::Statement,
+        thread_safe_connection::ThreadSafeConnection,
+    },
+    sqlez_macros::sql,
+};
+use feature_flags::{AgentV2FeatureFlag, FeatureFlagAppExt};
+use gpui::{AppContext as _, Entity, Global, Subscription, Task};
+use project::AgentId;
+use ui::{App, Context, SharedString};
+use workspace::PathList;
+
+pub fn init(cx: &mut App) {
+    ThreadMetadataStore::init_global(cx);
+
+    if cx.has_flag::<AgentV2FeatureFlag>() {
+        migrate_thread_metadata(cx);
+    }
+    cx.observe_flag::<AgentV2FeatureFlag, _>(|has_flag, cx| {
+        if has_flag {
+            migrate_thread_metadata(cx);
+        }
+    })
+    .detach();
+}
+
+/// Migrate existing thread metadata from native agent thread store to the new metadata storage.
+///
+/// TODO: Remove this after N weeks of shipping the sidebar
+fn migrate_thread_metadata(cx: &mut App) {
+    ThreadMetadataStore::global(cx).update(cx, |store, cx| {
+        let list = store.list(cx);
+        cx.spawn(async move |this, cx| {
+            let Ok(list) = list.await else {
+                return;
+            };
+            if list.is_empty() {
+                this.update(cx, |this, cx| {
+                    let metadata = ThreadStore::global(cx)
+                        .read(cx)
+                        .entries()
+                        .map(|entry| ThreadMetadata {
+                            session_id: entry.id,
+                            agent_id: None,
+                            title: entry.title,
+                            updated_at: entry.updated_at,
+                            created_at: entry.created_at,
+                            folder_paths: entry.folder_paths,
+                        })
+                        .collect::<Vec<_>>();
+                    for entry in metadata {
+                        this.save(entry, cx).detach_and_log_err(cx);
+                    }
+                })
+                .ok();
+            }
+        })
+        .detach();
+    });
+}
+
+struct GlobalThreadMetadataStore(Entity<ThreadMetadataStore>);
+impl Global for GlobalThreadMetadataStore {}
+
+/// Lightweight metadata for any thread (native or ACP), enough to populate
+/// the sidebar list and route to the correct load path when clicked.
+#[derive(Debug, Clone)]
+pub struct ThreadMetadata {
+    pub session_id: acp::SessionId,
+    /// `None` for native Zed threads, `Some("claude-code")` etc. for ACP agents.
+    pub agent_id: Option<AgentId>,
+    pub title: SharedString,
+    pub updated_at: DateTime<Utc>,
+    pub created_at: Option<DateTime<Utc>>,
+    pub folder_paths: PathList,
+}
+
+pub struct ThreadMetadataStore {
+    db: ThreadMetadataDb,
+    session_subscriptions: HashMap<acp::SessionId, Subscription>,
+}
+
+impl ThreadMetadataStore {
+    #[cfg(not(any(test, feature = "test-support")))]
+    pub fn init_global(cx: &mut App) {
+        if cx.has_global::<Self>() {
+            return;
+        }
+
+        let db = THREAD_METADATA_DB.clone();
+        let thread_store = cx.new(|cx| Self::new(db, cx));
+        cx.set_global(GlobalThreadMetadataStore(thread_store));
+    }
+
+    #[cfg(any(test, feature = "test-support"))]
+    pub fn init_global(cx: &mut App) {
+        let thread = std::thread::current();
+        let test_name = thread.name().unwrap_or("unknown_test");
+        let db_name = format!("THREAD_METADATA_DB_{}", test_name);
+        let db = smol::block_on(db::open_test_db::<ThreadMetadataDb>(&db_name));
+        let thread_store = cx.new(|cx| Self::new(ThreadMetadataDb(db), cx));
+        cx.set_global(GlobalThreadMetadataStore(thread_store));
+    }
+
+    pub fn try_global(cx: &App) -> Option<Entity<Self>> {
+        cx.try_global::<GlobalThreadMetadataStore>()
+            .map(|store| store.0.clone())
+    }
+
+    pub fn global(cx: &App) -> Entity<Self> {
+        cx.global::<GlobalThreadMetadataStore>().0.clone()
+    }
+
+    pub fn list(&self, cx: &App) -> Task<Result<Vec<ThreadMetadata>>> {
+        let db = self.db.clone();
+        cx.background_spawn(async move {
+            let s = db.list()?;
+            Ok(s)
+        })
+    }
+
+    pub fn save(&mut self, metadata: ThreadMetadata, cx: &mut Context<Self>) -> Task<Result<()>> {
+        if !cx.has_flag::<AgentV2FeatureFlag>() {
+            return Task::ready(Ok(()));
+        }
+
+        let db = self.db.clone();
+        cx.spawn(async move |this, cx| {
+            db.save(metadata).await?;
+            this.update(cx, |_this, cx| cx.notify())
+        })
+    }
+
+    pub fn delete(
+        &mut self,
+        session_id: acp::SessionId,
+        cx: &mut Context<Self>,
+    ) -> Task<Result<()>> {
+        if !cx.has_flag::<AgentV2FeatureFlag>() {
+            return Task::ready(Ok(()));
+        }
+
+        let db = self.db.clone();
+        cx.spawn(async move |this, cx| {
+            db.delete(session_id).await?;
+            this.update(cx, |_this, cx| cx.notify())
+        })
+    }
+
+    fn new(db: ThreadMetadataDb, cx: &mut Context<Self>) -> Self {
+        let weak_store = cx.weak_entity();
+
+        cx.observe_new::<acp_thread::AcpThread>(move |thread, _window, cx| {
+            let thread_entity = cx.entity();
+
+            cx.on_release({
+                let weak_store = weak_store.clone();
+                move |thread, cx| {
+                    weak_store
+                        .update(cx, |store, _cx| {
+                            store.session_subscriptions.remove(thread.session_id());
+                        })
+                        .ok();
+                }
+            })
+            .detach();
+
+            weak_store
+                .update(cx, |this, cx| {
+                    let subscription = cx.subscribe(&thread_entity, Self::handle_thread_update);
+                    this.session_subscriptions
+                        .insert(thread.session_id().clone(), subscription);
+                })
+                .ok();
+        })
+        .detach();
+
+        Self {
+            db,
+            session_subscriptions: HashMap::default(),
+        }
+    }
+
+    fn handle_thread_update(
+        &mut self,
+        thread: Entity<acp_thread::AcpThread>,
+        event: &acp_thread::AcpThreadEvent,
+        cx: &mut Context<Self>,
+    ) {
+        match event {
+            acp_thread::AcpThreadEvent::NewEntry
+            | acp_thread::AcpThreadEvent::EntryUpdated(_)
+            | acp_thread::AcpThreadEvent::TitleUpdated => {
+                let metadata = Self::metadata_for_acp_thread(thread.read(cx), cx);
+                self.save(metadata, cx).detach_and_log_err(cx);
+            }
+            _ => {}
+        }
+    }
+
+    fn metadata_for_acp_thread(thread: &acp_thread::AcpThread, cx: &App) -> ThreadMetadata {
+        let session_id = thread.session_id().clone();
+        let title = thread.title();
+        let updated_at = Utc::now();
+
+        let agent_id = thread.connection().agent_id();
+
+        let agent_id = if agent_id.as_ref() == ZED_AGENT_ID.as_ref() {
+            None
+        } else {
+            Some(agent_id)
+        };
+
+        let folder_paths = {
+            let project = thread.project().read(cx);
+            let paths: Vec<Arc<Path>> = project
+                .visible_worktrees(cx)
+                .map(|worktree| worktree.read(cx).abs_path())
+                .collect();
+            PathList::new(&paths)
+        };
+
+        ThreadMetadata {
+            session_id,
+            agent_id,
+            title,
+            created_at: Some(updated_at), // handled by db `ON CONFLICT`
+            updated_at,
+            folder_paths,
+        }
+    }
+}
+
+impl Global for ThreadMetadataStore {}
+
+#[derive(Clone)]
+struct ThreadMetadataDb(ThreadSafeConnection);
+
+impl Domain for ThreadMetadataDb {
+    const NAME: &str = stringify!(ThreadMetadataDb);
+
+    const MIGRATIONS: &[&str] = &[sql!(
+        CREATE TABLE IF NOT EXISTS sidebar_threads(
+            session_id TEXT PRIMARY KEY,
+            agent_id TEXT,
+            title TEXT NOT NULL,
+            updated_at TEXT NOT NULL,
+            created_at TEXT,
+            folder_paths TEXT,
+            folder_paths_order TEXT
+        ) STRICT;
+    )];
+}
+
+db::static_connection!(THREAD_METADATA_DB, ThreadMetadataDb, []);
+
+impl ThreadMetadataDb {
+    /// List all sidebar thread metadata, ordered by updated_at descending.
+    pub fn list(&self) -> anyhow::Result<Vec<ThreadMetadata>> {
+        self.select::<ThreadMetadata>(
+            "SELECT session_id, agent_id, title, updated_at, created_at, folder_paths, folder_paths_order \
+             FROM sidebar_threads \
+             ORDER BY updated_at DESC"
+        )?()
+    }
+
+    /// Upsert metadata for a thread.
+    pub async fn save(&self, row: ThreadMetadata) -> anyhow::Result<()> {
+        let id = row.session_id.0.clone();
+        let agent_id = row.agent_id.as_ref().map(|id| id.0.to_string());
+        let title = row.title.to_string();
+        let updated_at = row.updated_at.to_rfc3339();
+        let created_at = row.created_at.map(|dt| dt.to_rfc3339());
+        let serialized = row.folder_paths.serialize();
+        let (folder_paths, folder_paths_order) = if row.folder_paths.is_empty() {
+            (None, None)
+        } else {
+            (Some(serialized.paths), Some(serialized.order))
+        };
+
+        self.write(move |conn| {
+            let sql = "INSERT INTO sidebar_threads(session_id, agent_id, title, updated_at, created_at, folder_paths, folder_paths_order) \
+                       VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7) \
+                       ON CONFLICT(session_id) DO UPDATE SET \
+                           agent_id = excluded.agent_id, \
+                           title = excluded.title, \
+                           updated_at = excluded.updated_at, \
+                           folder_paths = excluded.folder_paths, \
+                           folder_paths_order = excluded.folder_paths_order";
+            let mut stmt = Statement::prepare(conn, sql)?;
+            let mut i = stmt.bind(&id, 1)?;
+            i = stmt.bind(&agent_id, i)?;
+            i = stmt.bind(&title, i)?;
+            i = stmt.bind(&updated_at, i)?;
+            i = stmt.bind(&created_at, i)?;
+            i = stmt.bind(&folder_paths, i)?;
+            stmt.bind(&folder_paths_order, i)?;
+            stmt.exec()
+        })
+        .await
+    }
+
+    /// Delete metadata for a single thread.
+    pub async fn delete(&self, session_id: acp::SessionId) -> anyhow::Result<()> {
+        let id = session_id.0.clone();
+        self.write(move |conn| {
+            let mut stmt =
+                Statement::prepare(conn, "DELETE FROM sidebar_threads WHERE session_id = ?")?;
+            stmt.bind(&id, 1)?;
+            stmt.exec()
+        })
+        .await
+    }
+}
+
+impl Column for ThreadMetadata {
+    fn column(statement: &mut Statement, start_index: i32) -> anyhow::Result<(Self, i32)> {
+        let (id, next): (Arc<str>, i32) = Column::column(statement, start_index)?;
+        let (agent_id, next): (Option<String>, i32) = Column::column(statement, next)?;
+        let (title, next): (String, i32) = Column::column(statement, next)?;
+        let (updated_at_str, next): (String, i32) = Column::column(statement, next)?;
+        let (created_at_str, next): (Option<String>, i32) = Column::column(statement, next)?;
+        let (folder_paths_str, next): (Option<String>, i32) = Column::column(statement, next)?;
+        let (folder_paths_order_str, next): (Option<String>, i32) =
+            Column::column(statement, next)?;
+
+        let updated_at = DateTime::parse_from_rfc3339(&updated_at_str)?.with_timezone(&Utc);
+        let created_at = created_at_str
+            .as_deref()
+            .map(DateTime::parse_from_rfc3339)
+            .transpose()?
+            .map(|dt| dt.with_timezone(&Utc));
+
+        let folder_paths = folder_paths_str
+            .map(|paths| {
+                PathList::deserialize(&util::path_list::SerializedPathList {
+                    paths,
+                    order: folder_paths_order_str.unwrap_or_default(),
+                })
+            })
+            .unwrap_or_default();
+
+        Ok((
+            ThreadMetadata {
+                session_id: acp::SessionId::new(id),
+                agent_id: agent_id.map(|id| AgentId::new(id)),
+                title: title.into(),
+                updated_at,
+                created_at,
+                folder_paths,
+            },
+            next,
+        ))
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use agent::DbThread;
+    use gpui::TestAppContext;
+
+    fn make_db_thread(title: &str, updated_at: DateTime<Utc>) -> DbThread {
+        DbThread {
+            title: title.to_string().into(),
+            messages: Vec::new(),
+            updated_at,
+            detailed_summary: None,
+            initial_project_snapshot: None,
+            cumulative_token_usage: Default::default(),
+            request_token_usage: Default::default(),
+            model: None,
+            profile: None,
+            imported: false,
+            subagent_context: None,
+            speed: None,
+            thinking_enabled: false,
+            thinking_effort: None,
+            draft_prompt: None,
+            ui_scroll_position: None,
+        }
+    }
+
+    #[gpui::test]
+    async fn test_migrate_thread_metadata(cx: &mut TestAppContext) {
+        cx.update(|cx| {
+            ThreadStore::init_global(cx);
+            ThreadMetadataStore::init_global(cx);
+        });
+
+        // Verify the list is empty before migration
+        let metadata_list = cx.update(|cx| {
+            let store = ThreadMetadataStore::global(cx);
+            store.read(cx).list(cx)
+        });
+
+        let list = metadata_list.await.unwrap();
+        assert_eq!(list.len(), 0);
+
+        let now = Utc::now();
+
+        // Populate the native ThreadStore via save_thread
+        let save1 = cx.update(|cx| {
+            let thread_store = ThreadStore::global(cx);
+            thread_store.update(cx, |store, cx| {
+                store.save_thread(
+                    acp::SessionId::new("session-1"),
+                    make_db_thread("Thread 1", now),
+                    PathList::default(),
+                    cx,
+                )
+            })
+        });
+        save1.await.unwrap();
+        cx.run_until_parked();
+
+        let save2 = cx.update(|cx| {
+            let thread_store = ThreadStore::global(cx);
+            thread_store.update(cx, |store, cx| {
+                store.save_thread(
+                    acp::SessionId::new("session-2"),
+                    make_db_thread("Thread 2", now),
+                    PathList::default(),
+                    cx,
+                )
+            })
+        });
+        save2.await.unwrap();
+        cx.run_until_parked();
+
+        // Run migration
+        cx.update(|cx| {
+            migrate_thread_metadata(cx);
+        });
+
+        cx.run_until_parked();
+
+        // Verify the metadata was migrated
+        let metadata_list = cx.update(|cx| {
+            let store = ThreadMetadataStore::global(cx);
+            store.read(cx).list(cx)
+        });
+
+        let list = metadata_list.await.unwrap();
+        assert_eq!(list.len(), 2);
+
+        let metadata1 = list
+            .iter()
+            .find(|m| m.session_id.0.as_ref() == "session-1")
+            .expect("session-1 should be in migrated metadata");
+        assert_eq!(metadata1.title.as_ref(), "Thread 1");
+        assert!(metadata1.agent_id.is_none());
+
+        let metadata2 = list
+            .iter()
+            .find(|m| m.session_id.0.as_ref() == "session-2")
+            .expect("session-2 should be in migrated metadata");
+        assert_eq!(metadata2.title.as_ref(), "Thread 2");
+        assert!(metadata2.agent_id.is_none());
+    }
+
+    #[gpui::test]
+    async fn test_migrate_thread_metadata_skips_when_data_exists(cx: &mut TestAppContext) {
+        cx.update(|cx| {
+            ThreadStore::init_global(cx);
+            ThreadMetadataStore::init_global(cx);
+        });
+
+        // Pre-populate the metadata store with existing data
+        let existing_metadata = ThreadMetadata {
+            session_id: acp::SessionId::new("existing-session"),
+            agent_id: None,
+            title: "Existing Thread".into(),
+            updated_at: Utc::now(),
+            created_at: Some(Utc::now()),
+            folder_paths: PathList::default(),
+        };
+
+        cx.update(|cx| {
+            let store = ThreadMetadataStore::global(cx);
+            store.update(cx, |store, cx| {
+                store.save(existing_metadata, cx).detach();
+            });
+        });
+
+        cx.run_until_parked();
+
+        // Add an entry to native thread store that should NOT be migrated
+        let save_task = cx.update(|cx| {
+            let thread_store = ThreadStore::global(cx);
+            thread_store.update(cx, |store, cx| {
+                store.save_thread(
+                    acp::SessionId::new("native-session"),
+                    make_db_thread("Native Thread", Utc::now()),
+                    PathList::default(),
+                    cx,
+                )
+            })
+        });
+        save_task.await.unwrap();
+        cx.run_until_parked();
+
+        // Run migration - should skip because metadata store is not empty
+        cx.update(|cx| {
+            migrate_thread_metadata(cx);
+        });
+
+        cx.run_until_parked();
+
+        // Verify only the existing metadata is present (migration was skipped)
+        let metadata_list = cx.update(|cx| {
+            let store = ThreadMetadataStore::global(cx);
+            store.read(cx).list(cx)
+        });
+
+        let list = metadata_list.await.unwrap();
+        assert_eq!(list.len(), 1);
+        assert_eq!(list[0].session_id.0.as_ref(), "existing-session");
+    }
+}

crates/agent_ui/src/threads_archive_view.rs 🔗

@@ -16,7 +16,7 @@ use gpui::{
 };
 use itertools::Itertools as _;
 use menu::{Confirm, SelectFirst, SelectLast, SelectNext, SelectPrevious};
-use project::{AgentServerStore, ExternalAgentServerName};
+use project::{AgentId, AgentServerStore};
 use theme::ActiveTheme;
 use ui::{
     ButtonLike, CommonAnimationExt, ContextMenu, ContextMenuEntry, HighlightedLabel, ListItem,
@@ -530,9 +530,9 @@ impl ThreadsArchiveView {
             (IconName::ChevronDown, Color::Muted)
         };
 
-        let selected_agent_icon = if let Agent::Custom { name } = &self.selected_agent {
+        let selected_agent_icon = if let Agent::Custom { id } = &self.selected_agent {
             let store = agent_server_store.read(cx);
-            let icon = store.agent_icon(&ExternalAgentServerName(name.clone()));
+            let icon = store.agent_icon(&id);
 
             if let Some(icon) = icon {
                 Icon::from_external_svg(icon)
@@ -584,24 +584,24 @@ impl ThreadsArchiveView {
                         let registry_store_ref = registry_store.as_ref().map(|s| s.read(cx));
 
                         struct AgentMenuItem {
-                            id: ExternalAgentServerName,
+                            id: AgentId,
                             display_name: SharedString,
                         }
 
                         let agent_items = agent_server_store
                             .external_agents()
-                            .map(|name| {
+                            .map(|agent_id| {
                                 let display_name = agent_server_store
-                                    .agent_display_name(name)
+                                    .agent_display_name(agent_id)
                                     .or_else(|| {
                                         registry_store_ref
                                             .as_ref()
-                                            .and_then(|store| store.agent(name.0.as_ref()))
+                                            .and_then(|store| store.agent(agent_id))
                                             .map(|a| a.name().clone())
                                     })
-                                    .unwrap_or_else(|| name.0.clone());
+                                    .unwrap_or_else(|| agent_id.0.clone());
                                 AgentMenuItem {
-                                    id: name.clone(),
+                                    id: agent_id.clone(),
                                     display_name,
                                 }
                             })
@@ -614,7 +614,7 @@ impl ThreadsArchiveView {
                             let icon_path = agent_server_store.agent_icon(&item.id).or_else(|| {
                                 registry_store_ref
                                     .as_ref()
-                                    .and_then(|store| store.agent(item.id.0.as_str()))
+                                    .and_then(|store| store.agent(&item.id))
                                     .and_then(|a| a.icon_path().cloned())
                             });
 
@@ -627,7 +627,7 @@ impl ThreadsArchiveView {
                             entry = entry.icon_color(Color::Muted).handler({
                                 let this = this.clone();
                                 let agent = Agent::Custom {
-                                    name: item.id.0.clone(),
+                                    id: item.id.clone(),
                                 };
                                 move |window, cx| {
                                     this.update(cx, |this, cx| {

crates/agent_ui/src/threads_panel.rs 🔗

@@ -1,9 +1,10 @@
+use crate::thread_metadata_store::{ThreadMetadata, ThreadMetadataStore};
 use crate::threads_archive_view::{ThreadsArchiveView, ThreadsArchiveViewEvent};
 use crate::{Agent, AgentPanel, AgentPanelEvent, NewThread, RemoveSelectedThread};
 use acp_thread::ThreadStatus;
 use action_log::DiffStats;
 use agent::ThreadStore;
-use agent_client_protocol as acp;
+use agent_client_protocol::{self as acp};
 use agent_settings::AgentSettings;
 use chrono::Utc;
 use db::kvp::KEY_VALUE_STORE;
@@ -15,7 +16,7 @@ use gpui::{
     px,
 };
 use menu::{Cancel, Confirm, SelectFirst, SelectLast, SelectNext, SelectPrevious};
-use project::Event as ProjectEvent;
+use project::{AgentId, Event as ProjectEvent};
 use settings::Settings;
 use std::collections::{HashMap, HashSet};
 use std::mem;
@@ -95,7 +96,7 @@ impl From<&ActiveThreadInfo> for acp_thread::AgentSessionInfo {
     fn from(info: &ActiveThreadInfo) -> Self {
         Self {
             session_id: info.session_id.clone(),
-            cwd: None,
+            work_dirs: None,
             title: Some(info.title.clone()),
             updated_at: Some(Utc::now()),
             created_at: Some(Utc::now()),
@@ -255,6 +256,7 @@ pub struct ThreadsPanel {
     view: SidebarView,
     archive_view: Option<Entity<ThreadsArchiveView>>,
     _subscriptions: Vec<gpui::Subscription>,
+    _update_entries_task: Option<gpui::Task<()>>,
 }
 
 impl ThreadsPanel {
@@ -295,14 +297,14 @@ impl ThreadsPanel {
             window,
             |this, _multi_workspace, event: &MultiWorkspaceEvent, window, cx| match event {
                 MultiWorkspaceEvent::ActiveWorkspaceChanged => {
-                    this.update_entries(cx);
+                    this.update_entries(false, cx);
                 }
                 MultiWorkspaceEvent::WorkspaceAdded(workspace) => {
                     this.subscribe_to_workspace(workspace, window, cx);
-                    this.update_entries(cx);
+                    this.update_entries(false, cx);
                 }
                 MultiWorkspaceEvent::WorkspaceRemoved(_) => {
-                    this.update_entries(cx);
+                    this.update_entries(false, cx);
                 }
             },
         )
@@ -314,33 +316,18 @@ impl ThreadsPanel {
                 if !query.is_empty() {
                     this.selection.take();
                 }
-                this.update_entries(cx);
-                if !query.is_empty() {
-                    this.selection = this
-                        .contents
-                        .entries
-                        .iter()
-                        .position(|entry| matches!(entry, ListEntry::Thread(_)))
-                        .or_else(|| {
-                            if this.contents.entries.is_empty() {
-                                None
-                            } else {
-                                Some(0)
-                            }
-                        });
-                }
+                this.update_entries(!query.is_empty(), cx);
             }
         })
         .detach();
 
-        let thread_store = ThreadStore::global(cx);
-        cx.observe_in(&thread_store, window, |this, _, _window, cx| {
-            this.update_entries(cx);
+        cx.observe(&ThreadMetadataStore::global(cx), |this, _store, cx| {
+            this.update_entries(false, cx);
         })
         .detach();
 
         cx.observe_flag::<AgentV2FeatureFlag, _>(window, |_is_enabled, this, _window, cx| {
-            this.update_entries(cx);
+            this.update_entries(false, cx);
         })
         .detach();
 
@@ -349,7 +336,7 @@ impl ThreadsPanel {
             for workspace in &workspaces {
                 this.subscribe_to_workspace(workspace, window, cx);
             }
-            this.update_entries(cx);
+            this.update_entries(false, cx);
         });
 
         let persistence_key = multi_workspace.read(cx).database_id().map(|id| id.0);
@@ -358,6 +345,7 @@ impl ThreadsPanel {
             .unwrap_or(false);
 
         Self {
+            _update_entries_task: None,
             multi_workspace: multi_workspace.downgrade(),
             persistence_key,
             is_open,
@@ -392,7 +380,7 @@ impl ThreadsPanel {
                 ProjectEvent::WorktreeAdded(_)
                 | ProjectEvent::WorktreeRemoved(_)
                 | ProjectEvent::WorktreeOrderChanged => {
-                    this.update_entries(cx);
+                    this.update_entries(false, cx);
                 }
                 _ => {}
             },
@@ -413,7 +401,7 @@ impl ThreadsPanel {
                     )
                 ) {
                     this.prune_stale_worktree_workspaces(window, cx);
-                    this.update_entries(cx);
+                    this.update_entries(false, cx);
                 }
             },
         )
@@ -450,7 +438,7 @@ impl ThreadsPanel {
                 AgentPanelEvent::ActiveViewChanged
                 | AgentPanelEvent::ThreadFocused
                 | AgentPanelEvent::BackgroundThreadChanged => {
-                    this.update_entries(cx);
+                    this.update_entries(false, cx);
                 }
             },
         )
@@ -508,7 +496,7 @@ impl ThreadsPanel {
             .collect()
     }
 
-    fn rebuild_contents(&mut self, cx: &App) {
+    fn rebuild_contents(&mut self, thread_entries: Vec<ThreadMetadata>, cx: &App) {
         let Some(multi_workspace) = self.multi_workspace.upgrade() else {
             return;
         };
@@ -522,7 +510,19 @@ impl ThreadsPanel {
             .and_then(|panel| panel.read(cx).active_connection_view().cloned())
             .and_then(|cv| cv.read(cx).parent_id(cx));
 
-        let thread_store = ThreadStore::try_global(cx);
+        let mut threads_by_paths: HashMap<PathList, Vec<ThreadMetadata>> = HashMap::new();
+        for row in thread_entries {
+            threads_by_paths
+                .entry(row.folder_paths.clone())
+                .or_default()
+                .push(row);
+        }
+
+        // Build a lookup for agent icons from the first workspace's AgentServerStore.
+        let agent_server_store = workspaces
+            .first()
+            .map(|ws| ws.read(cx).project().read(cx).agent_server_store().clone());
+
         let query = self.filter_editor.read(cx).text(cx);
 
         let previous = mem::take(&mut self.contents);
@@ -607,14 +607,35 @@ impl ThreadsPanel {
             if should_load_threads {
                 let mut seen_session_ids: HashSet<acp::SessionId> = HashSet::new();
 
-                if let Some(ref thread_store) = thread_store {
-                    for meta in thread_store.read(cx).threads_for_paths(&path_list) {
-                        seen_session_ids.insert(meta.id.clone());
+                // Read threads from SidebarDb for this workspace's path list.
+                if let Some(rows) = threads_by_paths.get(&path_list) {
+                    for row in rows {
+                        seen_session_ids.insert(row.session_id.clone());
+                        let (agent, icon, icon_from_external_svg) = match &row.agent_id {
+                            None => (Agent::NativeAgent, IconName::ZedAgent, None),
+                            Some(id) => {
+                                let custom_icon = agent_server_store
+                                    .as_ref()
+                                    .and_then(|store| store.read(cx).agent_icon(&id));
+                                (
+                                    Agent::Custom { id: id.clone() },
+                                    IconName::Terminal,
+                                    custom_icon,
+                                )
+                            }
+                        };
                         threads.push(ThreadEntry {
-                            agent: Agent::NativeAgent,
-                            session_info: meta.into(),
-                            icon: IconName::ZedAgent,
-                            icon_from_external_svg: None,
+                            agent,
+                            session_info: acp_thread::AgentSessionInfo {
+                                session_id: row.session_id.clone(),
+                                work_dirs: None,
+                                title: Some(row.title.clone()),
+                                updated_at: Some(row.updated_at),
+                                created_at: row.created_at,
+                                meta: None,
+                            },
+                            icon,
+                            icon_from_external_svg,
                             status: AgentThreadStatus::default(),
                             workspace: ThreadEntryWorkspace::Open(workspace.clone()),
                             is_live: false,
@@ -629,7 +650,7 @@ impl ThreadsPanel {
                 }
 
                 // Load threads from linked git worktrees of this workspace's repos.
-                if let Some(ref thread_store) = thread_store {
+                {
                     let mut linked_worktree_queries: Vec<(PathList, SharedString, Arc<Path>)> =
                         Vec::new();
                     for snapshot in root_repository_snapshots(workspace, cx) {
@@ -660,25 +681,52 @@ impl ThreadsPanel {
                                 None => ThreadEntryWorkspace::Closed(worktree_path_list.clone()),
                             };
 
-                        for meta in thread_store.read(cx).threads_for_paths(worktree_path_list) {
-                            if !seen_session_ids.insert(meta.id.clone()) {
-                                continue;
+                        if let Some(rows) = threads_by_paths.get(worktree_path_list) {
+                            for row in rows {
+                                if !seen_session_ids.insert(row.session_id.clone()) {
+                                    continue;
+                                }
+                                let (agent, icon, icon_from_external_svg) = match &row.agent_id {
+                                    None => (Agent::NativeAgent, IconName::ZedAgent, None),
+                                    Some(name) => {
+                                        let custom_icon =
+                                            agent_server_store.as_ref().and_then(|store| {
+                                                store
+                                                    .read(cx)
+                                                    .agent_icon(&AgentId(name.clone().into()))
+                                            });
+                                        (
+                                            Agent::Custom {
+                                                id: AgentId::new(name.clone()),
+                                            },
+                                            IconName::Terminal,
+                                            custom_icon,
+                                        )
+                                    }
+                                };
+                                threads.push(ThreadEntry {
+                                    agent,
+                                    session_info: acp_thread::AgentSessionInfo {
+                                        session_id: row.session_id.clone(),
+                                        work_dirs: None,
+                                        title: Some(row.title.clone()),
+                                        updated_at: Some(row.updated_at),
+                                        created_at: row.created_at,
+                                        meta: None,
+                                    },
+                                    icon,
+                                    icon_from_external_svg,
+                                    status: AgentThreadStatus::default(),
+                                    workspace: target_workspace.clone(),
+                                    is_live: false,
+                                    is_background: false,
+                                    is_title_generating: false,
+                                    highlight_positions: Vec::new(),
+                                    worktree_name: Some(worktree_name.clone()),
+                                    worktree_highlight_positions: Vec::new(),
+                                    diff_stats: DiffStats::default(),
+                                });
                             }
-                            threads.push(ThreadEntry {
-                                agent: Agent::NativeAgent,
-                                session_info: meta.into(),
-                                icon: IconName::ZedAgent,
-                                icon_from_external_svg: None,
-                                status: AgentThreadStatus::default(),
-                                workspace: target_workspace.clone(),
-                                is_live: false,
-                                is_background: false,
-                                is_title_generating: false,
-                                highlight_positions: Vec::new(),
-                                worktree_name: Some(worktree_name.clone()),
-                                worktree_highlight_positions: Vec::new(),
-                                diff_stats: DiffStats::default(),
-                            });
                         }
                     }
                 }
@@ -887,7 +935,7 @@ impl ThreadsPanel {
         };
     }
 
-    fn update_entries(&mut self, cx: &mut Context<Self>) {
+    fn update_entries(&mut self, select_first_thread: bool, cx: &mut Context<Self>) {
         let Some(multi_workspace) = self.multi_workspace.upgrade() else {
             return;
         };
@@ -899,18 +947,44 @@ impl ThreadsPanel {
 
         let scroll_position = self.list_state.logical_scroll_top();
 
-        self.rebuild_contents(cx);
+        let list_thread_entries_task = ThreadMetadataStore::global(cx).read(cx).list(cx);
 
-        self.list_state.reset(self.contents.entries.len());
-        self.list_state.scroll_to(scroll_position);
+        self._update_entries_task.take();
+        self._update_entries_task = Some(cx.spawn(async move |this, cx| {
+            let Some(thread_entries) = list_thread_entries_task.await.log_err() else {
+                return;
+            };
+            this.update(cx, |this, cx| {
+                this.rebuild_contents(thread_entries, cx);
 
-        if had_notifications != self.has_notifications(cx) {
-            multi_workspace.update(cx, |_, cx| {
-                cx.notify();
-            });
-        }
+                if select_first_thread {
+                    this.selection = this
+                        .contents
+                        .entries
+                        .iter()
+                        .position(|entry| matches!(entry, ListEntry::Thread(_)))
+                        .or_else(|| {
+                            if this.contents.entries.is_empty() {
+                                None
+                            } else {
+                                Some(0)
+                            }
+                        });
+                }
 
-        cx.notify();
+                this.list_state.reset(this.contents.entries.len());
+                this.list_state.scroll_to(scroll_position);
+
+                if had_notifications != this.has_notifications(cx) {
+                    multi_workspace.update(cx, |_, cx| {
+                        cx.notify();
+                    });
+                }
+
+                cx.notify();
+            })
+            .ok();
+        }));
     }
 
     fn render_list_entry(
@@ -1094,7 +1168,7 @@ impl ThreadsPanel {
                                 move |this, _, _window, cx| {
                                     this.selection = None;
                                     this.expanded_groups.remove(&path_list_for_collapse);
-                                    this.update_entries(cx);
+                                    this.update_entries(false, cx);
                                 }
                             })),
                         )
@@ -1300,14 +1374,14 @@ impl ThreadsPanel {
         } else {
             self.collapsed_groups.insert(path_list.clone());
         }
-        self.update_entries(cx);
+        self.update_entries(false, cx);
     }
 
     fn focus_in(&mut self, _window: &mut Window, _cx: &mut Context<Self>) {}
 
     fn cancel(&mut self, _: &Cancel, window: &mut Window, cx: &mut Context<Self>) {
         if self.reset_filter_editor_text(window, cx) {
-            self.update_entries(cx);
+            self.update_entries(false, cx);
         } else {
             self.focus_handle.focus(window, cx);
         }
@@ -1426,7 +1500,7 @@ impl ThreadsPanel {
                     let current = self.expanded_groups.get(&path_list).copied().unwrap_or(0);
                     self.expanded_groups.insert(path_list, current + 1);
                 }
-                self.update_entries(cx);
+                self.update_entries(false, cx);
             }
             ListEntry::NewThread { workspace, .. } => {
                 let workspace = workspace.clone();
@@ -1460,7 +1534,7 @@ impl ThreadsPanel {
                 panel.load_agent_thread(
                     agent,
                     session_info.session_id,
-                    session_info.cwd,
+                    session_info.work_dirs,
                     session_info.title,
                     true,
                     window,
@@ -1469,7 +1543,7 @@ impl ThreadsPanel {
             });
         }
 
-        self.update_entries(cx);
+        self.update_entries(false, cx);
     }
 
     fn open_workspace_and_activate_thread(
@@ -1520,24 +1594,11 @@ impl ThreadsPanel {
         window: &mut Window,
         cx: &mut Context<Self>,
     ) {
-        let saved_path_list = ThreadStore::try_global(cx).and_then(|thread_store| {
-            thread_store
-                .read(cx)
-                .thread_from_session_id(&session_info.session_id)
-                .map(|thread| thread.folder_paths.clone())
-        });
-        let path_list = saved_path_list.or_else(|| {
-            // we don't have saved metadata, so create path list based on the cwd
-            session_info
-                .cwd
-                .as_ref()
-                .map(|cwd| PathList::new(&[cwd.to_path_buf()]))
-        });
-
-        if let Some(path_list) = path_list {
+        if let Some(path_list) = &session_info.work_dirs {
             if let Some(workspace) = self.find_open_workspace_for_path_list(&path_list, cx) {
                 self.activate_thread(agent, session_info, &workspace, window, cx);
             } else {
+                let path_list = path_list.clone();
                 self.open_workspace_and_activate_thread(agent, session_info, path_list, window, cx);
             }
             return;
@@ -1568,7 +1629,7 @@ impl ThreadsPanel {
                 if self.collapsed_groups.contains(path_list) {
                     let path_list = path_list.clone();
                     self.collapsed_groups.remove(&path_list);
-                    self.update_entries(cx);
+                    self.update_entries(false, cx);
                 } else if ix + 1 < self.contents.entries.len() {
                     self.selection = Some(ix + 1);
                     self.list_state.scroll_to_reveal_item(ix + 1);
@@ -1592,7 +1653,7 @@ impl ThreadsPanel {
                 if !self.collapsed_groups.contains(path_list) {
                     let path_list = path_list.clone();
                     self.collapsed_groups.insert(path_list);
-                    self.update_entries(cx);
+                    self.update_entries(false, cx);
                 }
             }
             Some(
@@ -1605,7 +1666,7 @@ impl ThreadsPanel {
                         let path_list = path_list.clone();
                         self.selection = Some(i);
                         self.collapsed_groups.insert(path_list);
-                        self.update_entries(cx);
+                        self.update_entries(false, cx);
                         break;
                     }
                 }
@@ -1623,6 +1684,10 @@ impl ThreadsPanel {
                 .delete_thread(session_id.clone(), cx)
                 .detach_and_log_err(cx);
         });
+
+        ThreadMetadataStore::global(cx)
+            .update(cx, |store, cx| store.delete(session_id.clone(), cx))
+            .detach_and_log_err(cx);
     }
 
     fn remove_selected_thread(
@@ -1828,7 +1893,7 @@ impl ThreadsPanel {
                     let current = this.expanded_groups.get(&path_list).copied().unwrap_or(0);
                     this.expanded_groups.insert(path_list.clone(), current + 1);
                 }
-                this.update_entries(cx);
+                this.update_entries(false, cx);
             }))
             .into_any_element()
     }
@@ -1919,7 +1984,7 @@ impl ThreadsPanel {
                                 .tooltip(Tooltip::text("Clear Search"))
                                 .on_click(cx.listener(|this, _, window, cx| {
                                     this.reset_filter_editor_text(window, cx);
-                                    this.update_entries(cx);
+                                    this.update_entries(false, cx);
                                 })),
                         )
                     })
@@ -2209,7 +2274,8 @@ mod tests {
     use feature_flags::FeatureFlagAppExt as _;
     use fs::FakeFs;
     use gpui::TestAppContext;
-    use std::sync::Arc;
+    use pretty_assertions::assert_eq;
+    use std::{path::PathBuf, sync::Arc};
     use util::path_list::PathList;
 
     fn init_test(cx: &mut TestAppContext) {
@@ -2217,32 +2283,12 @@ mod tests {
         cx.update(|cx| {
             cx.update_flags(false, vec!["agent-v2".into()]);
             ThreadStore::init_global(cx);
+            ThreadMetadataStore::init_global(cx);
             language_model::LanguageModelRegistry::test(cx);
             prompt_store::init(cx);
         });
     }
 
-    fn make_test_thread(title: &str, updated_at: DateTime<Utc>) -> agent::DbThread {
-        agent::DbThread {
-            title: title.to_string().into(),
-            messages: Vec::new(),
-            updated_at,
-            detailed_summary: None,
-            initial_project_snapshot: None,
-            cumulative_token_usage: Default::default(),
-            request_token_usage: Default::default(),
-            model: None,
-            profile: None,
-            imported: false,
-            subagent_context: None,
-            speed: None,
-            thinking_enabled: false,
-            thinking_effort: None,
-            draft_prompt: None,
-            ui_scroll_position: None,
-        }
-    }
-
     async fn init_test_project(
         worktree_path: &str,
         cx: &mut TestAppContext,
@@ -2288,45 +2334,72 @@ mod tests {
         path_list: &PathList,
         cx: &mut gpui::VisualTestContext,
     ) {
-        let thread_store = cx.update(|_window, cx| ThreadStore::global(cx));
         for i in 0..count {
-            let save_task = thread_store.update(cx, |store, cx| {
-                store.save_thread(
-                    acp::SessionId::new(Arc::from(format!("thread-{}", i))),
-                    make_test_thread(
-                        &format!("Thread {}", i + 1),
-                        chrono::TimeZone::with_ymd_and_hms(&Utc, 2024, 1, 1, 0, 0, i).unwrap(),
-                    ),
-                    path_list.clone(),
-                    cx,
-                )
-            });
-            save_task.await.unwrap();
+            save_thread_metadata(
+                acp::SessionId::new(Arc::from(format!("thread-{}", i))),
+                format!("Thread {}", i + 1).into(),
+                chrono::TimeZone::with_ymd_and_hms(&Utc, 2024, 1, 1, 0, 0, i).unwrap(),
+                path_list.clone(),
+                cx,
+            )
+            .await;
         }
         cx.run_until_parked();
     }
 
-    async fn save_thread_to_store(
+    async fn save_test_thread_metadata(
         session_id: &acp::SessionId,
+        path_list: PathList,
+        cx: &mut TestAppContext,
+    ) {
+        save_thread_metadata(
+            session_id.clone(),
+            "Test".into(),
+            chrono::TimeZone::with_ymd_and_hms(&Utc, 2024, 1, 1, 0, 0, 0).unwrap(),
+            path_list,
+            cx,
+        )
+        .await;
+    }
+
+    async fn save_named_thread_metadata(
+        session_id: &str,
+        title: &str,
         path_list: &PathList,
         cx: &mut gpui::VisualTestContext,
     ) {
-        let thread_store = cx.update(|_window, cx| ThreadStore::global(cx));
-        let save_task = thread_store.update(cx, |store, cx| {
-            store.save_thread(
-                session_id.clone(),
-                make_test_thread(
-                    "Test",
-                    chrono::TimeZone::with_ymd_and_hms(&Utc, 2024, 1, 1, 0, 0, 0).unwrap(),
-                ),
-                path_list.clone(),
-                cx,
-            )
-        });
-        save_task.await.unwrap();
+        save_thread_metadata(
+            acp::SessionId::new(Arc::from(session_id)),
+            SharedString::from(title.to_string()),
+            chrono::TimeZone::with_ymd_and_hms(&Utc, 2024, 1, 1, 0, 0, 0).unwrap(),
+            path_list.clone(),
+            cx,
+        )
+        .await;
         cx.run_until_parked();
     }
 
+    async fn save_thread_metadata(
+        session_id: acp::SessionId,
+        title: SharedString,
+        updated_at: DateTime<Utc>,
+        path_list: PathList,
+        cx: &mut TestAppContext,
+    ) {
+        let metadata = ThreadMetadata {
+            session_id,
+            agent_id: None,
+            title,
+            updated_at,
+            created_at: None,
+            folder_paths: path_list,
+        };
+        let task = cx.update(|cx| {
+            ThreadMetadataStore::global(cx).update(cx, |store, cx| store.save(metadata, cx))
+        });
+        task.await.unwrap();
+    }
+
     fn open_and_focus_sidebar(sidebar: &Entity<ThreadsPanel>, cx: &mut gpui::VisualTestContext) {
         cx.run_until_parked();
         let workspace = sidebar.read_with(cx, |sidebar, cx| {
@@ -2446,33 +2519,24 @@ mod tests {
         let sidebar = setup_sidebar(&multi_workspace, cx);
 
         let path_list = PathList::new(&[std::path::PathBuf::from("/my-project")]);
-        let thread_store = cx.update(|_window, cx| ThreadStore::global(cx));
-
-        let save_task = thread_store.update(cx, |store, cx| {
-            store.save_thread(
-                acp::SessionId::new(Arc::from("thread-1")),
-                make_test_thread(
-                    "Fix crash in project panel",
-                    chrono::TimeZone::with_ymd_and_hms(&Utc, 2024, 1, 3, 0, 0, 0).unwrap(),
-                ),
-                path_list.clone(),
-                cx,
-            )
-        });
-        save_task.await.unwrap();
-
-        let save_task = thread_store.update(cx, |store, cx| {
-            store.save_thread(
-                acp::SessionId::new(Arc::from("thread-2")),
-                make_test_thread(
-                    "Add inline diff view",
-                    chrono::TimeZone::with_ymd_and_hms(&Utc, 2024, 1, 2, 0, 0, 0).unwrap(),
-                ),
-                path_list.clone(),
-                cx,
-            )
-        });
-        save_task.await.unwrap();
+
+        save_thread_metadata(
+            acp::SessionId::new(Arc::from("thread-1")),
+            "Fix crash in project panel".into(),
+            chrono::TimeZone::with_ymd_and_hms(&Utc, 2024, 1, 3, 0, 0, 0).unwrap(),
+            path_list.clone(),
+            cx,
+        )
+        .await;
+
+        save_thread_metadata(
+            acp::SessionId::new(Arc::from("thread-2")),
+            "Add inline diff view".into(),
+            chrono::TimeZone::with_ymd_and_hms(&Utc, 2024, 1, 2, 0, 0, 0).unwrap(),
+            path_list.clone(),
+            cx,
+        )
+        .await;
         cx.run_until_parked();
 
         multi_workspace.update_in(cx, |_, _window, cx| cx.notify());
@@ -2497,20 +2561,15 @@ mod tests {
 
         // Single workspace with a thread
         let path_list = PathList::new(&[std::path::PathBuf::from("/project-a")]);
-        let thread_store = cx.update(|_window, cx| ThreadStore::global(cx));
-
-        let save_task = thread_store.update(cx, |store, cx| {
-            store.save_thread(
-                acp::SessionId::new(Arc::from("thread-a1")),
-                make_test_thread(
-                    "Thread A1",
-                    chrono::TimeZone::with_ymd_and_hms(&Utc, 2024, 1, 1, 0, 0, 0).unwrap(),
-                ),
-                path_list.clone(),
-                cx,
-            )
-        });
-        save_task.await.unwrap();
+
+        save_thread_metadata(
+            acp::SessionId::new(Arc::from("thread-a1")),
+            "Thread A1".into(),
+            chrono::TimeZone::with_ymd_and_hms(&Utc, 2024, 1, 1, 0, 0, 0).unwrap(),
+            path_list.clone(),
+            cx,
+        )
+        .await;
         cx.run_until_parked();
 
         multi_workspace.update_in(cx, |_, _window, cx| cx.notify());
@@ -2612,7 +2671,7 @@ mod tests {
         sidebar.update_in(cx, |s, _window, cx| {
             let current = s.expanded_groups.get(&path_list).copied().unwrap_or(0);
             s.expanded_groups.insert(path_list.clone(), current + 1);
-            s.update_entries(cx);
+            s.update_entries(false, cx);
         });
         cx.run_until_parked();
 
@@ -2625,7 +2684,7 @@ mod tests {
         sidebar.update_in(cx, |s, _window, cx| {
             let current = s.expanded_groups.get(&path_list).copied().unwrap_or(0);
             s.expanded_groups.insert(path_list.clone(), current + 1);
-            s.update_entries(cx);
+            s.update_entries(false, cx);
         });
         cx.run_until_parked();
 
@@ -2638,7 +2697,7 @@ mod tests {
         // Click collapse - should go back to showing 5 threads
         sidebar.update_in(cx, |s, _window, cx| {
             s.expanded_groups.remove(&path_list);
-            s.update_entries(cx);
+            s.update_entries(false, cx);
         });
         cx.run_until_parked();
 
@@ -2719,7 +2778,7 @@ mod tests {
                     agent: Agent::NativeAgent,
                     session_info: acp_thread::AgentSessionInfo {
                         session_id: acp::SessionId::new(Arc::from("t-1")),
-                        cwd: None,
+                        work_dirs: None,
                         title: Some("Completed thread".into()),
                         updated_at: Some(Utc::now()),
                         created_at: Some(Utc::now()),
@@ -2742,7 +2801,7 @@ mod tests {
                     agent: Agent::NativeAgent,
                     session_info: acp_thread::AgentSessionInfo {
                         session_id: acp::SessionId::new(Arc::from("t-2")),
-                        cwd: None,
+                        work_dirs: None,
                         title: Some("Running thread".into()),
                         updated_at: Some(Utc::now()),
                         created_at: Some(Utc::now()),
@@ -2765,7 +2824,7 @@ mod tests {
                     agent: Agent::NativeAgent,
                     session_info: acp_thread::AgentSessionInfo {
                         session_id: acp::SessionId::new(Arc::from("t-3")),
-                        cwd: None,
+                        work_dirs: None,
                         title: Some("Error thread".into()),
                         updated_at: Some(Utc::now()),
                         created_at: Some(Utc::now()),
@@ -2788,7 +2847,7 @@ mod tests {
                     agent: Agent::NativeAgent,
                     session_info: acp_thread::AgentSessionInfo {
                         session_id: acp::SessionId::new(Arc::from("t-4")),
-                        cwd: None,
+                        work_dirs: None,
                         title: Some("Waiting thread".into()),
                         updated_at: Some(Utc::now()),
                         created_at: Some(Utc::now()),
@@ -2811,7 +2870,7 @@ mod tests {
                     agent: Agent::NativeAgent,
                     session_info: acp_thread::AgentSessionInfo {
                         session_id: acp::SessionId::new(Arc::from("t-5")),
-                        cwd: None,
+                        work_dirs: None,
                         title: Some("Notified thread".into()),
                         updated_at: Some(Utc::now()),
                         created_at: Some(Utc::now()),
@@ -3284,7 +3343,7 @@ mod tests {
         send_message(&panel, cx);
 
         let session_id_a = active_session_id(&panel, cx);
-        save_thread_to_store(&session_id_a, &path_list, cx).await;
+        save_test_thread_metadata(&session_id_a, path_list.clone(), cx).await;
 
         cx.update(|_, cx| {
             connection.send_update(
@@ -3303,7 +3362,7 @@ mod tests {
         send_message(&panel, cx);
 
         let session_id_b = active_session_id(&panel, cx);
-        save_thread_to_store(&session_id_b, &path_list, cx).await;
+        save_test_thread_metadata(&session_id_b, path_list.clone(), cx).await;
 
         cx.run_until_parked();
 
@@ -3330,7 +3389,7 @@ mod tests {
         send_message(&panel_a, cx);
 
         let session_id_a = active_session_id(&panel_a, cx);
-        save_thread_to_store(&session_id_a, &path_list_a, cx).await;
+        save_test_thread_metadata(&session_id_a, path_list_a.clone(), cx).await;
 
         cx.update(|_, cx| {
             connection_a.send_update(
@@ -3398,25 +3457,20 @@ mod tests {
         let sidebar = setup_sidebar(&multi_workspace, cx);
 
         let path_list = PathList::new(&[std::path::PathBuf::from("/my-project")]);
-        let thread_store = cx.update(|_window, cx| ThreadStore::global(cx));
 
         for (id, title, hour) in [
             ("t-1", "Fix crash in project panel", 3),
             ("t-2", "Add inline diff view", 2),
             ("t-3", "Refactor settings module", 1),
         ] {
-            let save_task = thread_store.update(cx, |store, cx| {
-                store.save_thread(
-                    acp::SessionId::new(Arc::from(id)),
-                    make_test_thread(
-                        title,
-                        chrono::TimeZone::with_ymd_and_hms(&Utc, 2024, 1, 1, hour, 0, 0).unwrap(),
-                    ),
-                    path_list.clone(),
-                    cx,
-                )
-            });
-            save_task.await.unwrap();
+            save_thread_metadata(
+                acp::SessionId::new(Arc::from(id)),
+                title.into(),
+                chrono::TimeZone::with_ymd_and_hms(&Utc, 2024, 1, 1, hour, 0, 0).unwrap(),
+                path_list.clone(),
+                cx,
+            )
+            .await;
         }
         cx.run_until_parked();
 
@@ -3456,20 +3510,15 @@ mod tests {
         let sidebar = setup_sidebar(&multi_workspace, cx);
 
         let path_list = PathList::new(&[std::path::PathBuf::from("/my-project")]);
-        let thread_store = cx.update(|_window, cx| ThreadStore::global(cx));
-
-        let save_task = thread_store.update(cx, |store, cx| {
-            store.save_thread(
-                acp::SessionId::new(Arc::from("thread-1")),
-                make_test_thread(
-                    "Fix Crash In Project Panel",
-                    chrono::TimeZone::with_ymd_and_hms(&Utc, 2024, 1, 1, 0, 0, 0).unwrap(),
-                ),
-                path_list.clone(),
-                cx,
-            )
-        });
-        save_task.await.unwrap();
+
+        save_thread_metadata(
+            acp::SessionId::new(Arc::from("thread-1")),
+            "Fix Crash In Project Panel".into(),
+            chrono::TimeZone::with_ymd_and_hms(&Utc, 2024, 1, 1, 0, 0, 0).unwrap(),
+            path_list.clone(),
+            cx,
+        )
+        .await;
         cx.run_until_parked();
 
         // Lowercase query matches mixed-case title.
@@ -3503,21 +3552,16 @@ mod tests {
         let sidebar = setup_sidebar(&multi_workspace, cx);
 
         let path_list = PathList::new(&[std::path::PathBuf::from("/my-project")]);
-        let thread_store = cx.update(|_window, cx| ThreadStore::global(cx));
 
         for (id, title, hour) in [("t-1", "Alpha thread", 2), ("t-2", "Beta thread", 1)] {
-            let save_task = thread_store.update(cx, |store, cx| {
-                store.save_thread(
-                    acp::SessionId::new(Arc::from(id)),
-                    make_test_thread(
-                        title,
-                        chrono::TimeZone::with_ymd_and_hms(&Utc, 2024, 1, 1, hour, 0, 0).unwrap(),
-                    ),
-                    path_list.clone(),
-                    cx,
-                )
-            });
-            save_task.await.unwrap();
+            save_thread_metadata(
+                acp::SessionId::new(Arc::from(id)),
+                title.into(),
+                chrono::TimeZone::with_ymd_and_hms(&Utc, 2024, 1, 1, hour, 0, 0).unwrap(),
+                path_list.clone(),
+                cx,
+            )
+            .await;
         }
         cx.run_until_parked();
 
@@ -3556,24 +3600,19 @@ mod tests {
         let sidebar = setup_sidebar(&multi_workspace, cx);
 
         let path_list_a = PathList::new(&[std::path::PathBuf::from("/project-a")]);
-        let thread_store = cx.update(|_window, cx| ThreadStore::global(cx));
 
         for (id, title, hour) in [
             ("a1", "Fix bug in sidebar", 2),
             ("a2", "Add tests for editor", 1),
         ] {
-            let save_task = thread_store.update(cx, |store, cx| {
-                store.save_thread(
-                    acp::SessionId::new(Arc::from(id)),
-                    make_test_thread(
-                        title,
-                        chrono::TimeZone::with_ymd_and_hms(&Utc, 2024, 1, 1, hour, 0, 0).unwrap(),
-                    ),
-                    path_list_a.clone(),
-                    cx,
-                )
-            });
-            save_task.await.unwrap();
+            save_thread_metadata(
+                acp::SessionId::new(Arc::from(id)),
+                title.into(),
+                chrono::TimeZone::with_ymd_and_hms(&Utc, 2024, 1, 1, hour, 0, 0).unwrap(),
+                path_list_a.clone(),
+                cx,
+            )
+            .await;
         }
 
         // Add a second workspace.
@@ -3588,18 +3627,14 @@ mod tests {
             ("b1", "Refactor sidebar layout", 3),
             ("b2", "Fix typo in README", 1),
         ] {
-            let save_task = thread_store.update(cx, |store, cx| {
-                store.save_thread(
-                    acp::SessionId::new(Arc::from(id)),
-                    make_test_thread(
-                        title,
-                        chrono::TimeZone::with_ymd_and_hms(&Utc, 2024, 1, 1, hour, 0, 0).unwrap(),
-                    ),
-                    path_list_b.clone(),
-                    cx,
-                )
-            });
-            save_task.await.unwrap();
+            save_thread_metadata(
+                acp::SessionId::new(Arc::from(id)),
+                title.into(),
+                chrono::TimeZone::with_ymd_and_hms(&Utc, 2024, 1, 1, hour, 0, 0).unwrap(),
+                path_list_b.clone(),
+                cx,
+            )
+            .await;
         }
         cx.run_until_parked();
 
@@ -3655,24 +3690,19 @@ mod tests {
         let sidebar = setup_sidebar(&multi_workspace, cx);
 
         let path_list_a = PathList::new(&[std::path::PathBuf::from("/alpha-project")]);
-        let thread_store = cx.update(|_window, cx| ThreadStore::global(cx));
 
         for (id, title, hour) in [
             ("a1", "Fix bug in sidebar", 2),
             ("a2", "Add tests for editor", 1),
         ] {
-            let save_task = thread_store.update(cx, |store, cx| {
-                store.save_thread(
-                    acp::SessionId::new(Arc::from(id)),
-                    make_test_thread(
-                        title,
-                        chrono::TimeZone::with_ymd_and_hms(&Utc, 2024, 1, 1, hour, 0, 0).unwrap(),
-                    ),
-                    path_list_a.clone(),
-                    cx,
-                )
-            });
-            save_task.await.unwrap();
+            save_thread_metadata(
+                acp::SessionId::new(Arc::from(id)),
+                title.into(),
+                chrono::TimeZone::with_ymd_and_hms(&Utc, 2024, 1, 1, hour, 0, 0).unwrap(),
+                path_list_a.clone(),
+                cx,
+            )
+            .await;
         }
 
         // Add a second workspace.

crates/agent_ui/src/ui/acp_onboarding_modal.rs 🔗

@@ -2,7 +2,7 @@ use gpui::{
     ClickEvent, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, MouseDownEvent, Render,
     linear_color_stop, linear_gradient,
 };
-use project::agent_server_store::GEMINI_NAME;
+use project::agent_server_store::GEMINI_ID;
 use ui::{TintColor, Vector, VectorName, prelude::*};
 use workspace::{ModalView, Workspace};
 
@@ -39,7 +39,7 @@ impl AcpOnboardingModal {
                 panel.update(cx, |panel, cx| {
                     panel.new_agent_thread(
                         AgentType::Custom {
-                            name: GEMINI_NAME.into(),
+                            id: GEMINI_ID.into(),
                         },
                         window,
                         cx,

crates/agent_ui/src/ui/claude_agent_onboarding_modal.rs 🔗

@@ -2,7 +2,7 @@ use gpui::{
     ClickEvent, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, MouseDownEvent, Render,
     linear_color_stop, linear_gradient,
 };
-use project::agent_server_store::CLAUDE_AGENT_NAME;
+use project::agent_server_store::CLAUDE_AGENT_ID;
 use ui::{TintColor, Vector, VectorName, prelude::*};
 use workspace::{ModalView, Workspace};
 
@@ -39,7 +39,7 @@ impl ClaudeCodeOnboardingModal {
                 panel.update(cx, |panel, cx| {
                     panel.new_agent_thread(
                         AgentType::Custom {
-                            name: CLAUDE_AGENT_NAME.into(),
+                            id: CLAUDE_AGENT_ID.into(),
                         },
                         window,
                         cx,

crates/anthropic/src/anthropic.rs 🔗

@@ -78,23 +78,20 @@ pub enum Model {
         alias = "claude-opus-4-5-thinking-latest"
     )]
     ClaudeOpus4_5Thinking,
-    #[serde(rename = "claude-opus-4-6", alias = "claude-opus-4-6-latest")]
-    ClaudeOpus4_6,
-    #[serde(
-        rename = "claude-opus-4-6-thinking",
-        alias = "claude-opus-4-6-thinking-latest"
-    )]
-    ClaudeOpus4_6Thinking,
     #[serde(
-        rename = "claude-opus-4-6-1m-context",
+        rename = "claude-opus-4-6",
+        alias = "claude-opus-4-6-latest",
+        alias = "claude-opus-4-6-1m-context",
         alias = "claude-opus-4-6-1m-context-latest"
     )]
-    ClaudeOpus4_6_1mContext,
+    ClaudeOpus4_6,
     #[serde(
-        rename = "claude-opus-4-6-1m-context-thinking",
+        rename = "claude-opus-4-6-thinking",
+        alias = "claude-opus-4-6-thinking-latest",
+        alias = "claude-opus-4-6-1m-context-thinking",
         alias = "claude-opus-4-6-1m-context-thinking-latest"
     )]
-    ClaudeOpus4_6_1mContextThinking,
+    ClaudeOpus4_6Thinking,
     #[serde(rename = "claude-sonnet-4", alias = "claude-sonnet-4-latest")]
     ClaudeSonnet4,
     #[serde(
@@ -120,23 +117,20 @@ pub enum Model {
     )]
     ClaudeSonnet4_5_1mContextThinking,
     #[default]
-    #[serde(rename = "claude-sonnet-4-6", alias = "claude-sonnet-4-6-latest")]
-    ClaudeSonnet4_6,
     #[serde(
-        rename = "claude-sonnet-4-6-thinking",
-        alias = "claude-sonnet-4-6-thinking-latest"
-    )]
-    ClaudeSonnet4_6Thinking,
-    #[serde(
-        rename = "claude-sonnet-4-6-1m-context",
+        rename = "claude-sonnet-4-6",
+        alias = "claude-sonnet-4-6-latest",
+        alias = "claude-sonnet-4-6-1m-context",
         alias = "claude-sonnet-4-6-1m-context-latest"
     )]
-    ClaudeSonnet4_6_1mContext,
+    ClaudeSonnet4_6,
     #[serde(
-        rename = "claude-sonnet-4-6-1m-context-thinking",
+        rename = "claude-sonnet-4-6-thinking",
+        alias = "claude-sonnet-4-6-thinking-latest",
+        alias = "claude-sonnet-4-6-1m-context-thinking",
         alias = "claude-sonnet-4-6-1m-context-thinking-latest"
     )]
-    ClaudeSonnet4_6_1mContextThinking,
+    ClaudeSonnet4_6Thinking,
     #[serde(rename = "claude-haiku-4-5", alias = "claude-haiku-4-5-latest")]
     ClaudeHaiku4_5,
     #[serde(
@@ -172,11 +166,11 @@ impl Model {
 
     pub fn from_id(id: &str) -> Result<Self> {
         if id.starts_with("claude-opus-4-6-1m-context-thinking") {
-            return Ok(Self::ClaudeOpus4_6_1mContextThinking);
+            return Ok(Self::ClaudeOpus4_6Thinking);
         }
 
         if id.starts_with("claude-opus-4-6-1m-context") {
-            return Ok(Self::ClaudeOpus4_6_1mContext);
+            return Ok(Self::ClaudeOpus4_6);
         }
 
         if id.starts_with("claude-opus-4-6-thinking") {
@@ -212,11 +206,11 @@ impl Model {
         }
 
         if id.starts_with("claude-sonnet-4-6-1m-context-thinking") {
-            return Ok(Self::ClaudeSonnet4_6_1mContextThinking);
+            return Ok(Self::ClaudeSonnet4_6Thinking);
         }
 
         if id.starts_with("claude-sonnet-4-6-1m-context") {
-            return Ok(Self::ClaudeSonnet4_6_1mContext);
+            return Ok(Self::ClaudeSonnet4_6);
         }
 
         if id.starts_with("claude-sonnet-4-6-thinking") {
@@ -276,8 +270,6 @@ impl Model {
             Self::ClaudeOpus4_5Thinking => "claude-opus-4-5-thinking-latest",
             Self::ClaudeOpus4_6 => "claude-opus-4-6-latest",
             Self::ClaudeOpus4_6Thinking => "claude-opus-4-6-thinking-latest",
-            Self::ClaudeOpus4_6_1mContext => "claude-opus-4-6-1m-context-latest",
-            Self::ClaudeOpus4_6_1mContextThinking => "claude-opus-4-6-1m-context-thinking-latest",
             Self::ClaudeSonnet4 => "claude-sonnet-4-latest",
             Self::ClaudeSonnet4Thinking => "claude-sonnet-4-thinking-latest",
             Self::ClaudeSonnet4_5 => "claude-sonnet-4-5-latest",
@@ -288,10 +280,6 @@ impl Model {
             }
             Self::ClaudeSonnet4_6 => "claude-sonnet-4-6-latest",
             Self::ClaudeSonnet4_6Thinking => "claude-sonnet-4-6-thinking-latest",
-            Self::ClaudeSonnet4_6_1mContext => "claude-sonnet-4-6-1m-context-latest",
-            Self::ClaudeSonnet4_6_1mContextThinking => {
-                "claude-sonnet-4-6-1m-context-thinking-latest"
-            }
             Self::ClaudeHaiku4_5 => "claude-haiku-4-5-latest",
             Self::ClaudeHaiku4_5Thinking => "claude-haiku-4-5-thinking-latest",
             Self::Claude3Haiku => "claude-3-haiku-20240307",
@@ -305,19 +293,13 @@ impl Model {
             Self::ClaudeOpus4 | Self::ClaudeOpus4Thinking => "claude-opus-4-20250514",
             Self::ClaudeOpus4_1 | Self::ClaudeOpus4_1Thinking => "claude-opus-4-1-20250805",
             Self::ClaudeOpus4_5 | Self::ClaudeOpus4_5Thinking => "claude-opus-4-5-20251101",
-            Self::ClaudeOpus4_6
-            | Self::ClaudeOpus4_6Thinking
-            | Self::ClaudeOpus4_6_1mContext
-            | Self::ClaudeOpus4_6_1mContextThinking => "claude-opus-4-6",
+            Self::ClaudeOpus4_6 | Self::ClaudeOpus4_6Thinking => "claude-opus-4-6",
             Self::ClaudeSonnet4 | Self::ClaudeSonnet4Thinking => "claude-sonnet-4-20250514",
             Self::ClaudeSonnet4_5
             | Self::ClaudeSonnet4_5Thinking
             | Self::ClaudeSonnet4_5_1mContext
             | Self::ClaudeSonnet4_5_1mContextThinking => "claude-sonnet-4-5-20250929",
-            Self::ClaudeSonnet4_6
-            | Self::ClaudeSonnet4_6Thinking
-            | Self::ClaudeSonnet4_6_1mContext
-            | Self::ClaudeSonnet4_6_1mContextThinking => "claude-sonnet-4-6",
+            Self::ClaudeSonnet4_6 | Self::ClaudeSonnet4_6Thinking => "claude-sonnet-4-6",
             Self::ClaudeHaiku4_5 | Self::ClaudeHaiku4_5Thinking => "claude-haiku-4-5-20251001",
             Self::Claude3Haiku => "claude-3-haiku-20240307",
             Self::Custom { name, .. } => name,
@@ -334,8 +316,6 @@ impl Model {
             Self::ClaudeOpus4_5Thinking => "Claude Opus 4.5 Thinking",
             Self::ClaudeOpus4_6 => "Claude Opus 4.6",
             Self::ClaudeOpus4_6Thinking => "Claude Opus 4.6 Thinking",
-            Self::ClaudeOpus4_6_1mContext => "Claude Opus 4.6 (1M context)",
-            Self::ClaudeOpus4_6_1mContextThinking => "Claude Opus 4.6 Thinking (1M context)",
             Self::ClaudeSonnet4 => "Claude Sonnet 4",
             Self::ClaudeSonnet4Thinking => "Claude Sonnet 4 Thinking",
             Self::ClaudeSonnet4_5 => "Claude Sonnet 4.5",
@@ -344,8 +324,6 @@ impl Model {
             Self::ClaudeSonnet4_5_1mContextThinking => "Claude Sonnet 4.5 Thinking (1M context)",
             Self::ClaudeSonnet4_6 => "Claude Sonnet 4.6",
             Self::ClaudeSonnet4_6Thinking => "Claude Sonnet 4.6 Thinking",
-            Self::ClaudeSonnet4_6_1mContext => "Claude Sonnet 4.6 (1M context)",
-            Self::ClaudeSonnet4_6_1mContextThinking => "Claude Sonnet 4.6 Thinking (1M context)",
             Self::ClaudeHaiku4_5 => "Claude Haiku 4.5",
             Self::ClaudeHaiku4_5Thinking => "Claude Haiku 4.5 Thinking",
             Self::Claude3Haiku => "Claude 3 Haiku",
@@ -365,8 +343,6 @@ impl Model {
             | Self::ClaudeOpus4_5Thinking
             | Self::ClaudeOpus4_6
             | Self::ClaudeOpus4_6Thinking
-            | Self::ClaudeOpus4_6_1mContext
-            | Self::ClaudeOpus4_6_1mContextThinking
             | Self::ClaudeSonnet4
             | Self::ClaudeSonnet4Thinking
             | Self::ClaudeSonnet4_5
@@ -375,8 +351,6 @@ impl Model {
             | Self::ClaudeSonnet4_5_1mContextThinking
             | Self::ClaudeSonnet4_6
             | Self::ClaudeSonnet4_6Thinking
-            | Self::ClaudeSonnet4_6_1mContext
-            | Self::ClaudeSonnet4_6_1mContextThinking
             | Self::ClaudeHaiku4_5
             | Self::ClaudeHaiku4_5Thinking
             | Self::Claude3Haiku => Some(AnthropicModelCacheConfiguration {
@@ -399,23 +373,19 @@ impl Model {
             | Self::ClaudeOpus4_1Thinking
             | Self::ClaudeOpus4_5
             | Self::ClaudeOpus4_5Thinking
-            | Self::ClaudeOpus4_6
-            | Self::ClaudeOpus4_6Thinking
             | Self::ClaudeSonnet4
             | Self::ClaudeSonnet4Thinking
             | Self::ClaudeSonnet4_5
             | Self::ClaudeSonnet4_5Thinking
-            | Self::ClaudeSonnet4_6
-            | Self::ClaudeSonnet4_6Thinking
             | Self::ClaudeHaiku4_5
             | Self::ClaudeHaiku4_5Thinking
             | Self::Claude3Haiku => 200_000,
-            Self::ClaudeOpus4_6_1mContext
-            | Self::ClaudeOpus4_6_1mContextThinking
+            Self::ClaudeOpus4_6
+            | Self::ClaudeOpus4_6Thinking
             | Self::ClaudeSonnet4_5_1mContext
             | Self::ClaudeSonnet4_5_1mContextThinking
-            | Self::ClaudeSonnet4_6_1mContext
-            | Self::ClaudeSonnet4_6_1mContextThinking => 1_000_000,
+            | Self::ClaudeSonnet4_6
+            | Self::ClaudeSonnet4_6Thinking => 1_000_000,
             Self::Custom { max_tokens, .. } => *max_tokens,
         }
     }
@@ -436,14 +406,9 @@ impl Model {
             | Self::ClaudeSonnet4_5_1mContextThinking
             | Self::ClaudeSonnet4_6
             | Self::ClaudeSonnet4_6Thinking
-            | Self::ClaudeSonnet4_6_1mContext
-            | Self::ClaudeSonnet4_6_1mContextThinking
             | Self::ClaudeHaiku4_5
             | Self::ClaudeHaiku4_5Thinking => 64_000,
-            Self::ClaudeOpus4_6
-            | Self::ClaudeOpus4_6Thinking
-            | Self::ClaudeOpus4_6_1mContext
-            | Self::ClaudeOpus4_6_1mContextThinking => 128_000,
+            Self::ClaudeOpus4_6 | Self::ClaudeOpus4_6Thinking => 128_000,
             Self::Claude3Haiku => 4_096,
             Self::Custom {
                 max_output_tokens, ..
@@ -461,8 +426,6 @@ impl Model {
             | Self::ClaudeOpus4_5Thinking
             | Self::ClaudeOpus4_6
             | Self::ClaudeOpus4_6Thinking
-            | Self::ClaudeOpus4_6_1mContext
-            | Self::ClaudeOpus4_6_1mContextThinking
             | Self::ClaudeSonnet4
             | Self::ClaudeSonnet4Thinking
             | Self::ClaudeSonnet4_5
@@ -471,8 +434,6 @@ impl Model {
             | Self::ClaudeSonnet4_5_1mContextThinking
             | Self::ClaudeSonnet4_6
             | Self::ClaudeSonnet4_6Thinking
-            | Self::ClaudeSonnet4_6_1mContext
-            | Self::ClaudeSonnet4_6_1mContextThinking
             | Self::ClaudeHaiku4_5
             | Self::ClaudeHaiku4_5Thinking
             | Self::Claude3Haiku => 1.0,
@@ -489,24 +450,20 @@ impl Model {
             | Self::ClaudeOpus4_1
             | Self::ClaudeOpus4_5
             | Self::ClaudeOpus4_6
-            | Self::ClaudeOpus4_6_1mContext
             | Self::ClaudeSonnet4
             | Self::ClaudeSonnet4_5
             | Self::ClaudeSonnet4_5_1mContext
             | Self::ClaudeSonnet4_6
-            | Self::ClaudeSonnet4_6_1mContext
             | Self::ClaudeHaiku4_5
             | Self::Claude3Haiku => AnthropicModelMode::Default,
             Self::ClaudeOpus4Thinking
             | Self::ClaudeOpus4_1Thinking
             | Self::ClaudeOpus4_5Thinking
             | Self::ClaudeOpus4_6Thinking
-            | Self::ClaudeOpus4_6_1mContextThinking
             | Self::ClaudeSonnet4Thinking
             | Self::ClaudeSonnet4_5Thinking
             | Self::ClaudeSonnet4_5_1mContextThinking
             | Self::ClaudeSonnet4_6Thinking
-            | Self::ClaudeSonnet4_6_1mContextThinking
             | Self::ClaudeHaiku4_5Thinking => AnthropicModelMode::Thinking {
                 budget_tokens: Some(4_096),
             },
@@ -518,12 +475,7 @@ impl Model {
         let mut headers = vec![];
 
         match self {
-            Self::ClaudeOpus4_6_1mContext
-            | Self::ClaudeOpus4_6_1mContextThinking
-            | Self::ClaudeSonnet4_5_1mContext
-            | Self::ClaudeSonnet4_5_1mContextThinking
-            | Self::ClaudeSonnet4_6_1mContext
-            | Self::ClaudeSonnet4_6_1mContextThinking => {
+            Self::ClaudeSonnet4_5_1mContext | Self::ClaudeSonnet4_5_1mContextThinking => {
                 headers.push(CONTEXT_1M_BETA_HEADER.to_string());
             }
             Self::Custom {

crates/audio/src/audio.rs 🔗

@@ -1,77 +1,22 @@
-use anyhow::{Context as _, Result};
-use collections::HashMap;
-use cpal::{
-    DeviceDescription, DeviceId, default_host,
-    traits::{DeviceTrait, HostTrait},
-};
-use gpui::{App, AsyncApp, BackgroundExecutor, BorrowAppContext, Global};
+use std::time::Duration;
 
-#[cfg(not(any(all(target_os = "windows", target_env = "gnu"), target_os = "freebsd")))]
-mod non_windows_and_freebsd_deps {
-    pub(super) use cpal::Sample;
-    pub(super) use libwebrtc::native::apm;
-    pub(super) use parking_lot::Mutex;
-    pub(super) use rodio::source::LimitSettings;
-    pub(super) use std::sync::Arc;
-}
-
-#[cfg(not(any(all(target_os = "windows", target_env = "gnu"), target_os = "freebsd")))]
-use non_windows_and_freebsd_deps::*;
+use rodio::{ChannelCount, SampleRate, nz};
 
-use rodio::{
-    Decoder, DeviceSinkBuilder, MixerDeviceSink, Source,
-    mixer::Mixer,
-    nz,
-    source::{AutomaticGainControlSettings, Buffered},
-};
-use settings::Settings;
-use std::{io::Cursor, num::NonZero, path::PathBuf, sync::atomic::Ordering, time::Duration};
-use util::ResultExt;
+pub const REPLAY_DURATION: Duration = Duration::from_secs(30);
+pub const SAMPLE_RATE: SampleRate = nz!(48000);
+pub const CHANNEL_COUNT: ChannelCount = nz!(2);
 
 mod audio_settings;
-mod replays;
-mod rodio_ext;
 pub use audio_settings::AudioSettings;
-pub use rodio_ext::RodioExt;
 
-use crate::audio_settings::LIVE_SETTINGS;
-
-// We are migrating to 16kHz sample rate from 48kHz. In the future
-// once we are reasonably sure most users have upgraded we will
-// remove the LEGACY parameters.
-//
-// We migrate to 16kHz because it is sufficient for speech and required
-// by the denoiser and future Speech to Text layers.
-pub const SAMPLE_RATE: NonZero<u32> = nz!(16000);
-pub const CHANNEL_COUNT: NonZero<u16> = nz!(1);
-pub const BUFFER_SIZE: usize = // echo canceller and livekit want 10ms of audio
-    (SAMPLE_RATE.get() as usize / 100) * CHANNEL_COUNT.get() as usize;
-
-pub const LEGACY_SAMPLE_RATE: NonZero<u32> = nz!(48000);
-pub const LEGACY_CHANNEL_COUNT: NonZero<u16> = nz!(2);
-
-pub const REPLAY_DURATION: Duration = Duration::from_secs(30);
-
-pub fn init(cx: &mut App) {
-    LIVE_SETTINGS.initialize(cx);
-}
-
-// TODO(jk): this is currently cached only once - we should observe and react instead
-pub fn ensure_devices_initialized(cx: &mut App) {
-    if cx.has_global::<AvailableAudioDevices>() {
-        return;
-    }
-    cx.default_global::<AvailableAudioDevices>();
-    let task = cx
-        .background_executor()
-        .spawn(async move { get_available_audio_devices() });
-    cx.spawn(async move |cx: &mut AsyncApp| {
-        let devices = task.await;
-        cx.update(|cx| cx.set_global(AvailableAudioDevices(devices)));
-        cx.refresh();
-    })
-    .detach();
-}
+mod audio_pipeline;
+pub use audio_pipeline::{Audio, VoipParts};
+pub use audio_pipeline::{AudioDeviceInfo, AvailableAudioDevices};
+pub use audio_pipeline::{ensure_devices_initialized, resolve_device};
+// TODO(audio) replace with input test functionality in the audio crate
+pub use audio_pipeline::RodioExt;
+pub use audio_pipeline::init;
+pub use audio_pipeline::{open_input_stream, open_test_output};
 
 #[derive(Debug, Copy, Clone, Eq, Hash, PartialEq)]
 pub enum Sound {
@@ -99,359 +44,3 @@ impl Sound {
         }
     }
 }
-
-pub struct Audio {
-    output_handle: Option<MixerDeviceSink>,
-    #[cfg(not(any(all(target_os = "windows", target_env = "gnu"), target_os = "freebsd")))]
-    pub echo_canceller: Arc<Mutex<apm::AudioProcessingModule>>,
-    source_cache: HashMap<Sound, Buffered<Decoder<Cursor<Vec<u8>>>>>,
-    replays: replays::Replays,
-}
-
-impl Default for Audio {
-    fn default() -> Self {
-        Self {
-            output_handle: Default::default(),
-            #[cfg(not(any(
-                all(target_os = "windows", target_env = "gnu"),
-                target_os = "freebsd"
-            )))]
-            echo_canceller: Arc::new(Mutex::new(apm::AudioProcessingModule::new(
-                true, false, false, false,
-            ))),
-            source_cache: Default::default(),
-            replays: Default::default(),
-        }
-    }
-}
-
-impl Global for Audio {}
-
-impl Audio {
-    fn ensure_output_exists(&mut self, output_audio_device: Option<DeviceId>) -> Result<&Mixer> {
-        #[cfg(debug_assertions)]
-        log::warn!(
-            "Audio does not sound correct without optimizations. Use a release build to debug audio issues"
-        );
-
-        if self.output_handle.is_none() {
-            let output_handle = open_output_stream(output_audio_device)?;
-
-            // The webrtc apm is not yet compiling for windows & freebsd
-            #[cfg(not(any(
-                any(all(target_os = "windows", target_env = "gnu")),
-                target_os = "freebsd"
-            )))]
-            let echo_canceller = Arc::clone(&self.echo_canceller);
-
-            #[cfg(not(any(
-                any(all(target_os = "windows", target_env = "gnu")),
-                target_os = "freebsd"
-            )))]
-            {
-                let source = rodio::source::Zero::new(CHANNEL_COUNT, SAMPLE_RATE)
-                    .inspect_buffer::<BUFFER_SIZE, _>(move |buffer| {
-                        let mut buf: [i16; _] = buffer.map(|s| s.to_sample());
-                        echo_canceller
-                            .lock()
-                            .process_reverse_stream(
-                                &mut buf,
-                                SAMPLE_RATE.get() as i32,
-                                CHANNEL_COUNT.get().into(),
-                            )
-                            .expect("Audio input and output threads should not panic");
-                    });
-                output_handle.mixer().add(source);
-            }
-
-            #[cfg(any(
-                any(all(target_os = "windows", target_env = "gnu")),
-                target_os = "freebsd"
-            ))]
-            {
-                let source = rodio::source::Zero::new(CHANNEL_COUNT, SAMPLE_RATE);
-                output_handle.mixer().add(source);
-            }
-
-            self.output_handle = Some(output_handle);
-        }
-
-        Ok(self
-            .output_handle
-            .as_ref()
-            .map(|h| h.mixer())
-            .expect("we only get here if opening the outputstream succeeded"))
-    }
-
-    pub fn save_replays(
-        &self,
-        executor: BackgroundExecutor,
-    ) -> gpui::Task<anyhow::Result<(PathBuf, Duration)>> {
-        self.replays.replays_to_tar(executor)
-    }
-
-    #[cfg(not(any(all(target_os = "windows", target_env = "gnu"), target_os = "freebsd")))]
-    pub fn open_microphone(voip_parts: VoipParts) -> anyhow::Result<impl Source> {
-        let stream = open_input_stream(voip_parts.input_audio_device)?;
-        let stream = stream
-            .possibly_disconnected_channels_to_mono()
-            .constant_samplerate(SAMPLE_RATE)
-            .limit(LimitSettings::live_performance())
-            .process_buffer::<BUFFER_SIZE, _>(move |buffer| {
-                let mut int_buffer: [i16; _] = buffer.map(|s| s.to_sample());
-                if voip_parts
-                    .echo_canceller
-                    .lock()
-                    .process_stream(
-                        &mut int_buffer,
-                        SAMPLE_RATE.get() as i32,
-                        CHANNEL_COUNT.get() as i32,
-                    )
-                    .context("livekit audio processor error")
-                    .log_err()
-                    .is_some()
-                {
-                    for (sample, processed) in buffer.iter_mut().zip(&int_buffer) {
-                        *sample = (*processed).to_sample();
-                    }
-                }
-            })
-            .denoise()
-            .context("Could not set up denoiser")?
-            .automatic_gain_control(AutomaticGainControlSettings {
-                target_level: 0.90,
-                attack_time: Duration::from_secs(1),
-                release_time: Duration::from_secs(0),
-                absolute_max_gain: 5.0,
-            })
-            .periodic_access(Duration::from_millis(100), move |agc_source| {
-                agc_source
-                    .set_enabled(LIVE_SETTINGS.auto_microphone_volume.load(Ordering::Relaxed));
-                let denoise = agc_source.inner_mut();
-                denoise.set_enabled(LIVE_SETTINGS.denoise.load(Ordering::Relaxed));
-            });
-
-        let stream = if voip_parts.legacy_audio_compatible {
-            stream.constant_params(LEGACY_CHANNEL_COUNT, LEGACY_SAMPLE_RATE)
-        } else {
-            stream.constant_params(CHANNEL_COUNT, SAMPLE_RATE)
-        };
-
-        let (replay, stream) = stream.replayable(REPLAY_DURATION)?;
-        voip_parts
-            .replays
-            .add_voip_stream("local microphone".to_string(), replay);
-
-        Ok(stream)
-    }
-
-    pub fn play_voip_stream(
-        source: impl rodio::Source + Send + 'static,
-        speaker_name: String,
-        is_staff: bool,
-        cx: &mut App,
-    ) -> anyhow::Result<()> {
-        let (replay_source, source) = source
-            .constant_params(CHANNEL_COUNT, SAMPLE_RATE)
-            .automatic_gain_control(AutomaticGainControlSettings {
-                target_level: 0.90,
-                attack_time: Duration::from_secs(1),
-                release_time: Duration::from_secs(0),
-                absolute_max_gain: 5.0,
-            })
-            .periodic_access(Duration::from_millis(100), move |agc_source| {
-                agc_source.set_enabled(LIVE_SETTINGS.auto_speaker_volume.load(Ordering::Relaxed));
-            })
-            .replayable(REPLAY_DURATION)
-            .expect("REPLAY_DURATION is longer than 100ms");
-        let output_audio_device = AudioSettings::get_global(cx).output_audio_device.clone();
-
-        cx.update_default_global(|this: &mut Self, _cx| {
-            let output_mixer = this
-                .ensure_output_exists(output_audio_device)
-                .context("Could not get output mixer")?;
-            output_mixer.add(source);
-            if is_staff {
-                this.replays.add_voip_stream(speaker_name, replay_source);
-            }
-            Ok(())
-        })
-    }
-
-    pub fn play_sound(sound: Sound, cx: &mut App) {
-        let output_audio_device = AudioSettings::get_global(cx).output_audio_device.clone();
-        cx.update_default_global(|this: &mut Self, cx| {
-            let source = this.sound_source(sound, cx).log_err()?;
-            let output_mixer = this
-                .ensure_output_exists(output_audio_device)
-                .context("Could not get output mixer")
-                .log_err()?;
-
-            output_mixer.add(source);
-            Some(())
-        });
-    }
-
-    pub fn end_call(cx: &mut App) {
-        cx.update_default_global(|this: &mut Self, _cx| {
-            this.output_handle.take();
-        });
-    }
-
-    fn sound_source(&mut self, sound: Sound, cx: &App) -> Result<impl Source + use<>> {
-        if let Some(wav) = self.source_cache.get(&sound) {
-            return Ok(wav.clone());
-        }
-
-        let path = format!("sounds/{}.wav", sound.file());
-        let bytes = cx
-            .asset_source()
-            .load(&path)?
-            .map(anyhow::Ok)
-            .with_context(|| format!("No asset available for path {path}"))??
-            .into_owned();
-        let cursor = Cursor::new(bytes);
-        let source = Decoder::new(cursor)?.buffered();
-
-        self.source_cache.insert(sound, source.clone());
-
-        Ok(source)
-    }
-}
-
-#[cfg(not(any(all(target_os = "windows", target_env = "gnu"), target_os = "freebsd")))]
-pub struct VoipParts {
-    echo_canceller: Arc<Mutex<apm::AudioProcessingModule>>,
-    replays: replays::Replays,
-    legacy_audio_compatible: bool,
-    input_audio_device: Option<DeviceId>,
-}
-
-#[cfg(not(any(all(target_os = "windows", target_env = "gnu"), target_os = "freebsd")))]
-impl VoipParts {
-    pub fn new(cx: &AsyncApp) -> anyhow::Result<Self> {
-        let (apm, replays) = cx.read_default_global::<Audio, _>(|audio, _| {
-            (Arc::clone(&audio.echo_canceller), audio.replays.clone())
-        });
-        let legacy_audio_compatible =
-            AudioSettings::try_read_global(cx, |settings| settings.legacy_audio_compatible)
-                .unwrap_or(true);
-        let input_audio_device =
-            AudioSettings::try_read_global(cx, |settings| settings.input_audio_device.clone())
-                .flatten();
-
-        Ok(Self {
-            legacy_audio_compatible,
-            echo_canceller: apm,
-            replays,
-            input_audio_device,
-        })
-    }
-}
-
-pub fn open_input_stream(
-    device_id: Option<DeviceId>,
-) -> anyhow::Result<rodio::microphone::Microphone> {
-    let builder = rodio::microphone::MicrophoneBuilder::new();
-    let builder = if let Some(id) = device_id {
-        // TODO(jk): upstream patch
-        // if let Some(input_device) = default_host().device_by_id(id) {
-        //     builder.device(input_device);
-        // }
-        let mut found = None;
-        for input in rodio::microphone::available_inputs()? {
-            if input.clone().into_inner().id()? == id {
-                found = Some(builder.device(input));
-                break;
-            }
-        }
-        found.unwrap_or_else(|| builder.default_device())?
-    } else {
-        builder.default_device()?
-    };
-    let stream = builder
-        .default_config()?
-        .prefer_sample_rates([
-            SAMPLE_RATE,
-            SAMPLE_RATE.saturating_mul(rodio::nz!(2)),
-            SAMPLE_RATE.saturating_mul(rodio::nz!(3)),
-            SAMPLE_RATE.saturating_mul(rodio::nz!(4)),
-        ])
-        .prefer_channel_counts([rodio::nz!(1), rodio::nz!(2), rodio::nz!(3), rodio::nz!(4)])
-        .prefer_buffer_sizes(512..)
-        .open_stream()?;
-    log::info!("Opened microphone: {:?}", stream.config());
-    Ok(stream)
-}
-
-pub fn resolve_device(device_id: Option<&DeviceId>, input: bool) -> anyhow::Result<cpal::Device> {
-    if let Some(id) = device_id {
-        if let Some(device) = default_host().device_by_id(id) {
-            return Ok(device);
-        }
-        log::warn!("Selected audio device not found, falling back to default");
-    }
-    if input {
-        default_host()
-            .default_input_device()
-            .context("no audio input device available")
-    } else {
-        default_host()
-            .default_output_device()
-            .context("no audio output device available")
-    }
-}
-
-pub fn open_output_stream(device_id: Option<DeviceId>) -> anyhow::Result<MixerDeviceSink> {
-    let device = resolve_device(device_id.as_ref(), false)?;
-    let mut output_handle = DeviceSinkBuilder::from_device(device)?
-        .open_stream()
-        .context("Could not open output stream")?;
-    output_handle.log_on_drop(false);
-    log::info!("Output stream: {:?}", output_handle);
-    Ok(output_handle)
-}
-
-#[derive(Clone, Debug)]
-pub struct AudioDeviceInfo {
-    pub id: DeviceId,
-    pub desc: DeviceDescription,
-}
-
-impl AudioDeviceInfo {
-    pub fn matches_input(&self, is_input: bool) -> bool {
-        if is_input {
-            self.desc.supports_input()
-        } else {
-            self.desc.supports_output()
-        }
-    }
-
-    pub fn matches(&self, id: &DeviceId, is_input: bool) -> bool {
-        &self.id == id && self.matches_input(is_input)
-    }
-}
-
-impl std::fmt::Display for AudioDeviceInfo {
-    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
-        write!(f, "{} ({})", self.desc.name(), self.id)
-    }
-}
-
-fn get_available_audio_devices() -> Vec<AudioDeviceInfo> {
-    let Some(devices) = default_host().devices().ok() else {
-        return Vec::new();
-    };
-    devices
-        .filter_map(|device| {
-            let id = device.id().ok()?;
-            let desc = device.description().ok()?;
-            Some(AudioDeviceInfo { id, desc })
-        })
-        .collect()
-}
-
-#[derive(Default, Clone, Debug)]
-pub struct AvailableAudioDevices(pub Vec<AudioDeviceInfo>);
-
-impl Global for AvailableAudioDevices {}

crates/audio/src/audio_pipeline.rs 🔗

@@ -0,0 +1,355 @@
+use anyhow::{Context as _, Result};
+use collections::HashMap;
+use cpal::{
+    DeviceDescription, DeviceId, default_host,
+    traits::{DeviceTrait, HostTrait},
+};
+use gpui::{App, AsyncApp, BackgroundExecutor, BorrowAppContext, Global};
+
+pub(super) use cpal::Sample;
+pub(super) use rodio::source::LimitSettings;
+
+use rodio::{
+    Decoder, DeviceSinkBuilder, MixerDeviceSink, Source,
+    mixer::Mixer,
+    source::{AutomaticGainControlSettings, Buffered},
+};
+use settings::Settings;
+use std::{io::Cursor, path::PathBuf, sync::atomic::Ordering, time::Duration};
+use util::ResultExt;
+
+mod echo_canceller;
+use echo_canceller::EchoCanceller;
+mod replays;
+mod rodio_ext;
+pub use crate::audio_settings::AudioSettings;
+pub use rodio_ext::RodioExt;
+
+use crate::audio_settings::LIVE_SETTINGS;
+
+use crate::Sound;
+
+use super::{CHANNEL_COUNT, SAMPLE_RATE};
+pub const BUFFER_SIZE: usize = // echo canceller and livekit want 10ms of audio
+    (SAMPLE_RATE.get() as usize / 100) * CHANNEL_COUNT.get() as usize;
+
+pub fn init(cx: &mut App) {
+    LIVE_SETTINGS.initialize(cx);
+}
+
+// TODO(jk): this is currently cached only once - we should observe and react instead
+pub fn ensure_devices_initialized(cx: &mut App) {
+    if cx.has_global::<AvailableAudioDevices>() {
+        return;
+    }
+    cx.default_global::<AvailableAudioDevices>();
+    let task = cx
+        .background_executor()
+        .spawn(async move { get_available_audio_devices() });
+    cx.spawn(async move |cx: &mut AsyncApp| {
+        let devices = task.await;
+        cx.update(|cx| cx.set_global(AvailableAudioDevices(devices)));
+        cx.refresh();
+    })
+    .detach();
+}
+
+#[derive(Default)]
+pub struct Audio {
+    output: Option<(MixerDeviceSink, Mixer)>,
+    pub echo_canceller: EchoCanceller,
+    source_cache: HashMap<Sound, Buffered<Decoder<Cursor<Vec<u8>>>>>,
+    replays: replays::Replays,
+}
+
+impl Global for Audio {}
+
+impl Audio {
+    fn ensure_output_exists(&mut self, output_audio_device: Option<DeviceId>) -> Result<&Mixer> {
+        #[cfg(debug_assertions)]
+        log::warn!(
+            "Audio does not sound correct without optimizations. Use a release build to debug audio issues"
+        );
+
+        if self.output.is_none() {
+            let (output_handle, output_mixer) =
+                open_output_stream(output_audio_device, self.echo_canceller.clone())?;
+            self.output = Some((output_handle, output_mixer));
+        }
+
+        Ok(self
+            .output
+            .as_ref()
+            .map(|(_, mixer)| mixer)
+            .expect("we only get here if opening the outputstream succeeded"))
+    }
+
+    pub fn save_replays(
+        &self,
+        executor: BackgroundExecutor,
+    ) -> gpui::Task<anyhow::Result<(PathBuf, Duration)>> {
+        self.replays.replays_to_tar(executor)
+    }
+
+    pub fn open_microphone(mut voip_parts: VoipParts) -> anyhow::Result<impl Source> {
+        let stream = open_input_stream(voip_parts.input_audio_device)?;
+        let stream = stream
+            .possibly_disconnected_channels_to_mono()
+            .constant_params(CHANNEL_COUNT, SAMPLE_RATE)
+            .process_buffer::<BUFFER_SIZE, _>(move |buffer| {
+                let mut int_buffer: [i16; _] = buffer.map(|s| s.to_sample());
+                if voip_parts
+                    .echo_canceller
+                    .process_stream(&mut int_buffer)
+                    .log_err()
+                    .is_some()
+                {
+                    for (sample, processed) in buffer.iter_mut().zip(&int_buffer) {
+                        *sample = (*processed).to_sample();
+                    }
+                }
+            })
+            .limit(LimitSettings::live_performance())
+            .automatic_gain_control(AutomaticGainControlSettings {
+                target_level: 0.90,
+                attack_time: Duration::from_secs(1),
+                release_time: Duration::from_secs(0),
+                absolute_max_gain: 5.0,
+            })
+            .periodic_access(Duration::from_millis(100), move |agc_source| {
+                agc_source
+                    .set_enabled(LIVE_SETTINGS.auto_microphone_volume.load(Ordering::Relaxed));
+                let _ = LIVE_SETTINGS.denoise; // TODO(audio: re-introduce de-noising
+            });
+
+        let (replay, stream) = stream.replayable(crate::REPLAY_DURATION)?;
+        voip_parts
+            .replays
+            .add_voip_stream("local microphone".to_string(), replay);
+
+        Ok(stream)
+    }
+
+    pub fn play_voip_stream(
+        source: impl rodio::Source + Send + 'static,
+        speaker_name: String,
+        is_staff: bool,
+        cx: &mut App,
+    ) -> anyhow::Result<()> {
+        let (replay_source, source) = source
+            .automatic_gain_control(AutomaticGainControlSettings {
+                target_level: 0.90,
+                attack_time: Duration::from_secs(1),
+                release_time: Duration::from_secs(0),
+                absolute_max_gain: 5.0,
+            })
+            .periodic_access(Duration::from_millis(100), move |agc_source| {
+                agc_source.set_enabled(LIVE_SETTINGS.auto_speaker_volume.load(Ordering::Relaxed));
+            })
+            .replayable(crate::REPLAY_DURATION)
+            .expect("REPLAY_DURATION is longer than 100ms");
+        let output_audio_device = AudioSettings::get_global(cx).output_audio_device.clone();
+
+        cx.update_default_global(|this: &mut Self, _cx| {
+            let output_mixer = this
+                .ensure_output_exists(output_audio_device)
+                .context("Could not get output mixer")?;
+            output_mixer.add(source);
+            if is_staff {
+                this.replays.add_voip_stream(speaker_name, replay_source);
+            }
+            Ok(())
+        })
+    }
+
+    pub fn play_sound(sound: Sound, cx: &mut App) {
+        let output_audio_device = AudioSettings::get_global(cx).output_audio_device.clone();
+        cx.update_default_global(|this: &mut Self, cx| {
+            let source = this.sound_source(sound, cx).log_err()?;
+            let output_mixer = this
+                .ensure_output_exists(output_audio_device)
+                .context("Could not get output mixer")
+                .log_err()?;
+
+            output_mixer.add(source);
+            Some(())
+        });
+    }
+
+    pub fn end_call(cx: &mut App) {
+        cx.update_default_global(|this: &mut Self, _cx| {
+            this.output.take();
+        });
+    }
+
+    fn sound_source(&mut self, sound: Sound, cx: &App) -> Result<impl Source + use<>> {
+        if let Some(wav) = self.source_cache.get(&sound) {
+            return Ok(wav.clone());
+        }
+
+        let path = format!("sounds/{}.wav", sound.file());
+        let bytes = cx
+            .asset_source()
+            .load(&path)?
+            .map(anyhow::Ok)
+            .with_context(|| format!("No asset available for path {path}"))??
+            .into_owned();
+        let cursor = Cursor::new(bytes);
+        let source = Decoder::new(cursor)?.buffered();
+
+        self.source_cache.insert(sound, source.clone());
+
+        Ok(source)
+    }
+}
+
+pub struct VoipParts {
+    echo_canceller: EchoCanceller,
+    replays: replays::Replays,
+    input_audio_device: Option<DeviceId>,
+}
+
+impl VoipParts {
+    pub fn new(cx: &AsyncApp) -> anyhow::Result<Self> {
+        let (apm, replays) = cx.read_default_global::<Audio, _>(|audio, _| {
+            (audio.echo_canceller.clone(), audio.replays.clone())
+        });
+        let input_audio_device =
+            AudioSettings::try_read_global(cx, |settings| settings.input_audio_device.clone())
+                .flatten();
+
+        Ok(Self {
+            echo_canceller: apm,
+            replays,
+            input_audio_device,
+        })
+    }
+}
+
+pub fn open_input_stream(
+    device_id: Option<DeviceId>,
+) -> anyhow::Result<rodio::microphone::Microphone> {
+    let builder = rodio::microphone::MicrophoneBuilder::new();
+    let builder = if let Some(id) = device_id {
+        // TODO(jk): upstream patch
+        // if let Some(input_device) = default_host().device_by_id(id) {
+        //     builder.device(input_device);
+        // }
+        let mut found = None;
+        for input in rodio::microphone::available_inputs()? {
+            if input.clone().into_inner().id()? == id {
+                found = Some(builder.device(input));
+                break;
+            }
+        }
+        found.unwrap_or_else(|| builder.default_device())?
+    } else {
+        builder.default_device()?
+    };
+    let stream = builder
+        .default_config()?
+        .prefer_sample_rates([
+            SAMPLE_RATE,
+            SAMPLE_RATE.saturating_mul(rodio::nz!(2)),
+            SAMPLE_RATE.saturating_mul(rodio::nz!(3)),
+            SAMPLE_RATE.saturating_mul(rodio::nz!(4)),
+        ])
+        .prefer_channel_counts([rodio::nz!(1), rodio::nz!(2), rodio::nz!(3), rodio::nz!(4)])
+        .prefer_buffer_sizes(512..)
+        .open_stream()?;
+    log::info!("Opened microphone: {:?}", stream.config());
+    Ok(stream)
+}
+
+pub fn resolve_device(device_id: Option<&DeviceId>, input: bool) -> anyhow::Result<cpal::Device> {
+    if let Some(id) = device_id {
+        if let Some(device) = default_host().device_by_id(id) {
+            return Ok(device);
+        }
+        log::warn!("Selected audio device not found, falling back to default");
+    }
+    if input {
+        default_host()
+            .default_input_device()
+            .context("no audio input device available")
+    } else {
+        default_host()
+            .default_output_device()
+            .context("no audio output device available")
+    }
+}
+
+pub fn open_test_output(device_id: Option<DeviceId>) -> anyhow::Result<MixerDeviceSink> {
+    let device = resolve_device(device_id.as_ref(), false)?;
+    DeviceSinkBuilder::from_device(device)?
+        .open_stream()
+        .context("Could not open output stream")
+}
+
+pub fn open_output_stream(
+    device_id: Option<DeviceId>,
+    mut echo_canceller: EchoCanceller,
+) -> anyhow::Result<(MixerDeviceSink, Mixer)> {
+    let device = resolve_device(device_id.as_ref(), false)?;
+    let mut output_handle = DeviceSinkBuilder::from_device(device)?
+        .open_stream()
+        .context("Could not open output stream")?;
+    output_handle.log_on_drop(false);
+    log::info!("Output stream: {:?}", output_handle);
+
+    let (output_mixer, source) = rodio::mixer::mixer(CHANNEL_COUNT, SAMPLE_RATE);
+    // otherwise the mixer ends as it's empty
+    output_mixer.add(rodio::source::Zero::new(CHANNEL_COUNT, SAMPLE_RATE));
+    let echo_cancelling_source = source // apply echo cancellation just before output
+        .inspect_buffer::<BUFFER_SIZE, _>(move |buffer| {
+            let mut buf: [i16; _] = buffer.map(|s| s.to_sample());
+            echo_canceller.process_reverse_stream(&mut buf)
+        });
+    output_handle.mixer().add(echo_cancelling_source);
+
+    Ok((output_handle, output_mixer))
+}
+
+#[derive(Clone, Debug)]
+pub struct AudioDeviceInfo {
+    pub id: DeviceId,
+    pub desc: DeviceDescription,
+}
+
+impl AudioDeviceInfo {
+    pub fn matches_input(&self, is_input: bool) -> bool {
+        if is_input {
+            self.desc.supports_input()
+        } else {
+            self.desc.supports_output()
+        }
+    }
+
+    pub fn matches(&self, id: &DeviceId, is_input: bool) -> bool {
+        &self.id == id && self.matches_input(is_input)
+    }
+}
+
+impl std::fmt::Display for AudioDeviceInfo {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        write!(f, "{} ({})", self.desc.name(), self.id)
+    }
+}
+
+fn get_available_audio_devices() -> Vec<AudioDeviceInfo> {
+    let Some(devices) = default_host().devices().ok() else {
+        return Vec::new();
+    };
+    devices
+        .filter_map(|device| {
+            let id = device.id().ok()?;
+            let desc = device.description().ok()?;
+            Some(AudioDeviceInfo { id, desc })
+        })
+        .collect()
+}
+
+#[derive(Default, Clone, Debug)]
+pub struct AvailableAudioDevices(pub Vec<AudioDeviceInfo>);
+
+impl Global for AvailableAudioDevices {}

crates/audio/src/audio_pipeline/echo_canceller.rs 🔗

@@ -0,0 +1,54 @@
+#[cfg(not(any(all(target_os = "windows", target_env = "gnu"), target_os = "freebsd")))]
+mod real_implementation {
+    use anyhow::Context;
+    use libwebrtc::native::apm;
+    use parking_lot::Mutex;
+    use std::sync::Arc;
+
+    use crate::{CHANNEL_COUNT, SAMPLE_RATE};
+
+    #[derive(Clone)]
+    pub struct EchoCanceller(Arc<Mutex<apm::AudioProcessingModule>>);
+
+    impl Default for EchoCanceller {
+        fn default() -> Self {
+            Self(Arc::new(Mutex::new(apm::AudioProcessingModule::new(
+                true, false, false, false,
+            ))))
+        }
+    }
+
+    impl EchoCanceller {
+        pub fn process_reverse_stream(&mut self, buf: &mut [i16]) {
+            self.0
+                .lock()
+                .process_reverse_stream(buf, SAMPLE_RATE.get() as i32, CHANNEL_COUNT.get().into())
+                .expect("Audio input and output threads should not panic");
+        }
+
+        pub fn process_stream(&mut self, buf: &mut [i16]) -> anyhow::Result<()> {
+            self.0
+                .lock()
+                .process_stream(buf, SAMPLE_RATE.get() as i32, CHANNEL_COUNT.get() as i32)
+                .context("livekit audio processor error")
+        }
+    }
+}
+
+#[cfg(any(all(target_os = "windows", target_env = "gnu"), target_os = "freebsd"))]
+mod fake_implementation {
+    #[derive(Clone, Default)]
+    pub struct EchoCanceller;
+
+    impl EchoCanceller {
+        pub fn process_reverse_stream(&mut self, _buf: &mut [i16]) {}
+        pub fn process_stream(&mut self, _buf: &mut [i16]) -> anyhow::Result<()> {
+            Ok(())
+        }
+    }
+}
+
+#[cfg(any(all(target_os = "windows", target_env = "gnu"), target_os = "freebsd"))]
+pub use fake_implementation::EchoCanceller;
+#[cfg(not(any(all(target_os = "windows", target_env = "gnu"), target_os = "freebsd")))]
+pub use real_implementation::EchoCanceller;

crates/audio/src/replays.rs → crates/audio/src/audio_pipeline/replays.rs 🔗

@@ -8,7 +8,8 @@ use rodio::Source;
 use smol::fs::File;
 use std::{io, path::PathBuf, sync::Arc, time::Duration};
 
-use crate::{REPLAY_DURATION, rodio_ext::Replay};
+use crate::REPLAY_DURATION;
+use crate::audio_pipeline::rodio_ext::Replay;
 
 #[derive(Default, Clone)]
 pub(crate) struct Replays(Arc<Mutex<HashMap<String, Replay>>>);

crates/cloud_llm_client/src/cloud_llm_client.rs 🔗

@@ -144,6 +144,8 @@ pub struct AcceptEditPredictionBody {
     pub request_id: String,
     #[serde(default, skip_serializing_if = "Option::is_none")]
     pub model_version: Option<String>,
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    pub e2e_latency_ms: Option<u128>,
 }
 
 #[derive(Debug, Clone, Deserialize)]
@@ -164,6 +166,8 @@ pub struct EditPredictionRejection {
     pub was_shown: bool,
     #[serde(default, skip_serializing_if = "Option::is_none")]
     pub model_version: Option<String>,
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    pub e2e_latency_ms: Option<u128>,
 }
 
 #[derive(Default, Debug, Clone, Copy, Serialize, Deserialize, PartialEq)]

crates/collab_ui/src/notification_panel.rs 🔗

@@ -671,6 +671,18 @@ impl Panel for NotificationPanel {
         }
     }
 
+    fn icon_label(&self, _window: &Window, cx: &App) -> Option<String> {
+        if !NotificationPanelSettings::get_global(cx).show_count_badge {
+            return None;
+        }
+        let count = self.notification_store.read(cx).unread_notification_count();
+        if count == 0 {
+            None
+        } else {
+            Some(count.to_string())
+        }
+    }
+
     fn enabled(&self, cx: &App) -> bool {
         NotificationPanelSettings::get_global(cx).button
     }

crates/collab_ui/src/panel_settings.rs 🔗

@@ -15,6 +15,7 @@ pub struct NotificationPanelSettings {
     pub button: bool,
     pub dock: DockPosition,
     pub default_width: Pixels,
+    pub show_count_badge: bool,
 }
 
 impl Settings for CollaborationPanelSettings {
@@ -36,6 +37,7 @@ impl Settings for NotificationPanelSettings {
             button: panel.button.unwrap(),
             dock: panel.dock.unwrap().into(),
             default_width: panel.default_width.map(px).unwrap(),
+            show_count_badge: panel.show_count_badge.unwrap(),
         };
     }
 }

crates/edit_prediction/src/edit_prediction.rs 🔗

@@ -385,6 +385,7 @@ impl ProjectState {
                         EditPredictionRejectReason::Canceled,
                         false,
                         None,
+                        None,
                         cx,
                     );
                 })
@@ -413,6 +414,7 @@ struct CurrentEditPrediction {
     pub prediction: EditPrediction,
     pub was_shown: bool,
     pub shown_with: Option<edit_prediction_types::SuggestionDisplayType>,
+    pub e2e_latency: std::time::Duration,
 }
 
 impl CurrentEditPrediction {
@@ -506,12 +508,14 @@ impl std::ops::Deref for BufferEditPrediction<'_> {
 }
 
 #[derive(Clone)]
+
 struct PendingSettledPrediction {
     request_id: EditPredictionId,
     editable_anchor_range: Range<Anchor>,
     example: Option<ExampleSpec>,
     enqueued_at: Instant,
     last_edit_at: Instant,
+    e2e_latency: std::time::Duration,
 }
 
 struct RegisteredBuffer {
@@ -1686,6 +1690,7 @@ impl EditPredictionStore {
                                         request_id = pending_prediction.request_id.0.clone(),
                                         settled_editable_region,
                                         example = pending_prediction.example.take(),
+                                        e2e_latency = pending_prediction.e2e_latency.as_millis(),
                                     );
 
                                     return false;
@@ -1715,6 +1720,7 @@ impl EditPredictionStore {
         edited_buffer_snapshot: &BufferSnapshot,
         editable_offset_range: Range<usize>,
         example: Option<ExampleSpec>,
+        e2e_latency: std::time::Duration,
         cx: &mut Context<Self>,
     ) {
         let this = &mut *self;
@@ -1729,6 +1735,7 @@ impl EditPredictionStore {
                 editable_anchor_range: edited_buffer_snapshot
                     .anchor_range_around(editable_offset_range),
                 example,
+                e2e_latency,
                 enqueued_at: now,
                 last_edit_at: now,
             });
@@ -1751,6 +1758,7 @@ impl EditPredictionStore {
                     reason,
                     prediction.was_shown,
                     model_version,
+                    Some(prediction.e2e_latency),
                     cx,
                 );
             }
@@ -1812,6 +1820,7 @@ impl EditPredictionStore {
         reason: EditPredictionRejectReason,
         was_shown: bool,
         model_version: Option<String>,
+        e2e_latency: Option<std::time::Duration>,
         cx: &App,
     ) {
         match self.edit_prediction_model {
@@ -1835,6 +1844,7 @@ impl EditPredictionStore {
                                 reason,
                                 was_shown,
                                 model_version,
+                                e2e_latency_ms: e2e_latency.map(|latency| latency.as_millis()),
                             },
                             organization_id,
                         })
@@ -2008,6 +2018,7 @@ impl EditPredictionStore {
                                 EditPredictionResult {
                                     id: prediction_result.id,
                                     prediction: Err(EditPredictionRejectReason::CurrentPreferred),
+                                    e2e_latency: prediction_result.e2e_latency,
                                 }
                             },
                             PredictionRequestedBy::DiagnosticsUpdate,
@@ -2205,6 +2216,7 @@ impl EditPredictionStore {
                                 prediction,
                                 was_shown: false,
                                 shown_with: None,
+                                e2e_latency: prediction_result.e2e_latency,
                             };
 
                             if let Some(current_prediction) =
@@ -2225,6 +2237,7 @@ impl EditPredictionStore {
                                         EditPredictionRejectReason::CurrentPreferred,
                                         false,
                                         new_prediction.prediction.model_version,
+                                        Some(new_prediction.e2e_latency),
                                         cx,
                                     );
                                     None
@@ -2239,6 +2252,7 @@ impl EditPredictionStore {
                                 reject_reason,
                                 false,
                                 None,
+                                Some(prediction_result.e2e_latency),
                                 cx,
                             );
                             None

crates/edit_prediction/src/edit_prediction_tests.rs 🔗

@@ -1323,6 +1323,7 @@ async fn test_empty_prediction(cx: &mut TestAppContext) {
             reason: EditPredictionRejectReason::Empty,
             was_shown: false,
             model_version: None,
+            e2e_latency_ms: Some(0),
         }]
     );
 }
@@ -1384,6 +1385,7 @@ async fn test_interpolated_empty(cx: &mut TestAppContext) {
             reason: EditPredictionRejectReason::InterpolatedEmpty,
             was_shown: false,
             model_version: None,
+            e2e_latency_ms: Some(0),
         }]
     );
 }
@@ -1477,6 +1479,7 @@ async fn test_replace_current(cx: &mut TestAppContext) {
             reason: EditPredictionRejectReason::Replaced,
             was_shown: false,
             model_version: None,
+            e2e_latency_ms: Some(0),
         }]
     );
 }
@@ -1572,6 +1575,7 @@ async fn test_current_preferred(cx: &mut TestAppContext) {
             reason: EditPredictionRejectReason::CurrentPreferred,
             was_shown: false,
             model_version: None,
+            e2e_latency_ms: Some(0),
         }]
     );
 }
@@ -1664,6 +1668,7 @@ async fn test_cancel_earlier_pending_requests(cx: &mut TestAppContext) {
             reason: EditPredictionRejectReason::Canceled,
             was_shown: false,
             model_version: None,
+            e2e_latency_ms: None,
         }]
     );
 }
@@ -1795,12 +1800,14 @@ async fn test_cancel_second_on_third_request(cx: &mut TestAppContext) {
                 reason: EditPredictionRejectReason::Canceled,
                 was_shown: false,
                 model_version: None,
+                e2e_latency_ms: None,
             },
             EditPredictionRejection {
                 request_id: first_id,
                 reason: EditPredictionRejectReason::Replaced,
                 was_shown: false,
                 model_version: None,
+                e2e_latency_ms: Some(0),
             }
         ]
     );
@@ -1963,6 +1970,7 @@ async fn test_rejections_flushing(cx: &mut TestAppContext) {
             EditPredictionRejectReason::Discarded,
             false,
             None,
+            None,
             cx,
         );
         ep_store.reject_prediction(
@@ -1970,6 +1978,7 @@ async fn test_rejections_flushing(cx: &mut TestAppContext) {
             EditPredictionRejectReason::Canceled,
             true,
             None,
+            None,
             cx,
         );
     });
@@ -1989,6 +1998,7 @@ async fn test_rejections_flushing(cx: &mut TestAppContext) {
             reason: EditPredictionRejectReason::Discarded,
             was_shown: false,
             model_version: None,
+            e2e_latency_ms: None
         }
     );
     assert_eq!(
@@ -1998,6 +2008,7 @@ async fn test_rejections_flushing(cx: &mut TestAppContext) {
             reason: EditPredictionRejectReason::Canceled,
             was_shown: true,
             model_version: None,
+            e2e_latency_ms: None
         }
     );
 
@@ -2009,6 +2020,7 @@ async fn test_rejections_flushing(cx: &mut TestAppContext) {
                 EditPredictionRejectReason::Discarded,
                 false,
                 None,
+                None,
                 cx,
             );
         }
@@ -2041,6 +2053,7 @@ async fn test_rejections_flushing(cx: &mut TestAppContext) {
             EditPredictionRejectReason::Discarded,
             false,
             None,
+            None,
             cx,
         );
     });
@@ -2061,6 +2074,7 @@ async fn test_rejections_flushing(cx: &mut TestAppContext) {
             EditPredictionRejectReason::Discarded,
             false,
             None,
+            None,
             cx,
         );
     });
@@ -2394,8 +2408,6 @@ async fn test_edit_prediction_basic_interpolation(cx: &mut TestAppContext) {
             can_collect_data: false,
             repo_url: None,
         },
-        buffer_snapshotted_at: Instant::now(),
-        response_received_at: Instant::now(),
         model_version: None,
     };
 
@@ -3115,6 +3127,7 @@ async fn test_edit_prediction_settled(cx: &mut TestAppContext) {
             &snapshot_a,
             editable_region_a.clone(),
             None,
+            Duration::from_secs(0),
             cx,
         );
     });
@@ -3178,6 +3191,7 @@ async fn test_edit_prediction_settled(cx: &mut TestAppContext) {
             &snapshot_b2,
             editable_region_b.clone(),
             None,
+            Duration::from_secs(0),
             cx,
         );
     });

crates/edit_prediction/src/fim.rs 🔗

@@ -19,10 +19,8 @@ struct FimRequestOutput {
     request_id: String,
     edits: Vec<(std::ops::Range<Anchor>, Arc<str>)>,
     snapshot: BufferSnapshot,
-    response_received_at: Instant,
     inputs: ZetaPromptInput,
     buffer: Entity<Buffer>,
-    buffer_snapshotted_at: Instant,
 }
 
 pub fn request_prediction(
@@ -47,7 +45,7 @@ pub fn request_prediction(
 
     let http_client = cx.http_client();
     let cursor_point = position.to_point(&snapshot);
-    let buffer_snapshotted_at = Instant::now();
+    let request_start = cx.background_executor().now();
 
     let Some(settings) = (match provider {
         settings::EditPredictionProvider::Ollama => settings.ollama.clone(),
@@ -119,7 +117,7 @@ pub fn request_prediction(
 
         log::debug!(
             "fim: completion received ({:.2}s)",
-            (response_received_at - buffer_snapshotted_at).as_secs_f64()
+            (response_received_at - request_start).as_secs_f64()
         );
 
         let completion: Arc<str> = clean_fim_completion(&response_text).into();
@@ -135,10 +133,8 @@ pub fn request_prediction(
             request_id,
             edits,
             snapshot,
-            response_received_at,
             inputs,
             buffer,
-            buffer_snapshotted_at,
         })
     });
 
@@ -151,10 +147,9 @@ pub fn request_prediction(
                 &output.snapshot,
                 output.edits.into(),
                 None,
-                output.buffer_snapshotted_at,
-                output.response_received_at,
                 output.inputs,
                 None,
+                cx.background_executor().now() - request_start,
                 cx,
             )
             .await,

crates/edit_prediction/src/mercury.rs 🔗

@@ -14,7 +14,7 @@ use language::{ToOffset, ToPoint as _};
 use language_model::{ApiKeyState, EnvVar, env_var};
 use release_channel::AppVersion;
 use serde::{Deserialize, Serialize};
-use std::{mem, ops::Range, path::Path, sync::Arc, time::Instant};
+use std::{mem, ops::Range, path::Path, sync::Arc};
 use zeta_prompt::ZetaPromptInput;
 
 const MERCURY_API_URL: &str = "https://api.inceptionlabs.ai/v1/edit/completions";
@@ -67,7 +67,7 @@ impl Mercury {
 
         let http_client = cx.http_client();
         let cursor_point = position.to_point(&snapshot);
-        let buffer_snapshotted_at = Instant::now();
+        let request_start = cx.background_executor().now();
         let active_buffer = buffer.clone();
 
         let result = cx.background_spawn(async move {
@@ -171,7 +171,6 @@ impl Mercury {
                 .await
                 .context("Failed to read response body")?;
 
-            let response_received_at = Instant::now();
             if !response.status().is_success() {
                 if response.status() == StatusCode::PAYMENT_REQUIRED {
                     anyhow::bail!(MercuryPaymentRequiredError(
@@ -222,7 +221,7 @@ impl Mercury {
                 );
             }
 
-            anyhow::Ok((id, edits, snapshot, response_received_at, inputs))
+            anyhow::Ok((id, edits, snapshot, inputs))
         });
 
         cx.spawn(async move |ep_store, cx| {
@@ -240,7 +239,7 @@ impl Mercury {
                 cx.notify();
             })?;
 
-            let (id, edits, old_snapshot, response_received_at, inputs) = result?;
+            let (id, edits, old_snapshot, inputs) = result?;
             anyhow::Ok(Some(
                 EditPredictionResult::new(
                     EditPredictionId(id.into()),
@@ -248,10 +247,9 @@ impl Mercury {
                     &old_snapshot,
                     edits.into(),
                     None,
-                    buffer_snapshotted_at,
-                    response_received_at,
                     inputs,
                     None,
+                    cx.background_executor().now() - request_start,
                     cx,
                 )
                 .await,

crates/edit_prediction/src/prediction.rs 🔗

@@ -1,8 +1,4 @@
-use std::{
-    ops::Range,
-    sync::Arc,
-    time::{Duration, Instant},
-};
+use std::{ops::Range, sync::Arc};
 
 use cloud_llm_client::EditPredictionRejectReason;
 use edit_prediction_types::{PredictedCursorPosition, interpolate_edits};
@@ -29,6 +25,7 @@ impl std::fmt::Display for EditPredictionId {
 pub struct EditPredictionResult {
     pub id: EditPredictionId,
     pub prediction: Result<EditPrediction, EditPredictionRejectReason>,
+    pub e2e_latency: std::time::Duration,
 }
 
 impl EditPredictionResult {
@@ -38,15 +35,15 @@ impl EditPredictionResult {
         edited_buffer_snapshot: &BufferSnapshot,
         edits: Arc<[(Range<Anchor>, Arc<str>)]>,
         cursor_position: Option<PredictedCursorPosition>,
-        buffer_snapshotted_at: Instant,
-        response_received_at: Instant,
         inputs: ZetaPromptInput,
         model_version: Option<String>,
+        e2e_latency: std::time::Duration,
         cx: &mut AsyncApp,
     ) -> Self {
         if edits.is_empty() {
             return Self {
                 id,
+                e2e_latency,
                 prediction: Err(EditPredictionRejectReason::Empty),
             };
         }
@@ -62,6 +59,7 @@ impl EditPredictionResult {
         else {
             return Self {
                 id,
+                e2e_latency,
                 prediction: Err(EditPredictionRejectReason::InterpolatedEmpty),
             };
         };
@@ -70,6 +68,7 @@ impl EditPredictionResult {
 
         Self {
             id: id.clone(),
+            e2e_latency,
             prediction: Ok(EditPrediction {
                 id,
                 edits,
@@ -78,8 +77,6 @@ impl EditPredictionResult {
                 edit_preview,
                 inputs,
                 buffer: edited_buffer.clone(),
-                buffer_snapshotted_at,
-                response_received_at,
                 model_version,
             }),
         }
@@ -94,8 +91,6 @@ pub struct EditPrediction {
     pub snapshot: BufferSnapshot,
     pub edit_preview: EditPreview,
     pub buffer: Entity<Buffer>,
-    pub buffer_snapshotted_at: Instant,
-    pub response_received_at: Instant,
     pub inputs: zeta_prompt::ZetaPromptInput,
     pub model_version: Option<String>,
 }
@@ -111,10 +106,6 @@ impl EditPrediction {
     pub fn targets_buffer(&self, buffer: &Buffer) -> bool {
         self.snapshot.remote_id() == buffer.remote_id()
     }
-
-    pub fn latency(&self) -> Duration {
-        self.response_received_at - self.buffer_snapshotted_at
-    }
 }
 
 impl std::fmt::Debug for EditPrediction {
@@ -169,8 +160,6 @@ mod tests {
                 can_collect_data: false,
                 repo_url: None,
             },
-            buffer_snapshotted_at: Instant::now(),
-            response_received_at: Instant::now(),
         };
 
         cx.update(|cx| {

crates/edit_prediction/src/sweep_ai.rs 🔗

@@ -21,7 +21,6 @@ use std::{
     ops::Range,
     path::Path,
     sync::Arc,
-    time::Instant,
 };
 
 const SWEEP_API_URL: &str = "https://autocomplete.sweep.dev/backend/next_edit_autocomplete";
@@ -50,6 +49,7 @@ impl SweepAi {
             .sweep
             .privacy_mode;
         let debug_info = self.debug_info.clone();
+        let request_start = cx.background_executor().now();
         self.api_token.update(cx, |key_state, cx| {
             _ = key_state.load_if_needed(SWEEP_CREDENTIALS_URL, |s| s, cx);
         });
@@ -90,8 +90,6 @@ impl SweepAi {
             .take(3)
             .collect::<Vec<_>>();
 
-        let buffer_snapshotted_at = Instant::now();
-
         let result = cx.background_spawn(async move {
             let text = inputs.snapshot.text();
 
@@ -255,7 +253,6 @@ impl SweepAi {
             let mut body = String::new();
             response.body_mut().read_to_string(&mut body).await?;
 
-            let response_received_at = Instant::now();
             if !response.status().is_success() {
                 let message = format!(
                     "Request failed with status: {:?}\nBody: {}",
@@ -289,19 +286,13 @@ impl SweepAi {
                 })
                 .collect::<Vec<_>>();
 
-            anyhow::Ok((
-                response.autocomplete_id,
-                edits,
-                inputs.snapshot,
-                response_received_at,
-                ep_inputs,
-            ))
+            anyhow::Ok((response.autocomplete_id, edits, inputs.snapshot, ep_inputs))
         });
 
         let buffer = inputs.buffer.clone();
 
         cx.spawn(async move |cx| {
-            let (id, edits, old_snapshot, response_received_at, inputs) = result.await?;
+            let (id, edits, old_snapshot, inputs) = result.await?;
             anyhow::Ok(Some(
                 EditPredictionResult::new(
                     EditPredictionId(id.into()),
@@ -309,10 +300,9 @@ impl SweepAi {
                     &old_snapshot,
                     edits.into(),
                     None,
-                    buffer_snapshotted_at,
-                    response_received_at,
                     inputs,
                     None,
+                    cx.background_executor().now() - request_start,
                     cx,
                 )
                 .await,

crates/edit_prediction/src/zeta.rs 🔗

@@ -22,7 +22,7 @@ use ui::SharedString;
 use workspace::notifications::{ErrorMessagePrompt, NotificationId, show_app_notification};
 use zeta_prompt::{ParsedOutput, ZetaPromptInput};
 
-use std::{env, ops::Range, path::Path, sync::Arc, time::Instant};
+use std::{env, ops::Range, path::Path, sync::Arc};
 use zeta_prompt::{
     CURSOR_MARKER, ZetaFormat, format_zeta_prompt, get_prefill, parse_zeta2_model_output,
     prompt_input_contains_special_tokens, stop_tokens_for_format,
@@ -63,7 +63,7 @@ pub fn request_prediction_with_zeta(
     };
 
     let http_client = cx.http_client();
-    let buffer_snapshotted_at = Instant::now();
+    let request_start = cx.background_executor().now();
     let raw_config = store.zeta2_raw_config().cloned();
     let preferred_experiment = store.preferred_experiment().map(|s| s.to_owned());
     let open_ai_compatible_api_key = load_open_ai_compatible_api_key_if_needed(provider, cx);
@@ -100,7 +100,6 @@ pub fn request_prediction_with_zeta(
         snapshot: BufferSnapshot,
         edits: Vec<(Range<Anchor>, Arc<str>)>,
         cursor_position: Option<PredictedCursorPosition>,
-        received_response_at: Instant,
         editable_range_in_buffer: Range<usize>,
         model_version: Option<String>,
     }
@@ -295,8 +294,6 @@ pub fn request_prediction_with_zeta(
                 return Ok((None, None));
             };
 
-            let received_response_at = Instant::now();
-
             log::trace!("Got edit prediction response");
 
             let Some(ParsedOutput {
@@ -358,7 +355,6 @@ pub fn request_prediction_with_zeta(
                         snapshot: snapshot.clone(),
                         edits,
                         cursor_position,
-                        received_response_at,
                         editable_range_in_buffer,
                         model_version,
                     }),
@@ -369,6 +365,7 @@ pub fn request_prediction_with_zeta(
     });
 
     cx.spawn(async move |this, cx| {
+        let request_duration = cx.background_executor().now() - request_start;
         let Some((id, prediction)) = handle_api_response(&this, request_task.await, cx)? else {
             return Ok(None);
         };
@@ -379,13 +376,13 @@ pub fn request_prediction_with_zeta(
             snapshot: edited_buffer_snapshot,
             edits,
             cursor_position,
-            received_response_at,
             editable_range_in_buffer,
             model_version,
         }) = prediction
         else {
             return Ok(Some(EditPredictionResult {
                 id,
+                e2e_latency: request_duration,
                 prediction: Err(EditPredictionRejectReason::Empty),
             }));
         };
@@ -423,6 +420,7 @@ pub fn request_prediction_with_zeta(
                             &edited_buffer_snapshot,
                             editable_range_in_buffer,
                             example_spec,
+                            request_duration,
                             cx,
                         );
                     })
@@ -438,10 +436,9 @@ pub fn request_prediction_with_zeta(
                 &edited_buffer_snapshot,
                 edits.into(),
                 cursor_position,
-                buffer_snapshotted_at,
-                received_response_at,
                 inputs,
                 model_version,
+                request_duration,
                 cx,
             )
             .await,
@@ -590,6 +587,7 @@ pub(crate) fn edit_prediction_accepted(
 
     let request_id = current_prediction.prediction.id.to_string();
     let model_version = current_prediction.prediction.model_version;
+    let e2e_latency = current_prediction.e2e_latency;
     let require_auth = custom_accept_url.is_none();
     let client = store.client.clone();
     let llm_token = store.llm_token.clone();
@@ -615,6 +613,7 @@ pub(crate) fn edit_prediction_accepted(
                     serde_json::to_string(&AcceptEditPredictionBody {
                         request_id: request_id.clone(),
                         model_version: model_version.clone(),
+                        e2e_latency_ms: Some(e2e_latency.as_millis()),
                     })?
                     .into(),
                 );

crates/edit_prediction_ui/src/rate_prediction_modal.rs 🔗

@@ -13,7 +13,7 @@ use project::{
 };
 use settings::Settings as _;
 use std::rc::Rc;
-use std::{fmt::Write, sync::Arc, time::Duration};
+use std::{fmt::Write, sync::Arc};
 use theme::ThemeSettings;
 use ui::{
     ContextMenu, DropdownMenu, KeyBinding, List, ListItem, ListItemSpacing, PopoverMenuHandle,
@@ -850,30 +850,18 @@ impl RatePredictionsModal {
                             .gap_3()
                             .child(Icon::new(icon_name).color(icon_color).size(IconSize::Small))
                             .child(
-                                v_flex()
-                                    .child(
-                                        h_flex()
-                                            .gap_1()
-                                            .child(Label::new(file_name).size(LabelSize::Small))
-                                            .when_some(file_path, |this, p| {
-                                                this.child(
-                                                    Label::new(p)
-                                                        .size(LabelSize::Small)
-                                                        .color(Color::Muted),
-                                                )
-                                            }),
-                                    )
-                                    .child(
-                                        Label::new(format!(
-                                            "{} ago, {:.2?}",
-                                            format_time_ago(
-                                                completion.response_received_at.elapsed()
-                                            ),
-                                            completion.latency()
-                                        ))
-                                        .color(Color::Muted)
-                                        .size(LabelSize::XSmall),
-                                    ),
+                                v_flex().child(
+                                    h_flex()
+                                        .gap_1()
+                                        .child(Label::new(file_name).size(LabelSize::Small))
+                                        .when_some(file_path, |this, p| {
+                                            this.child(
+                                                Label::new(p)
+                                                    .size(LabelSize::Small)
+                                                    .color(Color::Muted),
+                                            )
+                                        }),
+                                ),
                             ),
                     )
                     .tooltip(Tooltip::text(tooltip_text))
@@ -977,23 +965,6 @@ impl Focusable for RatePredictionsModal {
 
 impl ModalView for RatePredictionsModal {}
 
-fn format_time_ago(elapsed: Duration) -> String {
-    let seconds = elapsed.as_secs();
-    if seconds < 120 {
-        "1 minute".to_string()
-    } else if seconds < 3600 {
-        format!("{} minutes", seconds / 60)
-    } else if seconds < 7200 {
-        "1 hour".to_string()
-    } else if seconds < 86400 {
-        format!("{} hours", seconds / 3600)
-    } else if seconds < 172800 {
-        "1 day".to_string()
-    } else {
-        format!("{} days", seconds / 86400)
-    }
-}
-
 struct FeedbackCompletionProvider;
 
 impl FeedbackCompletionProvider {

crates/eval_cli/src/main.rs 🔗

@@ -50,6 +50,7 @@ use gpui::{AppContext as _, AsyncApp, Entity, UpdateGlobal};
 use language_model::{LanguageModelRegistry, SelectedModel};
 use project::Project;
 use settings::SettingsStore;
+use util::path_list::PathList;
 
 use crate::headless::AgentCliAppState;
 
@@ -370,7 +371,11 @@ async fn run_agent(
 
     let connection = Rc::new(NativeAgentConnection(agent.clone()));
     let acp_thread = match cx
-        .update(|cx| connection.clone().new_session(project, workdir, cx))
+        .update(|cx| {
+            connection
+                .clone()
+                .new_session(project, PathList::new(&[workdir]), cx)
+        })
         .await
     {
         Ok(t) => t,

crates/file_finder/Cargo.toml 🔗

@@ -28,7 +28,6 @@ picker.workspace = true
 project.workspace = true
 settings.workspace = true
 serde.workspace = true
-text.workspace = true
 theme.workspace = true
 ui.workspace = true
 util.workspace = true

crates/file_finder/src/file_finder.rs 🔗

@@ -35,7 +35,6 @@ use std::{
         atomic::{self, AtomicBool},
     },
 };
-use text::Point;
 use ui::{
     ButtonLike, ContextMenu, HighlightedLabel, Indicator, KeyBinding, ListItem, ListItemSpacing,
     PopoverMenu, PopoverMenuHandle, TintColor, Tooltip, prelude::*,
@@ -1700,7 +1699,12 @@ impl PickerDelegate for FileFinderDelegate {
                     active_editor
                         .downgrade()
                         .update_in(cx, |editor, window, cx| {
-                            editor.go_to_singleton_buffer_point(Point::new(row, col), window, cx);
+                            let Some(buffer) = editor.buffer().read(cx).as_singleton() else {
+                                return;
+                            };
+                            let buffer_snapshot = buffer.read(cx).snapshot();
+                            let point = buffer_snapshot.point_from_external_input(row, col);
+                            editor.go_to_singleton_buffer_point(point, window, cx);
                         })
                         .log_err();
                 }

crates/file_finder/src/file_finder_tests.rs 🔗

@@ -521,6 +521,91 @@ async fn test_row_column_numbers_query_inside_file(cx: &mut TestAppContext) {
         });
 }
 
+#[gpui::test]
+async fn test_row_column_numbers_query_inside_unicode_file(cx: &mut TestAppContext) {
+    let app_state = init_test(cx);
+
+    let first_file_name = "first.rs";
+    let first_file_contents = "aéøbcdef";
+    app_state
+        .fs
+        .as_fake()
+        .insert_tree(
+            path!("/src"),
+            json!({
+                "test": {
+                    first_file_name: first_file_contents,
+                    "second.rs": "// Second Rust file",
+                }
+            }),
+        )
+        .await;
+
+    let project = Project::test(app_state.fs.clone(), [path!("/src").as_ref()], cx).await;
+
+    let (picker, workspace, cx) = build_find_picker(project, cx);
+
+    let file_query = &first_file_name[..3];
+    let file_row = 1;
+    let file_column = 5;
+    let query_inside_file = format!("{file_query}:{file_row}:{file_column}");
+    picker
+        .update_in(cx, |finder, window, cx| {
+            finder
+                .delegate
+                .update_matches(query_inside_file.to_string(), window, cx)
+        })
+        .await;
+    picker.update(cx, |finder, _| {
+        assert_match_at_position(finder, 1, &query_inside_file.to_string());
+        let finder = &finder.delegate;
+        assert_eq!(finder.matches.len(), 2);
+        let latest_search_query = finder
+            .latest_search_query
+            .as_ref()
+            .expect("Finder should have a query after the update_matches call");
+        assert_eq!(latest_search_query.raw_query, query_inside_file);
+        assert_eq!(latest_search_query.file_query_end, Some(file_query.len()));
+        assert_eq!(latest_search_query.path_position.row, Some(file_row));
+        assert_eq!(latest_search_query.path_position.column, Some(file_column));
+    });
+
+    cx.dispatch_action(Confirm);
+
+    let editor = cx.update(|_, cx| workspace.read(cx).active_item_as::<Editor>(cx).unwrap());
+    cx.executor().advance_clock(Duration::from_secs(2));
+
+    let expected_column = first_file_contents
+        .chars()
+        .take(file_column as usize - 1)
+        .map(|character| character.len_utf8())
+        .sum::<usize>();
+
+    editor.update(cx, |editor, cx| {
+        let all_selections = editor.selections.all_adjusted(&editor.display_snapshot(cx));
+        assert_eq!(
+            all_selections.len(),
+            1,
+            "Expected to have 1 selection (caret) after file finder confirm, but got: {all_selections:?}"
+        );
+        let caret_selection = all_selections.into_iter().next().unwrap();
+        assert_eq!(
+            caret_selection.start, caret_selection.end,
+            "Caret selection should have its start and end at the same position"
+        );
+        assert_eq!(
+            file_row,
+            caret_selection.start.row + 1,
+            "Query inside file should get caret with the same focus row"
+        );
+        assert_eq!(
+            expected_column,
+            caret_selection.start.column as usize,
+            "Query inside file should map user-visible columns to byte offsets for Unicode text"
+        );
+    });
+}
+
 #[gpui::test]
 async fn test_row_column_numbers_query_outside_file(cx: &mut TestAppContext) {
     let app_state = init_test(cx);

crates/git_ui/src/conflict_view.rs 🔗

@@ -18,10 +18,7 @@ use settings::Settings;
 use std::{cell::RefCell, ops::Range, rc::Rc, sync::Arc};
 use ui::{ActiveTheme, Divider, Element as _, Styled, Window, prelude::*};
 use util::{ResultExt as _, debug_panic, maybe};
-use workspace::{
-    Workspace,
-    notifications::{NotificationId, simple_message_notification::MessageNotification},
-};
+use workspace::{Workspace, notifications::simple_message_notification::MessageNotification};
 use zed_actions::agent::{
     ConflictContent, ResolveConflictedFilesWithAgent, ResolveConflictsWithAgent,
 };
@@ -500,12 +497,6 @@ fn render_conflict_buttons(
         .into_any()
 }
 
-struct MergeConflictNotification;
-
-fn merge_conflict_notification_id() -> NotificationId {
-    NotificationId::unique::<MergeConflictNotification>()
-}
-
 fn collect_conflicted_file_paths(workspace: &Workspace, cx: &App) -> Vec<String> {
     let project = workspace.project().read(cx);
     let git_store = project.git_store().read(cx);
@@ -547,8 +538,12 @@ pub(crate) fn register_conflict_notification(
             return;
         }
 
+        if workspace.is_notification_suppressed(workspace::merge_conflict_notification_id()) {
+            return;
+        }
+
         let paths = collect_conflicted_file_paths(workspace, cx);
-        let notification_id = merge_conflict_notification_id();
+        let notification_id = workspace::merge_conflict_notification_id();
         let current_paths_set: HashSet<String> = paths.iter().cloned().collect();
 
         if paths.is_empty() {

crates/git_ui/src/git_panel.rs 🔗

@@ -5801,6 +5801,14 @@ impl Panel for GitPanel {
         GitPanelSettings::get_global(cx).button
     }
 
+    fn icon_label(&self, _: &Window, cx: &App) -> Option<String> {
+        if !GitPanelSettings::get_global(cx).show_count_badge {
+            return None;
+        }
+        let total = self.changes_count;
+        (total > 0).then(|| total.to_string())
+    }
+
     fn activation_priority(&self) -> u32 {
         2
     }

crates/git_ui/src/git_panel_settings.rs 🔗

@@ -28,6 +28,7 @@ pub struct GitPanelSettings {
     pub collapse_untracked_diff: bool,
     pub tree_view: bool,
     pub diff_stats: bool,
+    pub show_count_badge: bool,
 }
 
 impl ScrollbarVisibility for GitPanelSettings {
@@ -64,6 +65,7 @@ impl Settings for GitPanelSettings {
             collapse_untracked_diff: git_panel.collapse_untracked_diff.unwrap(),
             tree_view: git_panel.tree_view.unwrap(),
             diff_stats: git_panel.diff_stats.unwrap(),
+            show_count_badge: git_panel.show_count_badge.unwrap(),
         }
     }
 }

crates/go_to_line/Cargo.toml 🔗

@@ -17,6 +17,7 @@ editor.workspace = true
 gpui.workspace = true
 language.workspace = true
 menu.workspace = true
+multi_buffer.workspace = true
 serde.workspace = true
 settings.workspace = true
 text.workspace = true

crates/go_to_line/src/go_to_line.rs 🔗

@@ -2,7 +2,7 @@ pub mod cursor_position;
 
 use cursor_position::UserCaretPosition;
 use editor::{
-    Anchor, Editor, MultiBufferSnapshot, RowHighlightOptions, SelectionEffects, ToOffset, ToPoint,
+    Anchor, Editor, MultiBufferSnapshot, RowHighlightOptions, SelectionEffects, ToPoint,
     actions::Tab,
     scroll::{Autoscroll, ScrollOffset},
 };
@@ -11,6 +11,7 @@ use gpui::{
     Subscription, div, prelude::*,
 };
 use language::Buffer;
+use multi_buffer::MultiBufferRow;
 use text::{Bias, Point};
 use theme::ActiveTheme;
 use ui::prelude::*;
@@ -228,31 +229,14 @@ impl GoToLine {
         let row = query_row.saturating_sub(1);
         let character = query_char.unwrap_or(0).saturating_sub(1);
 
-        let start_offset = Point::new(row, 0).to_offset(snapshot);
-        const MAX_BYTES_IN_UTF_8: u32 = 4;
-        let max_end_offset = snapshot
-            .clip_point(
-                Point::new(row, character * MAX_BYTES_IN_UTF_8 + 1),
-                Bias::Right,
-            )
-            .to_offset(snapshot);
-
-        let mut chars_to_iterate = character;
-        let mut end_offset = start_offset;
-        'outer: for text_chunk in snapshot.text_for_range(start_offset..max_end_offset) {
-            let mut offset_increment = 0;
-            for c in text_chunk.chars() {
-                if chars_to_iterate == 0 {
-                    end_offset += offset_increment;
-                    break 'outer;
-                } else {
-                    chars_to_iterate -= 1;
-                    offset_increment += c.len_utf8();
-                }
-            }
-            end_offset += offset_increment;
-        }
-        Some(snapshot.anchor_before(snapshot.clip_offset(end_offset, Bias::Left)))
+        let target_multi_buffer_row = MultiBufferRow(row);
+        let (buffer_snapshot, target_in_buffer, _) = snapshot.point_to_buffer_point(Point::new(
+            target_multi_buffer_row.min(snapshot.max_row()).0,
+            0,
+        ))?;
+        let target_point =
+            buffer_snapshot.point_from_external_input(target_in_buffer.row, character);
+        Some(snapshot.anchor_before(target_point))
     }
 
     fn relative_line_from_query(&self, cx: &App) -> Option<i32> {

crates/gpui/src/styled.rs 🔗

@@ -1,5 +1,5 @@
 use crate::{
-    self as gpui, AbsoluteLength, AlignContent, AlignItems, BorderStyle, CursorStyle,
+    self as gpui, AbsoluteLength, AlignContent, AlignItems, AlignSelf, BorderStyle, CursorStyle,
     DefiniteLength, Display, Fill, FlexDirection, FlexWrap, Font, FontFeatures, FontStyle,
     FontWeight, GridPlacement, Hsla, JustifyContent, Length, SharedString, StrikethroughStyle,
     StyleRefinement, TextAlign, TextOverflow, TextStyleRefinement, UnderlineStyle, WhiteSpace, px,
@@ -278,6 +278,55 @@ pub trait Styled: Sized {
         self
     }
 
+    /// Sets how this specific element is aligned along the container's cross axis.
+    /// [Docs](https://tailwindcss.com/docs/align-self#start)
+    fn self_start(mut self) -> Self {
+        self.style().align_self = Some(AlignSelf::Start);
+        self
+    }
+
+    /// Sets this element to align against the end of the container's cross axis.
+    /// [Docs](https://tailwindcss.com/docs/align-self#end)
+    fn self_end(mut self) -> Self {
+        self.style().align_self = Some(AlignSelf::End);
+        self
+    }
+
+    /// Sets this element to align against the start of the container's cross axis.
+    /// [Docs](https://tailwindcss.com/docs/align-self#start)
+    fn self_flex_start(mut self) -> Self {
+        self.style().align_self = Some(AlignSelf::FlexStart);
+        self
+    }
+
+    /// Sets this element to align against the end of the container's cross axis.
+    /// [Docs](https://tailwindcss.com/docs/align-self#end)
+    fn self_flex_end(mut self) -> Self {
+        self.style().align_self = Some(AlignSelf::FlexEnd);
+        self
+    }
+
+    /// Sets this element to align along the center of the container's cross axis.
+    /// [Docs](https://tailwindcss.com/docs/align-self#center)
+    fn self_center(mut self) -> Self {
+        self.style().align_self = Some(AlignSelf::Center);
+        self
+    }
+
+    /// Sets this element to align along the baseline of the container's cross axis.
+    /// [Docs](https://tailwindcss.com/docs/align-self#baseline)
+    fn self_baseline(mut self) -> Self {
+        self.style().align_self = Some(AlignSelf::Baseline);
+        self
+    }
+
+    /// Sets this element to stretch to fill the available space along the container's cross axis.
+    /// [Docs](https://tailwindcss.com/docs/align-self#stretch)
+    fn self_stretch(mut self) -> Self {
+        self.style().align_self = Some(AlignSelf::Stretch);
+        self
+    }
+
     /// Sets the element to justify flex items against the start of the container's main axis.
     /// [Docs](https://tailwindcss.com/docs/justify-content#start)
     fn justify_start(mut self) -> Self {

crates/language_selector/src/language_selector.rs 🔗

@@ -280,20 +280,28 @@ impl PickerDelegate for LanguageSelectorDelegate {
             };
 
             this.update_in(cx, |this, window, cx| {
-                let delegate = &mut this.delegate;
-                delegate.matches = matches;
-                delegate.selected_index = delegate
-                    .selected_index
-                    .min(delegate.matches.len().saturating_sub(1));
-
-                if query_is_empty {
-                    if let Some(index) = delegate
-                        .current_language_candidate_index
-                        .and_then(|ci| delegate.matches.iter().position(|m| m.candidate_id == ci))
-                    {
-                        this.set_selected_index(index, None, false, window, cx);
-                    }
+                if matches.is_empty() {
+                    this.delegate.matches = matches;
+                    this.delegate.selected_index = 0;
+                    cx.notify();
+                    return;
                 }
+
+                let selected_index = if query_is_empty {
+                    this.delegate
+                        .current_language_candidate_index
+                        .and_then(|current_language_candidate_index| {
+                            matches.iter().position(|mat| {
+                                mat.candidate_id == current_language_candidate_index
+                            })
+                        })
+                        .unwrap_or(0)
+                } else {
+                    0
+                };
+
+                this.delegate.matches = matches;
+                this.set_selected_index(selected_index, None, false, window, cx);
                 cx.notify();
             })
             .log_err();
@@ -345,28 +353,25 @@ mod tests {
     fn register_test_languages(project: &Entity<Project>, cx: &mut VisualTestContext) {
         project.read_with(cx, |project, _| {
             let language_registry = project.languages();
-            language_registry.add(Arc::new(Language::new(
-                LanguageConfig {
-                    name: "Rust".into(),
-                    matcher: LanguageMatcher {
-                        path_suffixes: vec!["rs".to_string()],
-                        ..Default::default()
-                    },
-                    ..Default::default()
-                },
-                None,
-            )));
-            language_registry.add(Arc::new(Language::new(
-                LanguageConfig {
-                    name: "TypeScript".into(),
-                    matcher: LanguageMatcher {
-                        path_suffixes: vec!["ts".to_string()],
+            for (language_name, path_suffix) in [
+                ("C", "c"),
+                ("Go", "go"),
+                ("Ruby", "rb"),
+                ("Rust", "rs"),
+                ("TypeScript", "ts"),
+            ] {
+                language_registry.add(Arc::new(Language::new(
+                    LanguageConfig {
+                        name: language_name.into(),
+                        matcher: LanguageMatcher {
+                            path_suffixes: vec![path_suffix.to_string()],
+                            ..Default::default()
+                        },
                         ..Default::default()
                     },
-                    ..Default::default()
-                },
-                None,
-            )));
+                    None,
+                )));
+            }
         });
     }
 
@@ -406,6 +411,24 @@ mod tests {
         workspace: &Entity<Workspace>,
         project: &Entity<Project>,
         cx: &mut VisualTestContext,
+    ) -> Entity<Editor> {
+        let editor = open_new_buffer_editor(workspace, project, cx).await;
+        // Ensure the buffer has no language after the editor is created
+        let (_, buffer, _) = editor.read_with(cx, |editor, cx| {
+            editor
+                .active_excerpt(cx)
+                .expect("editor should have an active excerpt")
+        });
+        buffer.update(cx, |buffer, cx| {
+            buffer.set_language(None, cx);
+        });
+        editor
+    }
+
+    async fn open_new_buffer_editor(
+        workspace: &Entity<Workspace>,
+        project: &Entity<Project>,
+        cx: &mut VisualTestContext,
     ) -> Entity<Editor> {
         let create_buffer = project.update(cx, |project, cx| project.create_buffer(None, true, cx));
         let buffer = create_buffer.await.expect("empty buffer should be created");
@@ -415,10 +438,6 @@ mod tests {
         workspace.update_in(cx, |workspace, window, cx| {
             workspace.add_item_to_center(Box::new(editor.clone()), window, cx);
         });
-        // Ensure the buffer has no language after the editor is created
-        buffer.update(cx, |buffer, cx| {
-            buffer.set_language(None, cx);
-        });
         editor
     }
 
@@ -559,15 +578,86 @@ mod tests {
 
         assert_selected_language_for_editor(&workspace, &rust_editor, Some("Rust"), cx);
         assert_selected_language_for_editor(&workspace, &typescript_editor, Some("TypeScript"), cx);
-        // Ensure the empty editor's buffer has no language before asserting
-        let (_, buffer, _) = empty_editor.read_with(cx, |editor, cx| {
-            editor
-                .active_excerpt(cx)
-                .expect("editor should have an active excerpt")
+        assert_selected_language_for_editor(&workspace, &empty_editor, None, cx);
+    }
+
+    #[gpui::test]
+    async fn test_language_selector_selects_first_match_after_querying_new_buffer(
+        cx: &mut TestAppContext,
+    ) {
+        let app_state = init_test(cx);
+        app_state
+            .fs
+            .as_fake()
+            .insert_tree(path!("/test"), json!({}))
+            .await;
+
+        let project = Project::test(app_state.fs.clone(), [path!("/test").as_ref()], cx).await;
+        let (multi_workspace, cx) =
+            cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
+        let workspace =
+            multi_workspace.read_with(cx, |multi_workspace, _| multi_workspace.workspace().clone());
+        register_test_languages(&project, cx);
+
+        let editor = open_new_buffer_editor(&workspace, &project, cx).await;
+        workspace.update_in(cx, |workspace, window, cx| {
+            let was_activated = workspace.activate_item(&editor, true, true, window, cx);
+            assert!(
+                was_activated,
+                "editor should be activated before opening the modal"
+            );
         });
-        buffer.update(cx, |buffer, cx| {
-            buffer.set_language(None, cx);
+        cx.run_until_parked();
+
+        let picker = open_selector(&workspace, cx);
+        picker.read_with(cx, |picker, _| {
+            let selected_match = picker
+                .delegate
+                .matches
+                .get(picker.delegate.selected_index)
+                .expect("selected index should point to a match");
+            let selected_candidate = picker
+                .delegate
+                .candidates
+                .get(selected_match.candidate_id)
+                .expect("selected match should map to a candidate");
+
+            assert_eq!(selected_candidate.string, "Plain Text");
+            assert!(
+                picker
+                    .delegate
+                    .current_language_candidate_index
+                    .is_some_and(|current_language_candidate_index| {
+                        current_language_candidate_index > 1
+                    }),
+                "test setup should place Plain Text after at least two earlier languages",
+            );
+        });
+
+        picker.update_in(cx, |picker, window, cx| {
+            picker.update_matches("ru".to_string(), window, cx)
+        });
+        cx.run_until_parked();
+
+        picker.read_with(cx, |picker, _| {
+            assert!(
+                picker.delegate.matches.len() > 1,
+                "query should return multiple matches"
+            );
+            assert_eq!(picker.delegate.selected_index, 0);
+
+            let first_match = picker
+                .delegate
+                .matches
+                .first()
+                .expect("query should produce at least one match");
+            let selected_match = picker
+                .delegate
+                .matches
+                .get(picker.delegate.selected_index)
+                .expect("selected index should point to a match");
+
+            assert_eq!(selected_match.candidate_id, first_match.candidate_id);
         });
-        assert_selected_language_for_editor(&workspace, &empty_editor, None, cx);
     }
 }

crates/livekit_client/src/livekit_client/playback.rs 🔗

@@ -1,6 +1,6 @@
 use anyhow::{Context as _, Result};
 
-use audio::{AudioSettings, CHANNEL_COUNT, LEGACY_CHANNEL_COUNT, LEGACY_SAMPLE_RATE, SAMPLE_RATE};
+use audio::{AudioSettings, CHANNEL_COUNT, SAMPLE_RATE};
 use cpal::DeviceId;
 use cpal::traits::{DeviceTrait, StreamTrait as _};
 use futures::channel::mpsc::Sender;
@@ -99,8 +99,8 @@ impl AudioStack {
         let next_ssrc = self.next_ssrc.fetch_add(1, Ordering::Relaxed);
         let source = AudioMixerSource {
             ssrc: next_ssrc,
-            sample_rate: LEGACY_SAMPLE_RATE.get(),
-            num_channels: LEGACY_CHANNEL_COUNT.get() as u32,
+            sample_rate: SAMPLE_RATE.get(),
+            num_channels: CHANNEL_COUNT.get() as u32,
             buffer: Arc::default(),
         };
         self.mixer.lock().add_source(source.clone());
@@ -145,8 +145,8 @@ impl AudioStack {
                     executor,
                     apm,
                     mixer,
-                    LEGACY_SAMPLE_RATE.get(),
-                    LEGACY_CHANNEL_COUNT.get().into(),
+                    SAMPLE_RATE.get(),
+                    CHANNEL_COUNT.get().into(),
                     output_audio_device,
                 )
                 .await
@@ -171,8 +171,9 @@ impl AudioStack {
             NativeAudioSource::new(
                 // n.b. this struct's options are always ignored, noise cancellation is provided by apm.
                 AudioSourceOptions::default(),
-                LEGACY_SAMPLE_RATE.get(),
-                LEGACY_CHANNEL_COUNT.get().into(),
+                SAMPLE_RATE.get(), // TODO(audio): this was legacy params,
+                // removed for now for simplicity
+                CHANNEL_COUNT.get().into(),
                 10,
             )
         } else {
@@ -233,8 +234,8 @@ impl AudioStack {
                     executor,
                     apm,
                     frame_tx,
-                    LEGACY_SAMPLE_RATE.get(),
-                    LEGACY_CHANNEL_COUNT.get().into(),
+                    SAMPLE_RATE.get(), // TODO(audio): was legacy removed for now
+                    CHANNEL_COUNT.get().into(),
                     input_audio_device,
                 )
                 .await

crates/livekit_client/src/livekit_client/playback/source.rs 🔗

@@ -7,7 +7,7 @@ use rodio::{
     ChannelCount, SampleRate, Source, buffer::SamplesBuffer, conversions::SampleTypeConverter,
 };
 
-use audio::{CHANNEL_COUNT, LEGACY_CHANNEL_COUNT, LEGACY_SAMPLE_RATE, SAMPLE_RATE};
+use audio::{CHANNEL_COUNT, SAMPLE_RATE};
 
 fn frame_to_samplesbuffer(frame: AudioFrame) -> SamplesBuffer {
     let samples = frame.data.iter().copied();
@@ -35,7 +35,8 @@ impl LiveKitStream {
         legacy: bool,
     ) -> Self {
         let (channel_count, sample_rate) = if legacy {
-            (LEGACY_CHANNEL_COUNT, LEGACY_SAMPLE_RATE)
+            // (LEGACY_CHANNEL_COUNT, LEGACY_SAMPLE_RATE) TODO(audio): do this or remove
+            (CHANNEL_COUNT, SAMPLE_RATE)
         } else {
             (CHANNEL_COUNT, SAMPLE_RATE)
         };

crates/multi_buffer/src/multi_buffer.rs 🔗

@@ -2141,7 +2141,7 @@ impl MultiBuffer {
             if point < start {
                 found = Some((start, excerpt_id));
             }
-            if point > end {
+            if point >= end {
                 found = Some((end, excerpt_id));
             }
         }

crates/multi_buffer/src/multi_buffer_tests.rs 🔗

@@ -72,6 +72,30 @@ fn test_singleton(cx: &mut App) {
     assert_consistent_line_numbers(&snapshot);
 }
 
+#[gpui::test]
+fn test_buffer_point_to_anchor_at_end_of_singleton_buffer(cx: &mut App) {
+    let buffer = cx.new(|cx| Buffer::local("abc", cx));
+    let multibuffer = cx.new(|cx| MultiBuffer::singleton(buffer.clone(), cx));
+
+    let excerpt_id = multibuffer
+        .read(cx)
+        .excerpt_ids()
+        .into_iter()
+        .next()
+        .unwrap();
+    let anchor = multibuffer
+        .read(cx)
+        .buffer_point_to_anchor(&buffer, Point::new(0, 3), cx);
+
+    assert_eq!(
+        anchor,
+        Some(Anchor::in_buffer(
+            excerpt_id,
+            buffer.read(cx).snapshot().anchor_after(Point::new(0, 3)),
+        ))
+    );
+}
+
 #[gpui::test]
 fn test_remote(cx: &mut App) {
     let host_buffer = cx.new(|cx| Buffer::local("a", cx));

crates/project/src/agent_registry_store.rs 🔗

@@ -11,14 +11,14 @@ use http_client::{AsyncBody, HttpClient};
 use serde::Deserialize;
 use settings::Settings as _;
 
-use crate::DisableAiSettings;
+use crate::{AgentId, DisableAiSettings};
 
 const REGISTRY_URL: &str = "https://cdn.agentclientprotocol.com/registry/v1/latest/registry.json";
 const REFRESH_THROTTLE_DURATION: Duration = Duration::from_secs(60 * 60);
 
 #[derive(Clone, Debug)]
 pub struct RegistryAgentMetadata {
-    pub id: SharedString,
+    pub id: AgentId,
     pub name: SharedString,
     pub description: SharedString,
     pub version: SharedString,
@@ -55,7 +55,7 @@ impl RegistryAgent {
         }
     }
 
-    pub fn id(&self) -> &SharedString {
+    pub fn id(&self) -> &AgentId {
         &self.metadata().id
     }
 
@@ -167,8 +167,8 @@ impl AgentRegistryStore {
         &self.agents
     }
 
-    pub fn agent(&self, id: &str) -> Option<&RegistryAgent> {
-        self.agents.iter().find(|agent| agent.id().as_ref() == id)
+    pub fn agent(&self, id: &AgentId) -> Option<&RegistryAgent> {
+        self.agents.iter().find(|agent| agent.id() == id)
     }
 
     pub fn is_fetching(&self) -> bool {
@@ -364,7 +364,7 @@ async fn build_registry_agents(
         .await?;
 
         let metadata = RegistryAgentMetadata {
-            id: entry.id.into(),
+            id: AgentId::new(entry.id),
             name: entry.name.into(),
             description: entry.description.into(),
             version: entry.version.into(),

crates/project/src/agent_server_store.rs 🔗

@@ -61,28 +61,43 @@ impl std::fmt::Debug for AgentServerCommand {
     }
 }
 
-#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
-pub struct ExternalAgentServerName(pub SharedString);
+#[derive(
+    Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize, JsonSchema,
+)]
+#[serde(transparent)]
+pub struct AgentId(pub SharedString);
+
+impl AgentId {
+    pub fn new(id: impl Into<SharedString>) -> Self {
+        AgentId(id.into())
+    }
+}
 
-impl std::fmt::Display for ExternalAgentServerName {
+impl std::fmt::Display for AgentId {
     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
         write!(f, "{}", self.0)
     }
 }
 
-impl From<&'static str> for ExternalAgentServerName {
+impl From<&'static str> for AgentId {
     fn from(value: &'static str) -> Self {
-        ExternalAgentServerName(value.into())
+        AgentId(value.into())
     }
 }
 
-impl From<ExternalAgentServerName> for SharedString {
-    fn from(value: ExternalAgentServerName) -> Self {
+impl From<AgentId> for SharedString {
+    fn from(value: AgentId) -> Self {
         value.0
     }
 }
 
-impl std::borrow::Borrow<str> for ExternalAgentServerName {
+impl AsRef<str> for AgentId {
+    fn as_ref(&self) -> &str {
+        &self.0
+    }
+}
+
+impl std::borrow::Borrow<str> for AgentId {
     fn borrow(&self) -> &str {
         &self.0
     }
@@ -163,7 +178,7 @@ impl ExternalAgentEntry {
 
 pub struct AgentServerStore {
     state: AgentServerStoreState,
-    pub external_agents: HashMap<ExternalAgentServerName, ExternalAgentEntry>,
+    pub external_agents: HashMap<AgentId, ExternalAgentEntry>,
 }
 
 pub struct AgentServersUpdated;
@@ -228,7 +243,7 @@ impl AgentServerStore {
                             .as_ref()
                             .map(|path| SharedString::from(path.clone()));
                         let icon = icon_path;
-                        let agent_server_name = ExternalAgentServerName(agent_name.clone().into());
+                        let agent_server_name = AgentId(agent_name.clone().into());
                         self.external_agents
                             .entry(agent_server_name.clone())
                             .and_modify(|entry| {
@@ -285,13 +300,13 @@ impl AgentServerStore {
         cx.emit(AgentServersUpdated);
     }
 
-    pub fn agent_icon(&self, name: &ExternalAgentServerName) -> Option<SharedString> {
+    pub fn agent_icon(&self, name: &AgentId) -> Option<SharedString> {
         self.external_agents
             .get(name)
             .and_then(|entry| entry.icon.clone())
     }
 
-    pub fn agent_source(&self, name: &ExternalAgentServerName) -> Option<ExternalAgentSource> {
+    pub fn agent_source(&self, name: &AgentId) -> Option<ExternalAgentSource> {
         self.external_agents.get(name).map(|entry| entry.source)
     }
 }
@@ -337,7 +352,7 @@ pub fn resolve_extension_icon_path(
 }
 
 impl AgentServerStore {
-    pub fn agent_display_name(&self, name: &ExternalAgentServerName) -> Option<SharedString> {
+    pub fn agent_display_name(&self, name: &AgentId) -> Option<SharedString> {
         self.external_agents
             .get(name)
             .and_then(|entry| entry.display_name.clone())
@@ -424,7 +439,7 @@ impl AgentServerStore {
 
         // Insert extension agents before custom/registry so registry entries override extensions.
         for (agent_name, ext_id, targets, env, icon_path, display_name) in extension_agents.iter() {
-            let name = ExternalAgentServerName(agent_name.clone().into());
+            let name = AgentId(agent_name.clone().into());
             let mut env = env.clone();
             if let Some(settings_env) =
                 new_settings
@@ -463,7 +478,7 @@ impl AgentServerStore {
         for (name, settings) in new_settings.iter() {
             match settings {
                 CustomAgentServerSettings::Custom { command, .. } => {
-                    let agent_name = ExternalAgentServerName(name.clone().into());
+                    let agent_name = AgentId(name.clone().into());
                     self.external_agents.insert(
                         agent_name.clone(),
                         ExternalAgentEntry::new(
@@ -485,7 +500,7 @@ impl AgentServerStore {
                         continue;
                     };
 
-                    let agent_name = ExternalAgentServerName(name.clone().into());
+                    let agent_name = AgentId(name.clone().into());
                     match agent {
                         RegistryAgent::Binary(agent) => {
                             if !agent.supports_current_platform {
@@ -650,7 +665,7 @@ impl AgentServerStore {
 
     pub fn get_external_agent(
         &mut self,
-        name: &ExternalAgentServerName,
+        name: &AgentId,
     ) -> Option<&mut (dyn ExternalAgentServer + 'static)> {
         self.external_agents
             .get_mut(name)
@@ -668,7 +683,7 @@ impl AgentServerStore {
         }
     }
 
-    pub fn external_agents(&self) -> impl Iterator<Item = &ExternalAgentServerName> {
+    pub fn external_agents(&self) -> impl Iterator<Item = &AgentId> {
         self.external_agents.keys()
     }
 
@@ -777,12 +792,12 @@ impl AgentServerStore {
                 .names
                 .into_iter()
                 .map(|name| {
-                    let agent_name = ExternalAgentServerName(name.into());
+                    let agent_id = AgentId(name.into());
                     let (icon, display_name, source) = metadata
-                        .remove(&agent_name)
+                        .remove(&agent_id)
                         .or_else(|| {
                             AgentRegistryStore::try_global(cx)
-                                .and_then(|store| store.read(cx).agent(&agent_name.0))
+                                .and_then(|store| store.read(cx).agent(&agent_id))
                                 .map(|s| {
                                     (
                                         s.icon_path().cloned(),
@@ -795,13 +810,13 @@ impl AgentServerStore {
                     let agent = RemoteExternalAgentServer {
                         project_id: *project_id,
                         upstream_client: upstream_client.clone(),
-                        name: agent_name.clone(),
+                        name: agent_id.clone(),
                         new_version_available_tx: new_version_available_txs
-                            .remove(&agent_name)
+                            .remove(&agent_id)
                             .flatten(),
                     };
                     (
-                        agent_name,
+                        agent_id,
                         ExternalAgentEntry::new(
                             Box::new(agent) as Box<dyn ExternalAgentServer>,
                             source,
@@ -877,10 +892,7 @@ impl AgentServerStore {
         Ok(())
     }
 
-    pub fn get_extension_id_for_agent(
-        &mut self,
-        name: &ExternalAgentServerName,
-    ) -> Option<Arc<str>> {
+    pub fn get_extension_id_for_agent(&mut self, name: &AgentId) -> Option<Arc<str>> {
         self.external_agents.get_mut(name).and_then(|entry| {
             entry
                 .server
@@ -894,7 +906,7 @@ impl AgentServerStore {
 struct RemoteExternalAgentServer {
     project_id: u64,
     upstream_client: Entity<RemoteClient>,
-    name: ExternalAgentServerName,
+    name: AgentId,
     new_version_available_tx: Option<watch::Sender<Option<String>>>,
 }
 
@@ -1434,9 +1446,9 @@ impl ExternalAgentServer for LocalCustomAgent {
     }
 }
 
-pub const GEMINI_NAME: &str = "gemini";
-pub const CLAUDE_AGENT_NAME: &str = "claude-acp";
-pub const CODEX_NAME: &str = "codex-acp";
+pub const GEMINI_ID: &str = "gemini";
+pub const CLAUDE_AGENT_ID: &str = "claude-acp";
+pub const CODEX_ID: &str = "codex-acp";
 
 #[derive(Default, Clone, JsonSchema, Debug, PartialEq, RegisterSetting)]
 pub struct AllAgentServersSettings(pub HashMap<String, CustomAgentServerSettings>);

crates/project/src/project.rs 🔗

@@ -43,9 +43,7 @@ use crate::{
     worktree_store::WorktreeIdCounter,
 };
 pub use agent_registry_store::{AgentRegistryStore, RegistryAgent};
-pub use agent_server_store::{
-    AgentServerStore, AgentServersUpdated, ExternalAgentServerName, ExternalAgentSource,
-};
+pub use agent_server_store::{AgentId, AgentServerStore, AgentServersUpdated, ExternalAgentSource};
 pub use git_store::{
     ConflictRegion, ConflictSet, ConflictSetSnapshot, ConflictSetUpdate,
     git_traversal::{ChildEntriesGitIter, GitEntry, GitEntryRef, GitTraversal},

crates/project/tests/integration/ext_agent_tests.rs 🔗

@@ -27,7 +27,7 @@ impl ExternalAgentServer for NoopExternalAgent {
 
 #[test]
 fn external_agent_server_name_display() {
-    let name = ExternalAgentServerName(SharedString::from("Ext: Tool"));
+    let name = AgentId(SharedString::from("Ext: Tool"));
     let mut s = String::new();
     write!(&mut s, "{name}").unwrap();
     assert_eq!(s, "Ext: Tool");
@@ -39,7 +39,7 @@ fn sync_extension_agents_removes_previous_extension_entries() {
 
     // Seed with a couple of agents that will be replaced by extensions
     store.external_agents.insert(
-        ExternalAgentServerName(SharedString::from("foo-agent")),
+        AgentId(SharedString::from("foo-agent")),
         ExternalAgentEntry::new(
             Box::new(NoopExternalAgent) as Box<dyn ExternalAgentServer>,
             ExternalAgentSource::Custom,
@@ -48,7 +48,7 @@ fn sync_extension_agents_removes_previous_extension_entries() {
         ),
     );
     store.external_agents.insert(
-        ExternalAgentServerName(SharedString::from("bar-agent")),
+        AgentId(SharedString::from("bar-agent")),
         ExternalAgentEntry::new(
             Box::new(NoopExternalAgent) as Box<dyn ExternalAgentServer>,
             ExternalAgentSource::Custom,
@@ -57,7 +57,7 @@ fn sync_extension_agents_removes_previous_extension_entries() {
         ),
     );
     store.external_agents.insert(
-        ExternalAgentServerName(SharedString::from("custom")),
+        AgentId(SharedString::from("custom")),
         ExternalAgentEntry::new(
             Box::new(NoopExternalAgent) as Box<dyn ExternalAgentServer>,
             ExternalAgentSource::Custom,

crates/project/tests/integration/extension_agent_tests.rs 🔗

@@ -9,14 +9,14 @@ use std::{any::Any, path::PathBuf, sync::Arc};
 #[test]
 fn extension_agent_constructs_proper_display_names() {
     // Verify the display name format for extension-provided agents
-    let name1 = ExternalAgentServerName(SharedString::from("Extension: Agent"));
+    let name1 = AgentId(SharedString::from("Extension: Agent"));
     assert!(name1.0.contains(": "));
 
-    let name2 = ExternalAgentServerName(SharedString::from("MyExt: MyAgent"));
+    let name2 = AgentId(SharedString::from("MyExt: MyAgent"));
     assert_eq!(name2.0, "MyExt: MyAgent");
 
     // Non-extension agents shouldn't have the separator
-    let custom = ExternalAgentServerName(SharedString::from("custom"));
+    let custom = AgentId(SharedString::from("custom"));
     assert!(!custom.0.contains(": "));
 }
 
@@ -47,7 +47,7 @@ fn sync_removes_only_extension_provided_agents() {
 
     // Seed with extension agents (contain ": ") and custom agents (don't contain ": ")
     store.external_agents.insert(
-        ExternalAgentServerName(SharedString::from("Ext1: Agent1")),
+        AgentId(SharedString::from("Ext1: Agent1")),
         ExternalAgentEntry::new(
             Box::new(NoopExternalAgent) as Box<dyn ExternalAgentServer>,
             ExternalAgentSource::Extension,
@@ -56,7 +56,7 @@ fn sync_removes_only_extension_provided_agents() {
         ),
     );
     store.external_agents.insert(
-        ExternalAgentServerName(SharedString::from("Ext2: Agent2")),
+        AgentId(SharedString::from("Ext2: Agent2")),
         ExternalAgentEntry::new(
             Box::new(NoopExternalAgent) as Box<dyn ExternalAgentServer>,
             ExternalAgentSource::Extension,
@@ -65,7 +65,7 @@ fn sync_removes_only_extension_provided_agents() {
         ),
     );
     store.external_agents.insert(
-        ExternalAgentServerName(SharedString::from("custom-agent")),
+        AgentId(SharedString::from("custom-agent")),
         ExternalAgentEntry::new(
             Box::new(NoopExternalAgent) as Box<dyn ExternalAgentServer>,
             ExternalAgentSource::Custom,
@@ -84,7 +84,7 @@ fn sync_removes_only_extension_provided_agents() {
     assert!(
         store
             .external_agents
-            .contains_key(&ExternalAgentServerName(SharedString::from("custom-agent")))
+            .contains_key(&AgentId(SharedString::from("custom-agent")))
     );
 }
 
@@ -117,7 +117,7 @@ fn archive_launcher_constructs_with_all_fields() {
     };
 
     // Verify display name construction
-    let expected_name = ExternalAgentServerName(SharedString::from("GitHub Agent"));
+    let expected_name = AgentId(SharedString::from("GitHub Agent"));
     assert_eq!(expected_name.0, "GitHub Agent");
 }
 
@@ -170,7 +170,7 @@ async fn archive_agent_uses_extension_and_agent_id_for_cache_key(cx: &mut TestAp
 fn sync_extension_agents_registers_archive_launcher() {
     use extension::AgentServerManifestEntry;
 
-    let expected_name = ExternalAgentServerName(SharedString::from("Release Agent"));
+    let expected_name = AgentId(SharedString::from("Release Agent"));
     assert_eq!(expected_name.0, "Release Agent");
 
     // Verify the manifest entry structure for archive-based installation

crates/recent_projects/src/remote_connections.rs 🔗

@@ -10,7 +10,6 @@ use extension_host::ExtensionStore;
 use futures::{FutureExt as _, channel::oneshot, select};
 use gpui::{AppContext, AsyncApp, PromptLevel, WindowHandle};
 
-use language::Point;
 use project::trusted_worktrees;
 use remote::{
     DockerConnectionOptions, Interactive, RemoteConnection, RemoteConnectionOptions,
@@ -458,7 +457,12 @@ pub fn navigate_to_positions(
                     active_editor.update(cx, |editor, cx| {
                         let row = row.saturating_sub(1);
                         let col = path.column.unwrap_or(0).saturating_sub(1);
-                        editor.go_to_singleton_buffer_point(Point::new(row, col), window, cx);
+                        let Some(buffer) = editor.buffer().read(cx).as_singleton() else {
+                            return;
+                        };
+                        let buffer_snapshot = buffer.read(cx).snapshot();
+                        let point = buffer_snapshot.point_from_external_input(row, col);
+                        editor.go_to_singleton_buffer_point(point, window, cx);
                     });
                 })
                 .ok();

crates/recent_projects/src/remote_servers.rs 🔗

@@ -17,7 +17,6 @@ use gpui::{
     EventEmitter, FocusHandle, Focusable, PromptLevel, ScrollHandle, Subscription, Task,
     WeakEntity, Window, canvas,
 };
-use language::Point;
 use log::{debug, info};
 use open_path_prompt::OpenPathDelegate;
 use paths::{global_ssh_config_file, user_ssh_config_file};
@@ -519,11 +518,15 @@ impl ProjectPicker {
                                         active_editor.update(cx, |editor, cx| {
                                             let row = row.saturating_sub(1);
                                             let col = path.column.unwrap_or(0).saturating_sub(1);
-                                            editor.go_to_singleton_buffer_point(
-                                                Point::new(row, col),
-                                                window,
-                                                cx,
-                                            );
+                                            let Some(buffer) =
+                                                editor.buffer().read(cx).as_singleton()
+                                            else {
+                                                return;
+                                            };
+                                            let buffer_snapshot = buffer.read(cx).snapshot();
+                                            let point =
+                                                buffer_snapshot.point_from_external_input(row, col);
+                                            editor.go_to_singleton_buffer_point(point, window, cx);
                                         });
                                     })
                                     .ok();

crates/settings_content/src/settings_content.rs 🔗

@@ -635,6 +635,11 @@ pub struct GitPanelSettingsContent {
     ///
     /// Default: true
     pub diff_stats: Option<bool>,
+
+    /// Whether to show a badge on the git panel icon with the count of uncommitted changes.
+    ///
+    /// Default: false
+    pub show_count_badge: Option<bool>,
 }
 
 #[derive(
@@ -682,6 +687,10 @@ pub struct NotificationPanelSettingsContent {
     /// Default: 300
     #[serde(serialize_with = "crate::serialize_optional_f32_with_two_decimal_places")]
     pub default_width: Option<f32>,
+    /// Whether to show a badge on the notification panel icon with the count of unread notifications.
+    ///
+    /// Default: false
+    pub show_count_badge: Option<bool>,
 }
 
 #[with_fallible_options]

crates/settings_content/src/terminal.rs 🔗

@@ -171,6 +171,10 @@ pub struct TerminalSettingsContent {
     /// Default: 45
     #[serde(serialize_with = "crate::serialize_optional_f32_with_two_decimal_places")]
     pub minimum_contrast: Option<f32>,
+    /// Whether to show a badge on the terminal panel icon with the count of open terminals.
+    ///
+    /// Default: false
+    pub show_count_badge: Option<bool>,
 }
 
 /// Shell configuration to open the terminal with.

crates/settings_ui/src/page_data.rs 🔗

@@ -4820,7 +4820,7 @@ fn panels_page() -> SettingsPage {
         ]
     }
 
-    fn terminal_panel_section() -> [SettingsPageItem; 2] {
+    fn terminal_panel_section() -> [SettingsPageItem; 3] {
         [
             SettingsPageItem::SectionHeader("Terminal Panel"),
             SettingsPageItem::SettingItem(SettingItem {
@@ -4836,6 +4836,28 @@ fn panels_page() -> SettingsPage {
                 metadata: None,
                 files: USER,
             }),
+            SettingsPageItem::SettingItem(SettingItem {
+                title: "Show Count Badge",
+                description: "Show a badge on the terminal panel icon with the count of open terminals.",
+                field: Box::new(SettingField {
+                    json_path: Some("terminal.show_count_badge"),
+                    pick: |settings_content| {
+                        settings_content
+                            .terminal
+                            .as_ref()?
+                            .show_count_badge
+                            .as_ref()
+                    },
+                    write: |settings_content, value| {
+                        settings_content
+                            .terminal
+                            .get_or_insert_default()
+                            .show_count_badge = value;
+                    },
+                }),
+                metadata: None,
+                files: USER,
+            }),
         ]
     }
 
@@ -5048,7 +5070,7 @@ fn panels_page() -> SettingsPage {
         ]
     }
 
-    fn git_panel_section() -> [SettingsPageItem; 13] {
+    fn git_panel_section() -> [SettingsPageItem; 14] {
         [
             SettingsPageItem::SectionHeader("Git Panel"),
             SettingsPageItem::SettingItem(SettingItem {
@@ -5244,6 +5266,28 @@ fn panels_page() -> SettingsPage {
                 metadata: None,
                 files: USER,
             }),
+            SettingsPageItem::SettingItem(SettingItem {
+                title: "Show Count Badge",
+                description: "Whether to show a badge on the git panel icon with the count of uncommitted changes.",
+                field: Box::new(SettingField {
+                    json_path: Some("git_panel.show_count_badge"),
+                    pick: |settings_content| {
+                        settings_content
+                            .git_panel
+                            .as_ref()?
+                            .show_count_badge
+                            .as_ref()
+                    },
+                    write: |settings_content, value| {
+                        settings_content
+                            .git_panel
+                            .get_or_insert_default()
+                            .show_count_badge = value;
+                    },
+                }),
+                metadata: None,
+                files: USER,
+            }),
             SettingsPageItem::SettingItem(SettingItem {
                 title: "Scroll Bar",
                 description: "How and when the scrollbar should be displayed.",
@@ -5294,7 +5338,7 @@ fn panels_page() -> SettingsPage {
         ]
     }
 
-    fn notification_panel_section() -> [SettingsPageItem; 4] {
+    fn notification_panel_section() -> [SettingsPageItem; 5] {
         [
             SettingsPageItem::SectionHeader("Notification Panel"),
             SettingsPageItem::SettingItem(SettingItem {
@@ -5359,6 +5403,28 @@ fn panels_page() -> SettingsPage {
                 metadata: None,
                 files: USER,
             }),
+            SettingsPageItem::SettingItem(SettingItem {
+                title: "Show Count Badge",
+                description: "Show a badge on the notification panel icon with the count of unread notifications.",
+                field: Box::new(SettingField {
+                    json_path: Some("notification_panel.show_count_badge"),
+                    pick: |settings_content| {
+                        settings_content
+                            .notification_panel
+                            .as_ref()?
+                            .show_count_badge
+                            .as_ref()
+                    },
+                    write: |settings_content, value| {
+                        settings_content
+                            .notification_panel
+                            .get_or_insert_default()
+                            .show_count_badge = value;
+                    },
+                }),
+                metadata: None,
+                files: USER,
+            }),
         ]
     }
 

crates/settings_ui/src/pages/audio_test_window.rs 🔗

@@ -88,7 +88,7 @@ fn start_test_playback(
                     }
                 };
 
-                let Ok(output) = audio::open_output_stream(output_device_id) else {
+                let Ok(output) = audio::open_test_output(output_device_id) else {
                     log::error!("Could not open output device for audio test");
                     return;
                 };

crates/terminal/src/terminal_settings.rs 🔗

@@ -50,6 +50,7 @@ pub struct TerminalSettings {
     pub minimum_contrast: f32,
     pub path_hyperlink_regexes: Vec<String>,
     pub path_hyperlink_timeout_ms: u64,
+    pub show_count_badge: bool,
 }
 
 #[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
@@ -129,6 +130,7 @@ impl settings::Settings for TerminalSettings {
                 })
                 .collect(),
             path_hyperlink_timeout_ms: project_content.path_hyperlink_timeout_ms.unwrap(),
+            show_count_badge: user_content.show_count_badge.unwrap(),
         }
     }
 }

crates/terminal_view/src/terminal_panel.rs 🔗

@@ -1605,6 +1605,23 @@ impl Panel for TerminalPanel {
         })
     }
 
+    fn icon_label(&self, _window: &Window, cx: &App) -> Option<String> {
+        if !TerminalSettings::get_global(cx).show_count_badge {
+            return None;
+        }
+        let count = self
+            .center
+            .panes()
+            .into_iter()
+            .map(|pane| pane.read(cx).items_len())
+            .sum::<usize>();
+        if count == 0 {
+            None
+        } else {
+            Some(count.to_string())
+        }
+    }
+
     fn persistent_name() -> &'static str {
         "TerminalPanel"
     }

crates/text/src/tests.rs 🔗

@@ -30,6 +30,24 @@ fn test_edit() {
     assert_eq!(buffer.text(), "ghiamnoef");
 }
 
+#[test]
+fn test_point_for_row_and_column_from_external_source() {
+    let buffer = Buffer::new(
+        ReplicaId::LOCAL,
+        BufferId::new(1).unwrap(),
+        "aéøbcdef\nsecond",
+    );
+    let snapshot = buffer.snapshot();
+
+    assert_eq!(snapshot.point_from_external_input(0, 0), Point::new(0, 0));
+    assert_eq!(snapshot.point_from_external_input(0, 4), Point::new(0, 6));
+    assert_eq!(
+        snapshot.point_from_external_input(0, 100),
+        Point::new(0, 10)
+    );
+    assert_eq!(snapshot.point_from_external_input(1, 3), Point::new(1, 3));
+}
+
 #[gpui::test(iterations = 100)]
 fn test_random_edits(mut rng: StdRng) {
     let operations = env::var("OPERATIONS")

crates/text/src/text.rs 🔗

@@ -2254,6 +2254,37 @@ impl BufferSnapshot {
         (row_end_offset - row_start_offset) as u32
     }
 
+    /// A function to convert character offsets from e.g. user's `go.mod:22:33` input into byte-offset Point columns.
+    pub fn point_from_external_input(&self, row: u32, characters: u32) -> Point {
+        const MAX_BYTES_IN_UTF_8: u32 = 4;
+
+        let row = row.min(self.max_point().row);
+        let start = Point::new(row, 0);
+        let end = self.clip_point(
+            Point::new(
+                row,
+                characters
+                    .saturating_mul(MAX_BYTES_IN_UTF_8)
+                    .saturating_add(1),
+            ),
+            Bias::Right,
+        );
+        let range = start..end;
+        let mut point = range.start;
+        let mut remaining_columns = characters;
+
+        for chunk in self.text_for_range(range) {
+            for character in chunk.chars() {
+                if remaining_columns == 0 {
+                    return point;
+                }
+                remaining_columns -= 1;
+                point.column += character.len_utf8() as u32;
+            }
+        }
+        point
+    }
+
     pub fn line_indents_in_row_range(
         &self,
         row_range: Range<u32>,

crates/ui/src/components.rs 🔗

@@ -6,6 +6,7 @@ mod callout;
 mod chip;
 mod collab;
 mod context_menu;
+mod count_badge;
 mod data_table;
 mod diff_stat;
 mod disclosure;
@@ -49,6 +50,7 @@ pub use callout::*;
 pub use chip::*;
 pub use collab::*;
 pub use context_menu::*;
+pub use count_badge::*;
 pub use data_table::*;
 pub use diff_stat::*;
 pub use disclosure::*;

crates/ui/src/components/count_badge.rs 🔗

@@ -0,0 +1,93 @@
+use gpui::FontWeight;
+
+use crate::prelude::*;
+
+/// A small, pill-shaped badge that displays a numeric count.
+///
+/// The count is capped at 99 and displayed as "99+" beyond that.
+#[derive(IntoElement, RegisterComponent)]
+pub struct CountBadge {
+    count: usize,
+}
+
+impl CountBadge {
+    pub fn new(count: usize) -> Self {
+        Self { count }
+    }
+}
+
+impl RenderOnce for CountBadge {
+    fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
+        let label = if self.count > 99 {
+            "99+".to_string()
+        } else {
+            self.count.to_string()
+        };
+
+        let bg = cx
+            .theme()
+            .colors()
+            .editor_background
+            .blend(cx.theme().status().error.opacity(0.4));
+
+        h_flex()
+            .absolute()
+            .top_0()
+            .right_0()
+            .p_px()
+            .h_3p5()
+            .min_w_3p5()
+            .rounded_full()
+            .justify_center()
+            .text_center()
+            .border_1()
+            .border_color(cx.theme().colors().border)
+            .bg(bg)
+            .shadow_sm()
+            .child(
+                Label::new(label)
+                    .size(LabelSize::Custom(rems_from_px(9.)))
+                    .weight(FontWeight::MEDIUM),
+            )
+    }
+}
+
+impl Component for CountBadge {
+    fn scope() -> ComponentScope {
+        ComponentScope::Status
+    }
+
+    fn description() -> Option<&'static str> {
+        Some("A small, pill-shaped badge that displays a numeric count.")
+    }
+
+    fn preview(_window: &mut Window, cx: &mut App) -> Option<AnyElement> {
+        let container = || {
+            div()
+                .relative()
+                .size_8()
+                .border_1()
+                .border_color(cx.theme().colors().border)
+                .bg(cx.theme().colors().background)
+        };
+
+        Some(
+            v_flex()
+                .gap_6()
+                .child(example_group_with_title(
+                    "Count Badge",
+                    vec![
+                        single_example(
+                            "Basic Count",
+                            container().child(CountBadge::new(3)).into_any_element(),
+                        ),
+                        single_example(
+                            "Capped Count",
+                            container().child(CountBadge::new(150)).into_any_element(),
+                        ),
+                    ],
+                ))
+                .into_any_element(),
+        )
+    }
+}

crates/util/src/path_list.rs 🔗

@@ -5,7 +5,7 @@ use std::{
 
 use crate::paths::SanitizedPath;
 use itertools::Itertools;
-use serde::{Deserialize, Deserializer, Serialize, Serializer};
+use serde::{Deserialize, Serialize};
 
 /// A list of absolute paths, in a specific order.
 ///
@@ -23,7 +23,7 @@ pub struct PathList {
     order: Arc<[usize]>,
 }
 
-#[derive(Debug)]
+#[derive(Debug, Serialize, Deserialize)]
 pub struct SerializedPathList {
     pub paths: String,
     pub order: String,
@@ -119,19 +119,6 @@ impl PathList {
     }
 }
 
-impl Serialize for PathList {
-    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
-        self.paths.serialize(serializer)
-    }
-}
-
-impl<'de> Deserialize<'de> for PathList {
-    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
-        let paths: Vec<PathBuf> = Vec::deserialize(deserializer)?;
-        Ok(PathList::new(&paths))
-    }
-}
-
 #[cfg(test)]
 mod tests {
     use super::*;

crates/workspace/src/dock.rs 🔗

@@ -12,8 +12,10 @@ use gpui::{
 };
 use settings::SettingsStore;
 use std::sync::Arc;
-use ui::{ContextMenu, Divider, DividerColor, IconButton, Tooltip, h_flex};
-use ui::{prelude::*, right_click_menu};
+use ui::{
+    ContextMenu, CountBadge, Divider, DividerColor, IconButton, Tooltip, prelude::*,
+    right_click_menu,
+};
 
 pub(crate) const RESIZE_HANDLE_SIZE: Pixels = px(6.);
 
@@ -41,6 +43,9 @@ pub trait Panel: Focusable + EventEmitter<PanelEvent> + Render + Sized {
     fn size(&self, window: &Window, cx: &App) -> Pixels;
     fn set_size(&mut self, size: Option<Pixels>, window: &mut Window, cx: &mut Context<Self>);
     fn icon_button(&self, window: &Window, cx: &App) -> PanelIconButton;
+    fn icon_label(&self, _: &Window, _: &App) -> Option<String> {
+        None
+    }
     fn secondary_button(&self, _window: &Window, _cx: &App) -> Option<(PanelIconButton, bool)> {
         None
     }
@@ -79,6 +84,7 @@ pub trait PanelHandle: Send + Sync {
     fn size(&self, window: &Window, cx: &App) -> Pixels;
     fn set_size(&self, size: Option<Pixels>, window: &mut Window, cx: &mut App);
     fn icon_button(&self, window: &Window, cx: &App) -> PanelIconButton;
+    fn icon_label(&self, _: &Window, _: &App) -> Option<String>;
     fn secondary_button(&self, window: &Window, cx: &App) -> Option<(PanelIconButton, bool)>;
     fn panel_focus_handle(&self, cx: &App) -> FocusHandle;
     fn to_any(&self) -> AnyView;
@@ -161,6 +167,10 @@ where
         self.read(cx).icon_button(window, cx)
     }
 
+    fn icon_label(&self, window: &Window, cx: &App) -> Option<String> {
+        self.read(cx).icon_label(window, cx)
+    }
+
     fn secondary_button(&self, window: &Window, cx: &App) -> Option<(PanelIconButton, bool)> {
         self.read(cx).secondary_button(window, cx)
     }
@@ -932,6 +942,7 @@ impl Render for PanelButtons {
                 };
 
                 let focus_handle = dock.focus_handle(cx);
+                let icon_label = entry.panel.icon_label(window, cx);
 
                 Some(
                     right_click_menu(name)
@@ -981,6 +992,14 @@ impl Render for PanelButtons {
                                     })
                                 });
 
+                            let button = div().relative().child(button).when_some(
+                                icon_label
+                                    .clone()
+                                    .filter(|_| !is_active_button)
+                                    .and_then(|label| label.parse::<usize>().ok()),
+                                |this, count| this.child(CountBadge::new(count)),
+                            );
+
                             match secondary_button {
                                 Some((secondary_button, secondary_button_is_active)) => {
                                     let action = secondary_button.action.boxed_clone();

crates/workspace/src/notifications.rs 🔗

@@ -234,6 +234,14 @@ impl Workspace {
         self.suppressed_notifications.insert(id.clone());
     }
 
+    pub fn is_notification_suppressed(&self, notification_id: NotificationId) -> bool {
+        self.suppressed_notifications.contains(&notification_id)
+    }
+
+    pub fn unsuppress(&mut self, notification_id: NotificationId) {
+        self.suppressed_notifications.remove(&notification_id);
+    }
+
     pub fn show_initial_notifications(&mut self, cx: &mut Context<Self>) {
         // Allow absence of the global so that tests don't need to initialize it.
         let app_notifications = GLOBAL_APP_NOTIFICATIONS

crates/workspace/src/workspace.rs 🔗

@@ -7616,6 +7616,12 @@ impl GlobalAnyActiveCall {
         cx.global()
     }
 }
+
+pub fn merge_conflict_notification_id() -> NotificationId {
+    struct MergeConflictNotification;
+    NotificationId::unique::<MergeConflictNotification>()
+}
+
 /// Workspace-local view of a remote participant's location.
 #[derive(Clone, Copy, Debug, PartialEq, Eq)]
 pub enum ParticipantLocation {

crates/zed/src/visual_test_runner.rs 🔗

@@ -103,10 +103,11 @@ use {
     feature_flags::FeatureFlagAppExt as _,
     git_ui::project_diff::ProjectDiff,
     gpui::{
-        Action as _, App, AppContext as _, Bounds, KeyBinding, Modifiers, SharedString,
-        VisualTestAppContext, WindowBounds, WindowHandle, WindowOptions, point, px, size,
+        Action as _, App, AppContext as _, Bounds, KeyBinding, Modifiers, VisualTestAppContext,
+        WindowBounds, WindowHandle, WindowOptions, point, px, size,
     },
     image::RgbaImage,
+    project::AgentId,
     project_panel::ProjectPanel,
     settings::{NotifyWhenAgentWaiting, Settings as _},
     settings_ui::SettingsWindow,
@@ -1958,7 +1959,7 @@ impl AgentServer for StubAgentServer {
         ui::IconName::ZedAssistant
     }
 
-    fn name(&self) -> SharedString {
+    fn agent_id(&self) -> AgentId {
         "Visual Test Agent".into()
     }
 

docs/acp-threads-in-sidebar-plan.md 🔗

@@ -0,0 +1,580 @@
+# Plan: Show ACP Threads in the Sidebar (Revised)
+
+## Problem
+
+The sidebar currently only shows **Zed-native agent threads** (from `ThreadStore`/`ThreadsDatabase`). ACP threads (Claude Code, Codex, Gemini, etc.) are invisible in the sidebar once they're no longer live.
+
+## Root Cause
+
+`ThreadStore` and `ThreadsDatabase` only persist metadata for native threads. When `rebuild_contents` populates the sidebar, it reads from `ThreadStore` for historical threads and overlays live info from the `AgentPanel` — but non-native threads never get written to `ThreadStore`, so once they stop being live, they disappear.
+
+## Solution Overview (Revised)
+
+**Key change from the original plan:** We completely remove the sidebar's dependency on `ThreadStore`. Instead, the `Sidebar` itself owns a **single, unified persistence layer** — a new `SidebarDb` domain stored in the workspace DB — that tracks metadata for _all_ thread types (native and ACP). The sidebar becomes the single source of truth for what threads appear in the list.
+
+### Why Remove the ThreadStore Dependency?
+
+1. **Single responsibility** — The sidebar is the only consumer of "which threads to show in the list." Having it depend on `ThreadStore` (which exists primarily for native agent save/load) creates an indirect coupling that makes ACP integration awkward.
+2. **No merge logic** — The original plan required merging native `ThreadStore` data with a separate `AcpThreadMetadataDb` in `ThreadStore::reload`. By moving all sidebar metadata into one place, there's nothing to merge.
+3. **Simpler data flow** — Writers (native agent, ACP connections) push metadata to the sidebar DB. The sidebar reads from one table. No cross-crate coordination needed.
+4. **ThreadStore stays focused** — `ThreadStore` continues to manage native thread blob storage (save/load message data) without being polluted with sidebar display concerns.
+
+### Architecture
+
+```
+  ┌─────────────────────┐      ┌─────────────────────────┐
+  │    NativeAgent      │      │   ACP Connections       │
+  │  (on save_thread)   │      │ (on create/update/list) │
+  └──────────┬──────────┘      └──────────┬──────────────┘
+             │                            │
+             │   save_sidebar_thread()    │
+             └──────────┬─────────────────┘
+                        ▼
+              ┌───────────────────┐
+              │   SidebarDb       │
+              │  (workspace DB)   │
+              │  sidebar_threads  │
+              └────────┬──────────┘
+                       │
+                       ▼
+              ┌───────────────────┐
+              │     Sidebar       │
+              │ rebuild_contents  │
+              └───────────────────┘
+```
+
+---
+
+## Step 1: Create `SidebarDb` Domain in `sidebar.rs`
+
+**File:** `crates/agent_ui/src/sidebar.rs`
+
+Add a `SidebarDb` domain using `db::static_connection!`, co-located in the sidebar module (or a small `persistence` submodule within `sidebar.rs` if it helps organization, but keeping it in the same file is fine for now).
+
+### Schema
+
+```rust
+use db::{
+    sqlez::{
+        bindable::Column, domain::Domain, statement::Statement,
+        thread_safe_connection::ThreadSafeConnection,
+    },
+    sqlez_macros::sql,
+};
+
+/// Lightweight metadata for any thread (native or ACP), enough to populate
+/// the sidebar list and route to the correct load path when clicked.
+#[derive(Debug, Clone)]
+pub struct SidebarThreadRow {
+    pub session_id: acp::SessionId,
+    /// `None` for native Zed threads, `Some("claude-code")` etc. for ACP agents.
+    pub agent_name: Option<String>,
+    pub title: SharedString,
+    pub updated_at: DateTime<Utc>,
+    pub created_at: Option<DateTime<Utc>>,
+    pub folder_paths: PathList,
+}
+
+pub struct SidebarDb(ThreadSafeConnection);
+
+impl Domain for SidebarDb {
+    const NAME: &str = stringify!(SidebarDb);
+
+    const MIGRATIONS: &[&str] = &[sql!(
+        CREATE TABLE IF NOT EXISTS sidebar_threads(
+            session_id TEXT PRIMARY KEY,
+            agent_name TEXT,
+            title TEXT NOT NULL,
+            updated_at TEXT NOT NULL,
+            created_at TEXT,
+            folder_paths TEXT,
+            folder_paths_order TEXT
+        ) STRICT;
+    )];
+}
+
+db::static_connection!(SIDEBAR_DB, SidebarDb, []);
+```
+
+### CRUD Methods
+
+```rust
+impl SidebarDb {
+    /// Upsert metadata for a thread (native or ACP).
+    pub async fn save(&self, row: &SidebarThreadRow) -> Result<()> {
+        let id = row.session_id.0.clone();
+        let agent_name = row.agent_name.clone();
+        let title = row.title.to_string();
+        let updated_at = row.updated_at.to_rfc3339();
+        let created_at = row.created_at.map(|dt| dt.to_rfc3339());
+        let serialized = row.folder_paths.serialize();
+        let (fp, fpo) = if row.folder_paths.is_empty() {
+            (None, None)
+        } else {
+            (Some(serialized.paths), Some(serialized.order))
+        };
+
+        self.write(move |conn| {
+            let mut stmt = Statement::prepare(
+                conn,
+                "INSERT INTO sidebar_threads(session_id, agent_name, title, updated_at, created_at, folder_paths, folder_paths_order)
+                 VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)
+                 ON CONFLICT(session_id) DO UPDATE SET
+                     agent_name = excluded.agent_name,
+                     title = excluded.title,
+                     updated_at = excluded.updated_at,
+                     folder_paths = excluded.folder_paths,
+                     folder_paths_order = excluded.folder_paths_order",
+            )?;
+            let mut i = stmt.bind(&id, 1)?;
+            i = stmt.bind(&agent_name, i)?;
+            i = stmt.bind(&title, i)?;
+            i = stmt.bind(&updated_at, i)?;
+            i = stmt.bind(&created_at, i)?;
+            i = stmt.bind(&fp, i)?;
+            stmt.bind(&fpo, i)?;
+            stmt.exec()
+        })
+        .await
+    }
+
+    /// List all sidebar thread metadata, ordered by updated_at descending.
+    pub fn list(&self) -> Result<Vec<SidebarThreadRow>> {
+        self.select::<SidebarThreadRow>(
+            "SELECT session_id, agent_name, title, updated_at, created_at, folder_paths, folder_paths_order
+             FROM sidebar_threads
+             ORDER BY updated_at DESC"
+        )?(())
+    }
+
+    /// List threads for a specific folder path set.
+    pub fn list_for_paths(&self, paths: &PathList) -> Result<Vec<SidebarThreadRow>> {
+        let serialized = paths.serialize();
+        self.select_bound::<String, SidebarThreadRow>(sql!(
+            SELECT session_id, agent_name, title, updated_at, created_at, folder_paths, folder_paths_order
+            FROM sidebar_threads
+            WHERE folder_paths = ?
+            ORDER BY updated_at DESC
+        ))?(serialized.paths)
+    }
+
+    /// Look up a single thread by session ID.
+    pub fn get(&self, session_id: &acp::SessionId) -> Result<Option<SidebarThreadRow>> {
+        let id = session_id.0.clone();
+        self.select_row_bound::<Arc<str>, SidebarThreadRow>(sql!(
+            SELECT session_id, agent_name, title, updated_at, created_at, folder_paths, folder_paths_order
+            FROM sidebar_threads
+            WHERE session_id = ?
+        ))?(id)
+    }
+
+    /// Return the total number of rows in the table.
+    pub fn count(&self) -> Result<usize> {
+        let count: (i32, i32) = self.select_row(sql!(
+            SELECT COUNT(*) FROM sidebar_threads
+        ))?(())?.unwrap_or_default();
+        Ok(count.0 as usize)
+    }
+
+    /// Delete metadata for a single thread.
+    pub async fn delete(&self, session_id: acp::SessionId) -> Result<()> {
+        let id = session_id.0;
+        self.write(move |conn| {
+            let mut stmt = Statement::prepare(
+                conn,
+                "DELETE FROM sidebar_threads WHERE session_id = ?",
+            )?;
+            stmt.bind(&id, 1)?;
+            stmt.exec()
+        })
+        .await
+    }
+
+    /// Delete all thread metadata.
+    pub async fn delete_all(&self) -> Result<()> {
+        self.write(move |conn| {
+            let mut stmt = Statement::prepare(
+                conn,
+                "DELETE FROM sidebar_threads",
+            )?;
+            stmt.exec()
+        })
+        .await
+    }
+}
+```
+
+### `Column` Implementation
+
+```rust
+impl Column for SidebarThreadRow {
+    fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> {
+        let (id, next): (Arc<str>, i32) = Column::column(statement, start_index)?;
+        let (agent_name, next): (Option<String>, i32) = Column::column(statement, next)?;
+        let (title, next): (String, i32) = Column::column(statement, next)?;
+        let (updated_at_str, next): (String, i32) = Column::column(statement, next)?;
+        let (created_at_str, next): (Option<String>, i32) = Column::column(statement, next)?;
+        let (folder_paths_str, next): (Option<String>, i32) = Column::column(statement, next)?;
+        let (folder_paths_order_str, next): (Option<String>, i32) = Column::column(statement, next)?;
+
+        let updated_at = DateTime::parse_from_rfc3339(&updated_at_str)?.with_timezone(&Utc);
+        let created_at = created_at_str
+            .as_deref()
+            .map(DateTime::parse_from_rfc3339)
+            .transpose()?
+            .map(|dt| dt.with_timezone(&Utc));
+
+        let folder_paths = folder_paths_str
+            .map(|paths| {
+                PathList::deserialize(&util::path_list::SerializedPathList {
+                    paths,
+                    order: folder_paths_order_str.unwrap_or_default(),
+                })
+            })
+            .unwrap_or_default();
+
+        Ok((
+            SidebarThreadRow {
+                session_id: acp::SessionId::new(id),
+                agent_name,
+                title: title.into(),
+                updated_at,
+                created_at,
+                folder_paths,
+            },
+            next,
+        ))
+    }
+}
+```
+
+**Key points:**
+
+- `SIDEBAR_DB` is a `LazyLock` static — initialized on first use, no manual connection management.
+- The `agent_name` column is `NULL` for native Zed threads and a string like `"claude-code"` for ACP agents. This replaces the `agent_type` field from the original plan.
+- The DB file lives alongside other `static_connection!` databases.
+- `ThreadsDatabase` and `ThreadStore` are **completely unchanged** by this step.
+
+---
+
+## Step 2: Replace `ThreadStore` Reads in `rebuild_contents` with `SidebarDb` Reads
+
+**File:** `crates/agent_ui/src/sidebar.rs`
+
+### Remove `ThreadStore` Dependency
+
+1. **Remove** `ThreadStore::global(cx)` and `ThreadStore::try_global(cx)` from `Sidebar::new` and `rebuild_contents`.
+2. **Remove** the `cx.observe_in(&thread_store, ...)` subscription that triggers `update_entries` when `ThreadStore` changes.
+3. **Replace** `thread_store.read(cx).threads_for_paths(&path_list)` calls with `SIDEBAR_DB.list_for_paths(&path_list)` (or read all rows once at the top of `rebuild_contents` and index them in memory, which is simpler and avoids repeated DB calls).
+
+### New Data Flow in `rebuild_contents`
+
+```rust
+fn rebuild_contents(&mut self, cx: &App) {
+    // ... existing workspace iteration setup ...
+
+    // Read ALL sidebar thread metadata once, index by folder_paths.
+    let all_sidebar_threads = SIDEBAR_DB.list().unwrap_or_default();
+    let mut threads_by_paths: HashMap<PathList, Vec<SidebarThreadRow>> = HashMap::new();
+    for row in all_sidebar_threads {
+        threads_by_paths
+            .entry(row.folder_paths.clone())
+            .or_default()
+            .push(row);
+    }
+
+    for (ws_index, workspace) in workspaces.iter().enumerate() {
+        // ... existing absorbed-workspace logic ...
+
+        let path_list = workspace_path_list(workspace, cx);
+
+        if should_load_threads {
+            let mut seen_session_ids: HashSet<acp::SessionId> = HashSet::new();
+
+            // Read from SidebarDb instead of ThreadStore
+            if let Some(rows) = threads_by_paths.get(&path_list) {
+                for row in rows {
+                    seen_session_ids.insert(row.session_id.clone());
+                    let (agent, icon) = match &row.agent_name {
+                        None => (Agent::NativeAgent, IconName::ZedAgent),
+                        Some(name) => (
+                            Agent::Custom { name: name.clone().into() },
+                            IconName::ZedAgent, // placeholder, resolved in Step 5
+                        ),
+                    };
+                    threads.push(ThreadEntry {
+                        agent,
+                        session_info: AgentSessionInfo {
+                            session_id: row.session_id.clone(),
+                            cwd: None,
+                            title: Some(row.title.clone()),
+                            updated_at: Some(row.updated_at),
+                            created_at: row.created_at,
+                            meta: None,
+                        },
+                        icon,
+                        icon_from_external_svg: None,
+                        status: AgentThreadStatus::default(),
+                        workspace: ThreadEntryWorkspace::Open(workspace.clone()),
+                        is_live: false,
+                        is_background: false,
+                        highlight_positions: Vec::new(),
+                        worktree_name: None,
+                        worktree_highlight_positions: Vec::new(),
+                        diff_stats: DiffStats::default(),
+                    });
+                }
+            }
+
+            // ... existing linked git worktree logic, also reading from threads_by_paths ...
+            // ... existing live thread overlay logic (unchanged) ...
+        }
+    }
+}
+```
+
+### What Changes
+
+- `rebuild_contents` reads from `SIDEBAR_DB` instead of `ThreadStore`.
+- The `ThreadEntry.agent` field now carries `Agent::Custom { name }` for ACP threads, enabling correct routing in `activate_thread`.
+- The live thread overlay logic (from `all_thread_infos_for_workspace`) is **unchanged** — it still reads from `AgentPanel` to get real-time status of running threads.
+
+### What Stays the Same
+
+- The entire workspace/absorbed-workspace/git-worktree structure.
+- The live thread overlay pass.
+- The notification tracking logic.
+- The search/filter logic.
+
+---
+
+## Step 3: Write Native Thread Metadata to `SidebarDb`
+
+**File:** `crates/agent_ui/src/sidebar.rs` and/or `crates/agent_ui/src/agent_panel.rs`
+
+When a native thread is saved (after conversation, on title update, etc.), we also write its metadata to `SidebarDb`. There are two approaches:
+
+### Option A: Subscribe to `ThreadStore` Changes (Recommended)
+
+Keep a one-directional sync: when `ThreadStore` finishes a `save_thread` or `reload`, the sidebar syncs the metadata to `SidebarDb`. This can be done in the sidebar's workspace subscription or by observing `ThreadStore` changes purely for the purpose of syncing (not for reading).
+
+```rust
+// In Sidebar::subscribe_to_workspace or a dedicated sync method:
+fn sync_native_threads_to_sidebar_db(&self, cx: &App) {
+    if let Some(thread_store) = ThreadStore::try_global(cx) {
+        let entries: Vec<_> = thread_store.read(cx).entries().collect();
+        cx.background_spawn(async move {
+            for meta in entries {
+                SIDEBAR_DB.save(&SidebarThreadRow {
+                    session_id: meta.id,
+                    agent_name: None, // native
+                    title: meta.title,
+                    updated_at: meta.updated_at,
+                    created_at: meta.created_at,
+                    folder_paths: meta.folder_paths,
+                }).await.log_err();
+            }
+        }).detach();
+    }
+}
+```
+
+### Option B: Write at the Point of Save
+
+In `AgentPanel` or wherever `thread_store.save_thread()` is called, also call `SIDEBAR_DB.save(...)`. This is more direct but requires touching more call sites.
+
+**Recommendation:** Option A is simpler for the initial implementation. We observe `ThreadStore` changes, diff against `SidebarDb`, and sync. Later, if we want to remove `ThreadStore` entirely from the write path for native threads, we can switch to Option B.
+
+---
+
+## Step 4: Write ACP Thread Metadata to `SidebarDb`
+
+**File:** `crates/agent_ui/src/connection_view.rs` (or `agent_panel.rs`)
+
+When ACP sessions are created, updated, or listed, write metadata directly to `SidebarDb`:
+
+- **On new session creation:** After `connection.new_session()` returns the `AcpThread`, call `SIDEBAR_DB.save(...)`.
+- **On title update:** ACP threads receive title updates via `SessionInfoUpdate`. When these come in, call `SIDEBAR_DB.save(...)` with the new title and updated timestamp.
+- **On session list refresh:** When `AgentSessionList::list_sessions` returns for an ACP agent, bulk-sync the metadata into `SidebarDb`.
+
+After any write, call `cx.notify()` on the `Sidebar` entity (or use a channel/event) to trigger a `rebuild_contents`.
+
+### Triggering Sidebar Refresh
+
+Since the sidebar no longer observes `ThreadStore`, we need a mechanism to trigger `rebuild_contents` after DB writes. Options:
+
+1. **Emit an event from `AgentPanel`** — The sidebar already subscribes to `AgentPanelEvent`. Add a new variant like `AgentPanelEvent::ThreadMetadataChanged` and emit it after saving to `SidebarDb`.
+2. **Use `cx.notify()` directly** — If the save happens within a `Sidebar` method, just call `self.update_entries(cx)`.
+3. **Observe a lightweight signal entity** — A simple `Entity<()>` that gets notified after DB writes.
+
+**Recommendation:** Option 1 (emit from `AgentPanel`) is cleanest since the sidebar already subscribes to panel events.
+
+---
+
+## Step 5: Handle Agent Icon Resolution for ACP Threads
+
+**File:** `crates/agent_ui/src/sidebar.rs`
+
+For ACP threads in the sidebar, we need the correct agent icon. The `agent_name` string stored in `SidebarDb` maps to an agent in the `AgentServerStore`, which has icon info.
+
+In `rebuild_contents`, after building the initial thread list from `SidebarDb`, resolve icons for ACP threads:
+
+```rust
+// For ACP threads, look up the icon from the agent server store
+if let Some(name) = &row.agent_name {
+    if let Some(agent_server_store) = /* get from workspace */ {
+        // resolve icon from agent_server_store using name
+    }
+}
+```
+
+---
+
+## Step 6: Handle Delete Operations Correctly
+
+**File:** `crates/agent_ui/src/sidebar.rs`
+
+When the user deletes a thread from the sidebar:
+
+- **All threads** → Delete from `SidebarDb` via `SIDEBAR_DB.delete(session_id)`.
+- **Native threads** → _Also_ delete from `ThreadStore`/`ThreadsDatabase` (to clean up the blob data).
+- **ACP threads** → Optionally notify the ACP server via `AgentSessionList::delete_session`.
+
+The `agent_name` field on `SidebarThreadRow` (or the `Agent` enum on `ThreadEntry`) tells us which path to take.
+
+When the user clears all history:
+
+```rust
+// Delete all sidebar metadata
+SIDEBAR_DB.delete_all().await?;
+// Also clear native thread blobs
+thread_store.delete_threads(cx);
+// Optionally notify ACP servers
+```
+
+---
+
+## Step 7: Handle `activate_thread` Routing
+
+**File:** `crates/agent_ui/src/sidebar.rs`, `crates/agent_ui/src/agent_panel.rs`
+
+In `activate_thread`, branch on the `Agent` variant:
+
+- `Agent::NativeAgent` → Call `panel.load_agent_thread(Agent::NativeAgent, session_id, ...)` (current behavior).
+- `Agent::Custom { name }` → Call `panel.load_agent_thread(Agent::Custom { name }, session_id, ...)` so it routes to the correct `AgentConnection::load_session`.
+
+This is already partially set up — `activate_thread` takes an `Agent` parameter. The key change is that `ThreadEntry` now carries the correct `Agent` variant based on `SidebarThreadRow.agent_name`.
+
+---
+
+## Step 8: Handle `activate_archived_thread` Without ThreadStore
+
+**File:** `crates/agent_ui/src/sidebar.rs`
+
+Currently, `activate_archived_thread` looks up `saved_path_list` from `ThreadStore`:
+
+```rust
+let saved_path_list = ThreadStore::try_global(cx).and_then(|thread_store| {
+    thread_store
+        .read(cx)
+        .thread_from_session_id(&session_info.session_id)
+        .map(|thread| thread.folder_paths.clone())
+});
+```
+
+Replace this with a targeted `SidebarDb::get` lookup (single-row SELECT, no full table scan):
+
+```rust
+let saved_path_list = SIDEBAR_DB
+    .get(&session_info.session_id)
+    .ok()
+    .flatten()
+    .map(|row| row.folder_paths);
+```
+
+---
+
+## Step 9: Error Handling for Offline Agents
+
+When an ACP thread is clicked but the agent server is not running:
+
+- Show a toast/notification explaining the agent is offline.
+- Keep the metadata in the sidebar (don't remove it).
+- Optionally offer to start the agent server.
+
+---
+
+## Step 10: Migration — Backfill Existing Native Threads
+
+On first launch after this change, the `SidebarDb` will be empty while `ThreadsDatabase` has existing native threads. We need a one-time backfill:
+
+```rust
+// In Sidebar::new or a dedicated init method:
+fn backfill_native_threads_if_needed(cx: &App) {
+    if SIDEBAR_DB.count()  > 0 {
+        return; // Already populated
+    }
+
+    if let Some(thread_store) = ThreadStore::try_global(cx) {
+        let entries: Vec<_> = thread_store.read(cx).entries().collect();
+        cx.background_spawn(async move {
+            for meta in entries {
+                SIDEBAR_DB.save(&SidebarThreadRow {
+                    session_id: meta.id,
+                    agent_name: None,
+                    title: meta.title,
+                    updated_at: meta.updated_at,
+                    created_at: meta.created_at,
+                    folder_paths: meta.folder_paths,
+                }).await.log_err();
+            }
+        }).detach();
+    }
+}
+```
+
+---
+
+## Summary of Files to Change
+
+| File                                     | Changes                                                                                                                                                                                                                                                        |
+| ---------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| `crates/agent_ui/Cargo.toml`             | Add `db.workspace = true`, `sqlez.workspace = true`, `sqlez_macros.workspace = true`, `chrono.workspace = true` dependencies                                                                                                                                   |
+| `crates/agent_ui/src/sidebar.rs`         | **Main changes.** Add `SidebarDb` domain + `SIDEBAR_DB` static + `SidebarThreadRow`. Replace all `ThreadStore` reads in `rebuild_contents` with `SidebarDb` reads. Update `activate_archived_thread`. Add native thread sync logic. Add backfill on first run. |
+| `crates/agent_ui/src/agent_panel.rs`     | Emit `AgentPanelEvent::ThreadMetadataChanged` after thread saves. Potentially write ACP metadata to `SidebarDb` here.                                                                                                                                          |
+| `crates/agent_ui/src/connection_view.rs` | Write ACP metadata to `SidebarDb` on session creation, title updates, and session list refreshes.                                                                                                                                                              |
+
+## What Is NOT Changed
+
+| File / Area                                | Why                                                                                                                          |
+| ------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------- |
+| `threads` table schema                     | No migration needed — native blob persistence is completely untouched                                                        |
+| `ThreadsDatabase` methods                  | `save_thread_sync`, `load_thread`, `list_threads`, `delete_thread`, `delete_threads` — all unchanged                         |
+| `ThreadStore` struct/methods               | Stays exactly as-is. It's still used for native thread blob save/load. The sidebar just no longer reads from it for display. |
+| `NativeAgent::load_thread` / `open_thread` | These deserialize `DbThread` blobs — completely unaffected                                                                   |
+| `crates/acp_thread/`                       | No new persistence module needed there (unlike the original plan)                                                            |
+| `crates/agent/src/db.rs`                   | `DbThreadMetadata` is unchanged — no `agent_type` field added                                                                |
+
+## Execution Order
+
+1. **SidebarDb domain** (Step 1) — Create `SidebarDb`, `SidebarThreadRow`, `SIDEBAR_DB` static, CRUD methods in `sidebar.rs`.
+2. **Replace reads** (Step 2) — Swap `ThreadStore` reads in `rebuild_contents` for `SidebarDb` reads.
+3. **Native write path** (Step 3) — Sync native thread metadata from `ThreadStore` into `SidebarDb`.
+4. **ACP write path** (Step 4) — Write ACP thread metadata to `SidebarDb` from connection views.
+5. **Icon resolution** (Step 5) — Resolve ACP agent icons in the sidebar.
+6. **Delete path** (Step 6) — Route deletes to `SidebarDb` + native blob cleanup + ACP server notification.
+7. **Activate routing** (Step 7) — Ensure `activate_thread` routes correctly based on `Agent` variant.
+8. **Archive fix** (Step 8) — Update `activate_archived_thread` to use `SidebarDb`.
+9. **Migration** (Step 10) — Backfill existing native threads on first run.
+10. **Polish** (Step 9) — Error handling for offline agents.
+
+## Key Differences from Original Plan
+
+| Aspect                               | Original Plan                                                                              | Revised Plan                                                                    |
+| ------------------------------------ | ------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------- |
+| **Where ACP metadata lives**         | New `AcpThreadMetadataDb` in `crates/acp_thread/`                                          | `SidebarDb` in `crates/agent_ui/src/sidebar.rs`                                 |
+| **Where sidebar reads from**         | `ThreadStore` (which merges native + ACP)                                                  | `SidebarDb` directly (single source)                                            |
+| **ThreadStore changes**              | Added `agent_type` to `DbThreadMetadata`, merge logic in `reload`, new save/delete methods | **None** — ThreadStore is untouched                                             |
+| **`crates/agent/src/db.rs` changes** | Added `agent_type: Option<String>` to `DbThreadMetadata`                                   | **None**                                                                        |
+| **Merge complexity**                 | Two data sources merged in `ThreadStore::reload`                                           | No merge — one table, one read                                                  |
+| **Crate dependencies**               | `acp_thread` gains `db` dependency                                                         | `agent_ui` gains `db` dependency (more natural — it's a UI persistence concern) |

extensions/glsl/Cargo.toml 🔗

@@ -1,6 +1,6 @@
 [package]
 name = "zed_glsl"
-version = "0.2.1"
+version = "0.2.2"
 edition.workspace = true
 publish.workspace = true
 license = "Apache-2.0"

extensions/glsl/extension.toml 🔗

@@ -1,7 +1,7 @@
 id = "glsl"
 name = "GLSL"
 description = "GLSL support."
-version = "0.2.1"
+version = "0.2.2"
 schema_version = 1
 authors = ["Mikayla Maki <mikayla@zed.dev>"]
 repository = "https://github.com/zed-industries/zed"

output.txt 🔗

@@ -0,0 +1,1195 @@
+    Blocking waiting for file lock on artifact directory
+   Compiling agent_ui v0.1.0 (/Users/max/code/zed/crates/agent_ui)
+warning: unused variable: `window`
+   --> crates/agent_ui/src/slash_command_picker.rs:262:21
+    |
+262 |     fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
+    |                     ^^^^^^ help: if this is intentional, prefix it with an underscore: `_window`
+    |
+    = note: `#[warn(unused_variables)]` (part of `#[warn(unused)]`) on by default
+
+warning: unused variable: `delegate`
+   --> crates/agent_ui/src/slash_command_picker.rs:322:13
+    |
+322 |         let delegate = SlashCommandDelegate {
+    |             ^^^^^^^^ help: if this is intentional, prefix it with an underscore: `_delegate`
+
+warning: `agent_ui` (lib test) generated 2 warnings (run `cargo fix --lib -p agent_ui --tests` to apply 2 suggestions)
+      Timing report saved to /Users/max/code/zed/target/cargo-timings/cargo-timing-20260316T161229.056504Z.html
+    Finished `test` profile [unoptimized + debuginfo] target(s) in 13.67s
+     Running unittests src/agent_ui.rs (target/debug/deps/agent_ui-312efca0eb4585dc)
+
+running 1 test
+defer called:    0: std::backtrace_rs::backtrace::libunwind::trace
+             at /rustc/01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf/library/std/src/../../backtrace/src/backtrace/libunwind.rs:117:9
+   1: std::backtrace_rs::backtrace::trace_unsynchronized
+             at /rustc/01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf/library/std/src/../../backtrace/src/backtrace/mod.rs:66:14
+   2: std::backtrace::Backtrace::create
+             at /rustc/01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf/library/std/src/backtrace.rs:331:13
+   3: <gpui::app::App>::defer::<<gpui::app::App>::observe_global<settings::settings_store::SettingsStore, theme::init::{closure#0}>::{closure#1}>
+             at /Users/max/code/zed/crates/gpui/src/app.rs:1630:13
+   4: <gpui::app::App>::observe_global::<settings::settings_store::SettingsStore, theme::init::{closure#0}>
+             at /Users/max/code/zed/crates/gpui/src/app.rs:1731:14
+   5: theme::init
+             at /Users/max/code/zed/crates/theme/src/theme.rs:141:8
+   6: agent_ui::connection_view::tests::init_test::{closure#0}
+             at ./src/connection_view.rs:4429:13
+   7: <gpui::app::App>::update::<(), agent_ui::connection_view::tests::init_test::{closure#0}>
+             at /Users/max/code/zed/crates/gpui/src/app.rs:862:22
+   8: <gpui::app::test_context::TestAppContext>::update::<(), agent_ui::connection_view::tests::init_test::{closure#0}>
+             at /Users/max/code/zed/crates/gpui/src/app/test_context.rs:205:12
+   9: agent_ui::connection_view::tests::init_test
+             at ./src/connection_view.rs:4426:12
+  10: agent_ui::agent_panel::tests::test_new_text_thread_action_handler::__test_new_text_thread_action_handler::{closure#0}
+             at ./src/agent_panel.rs:5110:9
+  11: <gpui::executor::ForegroundExecutor>::block_test::<(), agent_ui::agent_panel::tests::test_new_text_thread_action_handler::__test_new_text_thread_action_handler::{closure#0}>::{closure#0}
+             at /Users/max/code/zed/crates/gpui/src/executor.rs:440:36
+  12: <scheduler::test_scheduler::TestScheduler as scheduler::Scheduler>::block
+             at /Users/max/code/zed/crates/scheduler/src/test_scheduler.rs:538:35
+  13: <gpui::executor::ForegroundExecutor>::block_test::<(), agent_ui::agent_panel::tests::test_new_text_thread_action_handler::__test_new_text_thread_action_handler::{closure#0}>
+             at /Users/max/code/zed/crates/gpui/src/executor.rs:447:19
+  14: agent_ui::agent_panel::tests::test_new_text_thread_action_handler::{closure#0}
+             at ./src/agent_panel.rs:5108:5
+  15: gpui::test::run_test::{closure#0}
+             at /Users/max/code/zed/crates/gpui/src/test.rs:88:17
+  16: std::panicking::catch_unwind::do_call::<gpui::test::run_test::{closure#0}, ()>
+             at /Users/max/.rustup/toolchains/1.93-aarch64-apple-darwin/lib/rustlib/src/rust/library/std/src/panicking.rs:581:40
+  17: ___rust_try
+  18: std::panicking::catch_unwind::<(), gpui::test::run_test::{closure#0}>
+             at /Users/max/.rustup/toolchains/1.93-aarch64-apple-darwin/lib/rustlib/src/rust/library/std/src/panicking.rs:544:19
+  19: std::panic::catch_unwind::<gpui::test::run_test::{closure#0}, ()>
+             at /Users/max/.rustup/toolchains/1.93-aarch64-apple-darwin/lib/rustlib/src/rust/library/std/src/panic.rs:359:14
+  20: gpui::test::run_test
+             at /Users/max/code/zed/crates/gpui/src/test.rs:85:26
+  21: agent_ui::agent_panel::tests::test_new_text_thread_action_handler
+             at ./src/agent_panel.rs:5108:5
+  22: agent_ui::agent_panel::tests::test_new_text_thread_action_handler::{closure#0}
+             at ./src/agent_panel.rs:5108:18
+  23: <agent_ui::agent_panel::tests::test_new_text_thread_action_handler::{closure#0} as core::ops::function::FnOnce<()>>::call_once
+             at /Users/max/.rustup/toolchains/1.93-aarch64-apple-darwin/lib/rustlib/src/rust/library/core/src/ops/function.rs:250:5
+  24: core::ops::function::FnOnce::call_once
+             at /rustc/01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf/library/core/src/ops/function.rs:250:5
+  25: test::__rust_begin_short_backtrace
+             at /rustc/01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf/library/test/src/lib.rs:663:18
+  26: test::run_test_in_process::{{closure}}
+             at /rustc/01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf/library/test/src/lib.rs:686:74
+  27: <core::panic::unwind_safe::AssertUnwindSafe<F> as core::ops::function::FnOnce<()>>::call_once
+             at /rustc/01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf/library/core/src/panic/unwind_safe.rs:274:9
+  28: std::panicking::catch_unwind::do_call
+             at /rustc/01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf/library/std/src/panicking.rs:581:40
+  29: std::panicking::catch_unwind
+             at /rustc/01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf/library/std/src/panicking.rs:544:19
+  30: std::panic::catch_unwind
+             at /rustc/01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf/library/std/src/panic.rs:359:14
+  31: test::run_test_in_process
+             at /rustc/01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf/library/test/src/lib.rs:686:27
+  32: test::run_test::{{closure}}
+             at /rustc/01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf/library/test/src/lib.rs:607:43
+  33: test::run_test::{{closure}}
+             at /rustc/01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf/library/test/src/lib.rs:637:41
+  34: std::sys::backtrace::__rust_begin_short_backtrace
+             at /rustc/01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf/library/std/src/sys/backtrace.rs:160:18
+  35: std::thread::lifecycle::spawn_unchecked::{{closure}}::{{closure}}
+             at /rustc/01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf/library/std/src/thread/lifecycle.rs:92:13
+  36: <core::panic::unwind_safe::AssertUnwindSafe<F> as core::ops::function::FnOnce<()>>::call_once
+             at /rustc/01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf/library/core/src/panic/unwind_safe.rs:274:9
+  37: std::panicking::catch_unwind::do_call
+             at /rustc/01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf/library/std/src/panicking.rs:581:40
+  38: std::panicking::catch_unwind
+             at /rustc/01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf/library/std/src/panicking.rs:544:19
+  39: std::panic::catch_unwind
+             at /rustc/01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf/library/std/src/panic.rs:359:14
+  40: std::thread::lifecycle::spawn_unchecked::{{closure}}
+             at /rustc/01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf/library/std/src/thread/lifecycle.rs:90:26
+  41: core::ops::function::FnOnce::call_once{{vtable.shim}}
+             at /rustc/01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf/library/core/src/ops/function.rs:250:5
+  42: <alloc::boxed::Box<F,A> as core::ops::function::FnOnce<Args>>::call_once
+             at /rustc/01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf/library/alloc/src/boxed.rs:2206:9
+  43: std::sys::thread::unix::Thread::new::thread_start
+             at /rustc/01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf/library/std/src/sys/thread/unix.rs:118:17
+  44: __pthread_cond_wait
+
+defer called:    0: std::backtrace_rs::backtrace::libunwind::trace
+             at /rustc/01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf/library/std/src/../../backtrace/src/backtrace/libunwind.rs:117:9
+   1: std::backtrace_rs::backtrace::trace_unsynchronized
+             at /rustc/01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf/library/std/src/../../backtrace/src/backtrace/mod.rs:66:14
+   2: std::backtrace::Backtrace::create
+             at /rustc/01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf/library/std/src/backtrace.rs:331:13
+   3: <gpui::app::App>::defer::<<gpui::app::App>::observe_global<settings::settings_store::SettingsStore, <client::telemetry::Telemetry>::new::{closure#1}>::{closure#1}>
+             at /Users/max/code/zed/crates/gpui/src/app.rs:1630:13
+   4: <gpui::app::App>::observe_global::<settings::settings_store::SettingsStore, <client::telemetry::Telemetry>::new::{closure#1}>
+             at /Users/max/code/zed/crates/gpui/src/app.rs:1731:14
+   5: <client::telemetry::Telemetry>::new
+             at /Users/max/code/zed/crates/client/src/telemetry.rs:229:12
+   6: <client::Client>::new
+             at /Users/max/code/zed/crates/client/src/client.rs:535:24
+   7: <project::Project>::test_project::<[&std::path::Path; 0]>::{closure#0}::{closure#0}
+             at /Users/max/code/zed/crates/project/src/project.rs:2021:37
+   8: <gpui::app::App>::update::<alloc::sync::Arc<client::Client>, <project::Project>::test_project<[&std::path::Path; 0]>::{closure#0}::{closure#0}>
+             at /Users/max/code/zed/crates/gpui/src/app.rs:862:22
+   9: <gpui::app::test_context::TestAppContext>::update::<alloc::sync::Arc<client::Client>, <project::Project>::test_project<[&std::path::Path; 0]>::{closure#0}::{closure#0}>
+             at /Users/max/code/zed/crates/gpui/src/app/test_context.rs:205:12
+  10: <project::Project>::test_project::<[&std::path::Path; 0]>::{closure#0}
+             at /Users/max/code/zed/crates/project/src/project.rs:2021:25
+  11: <project::Project>::test::<[&std::path::Path; 0]>::{closure#0}
+             at /Users/max/code/zed/crates/project/src/project.rs:1997:55
+  12: agent_ui::agent_panel::tests::test_new_text_thread_action_handler::__test_new_text_thread_action_handler::{closure#0}
+             at ./src/agent_panel.rs:5125:57
+  13: <gpui::executor::ForegroundExecutor>::block_test::<(), agent_ui::agent_panel::tests::test_new_text_thread_action_handler::__test_new_text_thread_action_handler::{closure#0}>::{closure#0}
+             at /Users/max/code/zed/crates/gpui/src/executor.rs:440:36
+  14: <scheduler::test_scheduler::TestScheduler as scheduler::Scheduler>::block
+             at /Users/max/code/zed/crates/scheduler/src/test_scheduler.rs:538:35
+  15: <gpui::executor::ForegroundExecutor>::block_test::<(), agent_ui::agent_panel::tests::test_new_text_thread_action_handler::__test_new_text_thread_action_handler::{closure#0}>
+             at /Users/max/code/zed/crates/gpui/src/executor.rs:447:19
+  16: agent_ui::agent_panel::tests::test_new_text_thread_action_handler::{closure#0}
+             at ./src/agent_panel.rs:5108:5
+  17: gpui::test::run_test::{closure#0}
+             at /Users/max/code/zed/crates/gpui/src/test.rs:88:17
+  18: std::panicking::catch_unwind::do_call::<gpui::test::run_test::{closure#0}, ()>
+             at /Users/max/.rustup/toolchains/1.93-aarch64-apple-darwin/lib/rustlib/src/rust/library/std/src/panicking.rs:581:40
+  19: ___rust_try
+  20: std::panicking::catch_unwind::<(), gpui::test::run_test::{closure#0}>
+             at /Users/max/.rustup/toolchains/1.93-aarch64-apple-darwin/lib/rustlib/src/rust/library/std/src/panicking.rs:544:19
+  21: std::panic::catch_unwind::<gpui::test::run_test::{closure#0}, ()>
+             at /Users/max/.rustup/toolchains/1.93-aarch64-apple-darwin/lib/rustlib/src/rust/library/std/src/panic.rs:359:14
+  22: gpui::test::run_test
+             at /Users/max/code/zed/crates/gpui/src/test.rs:85:26
+  23: agent_ui::agent_panel::tests::test_new_text_thread_action_handler
+             at ./src/agent_panel.rs:5108:5
+  24: agent_ui::agent_panel::tests::test_new_text_thread_action_handler::{closure#0}
+             at ./src/agent_panel.rs:5108:18
+  25: <agent_ui::agent_panel::tests::test_new_text_thread_action_handler::{closure#0} as core::ops::function::FnOnce<()>>::call_once
+             at /Users/max/.rustup/toolchains/1.93-aarch64-apple-darwin/lib/rustlib/src/rust/library/core/src/ops/function.rs:250:5
+  26: core::ops::function::FnOnce::call_once
+             at /rustc/01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf/library/core/src/ops/function.rs:250:5
+  27: test::__rust_begin_short_backtrace
+             at /rustc/01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf/library/test/src/lib.rs:663:18
+  28: test::run_test_in_process::{{closure}}
+             at /rustc/01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf/library/test/src/lib.rs:686:74
+  29: <core::panic::unwind_safe::AssertUnwindSafe<F> as core::ops::function::FnOnce<()>>::call_once
+             at /rustc/01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf/library/core/src/panic/unwind_safe.rs:274:9
+  30: std::panicking::catch_unwind::do_call
+             at /rustc/01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf/library/std/src/panicking.rs:581:40
+  31: std::panicking::catch_unwind
+             at /rustc/01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf/library/std/src/panicking.rs:544:19
+  32: std::panic::catch_unwind
+             at /rustc/01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf/library/std/src/panic.rs:359:14
+  33: test::run_test_in_process
+             at /rustc/01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf/library/test/src/lib.rs:686:27
+  34: test::run_test::{{closure}}
+             at /rustc/01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf/library/test/src/lib.rs:607:43
+  35: test::run_test::{{closure}}
+             at /rustc/01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf/library/test/src/lib.rs:637:41
+  36: std::sys::backtrace::__rust_begin_short_backtrace
+             at /rustc/01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf/library/std/src/sys/backtrace.rs:160:18
+  37: std::thread::lifecycle::spawn_unchecked::{{closure}}::{{closure}}
+             at /rustc/01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf/library/std/src/thread/lifecycle.rs:92:13
+  38: <core::panic::unwind_safe::AssertUnwindSafe<F> as core::ops::function::FnOnce<()>>::call_once
+             at /rustc/01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf/library/core/src/panic/unwind_safe.rs:274:9
+  39: std::panicking::catch_unwind::do_call
+             at /rustc/01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf/library/std/src/panicking.rs:581:40
+  40: std::panicking::catch_unwind
+             at /rustc/01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf/library/std/src/panicking.rs:544:19
+  41: std::panic::catch_unwind
+             at /rustc/01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf/library/std/src/panic.rs:359:14
+  42: std::thread::lifecycle::spawn_unchecked::{{closure}}
+             at /rustc/01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf/library/std/src/thread/lifecycle.rs:90:26
+  43: core::ops::function::FnOnce::call_once{{vtable.shim}}
+             at /rustc/01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf/library/core/src/ops/function.rs:250:5
+  44: <alloc::boxed::Box<F,A> as core::ops::function::FnOnce<Args>>::call_once
+             at /rustc/01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf/library/alloc/src/boxed.rs:2206:9
+  45: std::sys::thread::unix::Thread::new::thread_start
+             at /rustc/01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf/library/std/src/sys/thread/unix.rs:118:17
+  46: __pthread_cond_wait
+
+defer called:    0: std::backtrace_rs::backtrace::libunwind::trace
+             at /rustc/01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf/library/std/src/../../backtrace/src/backtrace/libunwind.rs:117:9
+   1: std::backtrace_rs::backtrace::trace_unsynchronized
+             at /rustc/01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf/library/std/src/../../backtrace/src/backtrace/mod.rs:66:14
+   2: std::backtrace::Backtrace::create
+             at /rustc/01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf/library/std/src/backtrace.rs:331:13
+   3: <gpui::app::App>::defer::<<gpui::app::App>::new_subscription::{closure#0}>
+             at /Users/max/code/zed/crates/gpui/src/app.rs:1630:13
+   4: <gpui::app::App>::new_subscription
+             at /Users/max/code/zed/crates/gpui/src/app.rs:989:14
+   5: <gpui::app::App>::subscribe_internal::<project::worktree_store::WorktreeStore, project::worktree_store::WorktreeStoreEvent, <gpui::app::context::Context<project::Project>>::subscribe<project::worktree_store::WorktreeStore, project::worktree_store::WorktreeStoreEvent, <project::Project>::on_worktree_store_event>::{closure#0}>
+             at /Users/max/code/zed/crates/gpui/src/app.rs:1003:14
+   6: <gpui::app::context::Context<project::Project>>::subscribe::<project::worktree_store::WorktreeStore, project::worktree_store::WorktreeStoreEvent, <project::Project>::on_worktree_store_event>
+             at /Users/max/code/zed/crates/gpui/src/app/context.rs:109:18
+   7: <project::Project>::local::{closure#0}
+             at /Users/max/code/zed/crates/project/src/project.rs:1165:16
+   8: <gpui::app::App as gpui::AppContext>::new::<project::Project, <project::Project>::local::{closure#0}>::{closure#0}
+             at /Users/max/code/zed/crates/gpui/src/app.rs:2332:26
+   9: <gpui::app::App>::update::<gpui::app::entity_map::Entity<project::Project>, <gpui::app::App as gpui::AppContext>::new<project::Project, <project::Project>::local::{closure#0}>::{closure#0}>
+             at /Users/max/code/zed/crates/gpui/src/app.rs:862:22
+  10: <gpui::app::App as gpui::AppContext>::new::<project::Project, <project::Project>::local::{closure#0}>
+             at /Users/max/code/zed/crates/gpui/src/app.rs:2329:14
+  11: <project::Project>::local
+             at /Users/max/code/zed/crates/project/src/project.rs:1149:12
+  12: <project::Project>::test_project::<[&std::path::Path; 0]>::{closure#0}::{closure#2}
+             at /Users/max/code/zed/crates/project/src/project.rs:2024:13
+  13: <gpui::app::App>::update::<gpui::app::entity_map::Entity<project::Project>, <project::Project>::test_project<[&std::path::Path; 0]>::{closure#0}::{closure#2}>
+             at /Users/max/code/zed/crates/gpui/src/app.rs:862:22
+  14: <gpui::app::test_context::TestAppContext>::update::<gpui::app::entity_map::Entity<project::Project>, <project::Project>::test_project<[&std::path::Path; 0]>::{closure#0}::{closure#2}>
+             at /Users/max/code/zed/crates/gpui/src/app/test_context.rs:205:12
+  15: <project::Project>::test_project::<[&std::path::Path; 0]>::{closure#0}
+             at /Users/max/code/zed/crates/project/src/project.rs:2023:26
+  16: <project::Project>::test::<[&std::path::Path; 0]>::{closure#0}
+             at /Users/max/code/zed/crates/project/src/project.rs:1997:55
+  17: agent_ui::agent_panel::tests::test_new_text_thread_action_handler::__test_new_text_thread_action_handler::{closure#0}
+             at ./src/agent_panel.rs:5125:57
+  18: <gpui::executor::ForegroundExecutor>::block_test::<(), agent_ui::agent_panel::tests::test_new_text_thread_action_handler::__test_new_text_thread_action_handler::{closure#0}>::{closure#0}
+             at /Users/max/code/zed/crates/gpui/src/executor.rs:440:36
+  19: <scheduler::test_scheduler::TestScheduler as scheduler::Scheduler>::block
+             at /Users/max/code/zed/crates/scheduler/src/test_scheduler.rs:538:35
+  20: <gpui::executor::ForegroundExecutor>::block_test::<(), agent_ui::agent_panel::tests::test_new_text_thread_action_handler::__test_new_text_thread_action_handler::{closure#0}>
+             at /Users/max/code/zed/crates/gpui/src/executor.rs:447:19
+  21: agent_ui::agent_panel::tests::test_new_text_thread_action_handler::{closure#0}
+             at ./src/agent_panel.rs:5108:5
+  22: gpui::test::run_test::{closure#0}
+             at /Users/max/code/zed/crates/gpui/src/test.rs:88:17
+  23: std::panicking::catch_unwind::do_call::<gpui::test::run_test::{closure#0}, ()>
+             at /Users/max/.rustup/toolchains/1.93-aarch64-apple-darwin/lib/rustlib/src/rust/library/std/src/panicking.rs:581:40
+  24: ___rust_try
+  25: std::panicking::catch_unwind::<(), gpui::test::run_test::{closure#0}>
+             at /Users/max/.rustup/toolchains/1.93-aarch64-apple-darwin/lib/rustlib/src/rust/library/std/src/panicking.rs:544:19
+  26: std::panic::catch_unwind::<gpui::test::run_test::{closure#0}, ()>
+             at /Users/max/.rustup/toolchains/1.93-aarch64-apple-darwin/lib/rustlib/src/rust/library/std/src/panic.rs:359:14
+  27: gpui::test::run_test
+             at /Users/max/code/zed/crates/gpui/src/test.rs:85:26
+  28: agent_ui::agent_panel::tests::test_new_text_thread_action_handler
+             at ./src/agent_panel.rs:5108:5
+  29: agent_ui::agent_panel::tests::test_new_text_thread_action_handler::{closure#0}
+             at ./src/agent_panel.rs:5108:18
+  30: <agent_ui::agent_panel::tests::test_new_text_thread_action_handler::{closure#0} as core::ops::function::FnOnce<()>>::call_once
+             at /Users/max/.rustup/toolchains/1.93-aarch64-apple-darwin/lib/rustlib/src/rust/library/core/src/ops/function.rs:250:5
+  31: core::ops::function::FnOnce::call_once
+             at /rustc/01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf/library/core/src/ops/function.rs:250:5
+  32: test::__rust_begin_short_backtrace
+             at /rustc/01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf/library/test/src/lib.rs:663:18
+  33: test::run_test_in_process::{{closure}}
+             at /rustc/01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf/library/test/src/lib.rs:686:74
+  34: <core::panic::unwind_safe::AssertUnwindSafe<F> as core::ops::function::FnOnce<()>>::call_once
+             at /rustc/01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf/library/core/src/panic/unwind_safe.rs:274:9
+  35: std::panicking::catch_unwind::do_call
+             at /rustc/01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf/library/std/src/panicking.rs:581:40
+  36: std::panicking::catch_unwind
+             at /rustc/01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf/library/std/src/panicking.rs:544:19
+  37: std::panic::catch_unwind
+             at /rustc/01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf/library/std/src/panic.rs:359:14
+  38: test::run_test_in_process
+             at /rustc/01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf/library/test/src/lib.rs:686:27
+  39: test::run_test::{{closure}}
+             at /rustc/01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf/library/test/src/lib.rs:607:43
+  40: test::run_test::{{closure}}
+             at /rustc/01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf/library/test/src/lib.rs:637:41
+  41: std::sys::backtrace::__rust_begin_short_backtrace
+             at /rustc/01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf/library/std/src/sys/backtrace.rs:160:18
+  42: std::thread::lifecycle::spawn_unchecked::{{closure}}::{{closure}}
+             at /rustc/01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf/library/std/src/thread/lifecycle.rs:92:13
+  43: <core::panic::unwind_safe::AssertUnwindSafe<F> as core::ops::function::FnOnce<()>>::call_once
+             at /rustc/01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf/library/core/src/panic/unwind_safe.rs:274:9
+  44: std::panicking::catch_unwind::do_call
+             at /rustc/01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf/library/std/src/panicking.rs:581:40
+  45: std::panicking::catch_unwind
+             at /rustc/01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf/library/std/src/panicking.rs:544:19
+  46: std::panic::catch_unwind
+             at /rustc/01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf/library/std/src/panic.rs:359:14
+  47: std::thread::lifecycle::spawn_unchecked::{{closure}}
+             at /rustc/01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf/library/std/src/thread/lifecycle.rs:90:26
+  48: core::ops::function::FnOnce::call_once{{vtable.shim}}
+             at /rustc/01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf/library/core/src/ops/function.rs:250:5
+  49: <alloc::boxed::Box<F,A> as core::ops::function::FnOnce<Args>>::call_once
+             at /rustc/01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf/library/alloc/src/boxed.rs:2206:9
+  50: std::sys::thread::unix::Thread::new::thread_start
+             at /rustc/01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf/library/std/src/sys/thread/unix.rs:118:17
+  51: __pthread_cond_wait
+
+defer called:    0: std::backtrace_rs::backtrace::libunwind::trace
+             at /rustc/01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf/library/std/src/../../backtrace/src/backtrace/libunwind.rs:117:9
+   1: std::backtrace_rs::backtrace::trace_unsynchronized
+             at /rustc/01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf/library/std/src/../../backtrace/src/backtrace/mod.rs:66:14
+   2: std::backtrace::Backtrace::create
+             at /rustc/01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf/library/std/src/backtrace.rs:331:13
+   3: <gpui::app::App>::defer::<<gpui::app::context::Context<project::context_server_store::ContextServerStore>>::observe_global<settings::settings_store::SettingsStore, <project::context_server_store::ContextServerStore>::new_internal::{closure#1}>::{closure#1}>
+             at /Users/max/code/zed/crates/gpui/src/app.rs:1630:13
+   4: <gpui::app::context::Context<project::context_server_store::ContextServerStore>>::observe_global::<settings::settings_store::SettingsStore, <project::context_server_store::ContextServerStore>::new_internal::{closure#1}>
+             at /Users/max/code/zed/crates/gpui/src/app/context.rs:188:14
+   5: <project::context_server_store::ContextServerStore>::new_internal
+             at /Users/max/code/zed/crates/project/src/context_server_store.rs:380:41
+   6: <project::context_server_store::ContextServerStore>::local
+             at /Users/max/code/zed/crates/project/src/context_server_store.rs:243:9
+   7: <project::Project>::local::{closure#0}::{closure#2}
+             at /Users/max/code/zed/crates/project/src/project.rs:1170:17
+   8: <gpui::app::App as gpui::AppContext>::new::<project::context_server_store::ContextServerStore, <project::Project>::local::{closure#0}::{closure#2}>::{closure#0}
+             at /Users/max/code/zed/crates/gpui/src/app.rs:2332:26
+   9: <gpui::app::App>::update::<gpui::app::entity_map::Entity<project::context_server_store::ContextServerStore>, <gpui::app::App as gpui::AppContext>::new<project::context_server_store::ContextServerStore, <project::Project>::local::{closure#0}::{closure#2}>::{closure#0}>
+             at /Users/max/code/zed/crates/gpui/src/app.rs:862:22
+  10: <gpui::app::App as gpui::AppContext>::new::<project::context_server_store::ContextServerStore, <project::Project>::local::{closure#0}::{closure#2}>
+             at /Users/max/code/zed/crates/gpui/src/app.rs:2329:14
+  11: <gpui::app::context::Context<project::Project> as gpui::AppContext>::new::<project::context_server_store::ContextServerStore, <project::Project>::local::{closure#0}::{closure#2}>
+             at /Users/max/code/zed/crates/gpui/src/app/context.rs:770:18
+  12: <project::Project>::local::{closure#0}
+             at /Users/max/code/zed/crates/project/src/project.rs:1169:43
+  13: <gpui::app::App as gpui::AppContext>::new::<project::Project, <project::Project>::local::{closure#0}>::{closure#0}
+             at /Users/max/code/zed/crates/gpui/src/app.rs:2332:26
+  14: <gpui::app::App>::update::<gpui::app::entity_map::Entity<project::Project>, <gpui::app::App as gpui::AppContext>::new<project::Project, <project::Project>::local::{closure#0}>::{closure#0}>
+             at /Users/max/code/zed/crates/gpui/src/app.rs:862:22
+  15: <gpui::app::App as gpui::AppContext>::new::<project::Project, <project::Project>::local::{closure#0}>
+             at /Users/max/code/zed/crates/gpui/src/app.rs:2329:14
+  16: <project::Project>::local
+             at /Users/max/code/zed/crates/project/src/project.rs:1149:12
+  17: <project::Project>::test_project::<[&std::path::Path; 0]>::{closure#0}::{closure#2}
+             at /Users/max/code/zed/crates/project/src/project.rs:2024:13
+  18: <gpui::app::App>::update::<gpui::app::entity_map::Entity<project::Project>, <project::Project>::test_project<[&std::path::Path; 0]>::{closure#0}::{closure#2}>
+             at /Users/max/code/zed/crates/gpui/src/app.rs:862:22
+  19: <gpui::app::test_context::TestAppContext>::update::<gpui::app::entity_map::Entity<project::Project>, <project::Project>::test_project<[&std::path::Path; 0]>::{closure#0}::{closure#2}>
+             at /Users/max/code/zed/crates/gpui/src/app/test_context.rs:205:12
+  20: <project::Project>::test_project::<[&std::path::Path; 0]>::{closure#0}
+             at /Users/max/code/zed/crates/project/src/project.rs:2023:26
+  21: <project::Project>::test::<[&std::path::Path; 0]>::{closure#0}
+             at /Users/max/code/zed/crates/project/src/project.rs:1997:55
+  22: agent_ui::agent_panel::tests::test_new_text_thread_action_handler::__test_new_text_thread_action_handler::{closure#0}
+             at ./src/agent_panel.rs:5125:57
+  23: <gpui::executor::ForegroundExecutor>::block_test::<(), agent_ui::agent_panel::tests::test_new_text_thread_action_handler::__test_new_text_thread_action_handler::{closure#0}>::{closure#0}
+             at /Users/max/code/zed/crates/gpui/src/executor.rs:440:36
+  24: <scheduler::test_scheduler::TestScheduler as scheduler::Scheduler>::block
+             at /Users/max/code/zed/crates/scheduler/src/test_scheduler.rs:538:35
+  25: <gpui::executor::ForegroundExecutor>::block_test::<(), agent_ui::agent_panel::tests::test_new_text_thread_action_handler::__test_new_text_thread_action_handler::{closure#0}>
+             at /Users/max/code/zed/crates/gpui/src/executor.rs:447:19
+  26: agent_ui::agent_panel::tests::test_new_text_thread_action_handler::{closure#0}
+             at ./src/agent_panel.rs:5108:5
+  27: gpui::test::run_test::{closure#0}
+             at /Users/max/code/zed/crates/gpui/src/test.rs:88:17
+  28: std::panicking::catch_unwind::do_call::<gpui::test::run_test::{closure#0}, ()>
+             at /Users/max/.rustup/toolchains/1.93-aarch64-apple-darwin/lib/rustlib/src/rust/library/std/src/panicking.rs:581:40
+  29: ___rust_try
+  30: std::panicking::catch_unwind::<(), gpui::test::run_test::{closure#0}>
+             at /Users/max/.rustup/toolchains/1.93-aarch64-apple-darwin/lib/rustlib/src/rust/library/std/src/panicking.rs:544:19
+  31: std::panic::catch_unwind::<gpui::test::run_test::{closure#0}, ()>
+             at /Users/max/.rustup/toolchains/1.93-aarch64-apple-darwin/lib/rustlib/src/rust/library/std/src/panic.rs:359:14
+  32: gpui::test::run_test
+             at /Users/max/code/zed/crates/gpui/src/test.rs:85:26
+  33: agent_ui::agent_panel::tests::test_new_text_thread_action_handler
+             at ./src/agent_panel.rs:5108:5
+  34: agent_ui::agent_panel::tests::test_new_text_thread_action_handler::{closure#0}
+             at ./src/agent_panel.rs:5108:18
+  35: <agent_ui::agent_panel::tests::test_new_text_thread_action_handler::{closure#0} as core::ops::function::FnOnce<()>>::call_once
+             at /Users/max/.rustup/toolchains/1.93-aarch64-apple-darwin/lib/rustlib/src/rust/library/core/src/ops/function.rs:250:5
+  36: core::ops::function::FnOnce::call_once
+             at /rustc/01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf/library/core/src/ops/function.rs:250:5
+  37: test::__rust_begin_short_backtrace
+             at /rustc/01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf/library/test/src/lib.rs:663:18
+  38: test::run_test_in_process::{{closure}}
+             at /rustc/01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf/library/test/src/lib.rs:686:74
+  39: <core::panic::unwind_safe::AssertUnwindSafe<F> as core::ops::function::FnOnce<()>>::call_once
+             at /rustc/01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf/library/core/src/panic/unwind_safe.rs:274:9
+  40: std::panicking::catch_unwind::do_call
+             at /rustc/01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf/library/std/src/panicking.rs:581:40
+  41: std::panicking::catch_unwind
+             at /rustc/01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf/library/std/src/panicking.rs:544:19
+  42: std::panic::catch_unwind
+             at /rustc/01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf/library/std/src/panic.rs:359:14
+  43: test::run_test_in_process
+             at /rustc/01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf/library/test/src/lib.rs:686:27
+  44: test::run_test::{{closure}}
+             at /rustc/01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf/library/test/src/lib.rs:607:43
+  45: test::run_test::{{closure}}
+             at /rustc/01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf/library/test/src/lib.rs:637:41
+  46: std::sys::backtrace::__rust_begin_short_backtrace
+             at /rustc/01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf/library/std/src/sys/backtrace.rs:160:18
+  47: std::thread::lifecycle::spawn_unchecked::{{closure}}::{{closure}}
+             at /rustc/01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf/library/std/src/thread/lifecycle.rs:92:13
+  48: <core::panic::unwind_safe::AssertUnwindSafe<F> as core::ops::function::FnOnce<()>>::call_once
+             at /rustc/01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf/library/core/src/panic/unwind_safe.rs:274:9
+  49: std::panicking::catch_unwind::do_call
+             at /rustc/01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf/library/std/src/panicking.rs:581:40
+  50: std::panicking::catch_unwind
+             at /rustc/01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf/library/std/src/panicking.rs:544:19
+  51: std::panic::catch_unwind
+             at /rustc/01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf/library/std/src/panic.rs:359:14
+  52: std::thread::lifecycle::spawn_unchecked::{{closure}}
+             at /rustc/01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf/library/std/src/thread/lifecycle.rs:90:26
+  53: core::ops::function::FnOnce::call_once{{vtable.shim}}
+             at /rustc/01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf/library/core/src/ops/function.rs:250:5
+  54: <alloc::boxed::Box<F,A> as core::ops::function::FnOnce<Args>>::call_once
+             at /rustc/01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf/library/alloc/src/boxed.rs:2206:9
+  55: std::sys::thread::unix::Thread::new::thread_start
+             at /rustc/01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf/library/std/src/sys/thread/unix.rs:118:17
+  56: __pthread_cond_wait
+
+defer called:    0: std::backtrace_rs::backtrace::libunwind::trace
+             at /rustc/01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf/library/std/src/../../backtrace/src/backtrace/libunwind.rs:117:9
+   1: std::backtrace_rs::backtrace::trace_unsynchronized
+             at /rustc/01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf/library/std/src/../../backtrace/src/backtrace/mod.rs:66:14
+   2: std::backtrace::Backtrace::create
+             at /rustc/01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf/library/std/src/backtrace.rs:331:13
+   3: <gpui::app::App>::defer::<<gpui::app::App>::new_observer::{closure#0}>
+             at /Users/max/code/zed/crates/gpui/src/app.rs:1630:13
+   4: <gpui::app::App>::new_observer
+             at /Users/max/code/zed/crates/gpui/src/app.rs:940:14
+   5: <gpui::app::App>::observe_internal::<project::context_server_store::registry::ContextServerDescriptorRegistry, <gpui::app::context::Context<project::context_server_store::ContextServerStore>>::observe<project::context_server_store::registry::ContextServerDescriptorRegistry, <project::context_server_store::ContextServerStore>::new_internal::{closure#0}>::{closure#0}>
+             at /Users/max/code/zed/crates/gpui/src/app.rs:954:14
+   6: <gpui::app::context::Context<project::context_server_store::ContextServerStore>>::observe::<project::context_server_store::registry::ContextServerDescriptorRegistry, <project::context_server_store::ContextServerStore>::new_internal::{closure#0}>
+             at /Users/max/code/zed/crates/gpui/src/app/context.rs:73:18
+   7: <project::context_server_store::ContextServerStore>::new_internal
+             at /Users/max/code/zed/crates/project/src/context_server_store.rs:409:35
+   8: <project::context_server_store::ContextServerStore>::local
+             at /Users/max/code/zed/crates/project/src/context_server_store.rs:243:9
+   9: <project::Project>::local::{closure#0}::{closure#2}
+             at /Users/max/code/zed/crates/project/src/project.rs:1170:17
+  10: <gpui::app::App as gpui::AppContext>::new::<project::context_server_store::ContextServerStore, <project::Project>::local::{closure#0}::{closure#2}>::{closure#0}
+             at /Users/max/code/zed/crates/gpui/src/app.rs:2332:26
+  11: <gpui::app::App>::update::<gpui::app::entity_map::Entity<project::context_server_store::ContextServerStore>, <gpui::app::App as gpui::AppContext>::new<project::context_server_store::ContextServerStore, <project::Project>::local::{closure#0}::{closure#2}>::{closure#0}>
+             at /Users/max/code/zed/crates/gpui/src/app.rs:862:22
+  12: <gpui::app::App as gpui::AppContext>::new::<project::context_server_store::ContextServerStore, <project::Project>::local::{closure#0}::{closure#2}>
+             at /Users/max/code/zed/crates/gpui/src/app.rs:2329:14
+  13: <gpui::app::context::Context<project::Project> as gpui::AppContext>::new::<project::context_server_store::ContextServerStore, <project::Project>::local::{closure#0}::{closure#2}>
+             at /Users/max/code/zed/crates/gpui/src/app/context.rs:770:18
+  14: <project::Project>::local::{closure#0}
+             at /Users/max/code/zed/crates/project/src/project.rs:1169:43
+  15: <gpui::app::App as gpui::AppContext>::new::<project::Project, <project::Project>::local::{closure#0}>::{closure#0}
+             at /Users/max/code/zed/crates/gpui/src/app.rs:2332:26
+  16: <gpui::app::App>::update::<gpui::app::entity_map::Entity<project::Project>, <gpui::app::App as gpui::AppContext>::new<project::Project, <project::Project>::local::{closure#0}>::{closure#0}>
+             at /Users/max/code/zed/crates/gpui/src/app.rs:862:22
+  17: <gpui::app::App as gpui::AppContext>::new::<project::Project, <project::Project>::local::{closure#0}>
+             at /Users/max/code/zed/crates/gpui/src/app.rs:2329:14
+  18: <project::Project>::local
+             at /Users/max/code/zed/crates/project/src/project.rs:1149:12
+  19: <project::Project>::test_project::<[&std::path::Path; 0]>::{closure#0}::{closure#2}
+             at /Users/max/code/zed/crates/project/src/project.rs:2024:13
+  20: <gpui::app::App>::update::<gpui::app::entity_map::Entity<project::Project>, <project::Project>::test_project<[&std::path::Path; 0]>::{closure#0}::{closure#2}>
+             at /Users/max/code/zed/crates/gpui/src/app.rs:862:22
+  21: <gpui::app::test_context::TestAppContext>::update::<gpui::app::entity_map::Entity<project::Project>, <project::Project>::test_project<[&std::path::Path; 0]>::{closure#0}::{closure#2}>
+             at /Users/max/code/zed/crates/gpui/src/app/test_context.rs:205:12
+  22: <project::Project>::test_project::<[&std::path::Path; 0]>::{closure#0}
+             at /Users/max/code/zed/crates/project/src/project.rs:2023:26
+  23: <project::Project>::test::<[&std::path::Path; 0]>::{closure#0}
+             at /Users/max/code/zed/crates/project/src/project.rs:1997:55
+  24: agent_ui::agent_panel::tests::test_new_text_thread_action_handler::__test_new_text_thread_action_handler::{closure#0}
+             at ./src/agent_panel.rs:5125:57
+  25: <gpui::executor::ForegroundExecutor>::block_test::<(), agent_ui::agent_panel::tests::test_new_text_thread_action_handler::__test_new_text_thread_action_handler::{closure#0}>::{closure#0}
+             at /Users/max/code/zed/crates/gpui/src/executor.rs:440:36
+  26: <scheduler::test_scheduler::TestScheduler as scheduler::Scheduler>::block
+             at /Users/max/code/zed/crates/scheduler/src/test_scheduler.rs:538:35
+  27: <gpui::executor::ForegroundExecutor>::block_test::<(), agent_ui::agent_panel::tests::test_new_text_thread_action_handler::__test_new_text_thread_action_handler::{closure#0}>
+             at /Users/max/code/zed/crates/gpui/src/executor.rs:447:19
+  28: agent_ui::agent_panel::tests::test_new_text_thread_action_handler::{closure#0}
+             at ./src/agent_panel.rs:5108:5
+  29: gpui::test::run_test::{closure#0}
+             at /Users/max/code/zed/crates/gpui/src/test.rs:88:17
+  30: std::panicking::catch_unwind::do_call::<gpui::test::run_test::{closure#0}, ()>
+             at /Users/max/.rustup/toolchains/1.93-aarch64-apple-darwin/lib/rustlib/src/rust/library/std/src/panicking.rs:581:40
+  31: ___rust_try
+  32: std::panicking::catch_unwind::<(), gpui::test::run_test::{closure#0}>
+             at /Users/max/.rustup/toolchains/1.93-aarch64-apple-darwin/lib/rustlib/src/rust/library/std/src/panicking.rs:544:19
+  33: std::panic::catch_unwind::<gpui::test::run_test::{closure#0}, ()>
+             at /Users/max/.rustup/toolchains/1.93-aarch64-apple-darwin/lib/rustlib/src/rust/library/std/src/panic.rs:359:14
+  34: gpui::test::run_test
+             at /Users/max/code/zed/crates/gpui/src/test.rs:85:26
+  35: agent_ui::agent_panel::tests::test_new_text_thread_action_handler
+             at ./src/agent_panel.rs:5108:5
+  36: agent_ui::agent_panel::tests::test_new_text_thread_action_handler::{closure#0}
+             at ./src/agent_panel.rs:5108:18
+  37: <agent_ui::agent_panel::tests::test_new_text_thread_action_handler::{closure#0} as core::ops::function::FnOnce<()>>::call_once
+             at /Users/max/.rustup/toolchains/1.93-aarch64-apple-darwin/lib/rustlib/src/rust/library/core/src/ops/function.rs:250:5
+  38: core::ops::function::FnOnce::call_once
+             at /rustc/01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf/library/core/src/ops/function.rs:250:5
+  39: test::__rust_begin_short_backtrace
+             at /rustc/01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf/library/test/src/lib.rs:663:18
+  40: test::run_test_in_process::{{closure}}
+             at /rustc/01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf/library/test/src/lib.rs:686:74
+  41: <core::panic::unwind_safe::AssertUnwindSafe<F> as core::ops::function::FnOnce<()>>::call_once
+             at /rustc/01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf/library/core/src/panic/unwind_safe.rs:274:9
+  42: std::panicking::catch_unwind::do_call
+             at /rustc/01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf/library/std/src/panicking.rs:581:40
+  43: std::panicking::catch_unwind
+             at /rustc/01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf/library/std/src/panicking.rs:544:19
+  44: std::panic::catch_unwind
+             at /rustc/01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf/library/std/src/panic.rs:359:14
+  45: test::run_test_in_process
+             at /rustc/01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf/library/test/src/lib.rs:686:27
+  46: test::run_test::{{closure}}
+             at /rustc/01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf/library/test/src/lib.rs:607:43
+  47: test::run_test::{{closure}}
+             at /rustc/01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf/library/test/src/lib.rs:637:41
+  48: std::sys::backtrace::__rust_begin_short_backtrace
+             at /rustc/01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf/library/std/src/sys/backtrace.rs:160:18
+  49: std::thread::lifecycle::spawn_unchecked::{{closure}}::{{closure}}
+             at /rustc/01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf/library/std/src/thread/lifecycle.rs:92:13
+  50: <core::panic::unwind_safe::AssertUnwindSafe<F> as core::ops::function::FnOnce<()>>::call_once
+             at /rustc/01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf/library/core/src/panic/unwind_safe.rs:274:9
+  51: std::panicking::catch_unwind::do_call
+             at /rustc/01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf/library/std/src/panicking.rs:581:40
+  52: std::panicking::catch_unwind
+             at /rustc/01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf/library/std/src/panicking.rs:544:19
+  53: std::panic::catch_unwind
+             at /rustc/01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf/library/std/src/panic.rs:359:14
+  54: std::thread::lifecycle::spawn_unchecked::{{closure}}
+             at /rustc/01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf/library/std/src/thread/lifecycle.rs:90:26
+  55: core::ops::function::FnOnce::call_once{{vtable.shim}}
+             at /rustc/01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf/library/core/src/ops/function.rs:250:5
+  56: <alloc::boxed::Box<F,A> as core::ops::function::FnOnce<Args>>::call_once
+             at /rustc/01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf/library/alloc/src/boxed.rs:2206:9
+  57: std::sys::thread::unix::Thread::new::thread_start
+             at /rustc/01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf/library/std/src/sys/thread/unix.rs:118:17
+  58: __pthread_cond_wait
+
+defer called:    0: std::backtrace_rs::backtrace::libunwind::trace
+             at /rustc/01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf/library/std/src/../../backtrace/src/backtrace/libunwind.rs:117:9
+   1: std::backtrace_rs::backtrace::trace_unsynchronized
+             at /rustc/01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf/library/std/src/../../backtrace/src/backtrace/mod.rs:66:14
+   2: std::backtrace::Backtrace::create
+             at /rustc/01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf/library/std/src/backtrace.rs:331:13
+   3: <gpui::app::App>::defer::<<gpui::app::App>::new_subscription::{closure#0}>
+             at /Users/max/code/zed/crates/gpui/src/app.rs:1630:13
+   4: <gpui::app::App>::new_subscription
+             at /Users/max/code/zed/crates/gpui/src/app.rs:989:14
+   5: <gpui::app::App>::subscribe_internal::<project::worktree_store::WorktreeStore, project::worktree_store::WorktreeStoreEvent, <gpui::app::context::Context<project::manifest_tree::ManifestTree>>::subscribe<project::worktree_store::WorktreeStore, project::worktree_store::WorktreeStoreEvent, <project::manifest_tree::ManifestTree>::on_worktree_store_event>::{closure#0}>
+             at /Users/max/code/zed/crates/gpui/src/app.rs:1003:14
+   6: <gpui::app::context::Context<project::manifest_tree::ManifestTree>>::subscribe::<project::worktree_store::WorktreeStore, project::worktree_store::WorktreeStoreEvent, <project::manifest_tree::ManifestTree>::on_worktree_store_event>
+             at /Users/max/code/zed/crates/gpui/src/app/context.rs:109:18
+   7: <project::manifest_tree::ManifestTree>::new::{closure#0}
+             at /Users/max/code/zed/crates/project/src/manifest_tree.rs:79:20
+   8: <gpui::app::App as gpui::AppContext>::new::<project::manifest_tree::ManifestTree, <project::manifest_tree::ManifestTree>::new::{closure#0}>::{closure#0}
+             at /Users/max/code/zed/crates/gpui/src/app.rs:2332:26
+   9: <gpui::app::App>::update::<gpui::app::entity_map::Entity<project::manifest_tree::ManifestTree>, <gpui::app::App as gpui::AppContext>::new<project::manifest_tree::ManifestTree, <project::manifest_tree::ManifestTree>::new::{closure#0}>::{closure#0}>
+             at /Users/max/code/zed/crates/gpui/src/app.rs:862:22
+  10: <gpui::app::App as gpui::AppContext>::new::<project::manifest_tree::ManifestTree, <project::manifest_tree::ManifestTree>::new::{closure#0}>
+             at /Users/max/code/zed/crates/gpui/src/app.rs:2329:14
+  11: <project::manifest_tree::ManifestTree>::new
+             at /Users/max/code/zed/crates/project/src/manifest_tree.rs:76:12
+  12: <project::Project>::local::{closure#0}
+             at /Users/max/code/zed/crates/project/src/project.rs:1181:33
+  13: <gpui::app::App as gpui::AppContext>::new::<project::Project, <project::Project>::local::{closure#0}>::{closure#0}
+             at /Users/max/code/zed/crates/gpui/src/app.rs:2332:26
+  14: <gpui::app::App>::update::<gpui::app::entity_map::Entity<project::Project>, <gpui::app::App as gpui::AppContext>::new<project::Project, <project::Project>::local::{closure#0}>::{closure#0}>
+             at /Users/max/code/zed/crates/gpui/src/app.rs:862:22
+  15: <gpui::app::App as gpui::AppContext>::new::<project::Project, <project::Project>::local::{closure#0}>
+             at /Users/max/code/zed/crates/gpui/src/app.rs:2329:14
+  16: <project::Project>::local
+             at /Users/max/code/zed/crates/project/src/project.rs:1149:12
+  17: <project::Project>::test_project::<[&std::path::Path; 0]>::{closure#0}::{closure#2}
+             at /Users/max/code/zed/crates/project/src/project.rs:2024:13
+  18: <gpui::app::App>::update::<gpui::app::entity_map::Entity<project::Project>, <project::Project>::test_project<[&std::path::Path; 0]>::{closure#0}::{closure#2}>
+             at /Users/max/code/zed/crates/gpui/src/app.rs:862:22
+  19: <gpui::app::test_context::TestAppContext>::update::<gpui::app::entity_map::Entity<project::Project>, <project::Project>::test_project<[&std::path::Path; 0]>::{closure#0}::{closure#2}>
+             at /Users/max/code/zed/crates/gpui/src/app/test_context.rs:205:12
+  20: <project::Project>::test_project::<[&std::path::Path; 0]>::{closure#0}
+             at /Users/max/code/zed/crates/project/src/project.rs:2023:26
+  21: <project::Project>::test::<[&std::path::Path; 0]>::{closure#0}
+             at /Users/max/code/zed/crates/project/src/project.rs:1997:55
+  22: agent_ui::agent_panel::tests::test_new_text_thread_action_handler::__test_new_text_thread_action_handler::{closure#0}
+             at ./src/agent_panel.rs:5125:57
+  23: <gpui::executor::ForegroundExecutor>::block_test::<(), agent_ui::agent_panel::tests::test_new_text_thread_action_handler::__test_new_text_thread_action_handler::{closure#0}>::{closure#0}
+             at /Users/max/code/zed/crates/gpui/src/executor.rs:440:36
+  24: <scheduler::test_scheduler::TestScheduler as scheduler::Scheduler>::block
+             at /Users/max/code/zed/crates/scheduler/src/test_scheduler.rs:538:35
+  25: <gpui::executor::ForegroundExecutor>::block_test::<(), agent_ui::agent_panel::tests::test_new_text_thread_action_handler::__test_new_text_thread_action_handler::{closure#0}>
+             at /Users/max/code/zed/crates/gpui/src/executor.rs:447:19
+  26: agent_ui::agent_panel::tests::test_new_text_thread_action_handler::{closure#0}
+             at ./src/agent_panel.rs:5108:5
+  27: gpui::test::run_test::{closure#0}
+             at /Users/max/code/zed/crates/gpui/src/test.rs:88:17
+  28: std::panicking::catch_unwind::do_call::<gpui::test::run_test::{closure#0}, ()>
+             at /Users/max/.rustup/toolchains/1.93-aarch64-apple-darwin/lib/rustlib/src/rust/library/std/src/panicking.rs:581:40
+  29: ___rust_try
+  30: std::panicking::catch_unwind::<(), gpui::test::run_test::{closure#0}>
+             at /Users/max/.rustup/toolchains/1.93-aarch64-apple-darwin/lib/rustlib/src/rust/library/std/src/panicking.rs:544:19
+  31: std::panic::catch_unwind::<gpui::test::run_test::{closure#0}, ()>
+             at /Users/max/.rustup/toolchains/1.93-aarch64-apple-darwin/lib/rustlib/src/rust/library/std/src/panic.rs:359:14
+  32: gpui::test::run_test
+             at /Users/max/code/zed/crates/gpui/src/test.rs:85:26
+  33: agent_ui::agent_panel::tests::test_new_text_thread_action_handler
+             at ./src/agent_panel.rs:5108:5
+  34: agent_ui::agent_panel::tests::test_new_text_thread_action_handler::{closure#0}
+             at ./src/agent_panel.rs:5108:18
+  35: <agent_ui::agent_panel::tests::test_new_text_thread_action_handler::{closure#0} as core::ops::function::FnOnce<()>>::call_once
+             at /Users/max/.rustup/toolchains/1.93-aarch64-apple-darwin/lib/rustlib/src/rust/library/core/src/ops/function.rs:250:5
+  36: core::ops::function::FnOnce::call_once
+             at /rustc/01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf/library/core/src/ops/function.rs:250:5
+  37: test::__rust_begin_short_backtrace
+             at /rustc/01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf/library/test/src/lib.rs:663:18
+  38: test::run_test_in_process::{{closure}}
+             at /rustc/01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf/library/test/src/lib.rs:686:74
+  39: <core::panic::unwind_safe::AssertUnwindSafe<F> as core::ops::function::FnOnce<()>>::call_once
+             at /rustc/01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf/library/core/src/panic/unwind_safe.rs:274:9
+  40: std::panicking::catch_unwind::do_call
+             at /rustc/01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf/library/std/src/panicking.rs:581:40
+  41: std::panicking::catch_unwind
+             at /rustc/01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf/library/std/src/panicking.rs:544:19
+  42: std::panic::catch_unwind
+             at /rustc/01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf/library/std/src/panic.rs:359:14
+  43: test::run_test_in_process
+             at /rustc/01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf/library/test/src/lib.rs:686:27
+  44: test::run_test::{{closure}}
+             at /rustc/01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf/library/test/src/lib.rs:607:43
+  45: test::run_test::{{closure}}
+             at /rustc/01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf/library/test/src/lib.rs:637:41
+  46: std::sys::backtrace::__rust_begin_short_backtrace
+             at /rustc/01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf/library/std/src/sys/backtrace.rs:160:18
+  47: std::thread::lifecycle::spawn_unchecked::{{closure}}::{{closure}}
+             at /rustc/01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf/library/std/src/thread/lifecycle.rs:92:13
+  48: <core::panic::unwind_safe::AssertUnwindSafe<F> as core::ops::function::FnOnce<()>>::call_once
+             at /rustc/01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf/library/core/src/panic/unwind_safe.rs:274:9
+  49: std::panicking::catch_unwind::do_call
+             at /rustc/01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf/library/std/src/panicking.rs:581:40
+  50: std::panicking::catch_unwind
+             at /rustc/01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf/library/std/src/panicking.rs:544:19
+  51: std::panic::catch_unwind
+             at /rustc/01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf/library/std/src/panic.rs:359:14
+  52: std::thread::lifecycle::spawn_unchecked::{{closure}}
+             at /rustc/01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf/library/std/src/thread/lifecycle.rs:90:26
+  53: core::ops::function::FnOnce::call_once{{vtable.shim}}
+             at /rustc/01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf/library/core/src/ops/function.rs:250:5
+  54: <alloc::boxed::Box<F,A> as core::ops::function::FnOnce<Args>>::call_once
+             at /rustc/01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf/library/alloc/src/boxed.rs:2206:9
+  55: std::sys::thread::unix::Thread::new::thread_start
+             at /rustc/01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf/library/std/src/sys/thread/unix.rs:118:17
+  56: __pthread_cond_wait
+
+defer called:    0: std::backtrace_rs::backtrace::libunwind::trace
+             at /rustc/01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf/library/std/src/../../backtrace/src/backtrace/libunwind.rs:117:9
+   1: std::backtrace_rs::backtrace::trace_unsynchronized
+             at /rustc/01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf/library/std/src/../../backtrace/src/backtrace/mod.rs:66:14
+   2: std::backtrace::Backtrace::create
+             at /rustc/01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf/library/std/src/backtrace.rs:331:13
+   3: <gpui::app::App>::defer::<<gpui::app::context::Context<project::manifest_tree::ManifestTree>>::observe_global<settings::settings_store::SettingsStore, <project::manifest_tree::ManifestTree>::new::{closure#0}::{closure#0}>::{closure#1}>
+             at /Users/max/code/zed/crates/gpui/src/app.rs:1630:13
+   4: <gpui::app::context::Context<project::manifest_tree::ManifestTree>>::observe_global::<settings::settings_store::SettingsStore, <project::manifest_tree::ManifestTree>::new::{closure#0}::{closure#0}>
+             at /Users/max/code/zed/crates/gpui/src/app/context.rs:188:14
+   5: <project::manifest_tree::ManifestTree>::new::{closure#0}
+             at /Users/max/code/zed/crates/project/src/manifest_tree.rs:80:20
+   6: <gpui::app::App as gpui::AppContext>::new::<project::manifest_tree::ManifestTree, <project::manifest_tree::ManifestTree>::new::{closure#0}>::{closure#0}
+             at /Users/max/code/zed/crates/gpui/src/app.rs:2332:26
+   7: <gpui::app::App>::update::<gpui::app::entity_map::Entity<project::manifest_tree::ManifestTree>, <gpui::app::App as gpui::AppContext>::new<project::manifest_tree::ManifestTree, <project::manifest_tree::ManifestTree>::new::{closure#0}>::{closure#0}>
+             at /Users/max/code/zed/crates/gpui/src/app.rs:862:22
+   8: <gpui::app::App as gpui::AppContext>::new::<project::manifest_tree::ManifestTree, <project::manifest_tree::ManifestTree>::new::{closure#0}>
+             at /Users/max/code/zed/crates/gpui/src/app.rs:2329:14
+   9: <project::manifest_tree::ManifestTree>::new
+             at /Users/max/code/zed/crates/project/src/manifest_tree.rs:76:12
+  10: <project::Project>::local::{closure#0}
+             at /Users/max/code/zed/crates/project/src/project.rs:1181:33
+  11: <gpui::app::App as gpui::AppContext>::new::<project::Project, <project::Project>::local::{closure#0}>::{closure#0}
+             at /Users/max/code/zed/crates/gpui/src/app.rs:2332:26
+  12: <gpui::app::App>::update::<gpui::app::entity_map::Entity<project::Project>, <gpui::app::App as gpui::AppContext>::new<project::Project, <project::Project>::local::{closure#0}>::{closure#0}>
+             at /Users/max/code/zed/crates/gpui/src/app.rs:862:22
+  13: <gpui::app::App as gpui::AppContext>::new::<project::Project, <project::Project>::local::{closure#0}>
+             at /Users/max/code/zed/crates/gpui/src/app.rs:2329:14
+  14: <project::Project>::local
+             at /Users/max/code/zed/crates/project/src/project.rs:1149:12
+  15: <project::Project>::test_project::<[&std::path::Path; 0]>::{closure#0}::{closure#2}
+             at /Users/max/code/zed/crates/project/src/project.rs:2024:13
+  16: <gpui::app::App>::update::<gpui::app::entity_map::Entity<project::Project>, <project::Project>::test_project<[&std::path::Path; 0]>::{closure#0}::{closure#2}>
+             at /Users/max/code/zed/crates/gpui/src/app.rs:862:22
+  17: <gpui::app::test_context::TestAppContext>::update::<gpui::app::entity_map::Entity<project::Project>, <project::Project>::test_project<[&std::path::Path; 0]>::{closure#0}::{closure#2}>
+             at /Users/max/code/zed/crates/gpui/src/app/test_context.rs:205:12
+  18: <project::Project>::test_project::<[&std::path::Path; 0]>::{closure#0}
+             at /Users/max/code/zed/crates/project/src/project.rs:2023:26
+  19: <project::Project>::test::<[&std::path::Path; 0]>::{closure#0}
+             at /Users/max/code/zed/crates/project/src/project.rs:1997:55
+  20: agent_ui::agent_panel::tests::test_new_text_thread_action_handler::__test_new_text_thread_action_handler::{closure#0}
+             at ./src/agent_panel.rs:5125:57
+  21: <gpui::executor::ForegroundExecutor>::block_test::<(), agent_ui::agent_panel::tests::test_new_text_thread_action_handler::__test_new_text_thread_action_handler::{closure#0}>::{closure#0}
+             at /Users/max/code/zed/crates/gpui/src/executor.rs:440:36
+  22: <scheduler::test_scheduler::TestScheduler as scheduler::Scheduler>::block
+             at /Users/max/code/zed/crates/scheduler/src/test_scheduler.rs:538:35
+  23: <gpui::executor::ForegroundExecutor>::block_test::<(), agent_ui::agent_panel::tests::test_new_text_thread_action_handler::__test_new_text_thread_action_handler::{closure#0}>
+             at /Users/max/code/zed/crates/gpui/src/executor.rs:447:19
+  24: agent_ui::agent_panel::tests::test_new_text_thread_action_handler::{closure#0}
+             at ./src/agent_panel.rs:5108:5
+  25: gpui::test::run_test::{closure#0}
+             at /Users/max/code/zed/crates/gpui/src/test.rs:88:17
+  26: std::panicking::catch_unwind::do_call::<gpui::test::run_test::{closure#0}, ()>
+             at /Users/max/.rustup/toolchains/1.93-aarch64-apple-darwin/lib/rustlib/src/rust/library/std/src/panicking.rs:581:40
+  27: ___rust_try
+  28: std::panicking::catch_unwind::<(), gpui::test::run_test::{closure#0}>
+             at /Users/max/.rustup/toolchains/1.93-aarch64-apple-darwin/lib/rustlib/src/rust/library/std/src/panicking.rs:544:19
+  29: std::panic::catch_unwind::<gpui::test::run_test::{closure#0}, ()>
+             at /Users/max/.rustup/toolchains/1.93-aarch64-apple-darwin/lib/rustlib/src/rust/library/std/src/panic.rs:359:14
+  30: gpui::test::run_test
+             at /Users/max/code/zed/crates/gpui/src/test.rs:85:26
+  31: agent_ui::agent_panel::tests::test_new_text_thread_action_handler
+             at ./src/agent_panel.rs:5108:5
+  32: agent_ui::agent_panel::tests::test_new_text_thread_action_handler::{closure#0}
+             at ./src/agent_panel.rs:5108:18
+  33: <agent_ui::agent_panel::tests::test_new_text_thread_action_handler::{closure#0} as core::ops::function::FnOnce<()>>::call_once
+             at /Users/max/.rustup/toolchains/1.93-aarch64-apple-darwin/lib/rustlib/src/rust/library/core/src/ops/function.rs:250:5
+  34: core::ops::function::FnOnce::call_once
+             at /rustc/01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf/library/core/src/ops/function.rs:250:5
+  35: test::__rust_begin_short_backtrace
+             at /rustc/01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf/library/test/src/lib.rs:663:18
+  36: test::run_test_in_process::{{closure}}
+             at /rustc/01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf/library/test/src/lib.rs:686:74
+  37: <core::panic::unwind_safe::AssertUnwindSafe<F> as core::ops::function::FnOnce<()>>::call_once
+             at /rustc/01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf/library/core/src/panic/unwind_safe.rs:274:9
+  38: std::panicking::catch_unwind::do_call
+             at /rustc/01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf/library/std/src/panicking.rs:581:40
+  39: std::panicking::catch_unwind
+             at /rustc/01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf/library/std/src/panicking.rs:544:19
+  40: std::panic::catch_unwind
+             at /rustc/01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf/library/std/src/panic.rs:359:14
+  41: test::run_test_in_process
+             at /rustc/01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf/library/test/src/lib.rs:686:27
+  42: test::run_test::{{closure}}
+             at /rustc/01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf/library/test/src/lib.rs:607:43
+  43: test::run_test::{{closure}}
+             at /rustc/01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf/library/test/src/lib.rs:637:41
+  44: std::sys::backtrace::__rust_begin_short_backtrace
+             at /rustc/01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf/library/std/src/sys/backtrace.rs:160:18
+  45: std::thread::lifecycle::spawn_unchecked::{{closure}}::{{closure}}
+             at /rustc/01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf/library/std/src/thread/lifecycle.rs:92:13
+  46: <core::panic::unwind_safe::AssertUnwindSafe<F> as core::ops::function::FnOnce<()>>::call_once
+             at /rustc/01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf/library/core/src/panic/unwind_safe.rs:274:9
+  47: std::panicking::catch_unwind::do_call
+             at /rustc/01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf/library/std/src/panicking.rs:581:40
+  48: std::panicking::catch_unwind
+             at /rustc/01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf/library/std/src/panicking.rs:544:19
+  49: std::panic::catch_unwind
+             at /rustc/01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf/library/std/src/panic.rs:359:14
+  50: std::thread::lifecycle::spawn_unchecked::{{closure}}
+             at /rustc/01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf/library/std/src/thread/lifecycle.rs:90:26
+  51: core::ops::function::FnOnce::call_once{{vtable.shim}}
+             at /rustc/01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf/library/core/src/ops/function.rs:250:5
+  52: <alloc::boxed::Box<F,A> as core::ops::function::FnOnce<Args>>::call_once
+             at /rustc/01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf/library/alloc/src/boxed.rs:2206:9
+  53: std::sys::thread::unix::Thread::new::thread_start
+             at /rustc/01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf/library/std/src/sys/thread/unix.rs:118:17
+  54: __pthread_cond_wait
+
+defer called:    0: std::backtrace_rs::backtrace::libunwind::trace
+             at /rustc/01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf/library/std/src/../../backtrace/src/backtrace/libunwind.rs:117:9
+   1: std::backtrace_rs::backtrace::trace_unsynchronized
+             at /rustc/01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf/library/std/src/../../backtrace/src/backtrace/mod.rs:66:14
+   2: std::backtrace::Backtrace::create
+             at /rustc/01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf/library/std/src/backtrace.rs:331:13
+   3: <gpui::app::App>::defer::<<gpui::app::App>::new_subscription::{closure#0}>
+             at /Users/max/code/zed/crates/gpui/src/app.rs:1630:13
+   4: <gpui::app::App>::new_subscription
+             at /Users/max/code/zed/crates/gpui/src/app.rs:989:14
+   5: <gpui::app::App>::subscribe_internal::<project::toolchain_store::LocalToolchainStore, project::toolchain_store::ToolchainStoreEvent, <gpui::app::context::Context<project::toolchain_store::ToolchainStore>>::subscribe<project::toolchain_store::LocalToolchainStore, project::toolchain_store::ToolchainStoreEvent, <project::toolchain_store::ToolchainStore>::local::{closure#1}>::{closure#0}>
+             at /Users/max/code/zed/crates/gpui/src/app.rs:1003:14
+   6: <gpui::app::context::Context<project::toolchain_store::ToolchainStore>>::subscribe::<project::toolchain_store::LocalToolchainStore, project::toolchain_store::ToolchainStoreEvent, <project::toolchain_store::ToolchainStore>::local::{closure#1}>
+             at /Users/max/code/zed/crates/gpui/src/app/context.rs:109:18
+   7: <project::toolchain_store::ToolchainStore>::local
+             at /Users/max/code/zed/crates/project/src/toolchain_store.rs:76:23
+   8: <project::Project>::local::{closure#0}::{closure#4}
+             at /Users/max/code/zed/crates/project/src/project.rs:1183:17
+   9: <gpui::app::App as gpui::AppContext>::new::<project::toolchain_store::ToolchainStore, <project::Project>::local::{closure#0}::{closure#4}>::{closure#0}
+             at /Users/max/code/zed/crates/gpui/src/app.rs:2332:26
+  10: <gpui::app::App>::update::<gpui::app::entity_map::Entity<project::toolchain_store::ToolchainStore>, <gpui::app::App as gpui::AppContext>::new<project::toolchain_store::ToolchainStore, <project::Project>::local::{closure#0}::{closure#4}>::{closure#0}>
+             at /Users/max/code/zed/crates/gpui/src/app.rs:862:22
+  11: <gpui::app::App as gpui::AppContext>::new::<project::toolchain_store::ToolchainStore, <project::Project>::local::{closure#0}::{closure#4}>
+             at /Users/max/code/zed/crates/gpui/src/app.rs:2329:14
+  12: <gpui::app::context::Context<project::Project> as gpui::AppContext>::new::<project::toolchain_store::ToolchainStore, <project::Project>::local::{closure#0}::{closure#4}>
+             at /Users/max/code/zed/crates/gpui/src/app/context.rs:770:18
+  13: <project::Project>::local::{closure#0}
+             at /Users/max/code/zed/crates/project/src/project.rs:1182:38
+  14: <gpui::app::App as gpui::AppContext>::new::<project::Project, <project::Project>::local::{closure#0}>::{closure#0}
+             at /Users/max/code/zed/crates/gpui/src/app.rs:2332:26
+  15: <gpui::app::App>::update::<gpui::app::entity_map::Entity<project::Project>, <gpui::app::App as gpui::AppContext>::new<project::Project, <project::Project>::local::{closure#0}>::{closure#0}>
+             at /Users/max/code/zed/crates/gpui/src/app.rs:862:22
+  16: <gpui::app::App as gpui::AppContext>::new::<project::Project, <project::Project>::local::{closure#0}>
+             at /Users/max/code/zed/crates/gpui/src/app.rs:2329:14
+  17: <project::Project>::local
+             at /Users/max/code/zed/crates/project/src/project.rs:1149:12
+  18: <project::Project>::test_project::<[&std::path::Path; 0]>::{closure#0}::{closure#2}
+             at /Users/max/code/zed/crates/project/src/project.rs:2024:13
+  19: <gpui::app::App>::update::<gpui::app::entity_map::Entity<project::Project>, <project::Project>::test_project<[&std::path::Path; 0]>::{closure#0}::{closure#2}>
+             at /Users/max/code/zed/crates/gpui/src/app.rs:862:22
+  20: <gpui::app::test_context::TestAppContext>::update::<gpui::app::entity_map::Entity<project::Project>, <project::Project>::test_project<[&std::path::Path; 0]>::{closure#0}::{closure#2}>
+             at /Users/max/code/zed/crates/gpui/src/app/test_context.rs:205:12
+  21: <project::Project>::test_project::<[&std::path::Path; 0]>::{closure#0}
+             at /Users/max/code/zed/crates/project/src/project.rs:2023:26
+  22: <project::Project>::test::<[&std::path::Path; 0]>::{closure#0}
+             at /Users/max/code/zed/crates/project/src/project.rs:1997:55
+  23: agent_ui::agent_panel::tests::test_new_text_thread_action_handler::__test_new_text_thread_action_handler::{closure#0}
+             at ./src/agent_panel.rs:5125:57
+  24: <gpui::executor::ForegroundExecutor>::block_test::<(), agent_ui::agent_panel::tests::test_new_text_thread_action_handler::__test_new_text_thread_action_handler::{closure#0}>::{closure#0}
+             at /Users/max/code/zed/crates/gpui/src/executor.rs:440:36
+  25: <scheduler::test_scheduler::TestScheduler as scheduler::Scheduler>::block
+             at /Users/max/code/zed/crates/scheduler/src/test_scheduler.rs:538:35
+  26: <gpui::executor::ForegroundExecutor>::block_test::<(), agent_ui::agent_panel::tests::test_new_text_thread_action_handler::__test_new_text_thread_action_handler::{closure#0}>
+             at /Users/max/code/zed/crates/gpui/src/executor.rs:447:19
+  27: agent_ui::agent_panel::tests::test_new_text_thread_action_handler::{closure#0}
+             at ./src/agent_panel.rs:5108:5
+  28: gpui::test::run_test::{closure#0}
+             at /Users/max/code/zed/crates/gpui/src/test.rs:88:17
+  29: std::panicking::catch_unwind::do_call::<gpui::test::run_test::{closure#0}, ()>
+             at /Users/max/.rustup/toolchains/1.93-aarch64-apple-darwin/lib/rustlib/src/rust/library/std/src/panicking.rs:581:40
+  30: ___rust_try
+  31: std::panicking::catch_unwind::<(), gpui::test::run_test::{closure#0}>
+             at /Users/max/.rustup/toolchains/1.93-aarch64-apple-darwin/lib/rustlib/src/rust/library/std/src/panicking.rs:544:19
+  32: std::panic::catch_unwind::<gpui::test::run_test::{closure#0}, ()>
+             at /Users/max/.rustup/toolchains/1.93-aarch64-apple-darwin/lib/rustlib/src/rust/library/std/src/panic.rs:359:14
+  33: gpui::test::run_test
+             at /Users/max/code/zed/crates/gpui/src/test.rs:85:26
+  34: agent_ui::agent_panel::tests::test_new_text_thread_action_handler
+             at ./src/agent_panel.rs:5108:5
+  35: agent_ui::agent_panel::tests::test_new_text_thread_action_handler::{closure#0}
+             at ./src/agent_panel.rs:5108:18
+  36: <agent_ui::agent_panel::tests::test_new_text_thread_action_handler::{closure#0} as core::ops::function::FnOnce<()>>::call_once
+             at /Users/max/.rustup/toolchains/1.93-aarch64-apple-darwin/lib/rustlib/src/rust/library/core/src/ops/function.rs:250:5
+  37: core::ops::function::FnOnce::call_once
+             at /rustc/01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf/library/core/src/ops/function.rs:250:5
+  38: test::__rust_begin_short_backtrace
+             at /rustc/01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf/library/test/src/lib.rs:663:18
+  39: test::run_test_in_process::{{closure}}
+             at /rustc/01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf/library/test/src/lib.rs:686:74
+  40: <core::panic::unwind_safe::AssertUnwindSafe<F> as core::ops::function::FnOnce<()>>::call_once
+             at /rustc/01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf/library/core/src/panic/unwind_safe.rs:274:9
+  41: std::panicking::catch_unwind::do_call
+             at /rustc/01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf/library/std/src/panicking.rs:581:40
+  42: std::panicking::catch_unwind
+             at /rustc/01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf/library/std/src/panicking.rs:544:19
+  43: std::panic::catch_unwind
+             at /rustc/01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf/library/std/src/panic.rs:359:14
+  44: test::run_test_in_process
+             at /rustc/01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf/library/test/src/lib.rs:686:27
+  45: test::run_test::{{closure}}
+             at /rustc/01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf/library/test/src/lib.rs:607:43
+  46: test::run_test::{{closure}}
+             at /rustc/01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf/library/test/src/lib.rs:637:41
+  47: std::sys::backtrace::__rust_begin_short_backtrace
+             at /rustc/01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf/library/std/src/sys/backtrace.rs:160:18
+  48: std::thread::lifecycle::spawn_unchecked::{{closure}}::{{closure}}
+             at /rustc/01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf/library/std/src/thread/lifecycle.rs:92:13
+  49: <core::panic::unwind_safe::AssertUnwindSafe<F> as core::ops::function::FnOnce<()>>::call_once
+             at /rustc/01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf/library/core/src/panic/unwind_safe.rs:274:9
+  50: std::panicking::catch_unwind::do_call
+             at /rustc/01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf/library/std/src/panicking.rs:581:40
+  51: std::panicking::catch_unwind
+             at /rustc/01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf/library/std/src/panicking.rs:544:19
+  52: std::panic::catch_unwind
+             at /rustc/01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf/library/std/src/panic.rs:359:14
+  53: std::thread::lifecycle::spawn_unchecked::{{closure}}
+             at /rustc/01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf/library/std/src/thread/lifecycle.rs:90:26
+  54: core::ops::function::FnOnce::call_once{{vtable.shim}}
+             at /rustc/01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf/library/core/src/ops/function.rs:250:5
+  55: <alloc::boxed::Box<F,A> as core::ops::function::FnOnce<Args>>::call_once
+             at /rustc/01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf/library/alloc/src/boxed.rs:2206:9
+  56: std::sys::thread::unix::Thread::new::thread_start
+             at /rustc/01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf/library/std/src/sys/thread/unix.rs:118:17
+  57: __pthread_cond_wait
+
+defer called:    0: std::backtrace_rs::backtrace::libunwind::trace
+             at /rustc/01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf/library/std/src/../../backtrace/src/backtrace/libunwind.rs:117:9
+   1: std::backtrace_rs::backtrace::trace_unsynchronized
+             at /rustc/01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf/library/std/src/../../backtrace/src/backtrace/mod.rs:66:14
+   2: std::backtrace::Backtrace::create
+             at /rustc/01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf/library/std/src/backtrace.rs:331:13
+   3: <gpui::app::App>::defer::<<gpui::app::App>::new_subscription::{closure#0}>
+             at /Users/max/code/zed/crates/gpui/src/app.rs:1630:13
+   4: <gpui::app::App>::new_subscription
+             at /Users/max/code/zed/crates/gpui/src/app.rs:989:14
+   5: <gpui::app::App>::subscribe_internal::<project::worktree_store::WorktreeStore, project::worktree_store::WorktreeStoreEvent, <gpui::app::context::Context<project::buffer_store::BufferStore>>::subscribe<project::worktree_store::WorktreeStore, project::worktree_store::WorktreeStoreEvent, <project::buffer_store::BufferStore>::local::{closure#0}>::{closure#0}>
+             at /Users/max/code/zed/crates/gpui/src/app.rs:1003:14
+   6: <gpui::app::context::Context<project::buffer_store::BufferStore>>::subscribe::<project::worktree_store::WorktreeStore, project::worktree_store::WorktreeStoreEvent, <project::buffer_store::BufferStore>::local::{closure#0}>
+             at /Users/max/code/zed/crates/gpui/src/app/context.rs:109:18
+   7: <project::buffer_store::BufferStore>::local
+             at /Users/max/code/zed/crates/project/src/buffer_store.rs:782:35
+   8: <project::Project>::local::{closure#0}::{closure#5}
+             at /Users/max/code/zed/crates/project/src/project.rs:1193:44
+   9: <gpui::app::App as gpui::AppContext>::new::<project::buffer_store::BufferStore, <project::Project>::local::{closure#0}::{closure#5}>::{closure#0}
+             at /Users/max/code/zed/crates/gpui/src/app.rs:2332:26
+  10: <gpui::app::App>::update::<gpui::app::entity_map::Entity<project::buffer_store::BufferStore>, <gpui::app::App as gpui::AppContext>::new<project::buffer_store::BufferStore, <project::Project>::local::{closure#0}::{closure#5}>::{closure#0}>
+             at /Users/max/code/zed/crates/gpui/src/app.rs:862:22
+  11: <gpui::app::App as gpui::AppContext>::new::<project::buffer_store::BufferStore, <project::Project>::local::{closure#0}::{closure#5}>
+             at /Users/max/code/zed/crates/gpui/src/app.rs:2329:14
+  12: <gpui::app::context::Context<project::Project> as gpui::AppContext>::new::<project::buffer_store::BufferStore, <project::Project>::local::{closure#0}::{closure#5}>
+             at /Users/max/code/zed/crates/gpui/src/app/context.rs:770:18
+  13: <project::Project>::local::{closure#0}
+             at /Users/max/code/zed/crates/project/src/project.rs:1193:35
+  14: <gpui::app::App as gpui::AppContext>::new::<project::Project, <project::Project>::local::{closure#0}>::{closure#0}
+             at /Users/max/code/zed/crates/gpui/src/app.rs:2332:26
+  15: <gpui::app::App>::update::<gpui::app::entity_map::Entity<project::Project>, <gpui::app::App as gpui::AppContext>::new<project::Project, <project::Project>::local::{closure#0}>::{closure#0}>
+             at /Users/max/code/zed/crates/gpui/src/app.rs:862:22
+  16: <gpui::app::App as gpui::AppContext>::new::<project::Project, <project::Project>::local::{closure#0}>
+             at /Users/max/code/zed/crates/gpui/src/app.rs:2329:14
+  17: <project::Project>::local
+             at /Users/max/code/zed/crates/project/src/project.rs:1149:12
+  18: <project::Project>::test_project::<[&std::path::Path; 0]>::{closure#0}::{closure#2}
+             at /Users/max/code/zed/crates/project/src/project.rs:2024:13
+  19: <gpui::app::App>::update::<gpui::app::entity_map::Entity<project::Project>, <project::Project>::test_project<[&std::path::Path; 0]>::{closure#0}::{closure#2}>
+             at /Users/max/code/zed/crates/gpui/src/app.rs:862:22
+  20: <gpui::app::test_context::TestAppContext>::update::<gpui::app::entity_map::Entity<project::Project>, <project::Project>::test_project<[&std::path::Path; 0]>::{closure#0}::{closure#2}>
+             at /Users/max/code/zed/crates/gpui/src/app/test_context.rs:205:12
+  21: <project::Project>::test_project::<[&std::path::Path; 0]>::{closure#0}
+             at /Users/max/code/zed/crates/project/src/project.rs:2023:26
+  22: <project::Project>::test::<[&std::path::Path; 0]>::{closure#0}
+             at /Users/max/code/zed/crates/project/src/project.rs:1997:55
+  23: agent_ui::agent_panel::tests::test_new_text_thread_action_handler::__test_new_text_thread_action_handler::{closure#0}
+             at ./src/agent_panel.rs:5125:57
+  24: <gpui::executor::ForegroundExecutor>::block_test::<(), agent_ui::agent_panel::tests::test_new_text_thread_action_handler::__test_new_text_thread_action_handler::{closure#0}>::{closure#0}
+             at /Users/max/code/zed/crates/gpui/src/executor.rs:440:36
+  25: <scheduler::test_scheduler::TestScheduler as scheduler::Scheduler>::block
+             at /Users/max/code/zed/crates/scheduler/src/test_scheduler.rs:538:35
+  26: <gpui::executor::ForegroundExecutor>::block_test::<(), agent_ui::agent_panel::tests::test_new_text_thread_action_handler::__test_new_text_thread_action_handler::{closure#0}>
+             at /Users/max/code/zed/crates/gpui/src/executor.rs:447:19
+  27: agent_ui::agent_panel::tests::test_new_text_thread_action_handler::{closure#0}
+             at ./src/agent_panel.rs:5108:5
+  28: gpui::test::run_test::{closure#0}
+             at /Users/max/code/zed/crates/gpui/src/test.rs:88:17
+  29: std::panicking::catch_unwind::do_call::<gpui::test::run_test::{closure#0}, ()>
+             at /Users/max/.rustup/toolchains/1.93-aarch64-apple-darwin/lib/rustlib/src/rust/library/std/src/panicking.rs:581:40
+  30: ___rust_try
+  31: std::panicking::catch_unwind::<(), gpui::test::run_test::{closure#0}>
+             at /Users/max/.rustup/toolchains/1.93-aarch64-apple-darwin/lib/rustlib/src/rust/library/std/src/panicking.rs:544:19
+  32: std::panic::catch_unwind::<gpui::test::run_test::{closure#0}, ()>
+             at /Users/max/.rustup/toolchains/1.93-aarch64-apple-darwin/lib/rustlib/src/rust/library/std/src/panic.rs:359:14
+  33: gpui::test::run_test
+             at /Users/max/code/zed/crates/gpui/src/test.rs:85:26
+  34: agent_ui::agent_panel::tests::test_new_text_thread_action_handler
+             at ./src/agent_panel.rs:5108:5
+  35: agent_ui::agent_panel::tests::test_new_text_thread_action_handler::{closure#0}
+             at ./src/agent_panel.rs:5108:18
+  36: <agent_ui::agent_panel::tests::test_new_text_thread_action_handler::{closure#0} as core::ops::function::FnOnce<()>>::call_once
+             at /Users/max/.rustup/toolchains/1.93-aarch64-apple-darwin/lib/rustlib/src/rust/library/core/src/ops/function.rs:250:5
+  37: core::ops::function::FnOnce::call_once
+             at /rustc/01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf/library/core/src/ops/function.rs:250:5
+  38: test::__rust_begin_short_backtrace
+             at /rustc/01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf/library/test/src/lib.rs:663:18
+  39: test::run_test_in_process::{{closure}}
+             at /rustc/01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf/library/test/src/lib.rs:686:74
+  40: <core::panic::unwind_safe::AssertUnwindSafe<F> as core::ops::function::FnOnce<()>>::call_once
+             at /rustc/01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf/library/core/src/panic/unwind_safe.rs:274:9
+  41: std::panicking::catch_unwind::do_call
+             at /rustc/01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf/library/std/src/panicking.rs:581:40
+  42: std::panicking::catch_unwind
+             at /rustc/01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf/library/std/src/panicking.rs:544:19
+  43: std::panic::catch_unwind
+             at /rustc/01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf/library/std/src/panic.rs:359:14
+  44: test::run_test_in_process
+             at /rustc/01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf/library/test/src/lib.rs:686:27
+  45: test::run_test::{{closure}}
+             at /rustc/01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf/library/test/src/lib.rs:607:43
+  46: test::run_test::{{closure}}
+             at /rustc/01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf/library/test/src/lib.rs:637:41
+  47: std::sys::backtrace::__rust_begin_short_backtrace
+             at /rustc/01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf/library/std/src/sys/backtrace.rs:160:18
+  48: std::thread::lifecycle::spawn_unchecked::{{closure}}::{{closure}}
+             at /rustc/01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf/library/std/src/thread/lifecycle.rs:92:13
+  49: <core::panic::unwind_safe::AssertUnwindSafe<F> as core::ops::function::FnOnce<()>>::call_once
+             at /rustc/01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf/library/core/src/panic/unwind_safe.rs:274:9
+  50: std::panicking::catch_unwind::do_call
+             at /rustc/01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf/library/std/src/panicking.rs:581:40
+  51: std::panicking::catch_unwind
+             at /rustc/01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf/library/std/src/panicking.rs:544:19
+  52: std::panic::catch_unwind
+             at /rustc/01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf/library/std/src/panic.rs:359:14
+  53: std::thread::lifecycle::spawn_unchecked::{{closure}}
+             at /rustc/01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf/library/std/src/thread/lifecycle.rs:90:26
+  54: core::ops::function::FnOnce::call_once{{vtable.shim}}
+             at /rustc/01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf/library/core/src/ops/function.rs:250:5
+  55: <alloc::boxed::Box<F,A> as core::ops::function::FnOnce<Args>>::call_once
+             at /rustc/01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf/library/alloc/src/boxed.rs:2206:9
+  56: std::sys::thread::unix::Thread::new::thread_start
+             at /rustc/01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf/library/std/src/sys/thread/unix.rs:118:17
+  57: __pthread_cond_wait
+
+defer called:    0: std::backtrace_rs::backtrace::libunwind::trace
+             at /rustc/01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf/library/std/src/../../backtrace/src/backtrace/libunwind.rs:117:9
+   1: std::backtrace_rs::backtrace::trace_unsynchronized
+             at /rustc/01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf/library/std/src/../../backtrace/src/backtrace/mod.rs:66:14
+   2: std::backtrace::Backtrace::create
+             at /rustc/01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf/library/std/src/backtrace.rs:331:13
+   3: <gpui::app::App>::defer::<<gpui::app::App>::new_subscription::{closure#0}>
+             at /Users/max/code/zed/crates/gpui/src/app.rs:1630:13
+   4: <gpui::app::App>::new_subscription
+             at /Users/max/code/zed/crates/gpui/src/app.rs:989:14
+   5: <gpui::app::App>::subscribe_internal::<project::buffer_store::BufferStore, project::buffer_store::BufferStoreEvent, <gpui::app::context::Context<project::Project>>::subscribe<project::buffer_store::BufferStore, project::buffer_store::BufferStoreEvent, <project::Project>::on_buffer_store_event>::{closure#0}>
+             at /Users/max/code/zed/crates/gpui/src/app.rs:1003:14
+   6: <gpui::app::context::Context<project::Project>>::subscribe::<project::buffer_store::BufferStore, project::buffer_store::BufferStoreEvent, <project::Project>::on_buffer_store_event>
+             at /Users/max/code/zed/crates/gpui/src/app/context.rs:109:18
+   7: <project::Project>::local::{closure#0}
+             at /Users/max/code/zed/crates/project/src/project.rs:1194:16
+   8: <gpui::app::App as gpui::AppContext>::new::<project::Project, <project::Project>::local::{closure#0}>::{closure#0}
+             at /Users/max/code/zed/crates/gpui/src/app.rs:2332:26
+   9: <gpui::app::App>::update::<gpui::app::entity_map::Entity<project::Project>, <gpui::app::App as gpui::AppContext>::new<project::Project, <project::Project>::local::{closure#0}>::{closure#0}>
+             at /Users/max/code/zed/crates/gpui/src/app.rs:862:22
+  10: <gpui::app::App as gpui::AppContext>::new::<project::Project, <project::Project>::local::{closure#0}>
+             at /Users/max/code/zed/crates/gpui/src/app.rs:2329:14
+  11: <project::Project>::local
+             at /Users/max/code/zed/crates/project/src/project.rs:1149:12
+  12: <project::Project>::test_project::<[&std::path::Path; 0]>::{closure#0}::{closure#2}
+             at /Users/max/code/zed/crates/project/src/project.rs:2024:13
+  13: <gpui::app::App>::update::<gpui::app::entity_map::Entity<project::Project>, <project::Project>::test_project<[&std::path::Path; 0]>::{closure#0}::{closure#2}>
+             at /Users/max/code/zed/crates/gpui/src/app.rs:862:22
+  14: <gpui::app::test_context::TestAppContext>::update::<gpui::app::entity_map::Entity<project::Project>, <project::Project>::test_project<[&std::path::Path; 0]>::{closure#0}::{closure#2}>
+             at /Users/max/code/zed/crates/gpui/src/app/test_context.rs:205:12
+  15: <project::Project>::test_project::<[&std::path::Path; 0]>::{closure#0}
+             at /Users/max/code/zed/crates/project/src/project.rs:2023:26
+  16: <project::Project>::test::<[&std::path::Path; 0]>::{closure#0}
+             at /Users/max/code/zed/crates/project/src/project.rs:1997:55
+  17: agent_ui::agent_panel::tests::test_new_text_thread_action_handler::__test_new_text_thread_action_handler::{closure#0}
+             at ./src/agent_panel.rs:5125:57
+  18: <gpui::executor::ForegroundExecutor>::block_test::<(), agent_ui::agent_panel::tests::test_new_text_thread_action_handler::__test_new_text_thread_action_handler::{closure#0}>::{closure#0}
+             at /Users/max/code/zed/crates/gpui/src/executor.rs:440:36
+  19: <scheduler::test_scheduler::TestScheduler as scheduler::Scheduler>::block
+             at /Users/max/code/zed/crates/scheduler/src/test_scheduler.rs:538:35
+  20: <gpui::executor::ForegroundExecutor>::block_test::<(), agent_ui::agent_panel::tests::test_new_text_thread_action_handler::__test_new_text_thread_action_handler::{closure#0}>
+             at /Users/max/code/zed/crates/gpui/src/executor.rs:447:19
+  21: agent_ui::agent_panel::tests::test_new_text_thread_action_handler::{closure#0}
+             at ./src/agent_panel.rs:5108:5
+  22: gpui::test::run_test::{closure#0}
+             at /Users/max/code/zed/crates/gpui/src/test.rs:88:17
+  23: std::panicking::catch_unwind::do_call::<gpui::test::run_test::{closure#0}, ()>
+             at /Users/max/.rustup/toolchains/1.93-aarch64-apple-darwin/lib/rustlib/src/rust/library/std/src/panicking.rs:581:40
+  24: ___rust_try
+  25: std::panicking::catch_unwind::<(), gpui::test::run_test::{closure#0}>
+             at /Users/max/.rustup/toolchains/1.93-aarch64-apple-darwin/lib/rustlib/src/rust/library/std/src/panicking.rs:544:19
+  26: std::panic::catch_unwind::<gpui::test::run_test::{closure#0}, ()>
+             at /Users/max/.rustup/toolchains/1.93-aarch64-apple-darwin/lib/rustlib/src/rust/library/std/src/panic.rs:359:14
+  27: gpui::test::run_test
+             at /Users/max/code/zed/crates/gpui/src/test.rs:85:26
+  28: agent_ui::agent_panel::tests::test_new_text_thread_action_handler
+             at ./src/agent_panel.rs:5108:5
+  29: agent_ui::agent_panel::tests::test_new_text_thread_action_handler::{closure#0}
+             at ./src/agent_panel.rs:5108:18
+  30: <agent_ui::agent_panel::tests::test_new_text_thread_action_handler::{closure#0} as core::ops::function::FnOnce<()>>::call_once
+             at /Users/max/.rustup/toolchains/1.93-aarch64-apple-darwin/lib/rustlib/src/rust/library/core/src/ops/function.rs:250:5
+  31: core::ops::function::FnOnce::call_once
+             at /rustc/01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf/library/core/src/ops/function.rs:250:5
+  32: test::__rust_begin_short_backtrace
+             at /rustc/01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf/library/test/src/lib.rs:663:18
+  33: test::run_test_in_process::{{closure}}
+             at /rustc/01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf/library/test/src/lib.rs:686:74
+  34: <core::panic::unwind_safe::AssertUnwindSafe<F> as core::ops::function::FnOnce<()>>::call_once
+             at /rustc/01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf/library/core/src/panic/unwind_safe.rs:274:9
+  35: std::panicking::catch_unwind::do_call
+             at /rustc/01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf/library/std/src/panicking.rs:581:40
+  36: std::panicking::catch_unwind
+             at /rustc/01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf/library/std/src/panicking.rs:544:19
+  37: std::panic::catch_unwind
+             at /rustc/01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf/library/std/src/panic.rs:359:14
+  38: test::run_test_in_process
+             at /rustc/01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf/library/test/src/lib.rs:686:27
+  39: test::run_test::{{closure}}
+             at /rustc/01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf/library/test/src/lib.rs:607:43
+  40: test::run_test::{{closure}}
+             at /rustc/01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf/library/test/src/lib.rs:637:41
+  41: std::sys::backtrace::__rust_begin_short_backtrace
+             at /rustc/01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf/library/std/src/sys/backtrace.rs:160:18
+  42: std::thread::lifecycle::spawn_unchecked::{{closure}}::{{closure}}
+             at /rustc/01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf/library/std/src/thread/lifecycle.rs:92:13
+  43: <core::panic::unwind_safe::AssertUnwindSafe<F> as core::ops::function::FnOnce<()>>::call_once
+             at /rustc/01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf/library/core/src/panic/unwind_safe.rs:274:9
+  44: std::panicking::catch_unwind::do_call
+             at /rustc/01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf/library/std/src/panicking.rs:581:40
+  45: std::panicking::catch_unwind
+             at /rustc/01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf/library/std/src/panicking.rs:544:19
+  46: std::panic::catch_unwind
+             at /rustc/01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf/library/std/src/panic.rs:359:14
+  47: std::thread::lifecycle::spawn_unchecked::{{closure}}
+             at /rustc/01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf/library/std/src/thread/lifecycle.rs:90:26
+  48: core::ops::function::FnOnce::call_once{{vtable.shim}}
+             at /rustc/01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf/library/core/src/ops/function.rs:250:5
+  49: <alloc::boxed::Box<F,A> as core::ops::function::FnOnce<Args>>::call_once
+             at /rustc/01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf/library/alloc/src/boxed.rs:2206:9
+  50: std::sys::thread::unix::Thread::new::thread_start
+             at /rustc/01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf/library/std/src/sys/thread/unix.rs:118:17
+  51: __pthread_cond_wait
+
+defer called:    0: std::backtrace_rs::backtrace::libunwind::trace
+             at /rustc/01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf/library/std/src/../../backtrace/src/backtrace/libunwind.rs:117:9
+   1: std::backtrace_rs::backtrace::trace_unsynchronized
+             at /rustc/01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf/library/std/src/../../backtrace/src/backtrace/mod.rs:66:14
+   2: std::backtrace::Backtrace::create
+             at /rustc/01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf/library/std/src/backtrace.rs:331:13
+   3: <gpui::app::App>::defer::<<gpui::app::App>::new_subscription::{closure#0}>
+             at /Users/max/code/zed/crates/gpui/src/app.rs:1630:13
+   4: <gpui::app::App>::new_subscription
+             at /Users/max/code/zed/crates/gpui/src/app.rs:989:14
+   5: <gpui::app::App>::subscribe_internal::<project::debugger::dap_store::DapStore, project::debugger::dap_store::DapStoreEvent, <gpui::app::context::Context<project::Project>>::subscribe<project::debugger::dap_store::DapStore, project::debugger::dap_store::DapStoreEvent, <project::Project>::on_dap_store_event>::{closure#0}>
+             at /Users/max/code/zed/crates/gpui/src/app.rs:1003:14
+   6: <gpui::app::context::Context<project::Project>>::subscribe::<project::debugger::dap_store::DapStore, project::debugger::dap_store::DapStoreEvent, <project::Project>::on_dap_store_event>
+             at /Users/max/code/zed/crates/gpui/src/app/context.rs:109:18
+   7: <project::Project>::local::{closure#0}
+             at /Users/max/code/zed/crates/project/src/project.rs:1213:16
+   8: <gpui::app::App as gpui::AppContext>::new::<project::Project, <project::Project>::local::{closure#0}>::{closure#0}
+             at /Users/max/code/zed/crates/gpui/src/app.rs:2332:26
+   9: <gpui::app::App>::update::<gpui::app::entity_map::Entity<project::Project>, <gpui::app::App as gpui::AppContext>::new<project::Project, <project::Project>::local::{closure#0}>::{closure#0}>
+             at /Users/max/code/zed/crates/gpui/src/app.rs:862:22
+  10: <gpui::app::App as gpui::AppContext>::new::<project::Project, <project::Project>::local::{closure#0}>
+             at /Users/max/code/zed/crates/gpui/src/app.rs:2329:14
+  11: <project::Project>::local
+             at /Users/max/code/zed/crates/project/src/project.rs:1149:12
+  12: <project::Project>::test_project::<[&std::path::Path; 0]>::{closure#0}::{closure#2}
+             at /Users/max/code/zed/crates/project/src/project.rs:2024:13
+  13: <gpui::app::App>::update::<gpui::app::entity_map::Entity<project::Project>, <project::Project>::test_project<[&std::path::Path; 0]>::{closure#0}::{closure#2}>
+             at /Users/max/code/zed/crates/gpui/src/app.rs:862:22
+  14: <gpui::app::test_context::TestAppContext>::update::<gpui::app::entity_map::Entity<project::Project>, <project::Project>::test_project<[&std::path::Path; 0]>::{closure#0}::{closure#2}>
+             at /Users/max/code/zed/crates/gpui/src/app/test_context.rs:205:12
+  15: <project::Project>::test_project::<[&std::path::Path; 0]>::{closure#0}
+             at /Users/max/code/zed/crates/project/src/project.rs:2023:26
+  16: <project::Project>::test::<[&std::path::Path; 0]>::{closure#0}
+             at /Users/max/code/zed/crates/project/src/project.rs:1997:55
+  17: agent_ui::agent_panel::tests::test_new_text_thread_action_handler::__test_new_text_thread_action_handler::{closure#0}
+             at ./src/agent_panel.rs:5125:57
+  18: <gpui::executor::ForegroundExecutor>::block_test::<(), agent_ui::agent_panel::tests::test_new_text_thread_action_handler::__test_new_text_thread_action_handler::{closure#0}>::{closure#0}
+             at /Users/max/code/zed/crates/gpui/src/executor.rs:440:36
+  19: <scheduler::test_scheduler::TestScheduler as scheduler::Scheduler>::block
+             at /Users/max/code/zed/crates/scheduler/src/test_scheduler.rs:538:35
+  20: <gpui::executor::ForegroundExecutor>::block_test::<(), agent_ui::agent_panel::tests::test_new_text_thread_action_handler::__test_new_text_thread_action_handler::{closure#0}>
+             at /Users/max/code/zed/crates/gpui/src/executor.rs:447:19
+  21: agent_ui::agent_panel::tests::test_new_text_thread_action_handler::{closure#0}
+             at ./src/agent_panel.rs:5108:5
+  22: gpui::test::run_test::{closure#0}
+             at /Users/max/code/zed/crates/gpui/src/test.rs:88:17
+  23: std::panicking::catch_unwind::do_call::<gpui::test::run_test::{closure#0}, ()>
+             at /Users/max/.rustup/toolchains/1.93-aarch64-apple-darwin/lib/rustlib/src/rust/library/std/src/panicking.rs:581:40
+  24: ___rust_try
+  25: std::panicking::catch_unwind::<(), gpui::test::run_test::{closure#0}>
+             at /Users/max/.rustup/toolchains/1.93-aarch64-apple-darwin/lib/rustlib/src/rust/library/std/src/panicking.rs:544:19
+  26: std::panic::catch_unwind::<gpui::test::run_test::{closure#0}, ()>
+             at /Users/max/.rustup/toolchains/1.93-aarch64-apple-darwin/lib/rustlib/src/rust/library/std/src/panic.rs:359:14
+  27: gpui::test::run_test
+             at /Users/max/code/zed/crates/gpui/src/test.rs:85:26
+  28: agent_ui::agent_panel::tests::test_new_text_thread_action_handler
+             at ./src/agent_panel.rs:5108:5
+  29: agent_ui::agent_panel::tests::test_new_text_thread_action_handler::{closure#0}
+             at ./src/agent_panel.rs:5108:18
+  30: <agent_ui::agent_panel::tests::test_new_text_thread_action_handler::{closure#0} as core::ops::function::FnOnce<()>>::call_once
+             at /Users/max/.rustup/toolchains/1.93-aarch64-apple-darwin/lib/rustlib/src/rust/library/core/src/ops/function.rs:250:5
+  31: core::ops::function::FnOnce::call_once
+             at /rustc/01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf/library/core/src/ops/function.rs:250:5
+  32: test::__rust_begin_short_backtrace
+             at /rustc/01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf/library/test/src/lib.rs:663:18
+  33: test::run_test_in_process::{{closure}}
+             at /rustc/01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf/library/test/src/lib.rs:686:74
+  34: <core::panic::unwind_safe::AssertUnwindSafe<F> as core::ops::function::FnOnce<()>>::call_once
+             at /rustc/01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf/library/core/src/panic/unwind_safe.rs:274:9
+  35: std::panicking::catch_unwind::do_call
+             at /rustc/01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf/library/std/src/panicking.rs:581:40
+  36: std::panicking::catch_unwind
+             at /rustc/01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf/library/std/src/panicking.rs:544:19
+  37: std::panic::catch_unwind
+             at /rustc/01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf/library/std/src/panic.rs:359:14
+  38: test::run_test_in_process
+             at /rustc/01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf/library/test/src/lib.rs:686:27
+  39: test::run_test::{{closure}}
+             at /rustc/01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf/library/test/src/lib.rs:607:43
+  40: test::run_test::{{closure}}
+             at /rustc/01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf/library/test/src/lib.rs:637:41
+  41: std::sys::backtrace::__rust_begin_short_backtrace
+             at /rustc/01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf/library/std/src/sys/backtrace.rs:160:18
+  42: std::thread::lifecycle::spawn_unchecked::{{closure}}::{{closure}}
+             at /rustc/01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf/library/std/src/thread/lifecycle.rs:92:13
+  43: <core::panic::unwind_safe::AssertUnwindSafe<F> as core::ops::function::FnOnce<()>>::call_once
+             at /rustc/01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf/library/core/src/panic/unwind_safe.rs:274:9
+  44: std::panicking::catch_unwind::do_call
+             at /rustc/01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf/library/std/src/panicking.rs:581:40
+  45: std::panicking::catch_unwind
+             at /rustc/01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf/library/std/src/panicking.rs:544:19
+  46: std::panic::catch_unwind
+             at /rustc/01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf/library/std/src/panic.rs:359:14
+  47: std::thread::lifecycle::spawn_unchecked::{{closure}}
+             at /rustc/01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf/library/std/src/thread/lifecycle.rs:90:26
+  48: core::ops::function::FnOnce::call_once{{vtable.shim}}
+             at /rustc/01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf/library/core/src/ops/function.rs:250:5
+  49: <alloc::boxed::Box<F,A> as core::ops::function::FnOnce<Args>>::call_once
+             at /rustc/01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf/library/alloc/src/boxed.rs:2206:9
+  50: std::sys::thread::unix::Thread::new::thread_start
+             at /rustc/01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf/library/std/src/sys/thread/unix.rs:118:17
+  51: __pthread_cond_wait
+

plan.md 🔗

@@ -0,0 +1,2 @@
+I want to restructure the workspace so that in addition to Panes in the Center, and Docks around the edges, you can add a view called a `inner_left_view` that appears after the Left dock with the same height as the left dock, or an `inner_right_view`, that appears before the right dock, with the same height as the left dock. These inner left and right views should have flexible width.
+

tooling/xtask/src/tasks/workflows/extension_bump.rs 🔗

@@ -430,7 +430,7 @@ fn release_action(
     named::uses(
         "zed-extensions",
         "update-action",
-        "543925fc45da8866b0d017218a656c8a3296ed3f",
+        "1ef53b23be40fe2549be0baffaa98e9f51838fef",
     )
     .add_with(("extension-name", extension_id.to_string()))
     .add_with(("push-to", "zed-industries/extensions"))