Merge remote-tracking branch 'origin/main' into AI-112/delete-worktree-on-archive

Richard Feldman created

# Conflicts:
#	crates/fs/src/fake_git_repo.rs

Change summary

Cargo.lock                                              | 466 +++++-----
Cargo.toml                                              |   8 
assets/icons/maximize_alt.svg                           |   6 
assets/icons/thread_import.svg                          |   5 
crates/agent_ui/src/agent_panel.rs                      |  11 
crates/agent_ui/src/config_options.rs                   |   2 
crates/agent_ui/src/thread_import.rs                    |  49 
crates/agent_ui/src/threads_archive_view.rs             |  75 -
crates/agent_ui/src/ui.rs                               |   2 
crates/agent_ui/src/ui/claude_agent_onboarding_modal.rs | 261 ------
crates/agent_ui/src/ui/model_selector_components.rs     |   2 
crates/collab/tests/integration/git_tests.rs            |   4 
crates/command_palette/src/command_palette.rs           |  22 
crates/edit_prediction/src/license_detection.rs         |   4 
crates/editor/src/editor_tests.rs                       |  59 +
crates/editor/src/element.rs                            |   7 
crates/extension_host/src/wasm_host.rs                  |  47 
crates/extension_host/src/wasm_host/wit.rs              |   8 
crates/extension_host/src/wasm_host/wit/since_v0_0_1.rs |  14 
crates/extension_host/src/wasm_host/wit/since_v0_0_4.rs |  14 
crates/extension_host/src/wasm_host/wit/since_v0_0_6.rs |  14 
crates/extension_host/src/wasm_host/wit/since_v0_1_0.rs |  14 
crates/extension_host/src/wasm_host/wit/since_v0_2_0.rs |  14 
crates/extension_host/src/wasm_host/wit/since_v0_3_0.rs |  14 
crates/extension_host/src/wasm_host/wit/since_v0_4_0.rs |  14 
crates/extension_host/src/wasm_host/wit/since_v0_5_0.rs |  14 
crates/extension_host/src/wasm_host/wit/since_v0_6_0.rs |  14 
crates/extension_host/src/wasm_host/wit/since_v0_8_0.rs |  14 
crates/fs/src/fake_git_repo.rs                          |   3 
crates/git/src/repository.rs                            |  18 
crates/git_graph/src/git_graph.rs                       |   6 
crates/git_ui/src/branch_picker.rs                      |  18 
crates/git_ui/src/stash_picker.rs                       |  54 
crates/git_ui/src/worktree_picker.rs                    |  49 
crates/gpui/examples/input.rs                           |  14 
crates/gpui/src/platform.rs                             |   2 
crates/gpui/src/window.rs                               |   6 
crates/gpui_linux/src/linux/wayland/client.rs           |   4 
crates/gpui_linux/src/linux/wayland/window.rs           |  12 
crates/gpui_linux/src/linux/x11/window.rs               |   5 
crates/gpui_macos/Cargo.toml                            |   1 
crates/gpui_macos/src/window.rs                         |   5 
crates/gpui_windows/src/window.rs                       |   9 
crates/icons/src/icons.rs                               |   2 
crates/markdown/src/markdown.rs                         |  10 
crates/project/src/git_store.rs                         |   2 
crates/project/src/lsp_store.rs                         |   3 
crates/project/src/manifest_tree.rs                     |   1 
crates/project/src/worktree_store.rs                    |   4 
crates/proto/proto/git.proto                            |   1 
crates/recent_projects/src/recent_projects.rs           |  27 
crates/recent_projects/src/remote_servers.rs            |  40 
crates/rules_library/src/rules_library.rs               |   2 
crates/sidebar/src/project_group_builder.rs             |   1 
crates/sidebar/src/sidebar.rs                           |  43 
crates/sidebar/src/sidebar_tests.rs                     |  17 
crates/tab_switcher/src/tab_switcher.rs                 |   2 
crates/tasks_ui/src/modal.rs                            |   2 
crates/title_bar/src/onboarding_banner.rs               |  26 
crates/title_bar/src/title_bar.rs                       |  21 
crates/ui/src/components/list/list_item.rs              |  75 +
crates/workspace/src/multi_workspace.rs                 | 175 ----
crates/workspace/src/multi_workspace_tests.rs           | 172 ++++
crates/workspace/src/workspace.rs                       |   2 
crates/worktree/src/worktree.rs                         |  35 
crates/worktree/tests/integration/main.rs               |  66 +
crates/zed_actions/src/lib.rs                           |   2 
67 files changed, 1,082 insertions(+), 1,033 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -111,22 +111,13 @@ dependencies = [
  "workspace",
 ]
 
-[[package]]
-name = "addr2line"
-version = "0.24.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1"
-dependencies = [
- "gimli 0.31.1",
-]
-
 [[package]]
 name = "addr2line"
 version = "0.25.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "1b5d307320b3181d6d7954e663bd7c774a838b8220fe0593c86d9fb09f498b4b"
 dependencies = [
- "gimli 0.32.3",
+ "gimli",
 ]
 
 [[package]]
@@ -674,7 +665,7 @@ version = "0.5.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "7eb93bbb63b9c227414f6eb3a0adfddca591a8ce1e9b60661bb08969b87e340b"
 dependencies = [
- "object 0.37.3",
+ "object",
 ]
 
 [[package]]
@@ -1821,11 +1812,11 @@ version = "0.3.76"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "bb531853791a215d7c62a30daf0dde835f381ab5de4589cfe7c649d2cbe92bd6"
 dependencies = [
- "addr2line 0.25.1",
+ "addr2line",
  "cfg-if",
  "libc",
  "miniz_oxide",
- "object 0.37.3",
+ "object",
  "rustc-demangle",
  "windows-link 0.2.1",
 ]
@@ -3818,36 +3809,36 @@ dependencies = [
 
 [[package]]
 name = "cranelift-assembler-x64"
-version = "0.120.2"
+version = "0.123.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a5023e06632d8f351c2891793ccccfe4aef957954904392434038745fb6f1f68"
+checksum = "ba33ddc4e157cb1abe9da6c821e8824f99e56d057c2c22536850e0141f281d61"
 dependencies = [
  "cranelift-assembler-x64-meta",
 ]
 
 [[package]]
 name = "cranelift-assembler-x64-meta"
-version = "0.120.2"
+version = "0.123.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b1c4012b4c8c1f6eb05c0a0a540e3e1ee992631af51aa2bbb3e712903ce4fd65"
+checksum = "69b23dd6ea360e6fb28a3f3b40b7f126509668f58076a4729b2cfd656f26a0ad"
 dependencies = [
  "cranelift-srcgen",
 ]
 
 [[package]]
 name = "cranelift-bforest"
-version = "0.120.2"
+version = "0.123.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4d6d883b4942ef3a7104096b8bc6f2d1a41393f159ac8de12aed27b25d67f895"
+checksum = "a9d81afcee8fe27ee2536987df3fadcb2e161af4edb7dbe3ef36838d0ce74382"
 dependencies = [
  "cranelift-entity",
 ]
 
 [[package]]
 name = "cranelift-bitset"
-version = "0.120.2"
+version = "0.123.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "db7b2ee9eec6ca8a716d900d5264d678fb2c290c58c46c8da7f94ee268175d17"
+checksum = "fb33595f1279fe7af03b28245060e9085caf98b10ed3137461a85796eb83972a"
 dependencies = [
  "serde",
  "serde_derive",
@@ -3855,9 +3846,9 @@ dependencies = [
 
 [[package]]
 name = "cranelift-codegen"
-version = "0.120.2"
+version = "0.123.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "aeda0892577afdce1ac2e9a983a55f8c5b87a59334e1f79d8f735a2d7ba4f4b4"
+checksum = "0230a6ac0660bfe31eb244cbb43dcd4f2b3c1c4e0addc3e0348c6053ea60272e"
 dependencies = [
  "bumpalo",
  "cranelift-assembler-x64",
@@ -3868,7 +3859,7 @@ dependencies = [
  "cranelift-control",
  "cranelift-entity",
  "cranelift-isle",
- "gimli 0.31.1",
+ "gimli",
  "hashbrown 0.15.5",
  "log",
  "postcard",
@@ -3880,40 +3871,42 @@ dependencies = [
  "sha2",
  "smallvec",
  "target-lexicon 0.13.3",
+ "wasmtime-internal-math",
 ]
 
 [[package]]
 name = "cranelift-codegen-meta"
-version = "0.120.2"
+version = "0.123.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e461480d87f920c2787422463313326f67664e68108c14788ba1676f5edfcd15"
+checksum = "96d6817fdc15cb8f236fc9d8e610767d3a03327ceca4abff7a14d8e2154c405e"
 dependencies = [
  "cranelift-assembler-x64-meta",
  "cranelift-codegen-shared",
  "cranelift-srcgen",
+ "heck 0.5.0",
  "pulley-interpreter",
 ]
 
 [[package]]
 name = "cranelift-codegen-shared"
-version = "0.120.2"
+version = "0.123.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "976584d09f200c6c84c4b9ff7af64fc9ad0cb64dffa5780991edd3fe143a30a1"
+checksum = "0403796328e9e2e7df2b80191cdbb473fd9ea3889eb45ef5632d0fef168ea032"
 
 [[package]]
 name = "cranelift-control"
-version = "0.120.2"
+version = "0.123.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "46d43d70f4e17c545aa88dbf4c84d4200755d27c6e3272ebe4de65802fa6a955"
+checksum = "188f04092279a3814e0b6235c2f9c2e34028e4beb72da7bfed55cbd184702bcc"
 dependencies = [
  "arbitrary",
 ]
 
 [[package]]
 name = "cranelift-entity"
-version = "0.120.2"
+version = "0.123.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d75418674520cb400c8772bfd6e11a62736c78fc1b6e418195696841d1bf91f1"
+checksum = "43f5e7391167605d505fe66a337e1a69583b3f34b63d359ffa5a430313c555e8"
 dependencies = [
  "cranelift-bitset",
  "serde",
@@ -3922,9 +3915,9 @@ dependencies = [
 
 [[package]]
 name = "cranelift-frontend"
-version = "0.120.2"
+version = "0.123.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3c8b1a91c86687a344f3c52dd6dfb6e50db0dfa7f2e9c7711b060b3623e1fdeb"
+checksum = "ea5440792eb2b5ba0a0976df371b9f94031bd853ae56f389de610bca7128a7cb"
 dependencies = [
  "cranelift-codegen",
  "log",
@@ -3934,15 +3927,15 @@ dependencies = [
 
 [[package]]
 name = "cranelift-isle"
-version = "0.120.2"
+version = "0.123.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "711baa4e3432d4129295b39ec2b4040cc1b558874ba0a37d08e832e857db7285"
+checksum = "1e5c05fab6fce38d729088f3fa1060eaa1ad54eefd473588887205ed2ab2f79e"
 
 [[package]]
 name = "cranelift-native"
-version = "0.120.2"
+version = "0.123.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "41c83e8666e3bcc5ffeaf6f01f356f0e1f9dcd69ce5511a1efd7ca5722001a3f"
+checksum = "9c9a0607a028edf5ba5bba7e7cf5ca1b7f0a030e3ae84dcd401e8b9b05192280"
 dependencies = [
  "cranelift-codegen",
  "libc",
@@ -3951,9 +3944,9 @@ dependencies = [
 
 [[package]]
 name = "cranelift-srcgen"
-version = "0.120.2"
+version = "0.123.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "02e3f4d783a55c64266d17dc67d2708852235732a100fc40dd9f1051adc64d7b"
+checksum = "cb0f2da72eb2472aaac6cfba4e785af42b1f2d82f5155f30c9c30e8cce351e17"
 
 [[package]]
 name = "crash-context"
@@ -7054,21 +7047,15 @@ dependencies = [
 
 [[package]]
 name = "gimli"
-version = "0.31.1"
+version = "0.32.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f"
+checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7"
 dependencies = [
  "fallible-iterator",
  "indexmap",
  "stable_deref_trait",
 ]
 
-[[package]]
-name = "gimli"
-version = "0.32.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7"
-
 [[package]]
 name = "gio-sys"
 version = "0.21.5"
@@ -7614,6 +7601,7 @@ dependencies = [
  "media",
  "metal",
  "objc",
+ "objc2-app-kit",
  "parking_lot",
  "pathfinder_geometry",
  "raw-window-handle",
@@ -11224,6 +11212,16 @@ dependencies = [
  "objc2-encode",
 ]
 
+[[package]]
+name = "objc2-app-kit"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e6f29f568bec459b0ddff777cec4fe3fd8666d82d5a40ebd0ff7e66134f89bcc"
+dependencies = [
+ "objc2",
+ "objc2-foundation",
+]
+
 [[package]]
 name = "objc2-audio-toolbox"
 version = "0.3.2"
@@ -11361,9 +11359,9 @@ dependencies = [
 
 [[package]]
 name = "object"
-version = "0.36.7"
+version = "0.37.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87"
+checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe"
 dependencies = [
  "crc32fast",
  "hashbrown 0.15.5",
@@ -11371,15 +11369,6 @@ dependencies = [
  "memchr",
 ]
 
-[[package]]
-name = "object"
-version = "0.37.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe"
-dependencies = [
- "memchr",
-]
-
 [[package]]
 name = "ollama"
 version = "0.1.0"
@@ -13518,13 +13507,25 @@ checksum = "bd348ff538bc9caeda7ee8cad2d1d48236a1f443c1fa3913c6a02fe0043b1dd3"
 
 [[package]]
 name = "pulley-interpreter"
-version = "33.0.2"
+version = "36.0.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "986beaef947a51d17b42b0ea18ceaa88450d35b6994737065ed505c39172db71"
+checksum = "499d922aa0f9faac8d92351416664f1b7acd914008a90fce2f0516d31efddf67"
 dependencies = [
  "cranelift-bitset",
  "log",
- "wasmtime-math",
+ "pulley-macros",
+ "wasmtime-internal-math",
+]
+
+[[package]]
+name = "pulley-macros"
+version = "36.0.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a3848fb193d6dffca43a21f24ca9492f22aab88af1223d06bac7f8a0ef405b81"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
 ]
 
 [[package]]
@@ -16195,12 +16196,6 @@ dependencies = [
  "der 0.7.10",
 ]
 
-[[package]]
-name = "sptr"
-version = "0.3.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3b9b39299b249ad65f3b7e96443bad61c02ca5cd3589f46cb6d610a0fd6c0d6a"
-
 [[package]]
 name = "sqlez"
 version = "0.1.0"
@@ -18243,17 +18238,6 @@ dependencies = [
  "windows-targets 0.52.6",
 ]
 
-[[package]]
-name = "trait-variant"
-version = "0.1.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "70977707304198400eb4835a78f6a9f928bf41bba420deb8fdb175cd965d77a7"
-dependencies = [
- "proc-macro2",
- "quote",
- "syn 2.0.117",
-]
-
 [[package]]
 name = "transpose"
 version = "0.2.3"
@@ -18266,9 +18250,9 @@ dependencies = [
 
 [[package]]
 name = "tree-sitter"
-version = "0.26.3"
+version = "0.26.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "974d205cc395652cfa8b37daa053fe56eebd429acf8dc055503fee648dae981e"
+checksum = "887bd495d0582c5e3e0d8ece2233666169fa56a9644d172fc22ad179ab2d0538"
 dependencies = [
  "cc",
  "regex",
@@ -19361,12 +19345,12 @@ dependencies = [
 
 [[package]]
 name = "wasm-encoder"
-version = "0.229.0"
+version = "0.236.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "38ba1d491ecacb085a2552025c10a675a6fddcbd03b1fc9b36c536010ce265d2"
+checksum = "724fccfd4f3c24b7e589d333fc0429c68042897a7e8a5f8694f31792471841e7"
 dependencies = [
  "leb128fmt",
- "wasmparser 0.229.0",
+ "wasmparser 0.236.1",
 ]
 
 [[package]]
@@ -19489,9 +19473,9 @@ dependencies = [
 
 [[package]]
 name = "wasmparser"
-version = "0.229.0"
+version = "0.236.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0cc3b1f053f5d41aa55640a1fa9b6d1b8a9e4418d118ce308d20e24ff3575a8c"
+checksum = "a9b1e81f3eb254cf7404a82cee6926a4a3ccc5aad80cc3d43608a070c67aa1d7"
 dependencies = [
  "bitflags 2.10.0",
  "hashbrown 0.15.5",
@@ -19514,22 +19498,22 @@ dependencies = [
 
 [[package]]
 name = "wasmprinter"
-version = "0.229.0"
+version = "0.236.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d25dac01892684a99b8fbfaf670eb6b56edea8a096438c75392daeb83156ae2e"
+checksum = "2df225df06a6df15b46e3f73ca066ff92c2e023670969f7d50ce7d5e695abbb1"
 dependencies = [
  "anyhow",
  "termcolor",
- "wasmparser 0.229.0",
+ "wasmparser 0.236.1",
 ]
 
 [[package]]
 name = "wasmtime"
-version = "33.0.2"
+version = "36.0.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "57373e1d8699662fb791270ac5dfac9da5c14f618ecf940cdb29dc3ad9472a3c"
+checksum = "6a2f8736ddc86e03a9d0e4c477a37939cfc53cd1b052ee38a3133679b87ef830"
 dependencies = [
- "addr2line 0.24.2",
+ "addr2line",
  "anyhow",
  "async-trait",
  "bitflags 2.10.0",
@@ -19543,10 +19527,9 @@ dependencies = [
  "log",
  "mach2 0.4.3",
  "memfd",
- "object 0.36.7",
+ "object",
  "once_cell",
  "postcard",
- "psm",
  "pulley-interpreter",
  "rayon",
  "rustix 1.1.2",
@@ -19554,82 +19537,109 @@ dependencies = [
  "serde",
  "serde_derive",
  "smallvec",
- "sptr",
  "target-lexicon 0.13.3",
- "trait-variant",
- "wasmparser 0.229.0",
- "wasmtime-asm-macros",
- "wasmtime-component-macro",
- "wasmtime-component-util",
- "wasmtime-cranelift",
+ "wasmparser 0.236.1",
  "wasmtime-environ",
- "wasmtime-fiber",
- "wasmtime-jit-icache-coherence",
- "wasmtime-math",
- "wasmtime-slab",
- "wasmtime-versioned-export-macros",
- "wasmtime-winch",
- "windows-sys 0.59.0",
+ "wasmtime-internal-asm-macros",
+ "wasmtime-internal-component-macro",
+ "wasmtime-internal-component-util",
+ "wasmtime-internal-cranelift",
+ "wasmtime-internal-fiber",
+ "wasmtime-internal-jit-debug",
+ "wasmtime-internal-jit-icache-coherence",
+ "wasmtime-internal-math",
+ "wasmtime-internal-slab",
+ "wasmtime-internal-unwinder",
+ "wasmtime-internal-versioned-export-macros",
+ "wasmtime-internal-winch",
+ "windows-sys 0.60.2",
 ]
 
 [[package]]
-name = "wasmtime-asm-macros"
-version = "33.0.2"
+name = "wasmtime-c-api-impl"
+version = "36.0.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bd0fc91372865167a695dc98d0d6771799a388a7541d3f34e939d0539d6583de"
+checksum = "f3c62ea3fa30e6b0cf61116b3035121b8f515c60ac118ebfdab2ee56d028ed1e"
 dependencies = [
- "cfg-if",
+ "anyhow",
+ "log",
+ "tracing",
+ "wasmtime",
+ "wasmtime-internal-c-api-macros",
 ]
 
 [[package]]
-name = "wasmtime-c-api-impl"
-version = "33.0.2"
+name = "wasmtime-environ"
+version = "36.0.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "46db556f1dccdd88e0672bd407162ab0036b72e5eccb0f4398d8251cba32dba1"
+checksum = "733682a327755c77153ac7455b1ba8f2db4d9946c1738f8002fe1fbda1d52e83"
 dependencies = [
  "anyhow",
+ "cpp_demangle",
+ "cranelift-bitset",
+ "cranelift-entity",
+ "gimli",
+ "indexmap",
  "log",
- "tracing",
- "wasmtime",
- "wasmtime-c-api-macros",
+ "object",
+ "postcard",
+ "rustc-demangle",
+ "semver",
+ "serde",
+ "serde_derive",
+ "smallvec",
+ "target-lexicon 0.13.3",
+ "wasm-encoder 0.236.1",
+ "wasmparser 0.236.1",
+ "wasmprinter",
+ "wasmtime-internal-component-util",
 ]
 
 [[package]]
-name = "wasmtime-c-api-macros"
-version = "33.0.2"
+name = "wasmtime-internal-asm-macros"
+version = "36.0.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "315cc6bc8cdc66f296accb26d7625ae64c1c7b6da6f189e8a72ce6594bf7bd36"
+checksum = "68288980a2e02bcb368d436da32565897033ea21918007e3f2bae18843326cf9"
+dependencies = [
+ "cfg-if",
+]
+
+[[package]]
+name = "wasmtime-internal-c-api-macros"
+version = "36.0.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3c8c61294155a6d23c202f08cf7a2f9392a866edd50517508208818be626ce9f"
 dependencies = [
  "proc-macro2",
  "quote",
 ]
 
 [[package]]
-name = "wasmtime-component-macro"
-version = "33.0.2"
+name = "wasmtime-internal-component-macro"
+version = "36.0.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "25c9c7526675ff9a9794b115023c4af5128e3eb21389bfc3dc1fd344d549258f"
+checksum = "5dea846da68f8e776c8a43bde3386022d7bb74e713b9654f7c0196e5ff2e4684"
 dependencies = [
  "anyhow",
  "proc-macro2",
  "quote",
  "syn 2.0.117",
- "wasmtime-component-util",
- "wasmtime-wit-bindgen",
- "wit-parser 0.229.0",
+ "wasmtime-internal-component-util",
+ "wasmtime-internal-wit-bindgen",
+ "wit-parser 0.236.1",
 ]
 
 [[package]]
-name = "wasmtime-component-util"
-version = "33.0.2"
+name = "wasmtime-internal-component-util"
+version = "36.0.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cc42ec8b078875804908d797cb4950fec781d9add9684c9026487fd8eb3f6291"
+checksum = "fe1e5735b3c8251510d2a55311562772d6c6fca9438a3d0329eb6e38af4957d6"
 
 [[package]]
-name = "wasmtime-cranelift"
-version = "33.0.2"
+name = "wasmtime-internal-cranelift"
+version = "36.0.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b2bd72f0a6a0ffcc6a184ec86ac35c174e48ea0e97bbae277c8f15f8bf77a566"
+checksum = "e89bb9ef571288e2be6b8a3c4763acc56c348dcd517500b1679d3ffad9e4a757"
 dependencies = [
  "anyhow",
  "cfg-if",
@@ -19638,104 +19648,132 @@ dependencies = [
  "cranelift-entity",
  "cranelift-frontend",
  "cranelift-native",
- "gimli 0.31.1",
+ "gimli",
  "itertools 0.14.0",
  "log",
- "object 0.36.7",
+ "object",
  "pulley-interpreter",
  "smallvec",
  "target-lexicon 0.13.3",
  "thiserror 2.0.17",
- "wasmparser 0.229.0",
+ "wasmparser 0.236.1",
  "wasmtime-environ",
- "wasmtime-versioned-export-macros",
+ "wasmtime-internal-math",
+ "wasmtime-internal-versioned-export-macros",
 ]
 
 [[package]]
-name = "wasmtime-environ"
-version = "33.0.2"
+name = "wasmtime-internal-fiber"
+version = "36.0.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e6187bb108a23eb25d2a92aa65d6c89fb5ed53433a319038a2558567f3011ff2"
+checksum = "b698d004b15ea1f1ae2d06e5e8b80080cbd684fd245220ce2fac3cdd5ecf87f2"
 dependencies = [
  "anyhow",
- "cpp_demangle",
- "cranelift-bitset",
- "cranelift-entity",
- "gimli 0.31.1",
- "indexmap",
- "log",
- "object 0.36.7",
- "postcard",
- "rustc-demangle",
- "semver",
- "serde",
- "serde_derive",
- "smallvec",
- "target-lexicon 0.13.3",
- "wasm-encoder 0.229.0",
- "wasmparser 0.229.0",
- "wasmprinter",
- "wasmtime-component-util",
+ "cc",
+ "cfg-if",
+ "libc",
+ "rustix 1.1.2",
+ "wasmtime-internal-asm-macros",
+ "wasmtime-internal-versioned-export-macros",
+ "windows-sys 0.60.2",
 ]
 
 [[package]]
-name = "wasmtime-fiber"
-version = "33.0.2"
+name = "wasmtime-internal-jit-debug"
+version = "36.0.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "dc8965d2128c012329f390e24b8b2758dd93d01bf67e1a1a0dd3d8fd72f56873"
+checksum = "c803a9fec05c3d7fa03474d4595079d546e77a3c71c1d09b21f74152e2165c17"
 dependencies = [
- "anyhow",
  "cc",
- "cfg-if",
- "rustix 1.1.2",
- "wasmtime-asm-macros",
- "wasmtime-versioned-export-macros",
- "windows-sys 0.59.0",
+ "wasmtime-internal-versioned-export-macros",
 ]
 
 [[package]]
-name = "wasmtime-jit-icache-coherence"
-version = "33.0.2"
+name = "wasmtime-internal-jit-icache-coherence"
+version = "36.0.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7af0e940cb062a45c0b3f01a926f77da5947149e99beb4e3dd9846d5b8f11619"
+checksum = "d3866909d37f7929d902e6011847748147e8734e9d7e0353e78fb8b98f586aee"
 dependencies = [
  "anyhow",
  "cfg-if",
  "libc",
- "windows-sys 0.59.0",
+ "windows-sys 0.60.2",
 ]
 
 [[package]]
-name = "wasmtime-math"
-version = "33.0.2"
+name = "wasmtime-internal-math"
+version = "36.0.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "acfca360e719dda9a27e26944f2754ff2fd5bad88e21919c42c5a5f38ddd93cb"
+checksum = "5a23b03fb14c64bd0dfcaa4653101f94ade76c34a3027ed2d6b373267536e45b"
 dependencies = [
  "libm",
 ]
 
 [[package]]
-name = "wasmtime-slab"
-version = "33.0.2"
+name = "wasmtime-internal-slab"
+version = "36.0.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "48e240559cada55c4b24af979d5f6c95e0029f5772f32027ec3c62b258aaff65"
+checksum = "fbff220b88cdb990d34a20b13344e5da2e7b99959a5b1666106bec94b58d6364"
 
 [[package]]
-name = "wasmtime-versioned-export-macros"
-version = "33.0.2"
+name = "wasmtime-internal-unwinder"
+version = "36.0.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d0963c1438357a3d8c0efe152b4ef5259846c1cf8b864340270744fe5b3bae5e"
+checksum = "13e1ad30e88988b20c0d1c56ea4b4fbc01a8c614653cbf12ca50c0dcc695e2f7"
+dependencies = [
+ "anyhow",
+ "cfg-if",
+ "cranelift-codegen",
+ "log",
+ "object",
+]
+
+[[package]]
+name = "wasmtime-internal-versioned-export-macros"
+version = "36.0.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "549aefdaa1398c2fcfbf69a7b882956bb5b6e8e5b600844ecb91a3b5bf658ca7"
 dependencies = [
  "proc-macro2",
  "quote",
  "syn 2.0.117",
 ]
 
+[[package]]
+name = "wasmtime-internal-winch"
+version = "36.0.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5cc96a84c5700171aeecf96fa9a9ab234f333f5afb295dabf3f8a812b70fe832"
+dependencies = [
+ "anyhow",
+ "cranelift-codegen",
+ "gimli",
+ "object",
+ "target-lexicon 0.13.3",
+ "wasmparser 0.236.1",
+ "wasmtime-environ",
+ "wasmtime-internal-cranelift",
+ "winch-codegen",
+]
+
+[[package]]
+name = "wasmtime-internal-wit-bindgen"
+version = "36.0.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c28dc9efea511598c88564ac1974e0825c07d9c0de902dbf68f227431cd4ff8c"
+dependencies = [
+ "anyhow",
+ "bitflags 2.10.0",
+ "heck 0.5.0",
+ "indexmap",
+ "wit-parser 0.236.1",
+]
+
 [[package]]
 name = "wasmtime-wasi"
-version = "33.0.2"
+version = "36.0.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4ae951b72c7c6749a1c15dcdfb6d940a2614c932b4a54f474636e78e2c744b4c"
+checksum = "c3c2e99fbaa0c26b4680e0c9af07e3f7b25f5fbc1ad97dd34067980bd027d3e5"
 dependencies = [
  "anyhow",
  "async-trait",
@@ -19759,14 +19797,14 @@ dependencies = [
  "wasmtime",
  "wasmtime-wasi-io",
  "wiggle",
- "windows-sys 0.59.0",
+ "windows-sys 0.60.2",
 ]
 
 [[package]]
 name = "wasmtime-wasi-io"
-version = "33.0.2"
+version = "36.0.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a835790dcecc3d7051ec67da52ba9e04af25e1bc204275b9391e3f0042b10797"
+checksum = "de2dc367052562c228ce51ee4426330840433c29c0ea3349eca5ddeb475ecdb9"
 dependencies = [
  "anyhow",
  "async-trait",
@@ -19775,35 +19813,6 @@ dependencies = [
  "wasmtime",
 ]
 
-[[package]]
-name = "wasmtime-winch"
-version = "33.0.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cbc3b117d03d6eeabfa005a880c5c22c06503bb8820f3aa2e30f0e8d87b6752f"
-dependencies = [
- "anyhow",
- "cranelift-codegen",
- "gimli 0.31.1",
- "object 0.36.7",
- "target-lexicon 0.13.3",
- "wasmparser 0.229.0",
- "wasmtime-cranelift",
- "wasmtime-environ",
- "winch-codegen",
-]
-
-[[package]]
-name = "wasmtime-wit-bindgen"
-version = "33.0.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1382f4f09390eab0d75d4994d0c3b0f6279f86a571807ec67a8253c87cf6a145"
-dependencies = [
- "anyhow",
- "heck 0.5.0",
- "indexmap",
- "wit-parser 0.229.0",
-]
-
 [[package]]
 name = "wast"
 version = "35.0.2"
@@ -20258,9 +20267,9 @@ dependencies = [
 
 [[package]]
 name = "wiggle"
-version = "33.0.2"
+version = "36.0.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "649c1aca13ef9e9dccf2d5efbbebf12025bc5521c3fb7754355ef60f5eb810be"
+checksum = "c13d1ae265bd6e5e608827d2535665453cae5cb64950de66e2d5767d3e32c43a"
 dependencies = [
  "anyhow",
  "async-trait",
@@ -20273,9 +20282,9 @@ dependencies = [
 
 [[package]]
 name = "wiggle-generate"
-version = "33.0.2"
+version = "36.0.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "164870fc34214ee42bd81b8ce9e7c179800fa1a7d4046d17a84e7f7bf422c8ad"
+checksum = "607c4966f6b30da20d24560220137cbd09df722f0558eac81c05624700af5e05"
 dependencies = [
  "anyhow",
  "heck 0.5.0",
@@ -20287,9 +20296,9 @@ dependencies = [
 
 [[package]]
 name = "wiggle-macro"
-version = "33.0.2"
+version = "36.0.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d873bb5b59ca703b5e41562e96a4796d1af61bf4cf80bf8a7abda755a380ec1c"
+checksum = "fc36e39412fa35f7cc86b3705dbe154168721dd3e71f6dc4a726b266d5c60c55"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -20330,21 +20339,22 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
 
 [[package]]
 name = "winch-codegen"
-version = "33.0.2"
+version = "36.0.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7914c296fbcef59d1b89a15e82384d34dc9669bc09763f2ef068a28dd3a64ebf"
+checksum = "06c0ec09e8eb5e850e432da6271ed8c4a9d459a9db3850c38e98a3ee9d015e79"
 dependencies = [
  "anyhow",
  "cranelift-assembler-x64",
  "cranelift-codegen",
- "gimli 0.31.1",
+ "gimli",
  "regalloc2",
  "smallvec",
  "target-lexicon 0.13.3",
  "thiserror 2.0.17",
- "wasmparser 0.229.0",
- "wasmtime-cranelift",
+ "wasmparser 0.236.1",
  "wasmtime-environ",
+ "wasmtime-internal-cranelift",
+ "wasmtime-internal-math",
 ]
 
 [[package]]
@@ -21370,9 +21380,9 @@ dependencies = [
 
 [[package]]
 name = "wit-parser"
-version = "0.229.0"
+version = "0.236.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "459c6ba62bf511d6b5f2a845a2a736822e38059c1cfa0b644b467bbbfae4efa6"
+checksum = "16e4833a20cd6e85d6abfea0e63a399472d6f88c6262957c17f546879a80ba15"
 dependencies = [
  "anyhow",
  "id-arena",
@@ -21383,7 +21393,7 @@ dependencies = [
  "serde_derive",
  "serde_json",
  "unicode-xid",
- "wasmparser 0.229.0",
+ "wasmparser 0.236.1",
 ]
 
 [[package]]

Cargo.toml 🔗

@@ -604,6 +604,7 @@ nbformat = "1.2.0"
 nix = "0.29"
 num-format = "0.4.4"
 objc = "0.2"
+objc2-app-kit = { version = "0.3", default-features = false, features = [ "NSGraphics" ] }
 objc2-foundation = { version = "=0.3.2", default-features = false, features = [
     "NSArray",
     "NSAttributedString",
@@ -732,7 +733,7 @@ toml_edit = { version = "0.22", default-features = false, features = [
     "serde",
 ] }
 tower-http = "0.4.4"
-tree-sitter = { version = "0.26", features = ["wasm"] }
+tree-sitter = { version = "0.26.8", features = ["wasm"] }
 tree-sitter-bash = "0.25.1"
 tree-sitter-c = "0.24.1"
 tree-sitter-cpp = { git = "https://github.com/tree-sitter/tree-sitter-cpp", rev = "5cb9b693cfd7bfacab1d9ff4acac1a4150700609" }
@@ -767,7 +768,7 @@ uuid = { version = "1.1.2", features = ["v4", "v5", "v7", "serde"] }
 walkdir = "2.5"
 wasm-encoder = "0.221"
 wasmparser = "0.221"
-wasmtime = { version = "33", default-features = false, features = [
+wasmtime = { version = "36", default-features = false, features = [
     "async",
     "demangle",
     "runtime",
@@ -776,7 +777,7 @@ wasmtime = { version = "33", default-features = false, features = [
     "incremental-cache",
     "parallel-compilation",
 ] }
-wasmtime-wasi = "33"
+wasmtime-wasi = "36"
 wax = "0.7"
 which = "6.0.0"
 wasm-bindgen = "0.2.113"
@@ -821,6 +822,7 @@ features = [
     "Win32_System_Com",
     "Win32_System_Com_StructuredStorage",
     "Win32_System_Console",
+    "Win32_System_Diagnostics_Debug",
     "Win32_System_DataExchange",
     "Win32_System_IO",
     "Win32_System_LibraryLoader",

assets/icons/maximize_alt.svg 🔗

@@ -0,0 +1,6 @@
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M2.25 4.80555V3.52777C2.25 3.18889 2.38462 2.86388 2.62425 2.62425C2.86388 2.38462 3.18889 2.25 3.52777 2.25H4.80555" stroke="#C6CAD0" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M11.1945 2.25H12.4722C12.8111 2.25 13.1361 2.38462 13.3758 2.62425C13.6154 2.86388 13.75 3.18889 13.75 3.52777V4.80555" stroke="#C6CAD0" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M13.75 11.1945V12.4722C13.75 12.8111 13.6154 13.1361 13.3758 13.3758C13.1361 13.6154 12.8111 13.75 12.4722 13.75H11.1945" stroke="#C6CAD0" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M4.80555 13.75H3.52777C3.18889 13.75 2.86388 13.6154 2.62425 13.3758C2.38462 13.1361 2.25 12.8111 2.25 12.4722V11.1945" stroke="#C6CAD0" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+</svg>

assets/icons/thread_import.svg 🔗

@@ -0,0 +1,5 @@
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M8.31947 5.03803L8.31947 9.28259" stroke="#C6CAD0" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M6.19576 7.67419L8.31948 9.79792L10.4432 7.67419" stroke="#C6CAD0" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M5.64894 12.8952C6.89401 13.5339 8.32626 13.7069 9.68759 13.383C11.0489 13.0592 12.2499 12.2598 13.0739 11.1288C13.8979 9.99787 14.291 8.60973 14.1821 7.21464C14.0733 5.81955 13.4698 4.5092 12.4803 3.51972C11.4908 2.53024 10.1805 1.92671 8.78535 1.81787C7.39026 1.70904 6.00218 2.10207 4.87122 2.92612C3.74026 3.75018 2.94082 4.95106 2.61695 6.3124C2.29307 7.67374 2.46606 9.10598 3.10475 10.3511L1.80005 14.1999L5.64894 12.8952Z" stroke="#C6CAD0" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+</svg>

crates/agent_ui/src/agent_panel.rs 🔗

@@ -21,8 +21,8 @@ use settings::{LanguageModelProviderSetting, LanguageModelSelection};
 
 use feature_flags::{AgentV2FeatureFlag, FeatureFlagAppExt as _};
 use zed_actions::agent::{
-    AddSelectionToThread, ConflictContent, OpenClaudeAgentOnboardingModal, ReauthenticateAgent,
-    ResolveConflictedFilesWithAgent, ResolveConflictsWithAgent, ReviewBranchDiff,
+    AddSelectionToThread, ConflictContent, ReauthenticateAgent, ResolveConflictedFilesWithAgent,
+    ResolveConflictsWithAgent, ReviewBranchDiff,
 };
 
 use crate::{
@@ -40,7 +40,7 @@ use crate::{
 };
 use crate::{
     DEFAULT_THREAD_TITLE,
-    ui::{AcpOnboardingModal, ClaudeCodeOnboardingModal, HoldForDefault},
+    ui::{AcpOnboardingModal, HoldForDefault},
 };
 use crate::{ExpandMessageEditor, ThreadHistoryView};
 use crate::{ManageProfiles, ThreadHistoryViewEvent};
@@ -245,11 +245,6 @@ pub fn init(cx: &mut App) {
                 .register_action(|workspace, _: &OpenAcpOnboardingModal, window, cx| {
                     AcpOnboardingModal::toggle(workspace, window, cx)
                 })
-                .register_action(
-                    |workspace, _: &OpenClaudeAgentOnboardingModal, window, cx| {
-                        ClaudeCodeOnboardingModal::toggle(workspace, window, cx)
-                    },
-                )
                 .register_action(|_workspace, _: &ResetOnboarding, window, cx| {
                     window.dispatch_action(workspace::RestoreBanner.boxed_clone(), cx);
                     window.refresh();

crates/agent_ui/src/config_options.rs 🔗

@@ -650,7 +650,7 @@ impl PickerDelegate for ConfigOptionPickerDelegate {
                                 .end_slot(div().pr_2().when(is_selected, |this| {
                                     this.child(Icon::new(IconName::Check).color(Color::Accent))
                                 }))
-                                .end_hover_slot(div().pr_1p5().child({
+                                .end_slot_on_hover(div().pr_1p5().child({
                                     let (icon, color, tooltip) = if is_favorite {
                                         (IconName::StarFilled, Color::Accent, "Unfavorite")
                                     } else {

crates/agent_ui/src/thread_import.rs 🔗

@@ -121,18 +121,6 @@ impl ThreadImportModal {
             .collect()
     }
 
-    fn set_agent_checked(&mut self, agent_id: AgentId, state: ToggleState, cx: &mut Context<Self>) {
-        match state {
-            ToggleState::Selected => {
-                self.unchecked_agents.remove(&agent_id);
-            }
-            ToggleState::Unselected | ToggleState::Indeterminate => {
-                self.unchecked_agents.insert(agent_id);
-            }
-        }
-        cx.notify();
-    }
-
     fn toggle_agent_checked(&mut self, agent_id: AgentId, cx: &mut Context<Self>) {
         if self.unchecked_agents.contains(&agent_id) {
             self.unchecked_agents.remove(&agent_id);
@@ -283,6 +271,11 @@ impl ModalView for ThreadImportModal {}
 
 impl Render for ThreadImportModal {
     fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
+        let has_agents = !self.agent_entries.is_empty();
+        let disabled_import_thread = self.is_importing
+            || !has_agents
+            || self.unchecked_agents.len() == self.agent_entries.len();
+
         let agent_rows = self
             .agent_entries
             .iter()
@@ -295,6 +288,7 @@ impl Render for ThreadImportModal {
                     .rounded()
                     .spacing(ListItemSpacing::Sparse)
                     .focused(is_focused)
+                    .disabled(self.is_importing)
                     .child(
                         h_flex()
                             .w_full()
@@ -311,22 +305,14 @@ impl Render for ThreadImportModal {
                             })
                             .child(Label::new(entry.display_name.clone())),
                     )
-                    .end_slot(
-                        Checkbox::new(
-                            ("thread-import-agent-checkbox", ix),
-                            if is_checked {
-                                ToggleState::Selected
-                            } else {
-                                ToggleState::Unselected
-                            },
-                        )
-                        .on_click({
-                            let agent_id = entry.agent_id.clone();
-                            cx.listener(move |this, state: &ToggleState, _window, cx| {
-                                this.set_agent_checked(agent_id.clone(), *state, cx);
-                            })
-                        }),
-                    )
+                    .end_slot(Checkbox::new(
+                        ("thread-import-agent-checkbox", ix),
+                        if is_checked {
+                            ToggleState::Selected
+                        } else {
+                            ToggleState::Unselected
+                        },
+                    ))
                     .on_click({
                         let agent_id = entry.agent_id.clone();
                         cx.listener(move |this, _event, _window, cx| {
@@ -336,11 +322,6 @@ impl Render for ThreadImportModal {
             })
             .collect::<Vec<_>>();
 
-        let has_agents = !self.agent_entries.is_empty();
-        let disabled_import_thread = self.is_importing
-            || !has_agents
-            || self.unchecked_agents.len() == self.agent_entries.len();
-
         v_flex()
             .id("thread-import-modal")
             .key_context("ThreadImportModal")
@@ -373,7 +354,7 @@ impl Render for ThreadImportModal {
                             v_flex()
                                 .id("thread-import-agent-list")
                                 .max_h(rems_from_px(320.))
-                                .pb_2()
+                                .pb_1()
                                 .overflow_y_scroll()
                                 .when(has_agents, |this| this.children(agent_rows))
                                 .when(!has_agents, |this| {

crates/agent_ui/src/threads_archive_view.rs 🔗

@@ -1,5 +1,5 @@
 use crate::agent_connection_store::AgentConnectionStore;
-use crate::thread_import::{AcpThreadImportOnboarding, ThreadImportModal};
+
 use crate::thread_metadata_store::{ThreadMetadata, ThreadMetadataStore};
 use crate::{Agent, RemoveSelectedThread};
 
@@ -15,15 +15,13 @@ use gpui::{
 };
 use itertools::Itertools as _;
 use menu::{Confirm, SelectFirst, SelectLast, SelectNext, SelectPrevious};
-use project::{AgentId, AgentRegistryStore, AgentServerStore};
+use project::{AgentId, AgentServerStore};
 use settings::Settings as _;
 use theme::ActiveTheme;
 use ui::ThreadItem;
 use ui::{
     Divider, KeyBinding, Tooltip, WithScrollbar, prelude::*, utils::platform_title_bar_height,
 };
-use util::ResultExt;
-use workspace::{MultiWorkspace, Workspace};
 
 use zed_actions::agents_sidebar::FocusSidebarFilter;
 use zed_actions::editor::{MoveDown, MoveUp};
@@ -114,18 +112,12 @@ pub struct ThreadsArchiveView {
     _refresh_history_task: Task<()>,
     agent_connection_store: WeakEntity<AgentConnectionStore>,
     agent_server_store: WeakEntity<AgentServerStore>,
-    agent_registry_store: WeakEntity<AgentRegistryStore>,
-    workspace: WeakEntity<Workspace>,
-    multi_workspace: WeakEntity<MultiWorkspace>,
 }
 
 impl ThreadsArchiveView {
     pub fn new(
         agent_connection_store: WeakEntity<AgentConnectionStore>,
         agent_server_store: WeakEntity<AgentServerStore>,
-        agent_registry_store: WeakEntity<AgentRegistryStore>,
-        workspace: WeakEntity<Workspace>,
-        multi_workspace: WeakEntity<MultiWorkspace>,
         window: &mut Window,
         cx: &mut Context<Self>,
     ) -> Self {
@@ -184,11 +176,8 @@ impl ThreadsArchiveView {
                 thread_metadata_store_subscription,
             ],
             _refresh_history_task: Task::ready(()),
-            agent_registry_store,
             agent_connection_store,
             agent_server_store,
-            workspace,
-            multi_workspace,
         };
 
         this.update_items(cx);
@@ -550,43 +539,6 @@ impl ThreadsArchiveView {
         .detach_and_log_err(cx);
     }
 
-    fn should_render_acp_import_onboarding(&self, cx: &App) -> bool {
-        let has_external_agents = self
-            .agent_server_store
-            .upgrade()
-            .map(|store| store.read(cx).has_external_agents())
-            .unwrap_or(false);
-
-        has_external_agents && !AcpThreadImportOnboarding::dismissed(cx)
-    }
-
-    fn show_thread_import_modal(&mut self, window: &mut Window, cx: &mut Context<Self>) {
-        let Some(agent_server_store) = self.agent_server_store.upgrade() else {
-            return;
-        };
-        let Some(agent_registry_store) = self.agent_registry_store.upgrade() else {
-            return;
-        };
-
-        let workspace_handle = self.workspace.clone();
-        let multi_workspace = self.multi_workspace.clone();
-
-        self.workspace
-            .update(cx, |workspace, cx| {
-                workspace.toggle_modal(window, cx, |window, cx| {
-                    ThreadImportModal::new(
-                        agent_server_store,
-                        agent_registry_store,
-                        workspace_handle.clone(),
-                        multi_workspace.clone(),
-                        window,
-                        cx,
-                    )
-                });
-            })
-            .log_err();
-    }
-
     fn render_header(&self, window: &Window, cx: &mut Context<Self>) -> impl IntoElement {
         let has_query = !self.filter_editor.read(cx).text(cx).is_empty();
         let sidebar_on_left = matches!(
@@ -729,28 +681,5 @@ impl Render for ThreadsArchiveView {
             .size_full()
             .child(self.render_header(window, cx))
             .child(content)
-            .when(!self.should_render_acp_import_onboarding(cx), |this| {
-                this.child(
-                    div()
-                        .w_full()
-                        .p_1p5()
-                        .border_t_1()
-                        .border_color(cx.theme().colors().border)
-                        .child(
-                            Button::new("import-acp", "Import ACP Threads")
-                                .full_width()
-                                .style(ButtonStyle::OutlinedCustom(cx.theme().colors().border))
-                                .label_size(LabelSize::Small)
-                                .start_icon(
-                                    Icon::new(IconName::ArrowDown)
-                                        .size(IconSize::XSmall)
-                                        .color(Color::Muted),
-                                )
-                                .on_click(cx.listener(|this, _, window, cx| {
-                                    this.show_thread_import_modal(window, cx);
-                                })),
-                        ),
-                )
-            })
     }
 }

crates/agent_ui/src/ui.rs 🔗

@@ -1,6 +1,5 @@
 mod acp_onboarding_modal;
 mod agent_notification;
-mod claude_agent_onboarding_modal;
 mod end_trial_upsell;
 mod hold_for_default;
 mod mention_crease;
@@ -9,7 +8,6 @@ mod undo_reject_toast;
 
 pub use acp_onboarding_modal::*;
 pub use agent_notification::*;
-pub use claude_agent_onboarding_modal::*;
 pub use end_trial_upsell::*;
 pub use hold_for_default::*;
 pub use mention_crease::*;

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

@@ -1,261 +0,0 @@
-use agent_servers::CLAUDE_AGENT_ID;
-use gpui::{
-    ClickEvent, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, MouseDownEvent, Render,
-    linear_color_stop, linear_gradient,
-};
-use ui::{TintColor, Vector, VectorName, prelude::*};
-use workspace::{ModalView, Workspace};
-
-use crate::{Agent, agent_panel::AgentPanel};
-
-macro_rules! claude_agent_onboarding_event {
-    ($name:expr) => {
-        telemetry::event!($name, source = "ACP Claude Code Onboarding");
-    };
-    ($name:expr, $($key:ident $(= $value:expr)?),+ $(,)?) => {
-        telemetry::event!($name, source = "ACP Claude Code Onboarding", $($key $(= $value)?),+);
-    };
-}
-
-pub struct ClaudeCodeOnboardingModal {
-    focus_handle: FocusHandle,
-    workspace: Entity<Workspace>,
-}
-
-impl ClaudeCodeOnboardingModal {
-    pub fn toggle(workspace: &mut Workspace, window: &mut Window, cx: &mut Context<Workspace>) {
-        let workspace_entity = cx.entity();
-        workspace.toggle_modal(window, cx, |_window, cx| Self {
-            workspace: workspace_entity,
-            focus_handle: cx.focus_handle(),
-        });
-    }
-
-    fn open_panel(&mut self, _: &ClickEvent, window: &mut Window, cx: &mut Context<Self>) {
-        self.workspace.update(cx, |workspace, cx| {
-            workspace.focus_panel::<AgentPanel>(window, cx);
-
-            if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
-                panel.update(cx, |panel, cx| {
-                    panel.new_agent_thread(
-                        Agent::Custom {
-                            id: CLAUDE_AGENT_ID.into(),
-                        },
-                        window,
-                        cx,
-                    );
-                });
-            }
-        });
-
-        cx.emit(DismissEvent);
-
-        claude_agent_onboarding_event!("Open Panel Clicked");
-    }
-
-    fn view_docs(&mut self, _: &ClickEvent, window: &mut Window, cx: &mut Context<Self>) {
-        window.dispatch_action(Box::new(zed_actions::AcpRegistry), cx);
-        cx.notify();
-
-        claude_agent_onboarding_event!("Documentation Link Clicked");
-    }
-
-    fn cancel(&mut self, _: &menu::Cancel, _: &mut Window, cx: &mut Context<Self>) {
-        cx.emit(DismissEvent);
-    }
-}
-
-impl EventEmitter<DismissEvent> for ClaudeCodeOnboardingModal {}
-
-impl Focusable for ClaudeCodeOnboardingModal {
-    fn focus_handle(&self, _cx: &App) -> FocusHandle {
-        self.focus_handle.clone()
-    }
-}
-
-impl ModalView for ClaudeCodeOnboardingModal {}
-
-impl Render for ClaudeCodeOnboardingModal {
-    fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
-        let illustration_element = |icon: IconName, label: Option<SharedString>, opacity: f32| {
-            h_flex()
-                .px_1()
-                .py_0p5()
-                .gap_1()
-                .rounded_sm()
-                .bg(cx.theme().colors().element_active.opacity(0.05))
-                .border_1()
-                .border_color(cx.theme().colors().border)
-                .border_dashed()
-                .child(
-                    Icon::new(icon)
-                        .size(IconSize::Small)
-                        .color(Color::Custom(cx.theme().colors().text_muted.opacity(0.15))),
-                )
-                .map(|this| {
-                    if let Some(label_text) = label {
-                        this.child(
-                            Label::new(label_text)
-                                .size(LabelSize::Small)
-                                .color(Color::Muted),
-                        )
-                    } else {
-                        this.child(
-                            div().w_16().h_1().rounded_full().bg(cx
-                                .theme()
-                                .colors()
-                                .element_active
-                                .opacity(0.6)),
-                        )
-                    }
-                })
-                .opacity(opacity)
-        };
-
-        let illustration = h_flex()
-            .relative()
-            .h(rems_from_px(126.))
-            .bg(cx.theme().colors().editor_background)
-            .border_b_1()
-            .border_color(cx.theme().colors().border_variant)
-            .justify_center()
-            .gap_8()
-            .rounded_t_md()
-            .overflow_hidden()
-            .child(
-                div().absolute().inset_0().w(px(515.)).h(px(126.)).child(
-                    Vector::new(VectorName::AcpGrid, rems_from_px(515.), rems_from_px(126.))
-                        .color(ui::Color::Custom(cx.theme().colors().text.opacity(0.02))),
-                ),
-            )
-            .child(div().absolute().inset_0().size_full().bg(linear_gradient(
-                0.,
-                linear_color_stop(
-                    cx.theme().colors().elevated_surface_background.opacity(0.1),
-                    0.9,
-                ),
-                linear_color_stop(
-                    cx.theme().colors().elevated_surface_background.opacity(0.),
-                    0.,
-                ),
-            )))
-            .child(
-                div()
-                    .absolute()
-                    .inset_0()
-                    .size_full()
-                    .bg(gpui::black().opacity(0.15)),
-            )
-            .child(
-                Vector::new(
-                    VectorName::AcpLogoSerif,
-                    rems_from_px(257.),
-                    rems_from_px(47.),
-                )
-                .color(ui::Color::Custom(cx.theme().colors().text.opacity(0.8))),
-            )
-            .child(
-                v_flex()
-                    .gap_1p5()
-                    .child(illustration_element(IconName::Stop, None, 0.15))
-                    .child(illustration_element(
-                        IconName::AiGemini,
-                        Some("New Gemini CLI Thread".into()),
-                        0.3,
-                    ))
-                    .child(
-                        h_flex()
-                            .pl_1()
-                            .pr_2()
-                            .py_0p5()
-                            .gap_1()
-                            .rounded_sm()
-                            .bg(cx.theme().colors().element_active.opacity(0.2))
-                            .border_1()
-                            .border_color(cx.theme().colors().border)
-                            .child(
-                                Icon::new(IconName::AiClaude)
-                                    .size(IconSize::Small)
-                                    .color(Color::Muted),
-                            )
-                            .child(Label::new("New Claude Agent Thread").size(LabelSize::Small)),
-                    )
-                    .child(illustration_element(
-                        IconName::Stop,
-                        Some("Your Agent Here".into()),
-                        0.3,
-                    ))
-                    .child(illustration_element(IconName::Stop, None, 0.15)),
-            );
-
-        let heading = v_flex()
-            .w_full()
-            .gap_1()
-            .child(
-                Label::new("Beta Release")
-                    .size(LabelSize::Small)
-                    .color(Color::Muted),
-            )
-            .child(Headline::new("Claude Agent: Natively in Zed").size(HeadlineSize::Large));
-
-        let copy = "Powered by the Agent Client Protocol, you can now run Claude Agent as\na first-class citizen in Zed's agent panel.";
-
-        let open_panel_button = Button::new("open-panel", "Start with Claude Agent")
-            .style(ButtonStyle::Tinted(TintColor::Accent))
-            .full_width()
-            .on_click(cx.listener(Self::open_panel));
-
-        let docs_button = Button::new("add-other-agents", "Add Other Agents")
-            .end_icon(
-                Icon::new(IconName::ArrowUpRight)
-                    .size(IconSize::Indicator)
-                    .color(Color::Muted),
-            )
-            .full_width()
-            .on_click(cx.listener(Self::view_docs));
-
-        let close_button = h_flex().absolute().top_2().right_2().child(
-            IconButton::new("cancel", IconName::Close).on_click(cx.listener(
-                |_, _: &ClickEvent, _window, cx| {
-                    claude_agent_onboarding_event!("Canceled", trigger = "X click");
-                    cx.emit(DismissEvent);
-                },
-            )),
-        );
-
-        v_flex()
-            .id("acp-onboarding")
-            .key_context("AcpOnboardingModal")
-            .relative()
-            .w(rems(34.))
-            .h_full()
-            .elevation_3(cx)
-            .track_focus(&self.focus_handle(cx))
-            .overflow_hidden()
-            .on_action(cx.listener(Self::cancel))
-            .on_action(cx.listener(|_, _: &menu::Cancel, _window, cx| {
-                claude_agent_onboarding_event!("Canceled", trigger = "Action");
-                cx.emit(DismissEvent);
-            }))
-            .on_any_mouse_down(cx.listener(|this, _: &MouseDownEvent, window, cx| {
-                this.focus_handle.focus(window, cx);
-            }))
-            .child(illustration)
-            .child(
-                v_flex()
-                    .p_4()
-                    .gap_2()
-                    .child(heading)
-                    .child(Label::new(copy).color(Color::Muted))
-                    .child(
-                        v_flex()
-                            .w_full()
-                            .mt_2()
-                            .gap_1()
-                            .child(open_panel_button)
-                            .child(docs_button),
-                    ),
-            )
-            .child(close_button)
-    }
-}

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

@@ -160,7 +160,7 @@ impl RenderOnce for ModelSelectorListItem {
             .end_slot(div().pr_2().when(self.is_selected, |this| {
                 this.child(Icon::new(IconName::Check).color(Color::Accent))
             }))
-            .end_hover_slot(div().pr_1p5().when_some(self.on_toggle_favorite, {
+            .end_slot_on_hover(div().pr_1p5().when_some(self.on_toggle_favorite, {
                 |this, handle_click| {
                     let (icon, color, tooltip) = if is_favorite {
                         (IconName::StarFilled, Color::Accent, "Unfavorite Model")

crates/collab/tests/integration/git_tests.rs 🔗

@@ -401,16 +401,19 @@ async fn test_linked_worktrees_sync(
                 path: PathBuf::from(path!("/project")),
                 ref_name: Some("refs/heads/main".into()),
                 sha: "aaa111".into(),
+                is_main: false,
             });
             state.worktrees.push(GitWorktree {
                 path: PathBuf::from(path!("/project/feature-branch")),
                 ref_name: Some("refs/heads/feature-branch".into()),
                 sha: "bbb222".into(),
+                is_main: false,
             });
             state.worktrees.push(GitWorktree {
                 path: PathBuf::from(path!("/project/bugfix-branch")),
                 ref_name: Some("refs/heads/bugfix-branch".into()),
                 sha: "ccc333".into(),
+                is_main: false,
             });
         })
         .unwrap();
@@ -480,6 +483,7 @@ async fn test_linked_worktrees_sync(
                 path: PathBuf::from(path!("/project/hotfix-branch")),
                 ref_name: Some("refs/heads/hotfix-branch".into()),
                 sha: "ddd444".into(),
+                is_main: false,
             });
         })
         .unwrap();

crates/command_palette/src/command_palette.rs 🔗

@@ -43,24 +43,28 @@ pub struct CommandPalette {
     picker: Entity<Picker<CommandPaletteDelegate>>,
 }
 
-/// Removes subsequent whitespace characters and double colons from the query.
+/// Removes subsequent whitespace characters and double colons from the query, and converts
+/// underscores to spaces.
 ///
 /// This improves the likelihood of a match by either humanized name or keymap-style name.
+/// Underscores are converted to spaces because `humanize_action_name` converts them to spaces
+/// when building the search candidates (e.g. `terminal_panel::Toggle` -> `terminal panel: toggle`).
 pub fn normalize_action_query(input: &str) -> String {
     let mut result = String::with_capacity(input.len());
     let mut last_char = None;
 
     for char in input.trim().chars() {
-        match (last_char, char) {
+        let normalized_char = if char == '_' { ' ' } else { char };
+        match (last_char, normalized_char) {
             (Some(':'), ':') => continue,
-            (Some(last_char), char) if last_char.is_whitespace() && char.is_whitespace() => {
+            (Some(last_char), c) if last_char.is_whitespace() && c.is_whitespace() => {
                 continue;
             }
             _ => {
-                last_char = Some(char);
+                last_char = Some(normalized_char);
             }
         }
-        result.push(char);
+        result.push(normalized_char);
     }
 
     result
@@ -775,6 +779,14 @@ mod tests {
             normalize_action_query("editor: :GoToDefinition"),
             "editor: :GoToDefinition"
         );
+        assert_eq!(
+            normalize_action_query("terminal_panel::Toggle"),
+            "terminal panel:Toggle"
+        );
+        assert_eq!(
+            normalize_action_query("project_panel::ToggleFocus"),
+            "project panel:ToggleFocus"
+        );
     }
 
     #[gpui::test]

crates/edit_prediction/src/license_detection.rs 🔗

@@ -308,7 +308,9 @@ impl LicenseDetectionWatcher {
                         }
                     }
                 }
-                worktree::Event::DeletedEntry(_) | worktree::Event::UpdatedGitRepositories(_) => {}
+                worktree::Event::DeletedEntry(_)
+                | worktree::Event::UpdatedGitRepositories(_)
+                | worktree::Event::Deleted => {}
             });
 
         let worktree_snapshot = worktree.read(cx).snapshot();

crates/editor/src/editor_tests.rs 🔗

@@ -31952,6 +31952,65 @@ async fn test_sticky_scroll_with_expanded_deleted_diff_hunks(
     assert_eq!(sticky_headers(6.0), vec![]);
 }
 
+#[gpui::test]
+async fn test_no_duplicated_sticky_headers(cx: &mut TestAppContext) {
+    init_test(cx, |_| {});
+    let mut cx = EditorTestContext::new(cx).await;
+
+    cx.set_state(indoc! {"
+        ˇimpl Foo { fn bar() {
+            let x = 1;
+            fn baz() {
+                let y = 2;
+            }
+        } }
+    "});
+
+    cx.update_editor(|e, _, cx| {
+        e.buffer()
+            .read(cx)
+            .as_singleton()
+            .unwrap()
+            .update(cx, |buffer, cx| {
+                buffer.set_language(Some(rust_lang()), cx);
+            })
+    });
+
+    let mut sticky_headers = |offset: ScrollOffset| {
+        cx.update_editor(|e, window, cx| {
+            e.scroll(gpui::Point { x: 0., y: offset }, None, window, cx);
+        });
+        cx.run_until_parked();
+        cx.update_editor(|e, window, cx| {
+            EditorElement::sticky_headers(&e, &e.snapshot(window, cx))
+                .into_iter()
+                .map(
+                    |StickyHeader {
+                         start_point,
+                         offset,
+                         ..
+                     }| { (start_point, offset) },
+                )
+                .collect::<Vec<_>>()
+        })
+    };
+
+    let struct_foo = Point { row: 0, column: 0 };
+    let fn_baz = Point { row: 2, column: 4 };
+
+    assert_eq!(sticky_headers(0.0), vec![]);
+    assert_eq!(sticky_headers(0.5), vec![(struct_foo, 0.0)]);
+    assert_eq!(sticky_headers(1.0), vec![(struct_foo, 0.0)]);
+    assert_eq!(sticky_headers(1.5), vec![(struct_foo, 0.0), (fn_baz, 1.0)]);
+    assert_eq!(sticky_headers(2.0), vec![(struct_foo, 0.0), (fn_baz, 1.0)]);
+    assert_eq!(sticky_headers(2.5), vec![(struct_foo, 0.0), (fn_baz, 0.5)]);
+    assert_eq!(sticky_headers(3.0), vec![(struct_foo, 0.0)]);
+    assert_eq!(sticky_headers(3.5), vec![(struct_foo, 0.0)]);
+    assert_eq!(sticky_headers(4.0), vec![(struct_foo, 0.0)]);
+    assert_eq!(sticky_headers(4.5), vec![(struct_foo, -0.5)]);
+    assert_eq!(sticky_headers(5.0), vec![]);
+}
+
 #[gpui::test]
 fn test_relative_line_numbers(cx: &mut TestAppContext) {
     init_test(cx, |_| {});

crates/editor/src/element.rs 🔗

@@ -4674,6 +4674,13 @@ impl EditorElement {
                 .display_snapshot
                 .point_to_display_point(start_point, Bias::Left)
                 .row();
+            if rows
+                .last()
+                .is_some_and(|last| last.sticky_row == sticky_row)
+            {
+                continue;
+            }
+
             let end_row = snapshot
                 .display_snapshot
                 .point_to_display_point(end_point, Bias::Left)

crates/extension_host/src/wasm_host.rs 🔗

@@ -42,7 +42,7 @@ use wasmtime::{
     CacheStore, Engine, Store,
     component::{Component, ResourceTable},
 };
-use wasmtime_wasi::p2::{self as wasi, IoView as _};
+use wasmtime_wasi::{WasiCtx, WasiCtxBuilder, WasiCtxView, WasiView};
 use wit::Extension;
 
 pub struct WasmHost {
@@ -93,7 +93,7 @@ impl extension::Extension for WasmExtension {
     ) -> Result<Command> {
         self.call(|extension, store| {
             async move {
-                let resource = store.data_mut().table().push(worktree)?;
+                let resource = store.data_mut().table.push(worktree)?;
                 let command = extension
                     .call_language_server_command(
                         store,
@@ -119,7 +119,7 @@ impl extension::Extension for WasmExtension {
     ) -> Result<Option<String>> {
         self.call(|extension, store| {
             async move {
-                let resource = store.data_mut().table().push(worktree)?;
+                let resource = store.data_mut().table.push(worktree)?;
                 let options = extension
                     .call_language_server_initialization_options(
                         store,
@@ -143,7 +143,7 @@ impl extension::Extension for WasmExtension {
     ) -> Result<Option<String>> {
         self.call(|extension, store| {
             async move {
-                let resource = store.data_mut().table().push(worktree)?;
+                let resource = store.data_mut().table.push(worktree)?;
                 let options = extension
                     .call_language_server_workspace_configuration(
                         store,
@@ -166,7 +166,7 @@ impl extension::Extension for WasmExtension {
     ) -> Result<Option<String>> {
         self.call(|extension, store| {
             async move {
-                let resource = store.data_mut().table().push(worktree)?;
+                let resource = store.data_mut().table.push(worktree)?;
                 extension
                     .call_language_server_initialization_options_schema(
                         store,
@@ -187,7 +187,7 @@ impl extension::Extension for WasmExtension {
     ) -> Result<Option<String>> {
         self.call(|extension, store| {
             async move {
-                let resource = store.data_mut().table().push(worktree)?;
+                let resource = store.data_mut().table.push(worktree)?;
                 extension
                     .call_language_server_workspace_configuration_schema(
                         store,
@@ -209,7 +209,7 @@ impl extension::Extension for WasmExtension {
     ) -> Result<Option<String>> {
         self.call(|extension, store| {
             async move {
-                let resource = store.data_mut().table().push(worktree)?;
+                let resource = store.data_mut().table.push(worktree)?;
                 let options = extension
                     .call_language_server_additional_initialization_options(
                         store,
@@ -234,7 +234,7 @@ impl extension::Extension for WasmExtension {
     ) -> Result<Option<String>> {
         self.call(|extension, store| {
             async move {
-                let resource = store.data_mut().table().push(worktree)?;
+                let resource = store.data_mut().table.push(worktree)?;
                 let options = extension
                     .call_language_server_additional_workspace_configuration(
                         store,
@@ -331,7 +331,7 @@ impl extension::Extension for WasmExtension {
         self.call(|extension, store| {
             async move {
                 let resource = if let Some(delegate) = delegate {
-                    Some(store.data_mut().table().push(delegate)?)
+                    Some(store.data_mut().table.push(delegate)?)
                 } else {
                     None
                 };
@@ -355,7 +355,7 @@ impl extension::Extension for WasmExtension {
     ) -> Result<Command> {
         self.call(|extension, store| {
             async move {
-                let project_resource = store.data_mut().table().push(project)?;
+                let project_resource = store.data_mut().table.push(project)?;
                 let command = extension
                     .call_context_server_command(store, context_server_id.clone(), project_resource)
                     .await?
@@ -374,7 +374,7 @@ impl extension::Extension for WasmExtension {
     ) -> Result<Option<ContextServerConfiguration>> {
         self.call(|extension, store| {
             async move {
-                let project_resource = store.data_mut().table().push(project)?;
+                let project_resource = store.data_mut().table.push(project)?;
                 let Some(configuration) = extension
                     .call_context_server_configuration(
                         store,
@@ -417,7 +417,7 @@ impl extension::Extension for WasmExtension {
     ) -> Result<()> {
         self.call(|extension, store| {
             async move {
-                let kv_store_resource = store.data_mut().table().push(kv_store)?;
+                let kv_store_resource = store.data_mut().table.push(kv_store)?;
                 extension
                     .call_index_docs(
                         store,
@@ -444,7 +444,7 @@ impl extension::Extension for WasmExtension {
     ) -> Result<DebugAdapterBinary> {
         self.call(|extension, store| {
             async move {
-                let resource = store.data_mut().table().push(worktree)?;
+                let resource = store.data_mut().table.push(worktree)?;
                 let dap_binary = extension
                     .call_get_dap_binary(store, dap_name, config, user_installed_path, resource)
                     .await?
@@ -532,7 +532,7 @@ impl extension::Extension for WasmExtension {
 pub struct WasmState {
     manifest: Arc<ExtensionManifest>,
     pub table: ResourceTable,
-    ctx: wasi::WasiCtx,
+    ctx: WasiCtx,
     pub host: Arc<WasmHost>,
     pub(crate) capability_granter: CapabilityGranter,
 }
@@ -726,7 +726,7 @@ impl WasmHost {
         })
     }
 
-    async fn build_wasi_ctx(&self, manifest: &Arc<ExtensionManifest>) -> Result<wasi::WasiCtx> {
+    async fn build_wasi_ctx(&self, manifest: &Arc<ExtensionManifest>) -> Result<WasiCtx> {
         let extension_work_dir = self.work_dir.join(manifest.id.as_ref());
         self.fs
             .create_dir(&extension_work_dir)
@@ -739,7 +739,7 @@ impl WasmHost {
         #[cfg(target_os = "windows")]
         let path = path.replace('\\', "/");
 
-        let mut ctx = wasi::WasiCtxBuilder::new();
+        let mut ctx = WasiCtxBuilder::new();
         ctx.inherit_stdio()
             .env("PWD", &path)
             .env("RUST_BACKTRACE", "full");
@@ -947,15 +947,16 @@ impl WasmState {
     }
 }
 
-impl wasi::IoView for WasmState {
-    fn table(&mut self) -> &mut ResourceTable {
-        &mut self.table
-    }
+impl wasmtime::component::HasData for WasmState {
+    type Data<'a> = &'a mut WasmState;
 }
 
-impl wasi::WasiView for WasmState {
-    fn ctx(&mut self) -> &mut wasi::WasiCtx {
-        &mut self.ctx
+impl WasiView for WasmState {
+    fn ctx(&mut self) -> WasiCtxView<'_> {
+        WasiCtxView {
+            ctx: &mut self.ctx,
+            table: &mut self.table,
+        }
     }
 }
 

crates/extension_host/src/wasm_host/wit.rs 🔗

@@ -42,18 +42,14 @@ pub use since_v0_0_4::LanguageServerConfig;
 
 pub fn new_linker(
     executor: &BackgroundExecutor,
-    f: impl Fn(&mut Linker<WasmState>, fn(&mut WasmState) -> &mut WasmState) -> Result<()>,
+    f: impl FnOnce(&mut Linker<WasmState>) -> Result<()>,
 ) -> Linker<WasmState> {
     let mut linker = Linker::new(&wasm_engine(executor));
     wasmtime_wasi::p2::add_to_linker_async(&mut linker).unwrap();
-    f(&mut linker, wasi_view).unwrap();
+    f(&mut linker).unwrap();
     linker
 }
 
-fn wasi_view(state: &mut WasmState) -> &mut WasmState {
-    state
-}
-
 /// Returns whether the given Wasm API version is supported by the Wasm host.
 pub fn is_supported_wasm_api_version(release_channel: ReleaseChannel, version: Version) -> bool {
     wasm_api_version_range(release_channel).contains(&version)

crates/extension_host/src/wasm_host/wit/since_v0_0_1.rs 🔗

@@ -12,8 +12,12 @@ use wasmtime::component::{Linker, Resource};
 pub const MIN_VERSION: Version = Version::new(0, 0, 1);
 
 wasmtime::component::bindgen!({
-    async: true,
-    trappable_imports: true,
+    imports: {
+        default: async | trappable,
+    },
+    exports: {
+        default: async,
+    },
     path: "../extension_api/wit/since_v0.0.1",
     with: {
          "worktree": ExtensionWorktree,
@@ -26,7 +30,11 @@ pub type ExtensionWorktree = Arc<dyn WorktreeDelegate>;
 
 pub fn linker(executor: &BackgroundExecutor) -> &'static Linker<WasmState> {
     static LINKER: OnceLock<Linker<WasmState>> = OnceLock::new();
-    LINKER.get_or_init(|| super::new_linker(executor, Extension::add_to_linker))
+    LINKER.get_or_init(|| {
+        super::new_linker(executor, |linker| {
+            Extension::add_to_linker::<_, WasmState>(linker, |s| s)
+        })
+    })
 }
 
 impl From<DownloadedFileType> for latest::DownloadedFileType {

crates/extension_host/src/wasm_host/wit/since_v0_0_4.rs 🔗

@@ -10,8 +10,12 @@ use wasmtime::component::{Linker, Resource};
 pub const MIN_VERSION: Version = Version::new(0, 0, 4);
 
 wasmtime::component::bindgen!({
-    async: true,
-    trappable_imports: true,
+    imports: {
+        default: async | trappable,
+    },
+    exports: {
+        default: async,
+    },
     path: "../extension_api/wit/since_v0.0.4",
     with: {
          "worktree": ExtensionWorktree,
@@ -24,7 +28,11 @@ pub type ExtensionWorktree = Arc<dyn WorktreeDelegate>;
 
 pub fn linker(executor: &BackgroundExecutor) -> &'static Linker<WasmState> {
     static LINKER: OnceLock<Linker<WasmState>> = OnceLock::new();
-    LINKER.get_or_init(|| super::new_linker(executor, Extension::add_to_linker))
+    LINKER.get_or_init(|| {
+        super::new_linker(executor, |linker| {
+            Extension::add_to_linker::<_, WasmState>(linker, |s| s)
+        })
+    })
 }
 
 impl From<DownloadedFileType> for latest::DownloadedFileType {

crates/extension_host/src/wasm_host/wit/since_v0_0_6.rs 🔗

@@ -10,8 +10,12 @@ use wasmtime::component::{Linker, Resource};
 pub const MIN_VERSION: Version = Version::new(0, 0, 6);
 
 wasmtime::component::bindgen!({
-    async: true,
-    trappable_imports: true,
+    imports: {
+        default: async | trappable,
+    },
+    exports: {
+        default: async,
+    },
     path: "../extension_api/wit/since_v0.0.6",
     with: {
          "worktree": ExtensionWorktree,
@@ -31,7 +35,11 @@ pub type ExtensionWorktree = Arc<dyn WorktreeDelegate>;
 
 pub fn linker(executor: &BackgroundExecutor) -> &'static Linker<WasmState> {
     static LINKER: OnceLock<Linker<WasmState>> = OnceLock::new();
-    LINKER.get_or_init(|| super::new_linker(executor, Extension::add_to_linker))
+    LINKER.get_or_init(|| {
+        super::new_linker(executor, |linker| {
+            Extension::add_to_linker::<_, WasmState>(linker, |s| s)
+        })
+    })
 }
 
 impl From<Command> for latest::Command {

crates/extension_host/src/wasm_host/wit/since_v0_1_0.rs 🔗

@@ -26,8 +26,12 @@ use super::{latest, since_v0_6_0};
 pub const MIN_VERSION: Version = Version::new(0, 1, 0);
 
 wasmtime::component::bindgen!({
-    async: true,
-    trappable_imports: true,
+    imports: {
+        default: async | trappable,
+    },
+    exports: {
+        default: async,
+    },
     path: "../extension_api/wit/since_v0.1.0",
     with: {
          "worktree": ExtensionWorktree,
@@ -52,7 +56,11 @@ pub type ExtensionHttpResponseStream = Arc<Mutex<::http_client::Response<AsyncBo
 
 pub fn linker(executor: &BackgroundExecutor) -> &'static Linker<WasmState> {
     static LINKER: OnceLock<Linker<WasmState>> = OnceLock::new();
-    LINKER.get_or_init(|| super::new_linker(executor, Extension::add_to_linker))
+    LINKER.get_or_init(|| {
+        super::new_linker(executor, |linker| {
+            Extension::add_to_linker::<_, WasmState>(linker, |s| s)
+        })
+    })
 }
 
 impl From<Command> for latest::Command {

crates/extension_host/src/wasm_host/wit/since_v0_2_0.rs 🔗

@@ -11,8 +11,12 @@ use super::{latest, since_v0_6_0};
 pub const MIN_VERSION: Version = Version::new(0, 2, 0);
 
 wasmtime::component::bindgen!({
-    async: true,
-    trappable_imports: true,
+    imports: {
+        default: async | trappable,
+    },
+    exports: {
+        default: async,
+    },
     path: "../extension_api/wit/since_v0.2.0",
     with: {
          "worktree": ExtensionWorktree,
@@ -40,7 +44,11 @@ pub type ExtensionKeyValueStore = Arc<dyn KeyValueStoreDelegate>;
 
 pub fn linker(executor: &BackgroundExecutor) -> &'static Linker<WasmState> {
     static LINKER: OnceLock<Linker<WasmState>> = OnceLock::new();
-    LINKER.get_or_init(|| super::new_linker(executor, Extension::add_to_linker))
+    LINKER.get_or_init(|| {
+        super::new_linker(executor, |linker| {
+            Extension::add_to_linker::<_, WasmState>(linker, |s| s)
+        })
+    })
 }
 
 impl From<Command> for latest::Command {

crates/extension_host/src/wasm_host/wit/since_v0_3_0.rs 🔗

@@ -11,8 +11,12 @@ use super::{latest, since_v0_6_0};
 pub const MIN_VERSION: Version = Version::new(0, 3, 0);
 
 wasmtime::component::bindgen!({
-    async: true,
-    trappable_imports: true,
+    imports: {
+        default: async | trappable,
+    },
+    exports: {
+        default: async,
+    },
     path: "../extension_api/wit/since_v0.3.0",
     with: {
          "worktree": ExtensionWorktree,
@@ -40,7 +44,11 @@ pub type ExtensionKeyValueStore = Arc<dyn KeyValueStoreDelegate>;
 
 pub fn linker(executor: &BackgroundExecutor) -> &'static Linker<WasmState> {
     static LINKER: OnceLock<Linker<WasmState>> = OnceLock::new();
-    LINKER.get_or_init(|| super::new_linker(executor, Extension::add_to_linker))
+    LINKER.get_or_init(|| {
+        super::new_linker(executor, |linker| {
+            Extension::add_to_linker::<_, WasmState>(linker, |s| s)
+        })
+    })
 }
 
 impl From<CodeLabel> for latest::CodeLabel {

crates/extension_host/src/wasm_host/wit/since_v0_4_0.rs 🔗

@@ -11,8 +11,12 @@ use super::{latest, since_v0_6_0};
 pub const MIN_VERSION: Version = Version::new(0, 4, 0);
 
 wasmtime::component::bindgen!({
-    async: true,
-    trappable_imports: true,
+    imports: {
+        default: async | trappable,
+    },
+    exports: {
+        default: async,
+    },
     path: "../extension_api/wit/since_v0.4.0",
     with: {
         "worktree": ExtensionWorktree,
@@ -40,7 +44,11 @@ pub type ExtensionKeyValueStore = Arc<dyn KeyValueStoreDelegate>;
 
 pub fn linker(executor: &BackgroundExecutor) -> &'static Linker<WasmState> {
     static LINKER: OnceLock<Linker<WasmState>> = OnceLock::new();
-    LINKER.get_or_init(|| super::new_linker(executor, Extension::add_to_linker))
+    LINKER.get_or_init(|| {
+        super::new_linker(executor, |linker| {
+            Extension::add_to_linker::<_, WasmState>(linker, |s| s)
+        })
+    })
 }
 
 impl From<CodeLabel> for latest::CodeLabel {

crates/extension_host/src/wasm_host/wit/since_v0_5_0.rs 🔗

@@ -11,8 +11,12 @@ use super::{latest, since_v0_6_0};
 pub const MIN_VERSION: Version = Version::new(0, 5, 0);
 
 wasmtime::component::bindgen!({
-    async: true,
-    trappable_imports: true,
+    imports: {
+        default: async | trappable,
+    },
+    exports: {
+        default: async,
+    },
     path: "../extension_api/wit/since_v0.5.0",
     with: {
         "worktree": ExtensionWorktree,
@@ -41,7 +45,11 @@ pub type ExtensionKeyValueStore = Arc<dyn KeyValueStoreDelegate>;
 
 pub fn linker(executor: &BackgroundExecutor) -> &'static Linker<WasmState> {
     static LINKER: OnceLock<Linker<WasmState>> = OnceLock::new();
-    LINKER.get_or_init(|| super::new_linker(executor, Extension::add_to_linker))
+    LINKER.get_or_init(|| {
+        super::new_linker(executor, |linker| {
+            Extension::add_to_linker::<_, WasmState>(linker, |s| s)
+        })
+    })
 }
 
 impl From<CodeLabel> for latest::CodeLabel {

crates/extension_host/src/wasm_host/wit/since_v0_6_0.rs 🔗

@@ -12,8 +12,12 @@ pub const MIN_VERSION: Version = Version::new(0, 6, 0);
 pub const MAX_VERSION: Version = Version::new(0, 7, 0);
 
 wasmtime::component::bindgen!({
-    async: true,
-    trappable_imports: true,
+    imports: {
+        default: async | trappable,
+    },
+    exports: {
+        default: async,
+    },
     path: "../extension_api/wit/since_v0.6.0",
     with: {
         "worktree": ExtensionWorktree,
@@ -43,7 +47,11 @@ pub type ExtensionKeyValueStore = Arc<dyn KeyValueStoreDelegate>;
 
 pub fn linker(executor: &BackgroundExecutor) -> &'static Linker<WasmState> {
     static LINKER: OnceLock<Linker<WasmState>> = OnceLock::new();
-    LINKER.get_or_init(|| super::new_linker(executor, Extension::add_to_linker))
+    LINKER.get_or_init(|| {
+        super::new_linker(executor, |linker| {
+            Extension::add_to_linker::<_, WasmState>(linker, |s| s)
+        })
+    })
 }
 
 impl From<CodeLabel> for latest::CodeLabel {

crates/extension_host/src/wasm_host/wit/since_v0_8_0.rs 🔗

@@ -40,8 +40,12 @@ pub const MIN_VERSION: Version = Version::new(0, 8, 0);
 pub const MAX_VERSION: Version = Version::new(0, 8, 0);
 
 wasmtime::component::bindgen!({
-    async: true,
-    trappable_imports: true,
+    imports: {
+        default: async | trappable,
+    },
+    exports: {
+        default: async,
+    },
     path: "../extension_api/wit/since_v0.8.0",
     with: {
          "worktree": ExtensionWorktree,
@@ -65,7 +69,11 @@ pub type ExtensionHttpResponseStream = Arc<Mutex<::http_client::Response<AsyncBo
 
 pub fn linker(executor: &BackgroundExecutor) -> &'static Linker<WasmState> {
     static LINKER: OnceLock<Linker<WasmState>> = OnceLock::new();
-    LINKER.get_or_init(|| super::new_linker(executor, Extension::add_to_linker))
+    LINKER.get_or_init(|| {
+        super::new_linker(executor, |linker| {
+            Extension::add_to_linker::<_, WasmState>(linker, |s| s)
+        })
+    })
 }
 
 impl From<Range> for std::ops::Range<usize> {

crates/fs/src/fake_git_repo.rs 🔗

@@ -481,6 +481,7 @@ impl GitRepository for FakeGitRepository {
                 path: work_dir,
                 ref_name: Some(branch_ref.into()),
                 sha: head_sha.into(),
+                is_main: true,
             };
             let mut all = vec![main_worktree];
             all.extend(state.worktrees.iter().cloned());
@@ -523,6 +524,7 @@ impl GitRepository for FakeGitRepository {
                             path,
                             ref_name: Some(ref_name.into()),
                             sha: sha.into(),
+                            is_main: false,
                         });
                         state.branches.insert(branch_name);
                     } else {
@@ -530,6 +532,7 @@ impl GitRepository for FakeGitRepository {
                             path,
                             ref_name: None,
                             sha: sha.into(),
+                            is_main: false,
                         });
                     }
                     Ok::<(), anyhow::Error>(())

crates/git/src/repository.rs 🔗

@@ -238,6 +238,7 @@ pub struct Worktree {
     pub ref_name: Option<SharedString>,
     // todo(git_worktree) This type should be a Oid
     pub sha: SharedString,
+    pub is_main: bool,
 }
 
 impl Worktree {
@@ -259,6 +260,7 @@ impl Worktree {
 
 pub fn parse_worktrees_from_str<T: AsRef<str>>(raw_worktrees: T) -> Vec<Worktree> {
     let mut worktrees = Vec::new();
+    let mut is_first = true;
     let normalized = raw_worktrees.as_ref().replace("\r\n", "\n");
     let entries = normalized.split("\n\n");
     for entry in entries {
@@ -286,7 +288,9 @@ pub fn parse_worktrees_from_str<T: AsRef<str>>(raw_worktrees: T) -> Vec<Worktree
                 path: PathBuf::from(path),
                 ref_name: ref_name.map(Into::into),
                 sha: sha.into(),
-            })
+                is_main: is_first,
+            });
+            is_first = false;
         }
     }
 
@@ -3935,6 +3939,7 @@ mod tests {
         assert_eq!(result[0].path, PathBuf::from("/home/user/project"));
         assert_eq!(result[0].sha.as_ref(), "abc123def");
         assert_eq!(result[0].ref_name, Some("refs/heads/main".into()));
+        assert!(result[0].is_main);
 
         // Multiple worktrees
         let input = "worktree /home/user/project\nHEAD abc123\nbranch refs/heads/main\n\n\
@@ -3943,8 +3948,10 @@ mod tests {
         assert_eq!(result.len(), 2);
         assert_eq!(result[0].path, PathBuf::from("/home/user/project"));
         assert_eq!(result[0].ref_name, Some("refs/heads/main".into()));
+        assert!(result[0].is_main);
         assert_eq!(result[1].path, PathBuf::from("/home/user/project-wt"));
         assert_eq!(result[1].ref_name, Some("refs/heads/feature".into()));
+        assert!(!result[1].is_main);
 
         // Detached HEAD entry (included with ref_name: None)
         let input = "worktree /home/user/project\nHEAD abc123\nbranch refs/heads/main\n\n\
@@ -3953,9 +3960,11 @@ mod tests {
         assert_eq!(result.len(), 2);
         assert_eq!(result[0].path, PathBuf::from("/home/user/project"));
         assert_eq!(result[0].ref_name, Some("refs/heads/main".into()));
+        assert!(result[0].is_main);
         assert_eq!(result[1].path, PathBuf::from("/home/user/detached"));
         assert_eq!(result[1].ref_name, None);
         assert_eq!(result[1].sha.as_ref(), "def456");
+        assert!(!result[1].is_main);
 
         // Bare repo entry (included with ref_name: None)
         let input = "worktree /home/user/bare.git\nHEAD abc123\nbare\n\n\
@@ -3964,8 +3973,10 @@ mod tests {
         assert_eq!(result.len(), 2);
         assert_eq!(result[0].path, PathBuf::from("/home/user/bare.git"));
         assert_eq!(result[0].ref_name, None);
+        assert!(result[0].is_main);
         assert_eq!(result[1].path, PathBuf::from("/home/user/project"));
         assert_eq!(result[1].ref_name, Some("refs/heads/main".into()));
+        assert!(!result[1].is_main);
 
         // Extra porcelain lines (locked, prunable) should be ignored
         let input = "worktree /home/user/project\nHEAD abc123\nbranch refs/heads/main\n\n\
@@ -3975,13 +3986,16 @@ mod tests {
         assert_eq!(result.len(), 3);
         assert_eq!(result[0].path, PathBuf::from("/home/user/project"));
         assert_eq!(result[0].ref_name, Some("refs/heads/main".into()));
+        assert!(result[0].is_main);
         assert_eq!(result[1].path, PathBuf::from("/home/user/locked-wt"));
         assert_eq!(result[1].ref_name, Some("refs/heads/locked-branch".into()));
+        assert!(!result[1].is_main);
         assert_eq!(result[2].path, PathBuf::from("/home/user/prunable-wt"));
         assert_eq!(
             result[2].ref_name,
             Some("refs/heads/prunable-branch".into())
         );
+        assert!(!result[2].is_main);
 
         // Leading/trailing whitespace on lines should be tolerated
         let input =
@@ -3991,6 +4005,7 @@ mod tests {
         assert_eq!(result[0].path, PathBuf::from("/home/user/project"));
         assert_eq!(result[0].sha.as_ref(), "abc123");
         assert_eq!(result[0].ref_name, Some("refs/heads/main".into()));
+        assert!(result[0].is_main);
 
         // Windows-style line endings should be handled
         let input = "worktree /home/user/project\r\nHEAD abc123\r\nbranch refs/heads/main\r\n\r\n";
@@ -3999,6 +4014,7 @@ mod tests {
         assert_eq!(result[0].path, PathBuf::from("/home/user/project"));
         assert_eq!(result[0].sha.as_ref(), "abc123");
         assert_eq!(result[0].ref_name, Some("refs/heads/main".into()));
+        assert!(result[0].is_main);
     }
 
     #[gpui::test]

crates/git_graph/src/git_graph.rs 🔗

@@ -1324,6 +1324,12 @@ impl GitGraph {
             editor.set_text_style_refinement(Default::default());
         });
 
+        if query.as_str().is_empty() {
+            self.search_state.state = QueryState::Empty;
+            cx.notify();
+            return;
+        }
+
         let (request_tx, request_rx) = smol::channel::unbounded::<Oid>();
 
         repo.update(cx, |repo, cx| {

crates/git_ui/src/branch_picker.rs 🔗

@@ -1087,13 +1087,8 @@ impl PickerDelegate for BranchListDelegate {
                         ),
                 )
                 .when(!is_new_items && !is_head_branch, |this| {
-                    this.map(|this| {
-                        if self.selected_index() == ix {
-                            this.end_slot(deleted_branch_icon(ix))
-                        } else {
-                            this.end_hover_slot(deleted_branch_icon(ix))
-                        }
-                    })
+                    this.end_slot(deleted_branch_icon(ix))
+                        .show_end_slot_on_hover()
                 })
                 .when_some(
                     if is_new_items {
@@ -1102,13 +1097,8 @@ impl PickerDelegate for BranchListDelegate {
                         None
                     },
                     |this, create_from_default_button| {
-                        this.map(|this| {
-                            if self.selected_index() == ix {
-                                this.end_slot(create_from_default_button)
-                            } else {
-                                this.end_hover_slot(create_from_default_button)
-                            }
-                        })
+                        this.end_slot(create_from_default_button)
+                            .show_end_slot_on_hover()
                     },
                 ),
         )

crates/git_ui/src/stash_picker.rs 🔗

@@ -501,16 +501,39 @@ impl PickerDelegate for StashListDelegate {
                     .size(LabelSize::Small),
             );
 
-        let focus_handle = self.focus_handle.clone();
+        let view_button = {
+            let focus_handle = self.focus_handle.clone();
+            IconButton::new(("view-stash", ix), IconName::Eye)
+                .icon_size(IconSize::Small)
+                .tooltip(move |_, cx| {
+                    Tooltip::for_action_in("View Stash", &ShowStashItem, &focus_handle, cx)
+                })
+                .on_click(cx.listener(move |this, _, window, cx| {
+                    this.delegate.show_stash_at(ix, window, cx);
+                }))
+        };
+
+        let pop_button = {
+            let focus_handle = self.focus_handle.clone();
+            IconButton::new(("pop-stash", ix), IconName::MaximizeAlt)
+                .icon_size(IconSize::Small)
+                .tooltip(move |_, cx| {
+                    Tooltip::for_action_in("Pop Stash", &menu::SecondaryConfirm, &focus_handle, cx)
+                })
+                .on_click(|_, window, cx| {
+                    window.dispatch_action(menu::SecondaryConfirm.boxed_clone(), cx);
+                })
+        };
 
-        let drop_button = |entry_ix: usize| {
-            IconButton::new(("drop-stash", entry_ix), IconName::Trash)
+        let drop_button = {
+            let focus_handle = self.focus_handle.clone();
+            IconButton::new(("drop-stash", ix), IconName::Trash)
                 .icon_size(IconSize::Small)
                 .tooltip(move |_, cx| {
                     Tooltip::for_action_in("Drop Stash", &DropStashItem, &focus_handle, cx)
                 })
                 .on_click(cx.listener(move |this, _, window, cx| {
-                    this.delegate.drop_stash_at(entry_ix, window, cx);
+                    this.delegate.drop_stash_at(ix, window, cx);
                 }))
         };
 
@@ -530,17 +553,14 @@ impl PickerDelegate for StashListDelegate {
                         )
                         .child(div().w_full().child(stash_label).child(branch_info)),
                 )
-                .tooltip(Tooltip::text(format!(
-                    "stash@{{{}}}",
-                    entry_match.entry.index
-                )))
-                .map(|this| {
-                    if selected {
-                        this.end_slot(drop_button(ix))
-                    } else {
-                        this.end_hover_slot(drop_button(ix))
-                    }
-                }),
+                .end_slot(
+                    h_flex()
+                        .gap_0p5()
+                        .child(view_button)
+                        .child(pop_button)
+                        .child(drop_button),
+                )
+                .show_end_slot_on_hover(),
         )
     }
 
@@ -549,6 +569,10 @@ impl PickerDelegate for StashListDelegate {
     }
 
     fn render_footer(&self, _: &mut Window, cx: &mut Context<Picker<Self>>) -> Option<AnyElement> {
+        if self.matches.is_empty() {
+            return None;
+        }
+
         let focus_handle = self.focus_handle.clone();
 
         Some(

crates/git_ui/src/worktree_picker.rs 🔗

@@ -254,6 +254,14 @@ struct WorktreeEntry {
     is_new: bool,
 }
 
+impl WorktreeEntry {
+    fn can_delete(&self, forbidden_deletion_path: Option<&PathBuf>) -> bool {
+        !self.is_new
+            && !self.worktree.is_main
+            && forbidden_deletion_path != Some(&self.worktree.path)
+    }
+}
+
 pub struct WorktreeListDelegate {
     matches: Vec<WorktreeEntry>,
     all_worktrees: Option<Vec<GitWorktree>>,
@@ -462,7 +470,7 @@ impl WorktreeListDelegate {
         let Some(entry) = self.matches.get(idx).cloned() else {
             return;
         };
-        if entry.is_new || self.forbidden_deletion_path.as_ref() == Some(&entry.worktree.path) {
+        if !entry.can_delete(self.forbidden_deletion_path.as_ref()) {
             return;
         }
         let Some(repo) = self.repo.clone() else {
@@ -719,6 +727,7 @@ impl PickerDelegate for WorktreeListDelegate {
                                 path: Default::default(),
                                 ref_name: Some(format!("refs/heads/{query}").into()),
                                 sha: Default::default(),
+                                is_main: false,
                             },
                             positions: Vec::new(),
                             is_new: true,
@@ -805,8 +814,7 @@ impl PickerDelegate for WorktreeListDelegate {
 
         let focus_handle = self.focus_handle.clone();
 
-        let can_delete =
-            !entry.is_new && self.forbidden_deletion_path.as_ref() != Some(&entry.worktree.path);
+        let can_delete = entry.can_delete(self.forbidden_deletion_path.as_ref());
 
         let delete_button = |entry_ix: usize| {
             IconButton::new(("delete-worktree", entry_ix), IconName::Trash)
@@ -876,12 +884,30 @@ impl PickerDelegate for WorktreeListDelegate {
                             }
                         })),
                 )
-                .when(can_delete, |this| {
-                    if selected {
-                        this.end_slot(delete_button(ix))
-                    } else {
-                        this.end_hover_slot(delete_button(ix))
-                    }
+                .when(!entry.is_new, |this| {
+                    let focus_handle = self.focus_handle.clone();
+                    let open_in_new_window_button =
+                        IconButton::new(("open-new-window", ix), IconName::ArrowUpRight)
+                            .icon_size(IconSize::Small)
+                            .tooltip(move |_, cx| {
+                                Tooltip::for_action_in(
+                                    "Open in New Window",
+                                    &menu::SecondaryConfirm,
+                                    &focus_handle,
+                                    cx,
+                                )
+                            })
+                            .on_click(|_, window, cx| {
+                                window.dispatch_action(menu::SecondaryConfirm.boxed_clone(), cx);
+                            });
+
+                    this.end_slot(
+                        h_flex()
+                            .gap_0p5()
+                            .child(open_in_new_window_button)
+                            .when(can_delete, |this| this.child(delete_button(ix))),
+                    )
+                    .show_end_slot_on_hover()
                 }),
         )
     }
@@ -894,9 +920,8 @@ impl PickerDelegate for WorktreeListDelegate {
         let focus_handle = self.focus_handle.clone();
         let selected_entry = self.matches.get(self.selected_index);
         let is_creating = selected_entry.is_some_and(|entry| entry.is_new);
-        let can_delete = selected_entry.is_some_and(|entry| {
-            !entry.is_new && self.forbidden_deletion_path.as_ref() != Some(&entry.worktree.path)
-        });
+        let can_delete = selected_entry
+            .is_some_and(|entry| entry.can_delete(self.forbidden_deletion_path.as_ref()));
 
         let footer_container = h_flex()
             .w_full()

crates/gpui/examples/input.rs 🔗

@@ -85,14 +85,24 @@ impl TextInput {
 
     fn backspace(&mut self, _: &Backspace, window: &mut Window, cx: &mut Context<Self>) {
         if self.selected_range.is_empty() {
-            self.select_to(self.previous_boundary(self.cursor_offset()), cx)
+            let prev = self.previous_boundary(self.cursor_offset());
+            if self.cursor_offset() == prev {
+                window.play_system_bell();
+                return;
+            }
+            self.select_to(prev, cx)
         }
         self.replace_text_in_range(None, "", window, cx)
     }
 
     fn delete(&mut self, _: &Delete, window: &mut Window, cx: &mut Context<Self>) {
         if self.selected_range.is_empty() {
-            self.select_to(self.next_boundary(self.cursor_offset()), cx)
+            let next = self.next_boundary(self.cursor_offset());
+            if self.cursor_offset() == next {
+                window.play_system_bell();
+                return;
+            }
+            self.select_to(next, cx)
         }
         self.replace_text_in_range(None, "", window, cx)
     }

crates/gpui/src/platform.rs 🔗

@@ -689,6 +689,8 @@ pub trait PlatformWindow: HasWindowHandle + HasDisplayHandle {
 
     fn update_ime_position(&self, _bounds: Bounds<Pixels>);
 
+    fn play_system_bell(&self) {}
+
     #[cfg(any(test, feature = "test-support"))]
     fn as_test(&mut self) -> Option<&mut TestWindow> {
         None

crates/gpui/src/window.rs 🔗

@@ -5024,6 +5024,12 @@ impl Window {
             .set_tabbing_identifier(tabbing_identifier)
     }
 
+    /// Request the OS to play an alert sound. On some platforms this is associated
+    /// with the window, for others it's just a simple global function call.
+    pub fn play_system_bell(&self) {
+        self.platform_window.play_system_bell()
+    }
+
     /// Toggles the inspector mode on this window.
     #[cfg(any(feature = "inspector", debug_assertions))]
     pub fn toggle_inspector(&mut self, cx: &mut App) {

crates/gpui_linux/src/linux/wayland/client.rs 🔗

@@ -58,6 +58,7 @@ use wayland_protocols::xdg::decoration::zv1::client::{
     zxdg_decoration_manager_v1, zxdg_toplevel_decoration_v1,
 };
 use wayland_protocols::xdg::shell::client::{xdg_surface, xdg_toplevel, xdg_wm_base};
+use wayland_protocols::xdg::system_bell::v1::client::xdg_system_bell_v1;
 use wayland_protocols::{
     wp::cursor_shape::v1::client::{wp_cursor_shape_device_v1, wp_cursor_shape_manager_v1},
     xdg::dialog::v1::client::xdg_wm_dialog_v1::{self, XdgWmDialogV1},
@@ -129,6 +130,7 @@ pub struct Globals {
     pub text_input_manager: Option<zwp_text_input_manager_v3::ZwpTextInputManagerV3>,
     pub gesture_manager: Option<zwp_pointer_gestures_v1::ZwpPointerGesturesV1>,
     pub dialog: Option<xdg_wm_dialog_v1::XdgWmDialogV1>,
+    pub system_bell: Option<xdg_system_bell_v1::XdgSystemBellV1>,
     pub executor: ForegroundExecutor,
 }
 
@@ -170,6 +172,7 @@ impl Globals {
             text_input_manager: globals.bind(&qh, 1..=1, ()).ok(),
             gesture_manager: globals.bind(&qh, 1..=3, ()).ok(),
             dialog: globals.bind(&qh, dialog_v..=dialog_v, ()).ok(),
+            system_bell: globals.bind(&qh, 1..=1, ()).ok(),
             executor,
             qh,
         }
@@ -1069,6 +1072,7 @@ impl Dispatch<wl_registry::WlRegistry, GlobalListContents> for WaylandClientStat
 }
 
 delegate_noop!(WaylandClientStatePtr: ignore xdg_activation_v1::XdgActivationV1);
+delegate_noop!(WaylandClientStatePtr: ignore xdg_system_bell_v1::XdgSystemBellV1);
 delegate_noop!(WaylandClientStatePtr: ignore wl_compositor::WlCompositor);
 delegate_noop!(WaylandClientStatePtr: ignore wp_cursor_shape_device_v1::WpCursorShapeDeviceV1);
 delegate_noop!(WaylandClientStatePtr: ignore wp_cursor_shape_manager_v1::WpCursorShapeManagerV1);

crates/gpui_linux/src/linux/wayland/window.rs 🔗

@@ -1479,6 +1479,18 @@ impl PlatformWindow for WaylandWindow {
     fn gpu_specs(&self) -> Option<GpuSpecs> {
         self.borrow().renderer.gpu_specs().into()
     }
+
+    fn play_system_bell(&self) {
+        let state = self.borrow();
+        let surface = if state.surface_state.toplevel().is_some() {
+            Some(&state.surface)
+        } else {
+            None
+        };
+        if let Some(bell) = state.globals.system_bell.as_ref() {
+            bell.ring(surface);
+        }
+    }
 }
 
 fn update_window(mut state: RefMut<WaylandWindowState>) {

crates/gpui_linux/src/linux/x11/window.rs 🔗

@@ -1846,4 +1846,9 @@ impl PlatformWindow for X11Window {
     fn gpu_specs(&self) -> Option<GpuSpecs> {
         self.0.state.borrow().renderer.gpu_specs().into()
     }
+
+    fn play_system_bell(&self) {
+        // Volume 0% means don't increase or decrease from system volume
+        let _ = self.0.xcb.bell(0);
+    }
 }

crates/gpui_macos/Cargo.toml 🔗

@@ -48,6 +48,7 @@ mach2.workspace = true
 media.workspace = true
 metal.workspace = true
 objc.workspace = true
+objc2-app-kit.workspace = true
 parking_lot.workspace = true
 pathfinder_geometry = "0.5"
 raw-window-handle = "0.6"

crates/gpui_macos/src/window.rs 🔗

@@ -49,6 +49,7 @@ use objc::{
     runtime::{BOOL, Class, NO, Object, Protocol, Sel, YES},
     sel, sel_impl,
 };
+use objc2_app_kit::NSBeep;
 use parking_lot::Mutex;
 use raw_window_handle as rwh;
 use smallvec::SmallVec;
@@ -1676,6 +1677,10 @@ impl PlatformWindow for MacWindow {
         }
     }
 
+    fn play_system_bell(&self) {
+        unsafe { NSBeep() }
+    }
+
     #[cfg(any(test, feature = "test-support"))]
     fn render_to_image(&self, scene: &gpui::Scene) -> Result<RgbaImage> {
         let mut this = self.0.lock();

crates/gpui_windows/src/window.rs 🔗

@@ -20,7 +20,9 @@ use windows::{
         Foundation::*,
         Graphics::Dwm::*,
         Graphics::Gdi::*,
-        System::{Com::*, LibraryLoader::*, Ole::*, SystemServices::*},
+        System::{
+            Com::*, Diagnostics::Debug::MessageBeep, LibraryLoader::*, Ole::*, SystemServices::*,
+        },
         UI::{Controls::*, HiDpi::*, Input::KeyboardAndMouse::*, Shell::*, WindowsAndMessaging::*},
     },
     core::*,
@@ -950,6 +952,11 @@ impl PlatformWindow for WindowsWindow {
 
         self.0.update_ime_position(self.0.hwnd, caret_position);
     }
+
+    fn play_system_bell(&self) {
+        // MB_OK: The sound specified as the Windows Default Beep sound.
+        let _ = unsafe { MessageBeep(MB_OK) };
+    }
 }
 
 #[implement(IDropTarget)]

crates/icons/src/icons.rs 🔗

@@ -174,6 +174,7 @@ pub enum IconName {
     LockOutlined,
     MagnifyingGlass,
     Maximize,
+    MaximizeAlt,
     Menu,
     MenuAltTemp,
     Mic,
@@ -240,6 +241,7 @@ pub enum IconName {
     ThinkingModeOff,
     Thread,
     ThreadFromSummary,
+    ThreadImport,
     ThreadsSidebarLeftClosed,
     ThreadsSidebarLeftOpen,
     ThreadsSidebarRightClosed,

crates/markdown/src/markdown.rs 🔗

@@ -1609,23 +1609,18 @@ impl Element for MarkdownElement {
                             builder.table.start(alignments.clone());
 
                             let column_count = alignments.len();
-                            builder.push_div(
-                                div().flex().flex_col().items_start(),
-                                range,
-                                markdown_end,
-                            );
                             builder.push_div(
                                 div()
                                     .id(("table", range.start))
-                                    .min_w_0()
                                     .grid()
                                     .grid_cols(column_count as u16)
                                     .when(self.style.table_columns_min_size, |this| {
                                         this.grid_cols_min_content(column_count as u16)
                                     })
                                     .when(!self.style.table_columns_min_size, |this| {
-                                        this.grid_cols_max_content(column_count as u16)
+                                        this.grid_cols(column_count as u16)
                                     })
+                                    .w_full()
                                     .mb_2()
                                     .border(px(1.5))
                                     .border_color(cx.theme().colors().border)
@@ -1770,7 +1765,6 @@ impl Element for MarkdownElement {
                         }
                     }
                     MarkdownTagEnd::Table => {
-                        builder.pop_div();
                         builder.pop_div();
                         builder.table.end();
                     }

crates/project/src/git_store.rs 🔗

@@ -7144,6 +7144,7 @@ fn worktree_to_proto(worktree: &git::repository::Worktree) -> proto::Worktree {
             .map(|s| s.to_string())
             .unwrap_or_default(),
         sha: worktree.sha.to_string(),
+        is_main: worktree.is_main,
     }
 }
 
@@ -7152,6 +7153,7 @@ fn proto_to_worktree(proto: &proto::Worktree) -> git::repository::Worktree {
         path: PathBuf::from(proto.path.clone()),
         ref_name: Some(SharedString::from(&proto.ref_name)),
         sha: proto.sha.clone().into(),
+        is_main: proto.is_main,
     }
 }
 

crates/project/src/lsp_store.rs 🔗

@@ -4413,7 +4413,8 @@ impl LspStore {
                         this.update_local_worktree_language_servers(&worktree, changes, cx);
                     }
                     worktree::Event::UpdatedGitRepositories(_)
-                    | worktree::Event::DeletedEntry(_) => {}
+                    | worktree::Event::DeletedEntry(_)
+                    | worktree::Event::Deleted => {}
                 })
                 .detach()
             }

crates/project/src/manifest_tree.rs 🔗

@@ -59,6 +59,7 @@ impl WorktreeRoots {
                         let path = TriePath::from(entry.path.as_ref());
                         this.roots.remove(&path);
                     }
+                    WorktreeEvent::Deleted => {}
                 }
             }),
         })

crates/project/src/worktree_store.rs 🔗

@@ -808,6 +808,10 @@ impl WorktreeStore {
                 worktree::Event::DeletedEntry(id) => {
                     cx.emit(WorktreeStoreEvent::WorktreeDeletedEntry(worktree_id, *id))
                 }
+                worktree::Event::Deleted => {
+                    // The worktree root itself has been deleted (for single-file worktrees)
+                    // The worktree will be removed via the observe_release callback
+                }
             }
         })
         .detach();

crates/proto/proto/git.proto 🔗

@@ -576,6 +576,7 @@ message Worktree {
   string path = 1;
   string ref_name = 2;
   string sha = 3;
+  bool is_main = 4;
 }
 
 message GitCreateWorktree {

crates/recent_projects/src/recent_projects.rs 🔗

@@ -1264,13 +1264,8 @@ impl PickerDelegate for RecentProjectsDelegate {
                                     this.tooltip(Tooltip::text(path.to_string_lossy().to_string()))
                                 }),
                         )
-                        .map(|el| {
-                            if self.selected_index == ix {
-                                el.end_slot(secondary_actions)
-                            } else {
-                                el.end_hover_slot(secondary_actions)
-                            }
-                        })
+                        .end_slot(secondary_actions)
+                        .show_end_slot_on_hover()
                         .into_any_element(),
                 )
             }
@@ -1363,13 +1358,8 @@ impl PickerDelegate for RecentProjectsDelegate {
                                 })
                                 .tooltip(Tooltip::text(tooltip_path)),
                         )
-                        .map(|el| {
-                            if self.selected_index == ix {
-                                el.end_slot(secondary_actions)
-                            } else {
-                                el.end_hover_slot(secondary_actions)
-                            }
-                        })
+                        .end_slot(secondary_actions)
+                        .show_end_slot_on_hover()
                         .into_any_element(),
                 )
             }
@@ -1503,13 +1493,8 @@ impl PickerDelegate for RecentProjectsDelegate {
                                 })
                                 .tooltip(Tooltip::text(tooltip_path)),
                         )
-                        .map(|el| {
-                            if self.selected_index == ix {
-                                el.end_slot(secondary_actions)
-                            } else {
-                                el.end_hover_slot(secondary_actions)
-                            }
-                        })
+                        .end_slot(secondary_actions)
+                        .show_end_slot_on_hover()
                         .into_any_element(),
                 )
             }

crates/recent_projects/src/remote_servers.rs 🔗

@@ -1621,23 +1621,24 @@ impl RemoteServerProjects {
                     }))
                     .tooltip(Tooltip::text(project.paths.join("\n")))
                     .when(is_from_zed, |server_list_item| {
-                        server_list_item.end_hover_slot::<AnyElement>(Some(
-                            div()
-                                .mr_2()
-                                .child({
-                                    let project = project.clone();
-                                    // Right-margin to offset it from the Scrollbar
-                                    IconButton::new("remove-remote-project", IconName::Trash)
-                                        .icon_size(IconSize::Small)
-                                        .shape(IconButtonShape::Square)
-                                        .size(ButtonSize::Large)
-                                        .tooltip(Tooltip::text("Delete Remote Project"))
-                                        .on_click(cx.listener(move |this, _, _, cx| {
-                                            this.delete_remote_project(server_ix, &project, cx)
-                                        }))
-                                })
-                                .into_any_element(),
-                        ))
+                        server_list_item
+                            .end_slot(
+                                div()
+                                    .mr_2()
+                                    .child({
+                                        let project = project.clone();
+                                        IconButton::new("remove-remote-project", IconName::Trash)
+                                            .icon_size(IconSize::Small)
+                                            .shape(IconButtonShape::Square)
+                                            .size(ButtonSize::Large)
+                                            .tooltip(Tooltip::text("Delete Remote Project"))
+                                            .on_click(cx.listener(move |this, _, _, cx| {
+                                                this.delete_remote_project(server_ix, &project, cx)
+                                            }))
+                                    })
+                                    .into_any_element(),
+                            )
+                            .show_end_slot_on_hover()
                     }),
             )
     }
@@ -2413,9 +2414,8 @@ impl RemoteServerProjects {
                             .spacing(ui::ListItemSpacing::Sparse)
                             .start_slot(Icon::new(IconName::Copy).color(Color::Muted))
                             .child(Label::new("Copy Server Address"))
-                            .end_hover_slot(
-                                Label::new(connection_string.clone()).color(Color::Muted),
-                            )
+                            .end_slot(Label::new(connection_string.clone()).color(Color::Muted))
+                            .show_end_slot_on_hover()
                             .on_click({
                                 let connection_string = connection_string.clone();
                                 move |_, _, cx| {

crates/rules_library/src/rules_library.rs 🔗

@@ -389,7 +389,7 @@ impl PickerDelegate for RulePickerDelegate {
                                 }))
                         }))
                         .when(!prompt_id.is_built_in(), |this| {
-                            this.end_hover_slot(
+                            this.end_slot_on_hover(
                                 h_flex()
                                     .child(
                                         IconButton::new("delete-rule", IconName::Trash)

crates/sidebar/src/project_group_builder.rs 🔗

@@ -255,6 +255,7 @@ mod tests {
                 path: std::path::PathBuf::from("/wt/feature-a"),
                 ref_name: Some("refs/heads/feature-a".into()),
                 sha: "abc".into(),
+                is_main: false,
             });
         })
         .expect("git state should be set");

crates/sidebar/src/sidebar.rs 🔗

@@ -3974,10 +3974,24 @@ impl Sidebar {
     }
 
     fn render_sidebar_bottom_bar(&mut self, cx: &mut Context<Self>) -> impl IntoElement {
-        let on_right = self.side(cx) == SidebarSide::Right;
         let is_archive = matches!(self.view, SidebarView::Archive(..));
+        let show_import_button = is_archive && !self.should_render_acp_import_onboarding(cx);
+        let on_right = self.side(cx) == SidebarSide::Right;
+
         let action_buttons = h_flex()
             .gap_1()
+            .when(on_right, |this| this.flex_row_reverse())
+            .when(show_import_button, |this| {
+                this.child(
+                    IconButton::new("thread-import", IconName::ThreadImport)
+                        .icon_size(IconSize::Small)
+                        .tooltip(Tooltip::text("Import ACP Threads"))
+                        .on_click(cx.listener(|this, _, window, cx| {
+                            this.show_archive(window, cx);
+                            this.show_thread_import_modal(window, cx);
+                        })),
+                )
+            })
             .child(
                 IconButton::new("archive", IconName::Archive)
                     .icon_size(IconSize::Small)
@@ -3990,21 +4004,16 @@ impl Sidebar {
                     })),
             )
             .child(self.render_recent_projects_button(cx));
-        let border_color = cx.theme().colors().border;
-        let toggle_button = self.render_sidebar_toggle_button(cx);
 
-        let bar = h_flex()
+        h_flex()
             .p_1()
             .gap_1()
+            .when(on_right, |this| this.flex_row_reverse())
             .justify_between()
             .border_t_1()
-            .border_color(border_color);
-
-        if on_right {
-            bar.child(action_buttons).child(toggle_button)
-        } else {
-            bar.child(toggle_button).child(action_buttons)
-        }
+            .border_color(cx.theme().colors().border)
+            .child(self.render_sidebar_toggle_button(cx))
+            .child(action_buttons)
     }
 
     fn active_workspace(&self, cx: &App) -> Option<Entity<Workspace>> {
@@ -4074,7 +4083,7 @@ impl Sidebar {
         v_flex()
             .min_w_0()
             .w_full()
-            .p_1p5()
+            .p_2()
             .border_t_1()
             .border_color(cx.theme().colors().border)
             .bg(linear_gradient(
@@ -4102,8 +4111,8 @@ impl Sidebar {
                     .style(ButtonStyle::OutlinedCustom(cx.theme().colors().border))
                     .label_size(LabelSize::Small)
                     .start_icon(
-                        Icon::new(IconName::ArrowDown)
-                            .size(IconSize::XSmall)
+                        Icon::new(IconName::ThreadImport)
+                            .size(IconSize::Small)
                             .color(Color::Muted),
                     )
                     .on_click(cx.listener(|this, _, window, cx| {
@@ -4132,9 +4141,6 @@ impl Sidebar {
         let Some(agent_panel) = active_workspace.read(cx).panel::<AgentPanel>(cx) else {
             return;
         };
-        let Some(agent_registry_store) = AgentRegistryStore::try_global(cx) else {
-            return;
-        };
 
         let agent_server_store = active_workspace
             .read(cx)
@@ -4149,9 +4155,6 @@ impl Sidebar {
             ThreadsArchiveView::new(
                 agent_connection_store.clone(),
                 agent_server_store.clone(),
-                agent_registry_store.downgrade(),
-                active_workspace.downgrade(),
-                self.multi_workspace.clone(),
                 window,
                 cx,
             )

crates/sidebar/src/sidebar_tests.rs 🔗

@@ -2463,6 +2463,7 @@ async fn test_cmd_n_shows_new_thread_entry_in_absorbed_worktree(cx: &mut TestApp
             path: std::path::PathBuf::from("/wt-feature-a"),
             ref_name: Some("refs/heads/feature-a".into()),
             sha: "aaa".into(),
+            is_main: false,
         });
     })
     .unwrap();
@@ -2577,6 +2578,7 @@ async fn test_search_matches_worktree_name(cx: &mut TestAppContext) {
                 path: std::path::PathBuf::from("/wt/rosewood"),
                 ref_name: Some("refs/heads/rosewood".into()),
                 sha: "abc".into(),
+                is_main: false,
             });
         })
         .unwrap();
@@ -2638,6 +2640,7 @@ async fn test_git_worktree_added_live_updates_sidebar(cx: &mut TestAppContext) {
                 path: std::path::PathBuf::from("/wt/rosewood"),
                 ref_name: Some("refs/heads/rosewood".into()),
                 sha: "abc".into(),
+                is_main: false,
             });
         })
         .unwrap();
@@ -2739,11 +2742,13 @@ async fn test_two_worktree_workspaces_absorbed_when_main_added(cx: &mut TestAppC
             path: std::path::PathBuf::from("/wt-feature-a"),
             ref_name: Some("refs/heads/feature-a".into()),
             sha: "aaa".into(),
+            is_main: false,
         });
         state.worktrees.push(git::repository::Worktree {
             path: std::path::PathBuf::from("/wt-feature-b"),
             ref_name: Some("refs/heads/feature-b".into()),
             sha: "bbb".into(),
+            is_main: false,
         });
     })
     .unwrap();
@@ -2821,11 +2826,13 @@ async fn test_threadless_workspace_shows_new_thread_with_worktree_chip(cx: &mut
             path: std::path::PathBuf::from("/wt-feature-a"),
             ref_name: Some("refs/heads/feature-a".into()),
             sha: "aaa".into(),
+            is_main: false,
         });
         state.worktrees.push(git::repository::Worktree {
             path: std::path::PathBuf::from("/wt-feature-b"),
             ref_name: Some("refs/heads/feature-b".into()),
             sha: "bbb".into(),
+            is_main: false,
         });
     })
     .unwrap();
@@ -2941,6 +2948,7 @@ async fn test_multi_worktree_thread_shows_multiple_chips(cx: &mut TestAppContext
                     path: std::path::PathBuf::from(format!("/worktrees/{repo}/{branch}/{repo}")),
                     ref_name: Some(format!("refs/heads/{branch}").into()),
                     sha: "aaa".into(),
+                    is_main: false,
                 });
             }
         })
@@ -3043,6 +3051,7 @@ async fn test_same_named_worktree_chips_are_deduplicated(cx: &mut TestAppContext
                 path: std::path::PathBuf::from(format!("/worktrees/{repo}/olivetti/{repo}")),
                 ref_name: Some("refs/heads/olivetti".into()),
                 sha: "aaa".into(),
+                is_main: false,
             });
         })
         .unwrap();
@@ -3133,6 +3142,7 @@ async fn test_absorbed_worktree_running_thread_shows_live_status(cx: &mut TestAp
             path: std::path::PathBuf::from("/wt-feature-a"),
             ref_name: Some("refs/heads/feature-a".into()),
             sha: "aaa".into(),
+            is_main: false,
         });
     })
     .unwrap();
@@ -3248,6 +3258,7 @@ async fn test_absorbed_worktree_completion_triggers_notification(cx: &mut TestAp
             path: std::path::PathBuf::from("/wt-feature-a"),
             ref_name: Some("refs/heads/feature-a".into()),
             sha: "aaa".into(),
+            is_main: false,
         });
     })
     .unwrap();
@@ -3354,6 +3365,7 @@ async fn test_clicking_worktree_thread_opens_workspace_when_none_exists(cx: &mut
             path: std::path::PathBuf::from("/wt-feature-a"),
             ref_name: Some("refs/heads/feature-a".into()),
             sha: "aaa".into(),
+            is_main: false,
         });
     })
     .unwrap();
@@ -3459,6 +3471,7 @@ async fn test_clicking_worktree_thread_does_not_briefly_render_as_separate_proje
             path: std::path::PathBuf::from("/wt-feature-a"),
             ref_name: Some("refs/heads/feature-a".into()),
             sha: "aaa".into(),
+            is_main: false,
         });
     })
     .unwrap();
@@ -3609,6 +3622,7 @@ async fn test_clicking_absorbed_worktree_thread_activates_worktree_workspace(
             path: std::path::PathBuf::from("/wt-feature-a"),
             ref_name: Some("refs/heads/feature-a".into()),
             sha: "aaa".into(),
+            is_main: false,
         });
     })
     .unwrap();
@@ -4203,6 +4217,7 @@ async fn test_archive_thread_uses_next_threads_own_workspace(cx: &mut TestAppCon
             path: std::path::PathBuf::from("/wt-feature-a"),
             ref_name: Some("refs/heads/feature-a".into()),
             sha: "aaa".into(),
+            is_main: false,
         });
     })
     .unwrap();
@@ -4374,6 +4389,7 @@ async fn test_linked_worktree_threads_not_duplicated_across_groups(cx: &mut Test
             path: std::path::PathBuf::from("/wt-feature-a"),
             ref_name: Some("refs/heads/feature-a".into()),
             sha: "aaa".into(),
+            is_main: false,
         });
     })
     .unwrap();
@@ -5362,6 +5378,7 @@ mod property_test {
                             path: worktree_pathbuf,
                             ref_name: Some(format!("refs/heads/{}", worktree_name).into()),
                             sha: "aaa".into(),
+                            is_main: false,
                         });
                     })
                     .unwrap();

crates/tab_switcher/src/tab_switcher.rs 🔗

@@ -875,7 +875,7 @@ impl PickerDelegate for TabSwitcherDelegate {
                         el.end_slot::<AnyElement>(close_button)
                     } else {
                         el.end_slot::<AnyElement>(indicator)
-                            .end_hover_slot::<AnyElement>(close_button)
+                            .end_slot_on_hover::<AnyElement>(close_button)
                     }
                 }),
         )

crates/tasks_ui/src/modal.rs 🔗

@@ -570,7 +570,7 @@ impl PickerDelegate for TasksModalDelegate {
                                     Tooltip::simple("Delete Previously Scheduled Task", cx)
                                 }),
                         );
-                        item.end_hover_slot(delete_button)
+                        item.end_slot_on_hover(delete_button)
                     } else {
                         item
                     }

crates/title_bar/src/onboarding_banner.rs 🔗

@@ -1,3 +1,7 @@
+// This module provides infrastructure for showing onboarding banners in the title bar.
+// It's currently not in use but is kept for future feature announcements.
+#![allow(dead_code)]
+
 use gpui::{Action, Entity, Global, Render, SharedString};
 use ui::{ButtonLike, Tooltip, prelude::*};
 use util::ResultExt;
@@ -94,21 +98,21 @@ fn persist_dismissed(source: &str, cx: &mut App) {
 }
 
 pub fn restore_banner(cx: &mut App) {
-    cx.defer(|cx| {
-        cx.global::<BannerGlobal>()
-            .entity
-            .clone()
-            .update(cx, |this, cx| {
+    if let Some(banner_global) = cx.try_global::<BannerGlobal>() {
+        let entity = banner_global.entity.clone();
+        cx.defer(move |cx| {
+            entity.update(cx, |this, cx| {
                 this.dismissed = false;
                 cx.notify();
             });
-    });
+        });
 
-    let source = &cx.global::<BannerGlobal>().entity.read(cx).source;
-    let dismissed_at = dismissed_at_key(source);
-    let kvp = db::kvp::KeyValueStore::global(cx);
-    cx.spawn(async move |_| kvp.delete_kvp(dismissed_at).await)
-        .detach_and_log_err(cx);
+        let source = &cx.global::<BannerGlobal>().entity.read(cx).source;
+        let dismissed_at = dismissed_at_key(source);
+        let kvp = db::kvp::KeyValueStore::global(cx);
+        cx.spawn(async move |_| kvp.delete_kvp(dismissed_at).await)
+            .detach_and_log_err(cx);
+    }
 }
 
 impl Render for OnboardingBanner {

crates/title_bar/src/title_bar.rs 🔗

@@ -155,7 +155,7 @@ pub struct TitleBar {
     multi_workspace: Option<WeakEntity<MultiWorkspace>>,
     application_menu: Option<Entity<ApplicationMenu>>,
     _subscriptions: Vec<Subscription>,
-    banner: Entity<OnboardingBanner>,
+    banner: Option<Entity<OnboardingBanner>>,
     update_version: Entity<UpdateVersion>,
     screen_share_popover_handle: PopoverMenuHandle<ContextMenu>,
     _diagnostics_subscription: Option<gpui::Subscription>,
@@ -246,7 +246,9 @@ impl Render for TitleBar {
         children.push(self.render_collaborator_list(window, cx).into_any_element());
 
         if title_bar_settings.show_onboarding_banner {
-            children.push(self.banner.clone().into_any_element())
+            if let Some(banner) = &self.banner {
+                children.push(banner.clone().into_any_element())
+            }
         }
 
         let status = self.client.status();
@@ -385,19 +387,6 @@ impl TitleBar {
             }));
         }
 
-        let banner = cx.new(|cx| {
-            OnboardingBanner::new(
-                "ACP Claude Code Onboarding",
-                IconName::AiClaude,
-                "Claude Agent",
-                Some("Introducing:".into()),
-                zed_actions::agent::OpenClaudeAgentOnboardingModal.boxed_clone(),
-                cx,
-            )
-            // When updating this to a non-AI feature release, remove this line.
-            .visible_when(|cx| !project::DisableAiSettings::get_global(cx).disable_ai)
-        });
-
         let update_version = cx.new(|cx| UpdateVersion::new(cx));
         let platform_titlebar = cx.new(|cx| {
             let mut titlebar = PlatformTitleBar::new(id, cx);
@@ -416,7 +405,7 @@ impl TitleBar {
             user_store,
             client,
             _subscriptions: subscriptions,
-            banner,
+            banner: None,
             update_version,
             screen_share_popover_handle: PopoverMenuHandle::default(),
             _diagnostics_subscription: None,

crates/ui/src/components/list/list_item.rs 🔗

@@ -14,6 +14,14 @@ pub enum ListItemSpacing {
     Sparse,
 }
 
+#[derive(Default)]
+enum EndSlotVisibility {
+    #[default]
+    Always,
+    OnHover,
+    SwapOnHover(AnyElement),
+}
+
 #[derive(IntoElement, RegisterComponent)]
 pub struct ListItem {
     id: ElementId,
@@ -28,9 +36,7 @@ pub struct ListItem {
     /// A slot for content that appears after the children, usually on the other side of the header.
     /// This might be a button, a disclosure arrow, a face pile, etc.
     end_slot: Option<AnyElement>,
-    /// A slot for content that appears on hover after the children
-    /// It will obscure the `end_slot` when visible.
-    end_hover_slot: Option<AnyElement>,
+    end_slot_visibility: EndSlotVisibility,
     toggle: Option<bool>,
     inset: bool,
     on_click: Option<Box<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>>,
@@ -61,7 +67,7 @@ impl ListItem {
             indent_step_size: px(12.),
             start_slot: None,
             end_slot: None,
-            end_hover_slot: None,
+            end_slot_visibility: EndSlotVisibility::default(),
             toggle: None,
             inset: false,
             on_click: None,
@@ -165,8 +171,14 @@ impl ListItem {
         self
     }
 
-    pub fn end_hover_slot<E: IntoElement>(mut self, end_hover_slot: impl Into<Option<E>>) -> Self {
-        self.end_hover_slot = end_hover_slot.into().map(IntoElement::into_any_element);
+    pub fn end_slot_on_hover<E: IntoElement>(mut self, end_slot_on_hover: E) -> Self {
+        self.end_slot_visibility =
+            EndSlotVisibility::SwapOnHover(end_slot_on_hover.into_any_element());
+        self
+    }
+
+    pub fn show_end_slot_on_hover(mut self) -> Self {
+        self.end_slot_visibility = EndSlotVisibility::OnHover;
         self
     }
 
@@ -234,9 +246,9 @@ impl RenderOnce for ListItem {
                 this.ml(self.indent_level as f32 * self.indent_step_size)
                     .px(DynamicSpacing::Base04.rems(cx))
             })
-            .when(!self.inset && !self.disabled, |this| {
+            .when(!self.inset, |this| {
                 this.when_some(self.focused, |this, focused| {
-                    if focused {
+                    if focused && !self.disabled {
                         this.border_1()
                             .when(self.docked_right, |this| this.border_r_2())
                             .border_color(cx.theme().colors().border_focused)
@@ -244,7 +256,7 @@ impl RenderOnce for ListItem {
                         this.border_1()
                     }
                 })
-                .when(self.selectable, |this| {
+                .when(self.selectable && !self.disabled, |this| {
                     this.hover(|style| style.bg(cx.theme().colors().ghost_element_hover))
                         .active(|style| style.bg(cx.theme().colors().ghost_element_active))
                         .when(self.outlined, |this| this.rounded_sm())
@@ -268,16 +280,16 @@ impl RenderOnce for ListItem {
                         ListItemSpacing::ExtraDense => this.py_neg_px(),
                         ListItemSpacing::Sparse => this.py_1(),
                     })
-                    .when(self.inset && !self.disabled, |this| {
+                    .when(self.inset, |this| {
                         this.when_some(self.focused, |this, focused| {
-                            if focused {
+                            if focused && !self.disabled {
                                 this.border_1()
                                     .border_color(cx.theme().colors().border_focused)
                             } else {
                                 this.border_1()
                             }
                         })
-                        .when(self.selectable, |this| {
+                        .when(self.selectable && !self.disabled, |this| {
                             this.hover(|style| style.bg(cx.theme().colors().ghost_element_hover))
                                 .active(|style| style.bg(cx.theme().colors().ghost_element_active))
                                 .when(self.selected, |this| {
@@ -338,28 +350,31 @@ impl RenderOnce for ListItem {
                             .children(self.start_slot)
                             .children(self.children),
                     )
+                    .when(self.end_slot.is_some(), |this| this.justify_between())
                     .when_some(self.end_slot, |this, end_slot| {
-                        this.justify_between().child(
-                            h_flex()
+                        this.child(match self.end_slot_visibility {
+                            EndSlotVisibility::Always => {
+                                h_flex().flex_shrink().overflow_hidden().child(end_slot)
+                            }
+                            EndSlotVisibility::OnHover => h_flex()
                                 .flex_shrink()
                                 .overflow_hidden()
-                                .when(self.end_hover_slot.is_some(), |this| {
-                                    this.visible()
-                                        .group_hover("list_item", |this| this.invisible())
-                                })
-                                .child(end_slot),
-                        )
-                    })
-                    .when_some(self.end_hover_slot, |this, end_hover_slot| {
-                        this.child(
-                            h_flex()
-                                .h_full()
-                                .absolute()
-                                .right(DynamicSpacing::Base06.rems(cx))
-                                .top_0()
                                 .visible_on_hover("list_item")
-                                .child(end_hover_slot),
-                        )
+                                .child(end_slot),
+                            EndSlotVisibility::SwapOnHover(hover_slot) => h_flex()
+                                .relative()
+                                .flex_shrink()
+                                .child(h_flex().visible_on_hover("list_item").child(hover_slot))
+                                .child(
+                                    h_flex()
+                                        .absolute()
+                                        .inset_0()
+                                        .justify_end()
+                                        .overflow_hidden()
+                                        .group_hover("list_item", |this| this.invisible())
+                                        .child(end_slot),
+                                ),
+                        })
                     }),
             )
     }

crates/workspace/src/multi_workspace.rs 🔗

@@ -1113,178 +1113,3 @@ impl Render for MultiWorkspace {
         )
     }
 }
-
-#[cfg(test)]
-mod tests {
-    use super::*;
-    use fs::FakeFs;
-    use gpui::TestAppContext;
-    use settings::SettingsStore;
-
-    fn init_test(cx: &mut TestAppContext) {
-        cx.update(|cx| {
-            let settings_store = SettingsStore::test(cx);
-            cx.set_global(settings_store);
-            theme_settings::init(theme::LoadThemes::JustBase, cx);
-            DisableAiSettings::register(cx);
-            cx.update_flags(false, vec!["agent-v2".into()]);
-        });
-    }
-
-    #[gpui::test]
-    async fn test_sidebar_disabled_when_disable_ai_is_enabled(cx: &mut TestAppContext) {
-        init_test(cx);
-        let fs = FakeFs::new(cx.executor());
-        let project = Project::test(fs, [], cx).await;
-
-        let (multi_workspace, cx) =
-            cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx));
-
-        multi_workspace.read_with(cx, |mw, cx| {
-            assert!(mw.multi_workspace_enabled(cx));
-        });
-
-        multi_workspace.update_in(cx, |mw, _window, cx| {
-            mw.open_sidebar(cx);
-            assert!(mw.sidebar_open());
-        });
-
-        cx.update(|_window, cx| {
-            DisableAiSettings::override_global(DisableAiSettings { disable_ai: true }, cx);
-        });
-        cx.run_until_parked();
-
-        multi_workspace.read_with(cx, |mw, cx| {
-            assert!(
-                !mw.sidebar_open(),
-                "Sidebar should be closed when disable_ai is true"
-            );
-            assert!(
-                !mw.multi_workspace_enabled(cx),
-                "Multi-workspace should be disabled when disable_ai is true"
-            );
-        });
-
-        multi_workspace.update_in(cx, |mw, window, cx| {
-            mw.toggle_sidebar(window, cx);
-        });
-        multi_workspace.read_with(cx, |mw, _cx| {
-            assert!(
-                !mw.sidebar_open(),
-                "Sidebar should remain closed when toggled with disable_ai true"
-            );
-        });
-
-        cx.update(|_window, cx| {
-            DisableAiSettings::override_global(DisableAiSettings { disable_ai: false }, cx);
-        });
-        cx.run_until_parked();
-
-        multi_workspace.read_with(cx, |mw, cx| {
-            assert!(
-                mw.multi_workspace_enabled(cx),
-                "Multi-workspace should be enabled after re-enabling AI"
-            );
-            assert!(
-                !mw.sidebar_open(),
-                "Sidebar should still be closed after re-enabling AI (not auto-opened)"
-            );
-        });
-
-        multi_workspace.update_in(cx, |mw, window, cx| {
-            mw.toggle_sidebar(window, cx);
-        });
-        multi_workspace.read_with(cx, |mw, _cx| {
-            assert!(
-                mw.sidebar_open(),
-                "Sidebar should open when toggled after re-enabling AI"
-            );
-        });
-    }
-
-    #[gpui::test]
-    async fn test_replace(cx: &mut TestAppContext) {
-        init_test(cx);
-        let fs = FakeFs::new(cx.executor());
-        let project_a = Project::test(fs.clone(), [], cx).await;
-        let project_b = Project::test(fs.clone(), [], cx).await;
-        let project_c = Project::test(fs.clone(), [], cx).await;
-        let project_d = Project::test(fs.clone(), [], cx).await;
-
-        let (multi_workspace, cx) = cx
-            .add_window_view(|window, cx| MultiWorkspace::test_new(project_a.clone(), window, cx));
-
-        let workspace_a_id =
-            multi_workspace.read_with(cx, |mw, _cx| mw.workspaces()[0].entity_id());
-
-        // Replace the only workspace (single-workspace case).
-        let workspace_b = multi_workspace.update_in(cx, |mw, window, cx| {
-            let workspace = cx.new(|cx| Workspace::test_new(project_b.clone(), window, cx));
-            mw.replace(workspace.clone(), &*window, cx);
-            workspace
-        });
-
-        multi_workspace.read_with(cx, |mw, _cx| {
-            assert_eq!(mw.workspaces().len(), 1);
-            assert_eq!(
-                mw.workspaces()[0].entity_id(),
-                workspace_b.entity_id(),
-                "slot should now be project_b"
-            );
-            assert_ne!(
-                mw.workspaces()[0].entity_id(),
-                workspace_a_id,
-                "project_a should be gone"
-            );
-        });
-
-        // Add project_c as a second workspace, then replace it with project_d.
-        let workspace_c = multi_workspace.update_in(cx, |mw, window, cx| {
-            mw.test_add_workspace(project_c.clone(), window, cx)
-        });
-
-        multi_workspace.read_with(cx, |mw, _cx| {
-            assert_eq!(mw.workspaces().len(), 2);
-            assert_eq!(mw.active_workspace_index(), 1);
-        });
-
-        let workspace_d = multi_workspace.update_in(cx, |mw, window, cx| {
-            let workspace = cx.new(|cx| Workspace::test_new(project_d.clone(), window, cx));
-            mw.replace(workspace.clone(), &*window, cx);
-            workspace
-        });
-
-        multi_workspace.read_with(cx, |mw, _cx| {
-            assert_eq!(mw.workspaces().len(), 2, "should still have 2 workspaces");
-            assert_eq!(mw.active_workspace_index(), 1);
-            assert_eq!(
-                mw.workspaces()[1].entity_id(),
-                workspace_d.entity_id(),
-                "active slot should now be project_d"
-            );
-            assert_ne!(
-                mw.workspaces()[1].entity_id(),
-                workspace_c.entity_id(),
-                "project_c should be gone"
-            );
-        });
-
-        // Replace with workspace_b which is already in the list — should just switch.
-        multi_workspace.update_in(cx, |mw, window, cx| {
-            mw.replace(workspace_b.clone(), &*window, cx);
-        });
-
-        multi_workspace.read_with(cx, |mw, _cx| {
-            assert_eq!(
-                mw.workspaces().len(),
-                2,
-                "no workspace should be added or removed"
-            );
-            assert_eq!(
-                mw.active_workspace_index(),
-                0,
-                "should have switched to workspace_b"
-            );
-        });
-    }
-}

crates/workspace/src/multi_workspace_tests.rs 🔗

@@ -0,0 +1,172 @@
+use super::*;
+use feature_flags::FeatureFlagAppExt;
+use fs::FakeFs;
+use gpui::TestAppContext;
+use project::DisableAiSettings;
+use settings::SettingsStore;
+
+fn init_test(cx: &mut TestAppContext) {
+    cx.update(|cx| {
+        let settings_store = SettingsStore::test(cx);
+        cx.set_global(settings_store);
+        theme_settings::init(theme::LoadThemes::JustBase, cx);
+        DisableAiSettings::register(cx);
+        cx.update_flags(false, vec!["agent-v2".into()]);
+    });
+}
+
+#[gpui::test]
+async fn test_sidebar_disabled_when_disable_ai_is_enabled(cx: &mut TestAppContext) {
+    init_test(cx);
+    let fs = FakeFs::new(cx.executor());
+    let project = Project::test(fs, [], cx).await;
+
+    let (multi_workspace, cx) =
+        cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx));
+
+    multi_workspace.read_with(cx, |mw, cx| {
+        assert!(mw.multi_workspace_enabled(cx));
+    });
+
+    multi_workspace.update_in(cx, |mw, _window, cx| {
+        mw.open_sidebar(cx);
+        assert!(mw.sidebar_open());
+    });
+
+    cx.update(|_window, cx| {
+        DisableAiSettings::override_global(DisableAiSettings { disable_ai: true }, cx);
+    });
+    cx.run_until_parked();
+
+    multi_workspace.read_with(cx, |mw, cx| {
+        assert!(
+            !mw.sidebar_open(),
+            "Sidebar should be closed when disable_ai is true"
+        );
+        assert!(
+            !mw.multi_workspace_enabled(cx),
+            "Multi-workspace should be disabled when disable_ai is true"
+        );
+    });
+
+    multi_workspace.update_in(cx, |mw, window, cx| {
+        mw.toggle_sidebar(window, cx);
+    });
+    multi_workspace.read_with(cx, |mw, _cx| {
+        assert!(
+            !mw.sidebar_open(),
+            "Sidebar should remain closed when toggled with disable_ai true"
+        );
+    });
+
+    cx.update(|_window, cx| {
+        DisableAiSettings::override_global(DisableAiSettings { disable_ai: false }, cx);
+    });
+    cx.run_until_parked();
+
+    multi_workspace.read_with(cx, |mw, cx| {
+        assert!(
+            mw.multi_workspace_enabled(cx),
+            "Multi-workspace should be enabled after re-enabling AI"
+        );
+        assert!(
+            !mw.sidebar_open(),
+            "Sidebar should still be closed after re-enabling AI (not auto-opened)"
+        );
+    });
+
+    multi_workspace.update_in(cx, |mw, window, cx| {
+        mw.toggle_sidebar(window, cx);
+    });
+    multi_workspace.read_with(cx, |mw, _cx| {
+        assert!(
+            mw.sidebar_open(),
+            "Sidebar should open when toggled after re-enabling AI"
+        );
+    });
+}
+
+#[gpui::test]
+async fn test_replace(cx: &mut TestAppContext) {
+    init_test(cx);
+    let fs = FakeFs::new(cx.executor());
+    let project_a = Project::test(fs.clone(), [], cx).await;
+    let project_b = Project::test(fs.clone(), [], cx).await;
+    let project_c = Project::test(fs.clone(), [], cx).await;
+    let project_d = Project::test(fs.clone(), [], cx).await;
+
+    let (multi_workspace, cx) =
+        cx.add_window_view(|window, cx| MultiWorkspace::test_new(project_a.clone(), window, cx));
+
+    let workspace_a_id = multi_workspace.read_with(cx, |mw, _cx| mw.workspaces()[0].entity_id());
+
+    // Replace the only workspace (single-workspace case).
+    let workspace_b = multi_workspace.update_in(cx, |mw, window, cx| {
+        let workspace = cx.new(|cx| Workspace::test_new(project_b.clone(), window, cx));
+        mw.replace(workspace.clone(), &*window, cx);
+        workspace
+    });
+
+    multi_workspace.read_with(cx, |mw, _cx| {
+        assert_eq!(mw.workspaces().len(), 1);
+        assert_eq!(
+            mw.workspaces()[0].entity_id(),
+            workspace_b.entity_id(),
+            "slot should now be project_b"
+        );
+        assert_ne!(
+            mw.workspaces()[0].entity_id(),
+            workspace_a_id,
+            "project_a should be gone"
+        );
+    });
+
+    // Add project_c as a second workspace, then replace it with project_d.
+    let workspace_c = multi_workspace.update_in(cx, |mw, window, cx| {
+        mw.test_add_workspace(project_c.clone(), window, cx)
+    });
+
+    multi_workspace.read_with(cx, |mw, _cx| {
+        assert_eq!(mw.workspaces().len(), 2);
+        assert_eq!(mw.active_workspace_index(), 1);
+    });
+
+    let workspace_d = multi_workspace.update_in(cx, |mw, window, cx| {
+        let workspace = cx.new(|cx| Workspace::test_new(project_d.clone(), window, cx));
+        mw.replace(workspace.clone(), &*window, cx);
+        workspace
+    });
+
+    multi_workspace.read_with(cx, |mw, _cx| {
+        assert_eq!(mw.workspaces().len(), 2, "should still have 2 workspaces");
+        assert_eq!(mw.active_workspace_index(), 1);
+        assert_eq!(
+            mw.workspaces()[1].entity_id(),
+            workspace_d.entity_id(),
+            "active slot should now be project_d"
+        );
+        assert_ne!(
+            mw.workspaces()[1].entity_id(),
+            workspace_c.entity_id(),
+            "project_c should be gone"
+        );
+    });
+
+    // Replace with workspace_b which is already in the list — should just switch.
+    multi_workspace.update_in(cx, |mw, window, cx| {
+        mw.replace(workspace_b.clone(), &*window, cx);
+    });
+
+    multi_workspace.read_with(cx, |mw, _cx| {
+        assert_eq!(
+            mw.workspaces().len(),
+            2,
+            "no workspace should be added or removed"
+        );
+        assert_eq!(
+            mw.active_workspace_index(),
+            0,
+            "should have switched to workspace_b"
+        );
+    });
+}

crates/workspace/src/workspace.rs 🔗

@@ -5,6 +5,8 @@ pub mod invalid_item_view;
 pub mod item;
 mod modal_layer;
 mod multi_workspace;
+#[cfg(test)]
+mod multi_workspace_tests;
 pub mod notifications;
 pub mod pane;
 pub mod pane_group;

crates/worktree/src/worktree.rs 🔗

@@ -355,6 +355,7 @@ enum ScanState {
     RootUpdated {
         new_path: Arc<SanitizedPath>,
     },
+    RootDeleted,
 }
 
 struct UpdateObservationState {
@@ -368,6 +369,8 @@ pub enum Event {
     UpdatedEntries(UpdatedEntriesSet),
     UpdatedGitRepositories(UpdatedGitRepositoriesSet),
     DeletedEntry(ProjectEntryId),
+    /// The worktree root itself has been deleted (for single-file worktrees)
+    Deleted,
 }
 
 impl EventEmitter<Event> for Worktree {}
@@ -1106,6 +1109,7 @@ impl LocalWorktree {
                 };
                 let fs_case_sensitive = fs.is_case_sensitive().await;
 
+                let is_single_file = snapshot.snapshot.root_dir().is_none();
                 let mut scanner = BackgroundScanner {
                     fs,
                     fs_case_sensitive,
@@ -1128,6 +1132,7 @@ impl LocalWorktree {
                     share_private_files,
                     settings,
                     watcher,
+                    is_single_file,
                 };
 
                 scanner
@@ -1156,6 +1161,13 @@ impl LocalWorktree {
                         ScanState::RootUpdated { new_path } => {
                             this.update_abs_path_and_refresh(new_path, cx);
                         }
+                        ScanState::RootDeleted => {
+                            log::info!(
+                                "worktree root {} no longer exists, closing worktree",
+                                this.abs_path().display()
+                            );
+                            cx.emit(Event::Deleted);
+                        }
                     }
                 });
             }
@@ -2764,7 +2776,7 @@ impl LocalSnapshot {
         for entry in self.entries_by_path.cursor::<()>(()) {
             if entry.is_file() {
                 assert_eq!(files.next().unwrap().inode, entry.inode);
-                if (!entry.is_ignored && !entry.is_external) || entry.is_always_included {
+                if !entry.is_ignored || entry.is_always_included {
                     assert_eq!(visible_files.next().unwrap().inode, entry.inode);
                 }
             }
@@ -3443,8 +3455,7 @@ pub struct Entry {
     /// symlink.
     ///
     /// We only scan entries outside of the worktree once the symlinked
-    /// directory is expanded. External entries are treated like gitignored
-    /// entries in that they are not included in searches.
+    /// directory is expanded.
     pub is_external: bool,
 
     /// Whether this entry is considered to be a `.env` file.
@@ -3649,8 +3660,7 @@ impl sum_tree::Item for Entry {
     type Summary = EntrySummary;
 
     fn summary(&self, _cx: ()) -> Self::Summary {
-        let non_ignored_count = if (self.is_ignored || self.is_external) && !self.is_always_included
-        {
+        let non_ignored_count = if self.is_ignored && !self.is_always_included {
             0
         } else {
             1
@@ -3799,6 +3809,9 @@ struct BackgroundScanner {
     watcher: Arc<dyn Watcher>,
     settings: WorktreeSettings,
     share_private_files: bool,
+    /// Whether this is a single-file worktree (root is a file, not a directory).
+    /// Used to determine if we should give up after repeated canonicalization failures.
+    is_single_file: bool,
 }
 
 #[derive(Copy, Clone, PartialEq)]
@@ -4096,6 +4109,18 @@ impl BackgroundScanner {
                         .ok();
                 } else {
                     log::error!("root path could not be canonicalized: {err:#}");
+
+                    // For single-file worktrees, if we can't canonicalize and the file handle
+                    // fallback also failed, the file is gone - close the worktree
+                    if self.is_single_file {
+                        log::info!(
+                            "single-file worktree root {:?} no longer exists, marking as deleted",
+                            root_path.as_path()
+                        );
+                        self.status_updates_tx
+                            .unbounded_send(ScanState::RootDeleted)
+                            .ok();
+                    }
                 }
                 return;
             }

crates/worktree/tests/integration/main.rs 🔗

@@ -14,10 +14,12 @@ use worktree::{Entry, EntryKind, Event, PathChange, Worktree, WorktreeModelHandl
 use serde_json::json;
 use settings::{SettingsStore, WorktreeId};
 use std::{
+    cell::Cell,
     env,
     fmt::Write,
     mem,
     path::{Path, PathBuf},
+    rc::Rc,
     sync::Arc,
 };
 use util::{
@@ -3105,3 +3107,67 @@ async fn test_refresh_entries_for_paths_creates_ancestors(cx: &mut TestAppContex
         );
     });
 }
+
+#[gpui::test]
+async fn test_single_file_worktree_deleted(cx: &mut TestAppContext) {
+    init_test(cx);
+    let fs = FakeFs::new(cx.background_executor.clone());
+
+    fs.insert_tree(
+        "/root",
+        json!({
+            "test.txt": "content",
+        }),
+    )
+    .await;
+
+    let tree = Worktree::local(
+        Path::new("/root/test.txt"),
+        true,
+        fs.clone(),
+        Default::default(),
+        true,
+        WorktreeId::from_proto(0),
+        &mut cx.to_async(),
+    )
+    .await
+    .unwrap();
+
+    cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
+        .await;
+
+    tree.read_with(cx, |tree, _| {
+        assert!(tree.is_single_file(), "Should be a single-file worktree");
+        assert_eq!(tree.abs_path().as_ref(), Path::new("/root/test.txt"));
+    });
+
+    // Delete the file
+    fs.remove_file(Path::new("/root/test.txt"), Default::default())
+        .await
+        .unwrap();
+
+    // Subscribe to worktree events
+    let deleted_event_received = Rc::new(Cell::new(false));
+    let _subscription = cx.update({
+        let deleted_event_received = deleted_event_received.clone();
+        |cx| {
+            cx.subscribe(&tree, move |_, event, _| {
+                if matches!(event, Event::Deleted) {
+                    deleted_event_received.set(true);
+                }
+            })
+        }
+    });
+
+    // Trigger filesystem events - the scanner should detect the file is gone immediately
+    // and emit a Deleted event
+    cx.background_executor.run_until_parked();
+    cx.background_executor
+        .advance_clock(std::time::Duration::from_secs(1));
+    cx.background_executor.run_until_parked();
+
+    assert!(
+        deleted_event_received.get(),
+        "Should receive Deleted event when single-file worktree root is deleted"
+    );
+}

crates/zed_actions/src/lib.rs 🔗

@@ -450,8 +450,6 @@ pub mod agent {
             OpenOnboardingModal,
             /// Opens the ACP onboarding modal.
             OpenAcpOnboardingModal,
-            /// Opens the Claude Agent onboarding modal.
-            OpenClaudeAgentOnboardingModal,
             /// Resets the agent onboarding state.
             ResetOnboarding,
             /// Starts a chat conversation with the agent.