diff --git a/Cargo.lock b/Cargo.lock
index 38dcf369b3739f9087b574489666f4f1dfa012e0..bfd80726843695dbfcb4baf1db4fe3e6ca9a4682 100644
--- a/Cargo.lock
+++ b/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]]
@@ -16194,12 +16195,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"
@@ -18242,17 +18237,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"
@@ -18265,9 +18249,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",
@@ -19360,12 +19344,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]]
@@ -19488,9 +19472,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",
@@ -19513,22 +19497,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",
@@ -19542,10 +19526,9 @@ dependencies = [
"log",
"mach2 0.4.3",
"memfd",
- "object 0.36.7",
+ "object",
"once_cell",
"postcard",
- "psm",
"pulley-interpreter",
"rayon",
"rustix 1.1.2",
@@ -19553,82 +19536,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",
@@ -19637,104 +19647,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",
@@ -19758,14 +19796,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",
@@ -19774,35 +19812,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"
@@ -20257,9 +20266,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",
@@ -20272,9 +20281,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",
@@ -20286,9 +20295,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",
@@ -20329,21 +20338,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]]
@@ -21369,9 +21379,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",
@@ -21382,7 +21392,7 @@ dependencies = [
"serde_derive",
"serde_json",
"unicode-xid",
- "wasmparser 0.229.0",
+ "wasmparser 0.236.1",
]
[[package]]
diff --git a/Cargo.toml b/Cargo.toml
index 6c8f2a78a401cc2adebb712cd8ce739c696af878..3a393237ab9f5a5a8cd4b02517f6d22382ff51ff 100644
--- a/Cargo.toml
+++ b/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",
diff --git a/assets/icons/maximize_alt.svg b/assets/icons/maximize_alt.svg
new file mode 100644
index 0000000000000000000000000000000000000000..b8b8705f902c2469ed959f93f89ca3caf3b8fc51
--- /dev/null
+++ b/assets/icons/maximize_alt.svg
@@ -0,0 +1,6 @@
+
diff --git a/assets/icons/thread_import.svg b/assets/icons/thread_import.svg
new file mode 100644
index 0000000000000000000000000000000000000000..a56b5a7cccc09c5795bfadff06f06d15833232f3
--- /dev/null
+++ b/assets/icons/thread_import.svg
@@ -0,0 +1,5 @@
+
diff --git a/assets/settings/default.json b/assets/settings/default.json
index 57bad245474b9469a0a9b9d5674c692059f039af..2e0ddc2da70af5516d14a2fa8418a759bec62eb1 100644
--- a/assets/settings/default.json
+++ b/assets/settings/default.json
@@ -1117,8 +1117,8 @@
"expand_terminal_card": true,
// How thinking blocks should be displayed by default in the agent panel.
//
- // Default: automatic
- "thinking_display": "automatic",
+ // Default: auto
+ "thinking_display": "auto",
// Whether clicking the stop button on a running terminal tool should also cancel the agent's generation.
// Note that this only applies to the stop button, not to ctrl+c inside the terminal.
//
diff --git a/crates/acp_tools/src/acp_tools.rs b/crates/acp_tools/src/acp_tools.rs
index 52a9d03f893d0b82bf6395b4c96bc9ebe14d3afe..ae8a39c8df4f73ae8be6b748694dbde5d2a0c102 100644
--- a/crates/acp_tools/src/acp_tools.rs
+++ b/crates/acp_tools/src/acp_tools.rs
@@ -13,7 +13,7 @@ use gpui::{
StyleRefinement, Subscription, Task, TextStyleRefinement, Window, actions, list, prelude::*,
};
use language::LanguageRegistry;
-use markdown::{CodeBlockRenderer, Markdown, MarkdownElement, MarkdownStyle};
+use markdown::{CodeBlockRenderer, CopyButtonVisibility, Markdown, MarkdownElement, MarkdownStyle};
use project::{AgentId, Project};
use settings::Settings;
use theme_settings::ThemeSettings;
@@ -384,8 +384,11 @@ impl AcpTools {
)
.code_block_renderer(
CodeBlockRenderer::Default {
- copy_button: false,
- copy_button_on_hover: expanded,
+ copy_button_visibility: if expanded {
+ CopyButtonVisibility::VisibleOnHover
+ } else {
+ CopyButtonVisibility::Hidden
+ },
border: false,
},
),
diff --git a/crates/agent_ui/src/agent_panel.rs b/crates/agent_ui/src/agent_panel.rs
index e6ef267a95110e745534010bae32b1b1fd6c0f0c..0ed0aeb78bf8889136a479ed2dac5caba633db55 100644
--- a/crates/agent_ui/src/agent_panel.rs
+++ b/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};
@@ -66,7 +66,10 @@ use project::project_settings::ProjectSettings;
use project::{Project, ProjectPath, Worktree};
use prompt_store::{PromptStore, UserPromptId};
use rules_library::{RulesLibrary, open_rules_library};
+use settings::TerminalDockPosition;
use settings::{Settings, update_settings_file};
+use terminal::terminal_settings::TerminalSettings;
+use terminal_view::{TerminalView, terminal_panel::TerminalPanel};
use theme_settings::ThemeSettings;
use ui::{
Button, Callout, CommonAnimationExt, ContextMenu, ContextMenuEntry, DocumentationSide,
@@ -86,6 +89,30 @@ use zed_actions::{
const AGENT_PANEL_KEY: &str = "agent_panel";
const RECENTLY_UPDATED_MENU_LIMIT: usize = 6;
+const LAST_USED_AGENT_KEY: &str = "agent_panel__last_used_external_agent";
+
+#[derive(Serialize, Deserialize)]
+struct LastUsedAgent {
+ agent: Agent,
+}
+
+/// Reads the most recently used agent across all workspaces. Used as a fallback
+/// when opening a workspace that has no per-workspace agent preference yet.
+fn read_global_last_used_agent(kvp: &KeyValueStore) -> Option {
+ kvp.read_kvp(LAST_USED_AGENT_KEY)
+ .log_err()
+ .flatten()
+ .and_then(|json| serde_json::from_str::(&json).log_err())
+ .map(|entry| entry.agent)
+}
+
+async fn write_global_last_used_agent(kvp: KeyValueStore, agent: Agent) {
+ if let Some(json) = serde_json::to_string(&LastUsedAgent { agent }).log_err() {
+ kvp.write_kvp(LAST_USED_AGENT_KEY.to_string(), json)
+ .await
+ .log_err();
+ }
+}
fn read_serialized_panel(
workspace_id: workspace::WorkspaceId,
@@ -245,11 +272,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();
@@ -404,6 +426,48 @@ pub fn init(cx: &mut App) {
})
.register_action(
|workspace: &mut Workspace, _: &AddSelectionToThread, window, cx| {
+ let active_editor = workspace
+ .active_item(cx)
+ .and_then(|item| item.act_as::(cx));
+ let has_editor_selection = active_editor.is_some_and(|editor| {
+ editor.update(cx, |editor, cx| {
+ editor.has_non_empty_selection(&editor.display_snapshot(cx))
+ })
+ });
+
+ let has_terminal_selection = workspace
+ .active_item(cx)
+ .and_then(|item| item.act_as::(cx))
+ .is_some_and(|terminal_view| {
+ terminal_view
+ .read(cx)
+ .terminal()
+ .read(cx)
+ .last_content
+ .selection_text
+ .as_ref()
+ .is_some_and(|text| !text.is_empty())
+ });
+
+ let has_terminal_panel_selection =
+ workspace.panel::(cx).is_some_and(|panel| {
+ let position = match TerminalSettings::get_global(cx).dock {
+ TerminalDockPosition::Left => DockPosition::Left,
+ TerminalDockPosition::Bottom => DockPosition::Bottom,
+ TerminalDockPosition::Right => DockPosition::Right,
+ };
+ let dock_is_open =
+ workspace.dock_at_position(position).read(cx).is_open();
+ dock_is_open && !panel.read(cx).terminal_selections(cx).is_empty()
+ });
+
+ if !has_editor_selection
+ && !has_terminal_selection
+ && !has_terminal_panel_selection
+ {
+ return;
+ }
+
let Some(panel) = workspace.panel::(cx) else {
return;
};
@@ -670,13 +734,18 @@ impl AgentPanel {
.ok()
.flatten();
- let serialized_panel = cx
+ let (serialized_panel, global_last_used_agent) = cx
.background_spawn(async move {
- kvp.and_then(|kvp| {
- workspace_id
- .and_then(|id| read_serialized_panel(id, &kvp))
- .or_else(|| read_legacy_serialized_panel(&kvp))
- })
+ match kvp {
+ Some(kvp) => {
+ let panel = workspace_id
+ .and_then(|id| read_serialized_panel(id, &kvp))
+ .or_else(|| read_legacy_serialized_panel(&kvp));
+ let global_agent = read_global_last_used_agent(&kvp);
+ (panel, global_agent)
+ }
+ None => (None, None),
+ }
})
.await;
@@ -715,10 +784,21 @@ impl AgentPanel {
let panel =
cx.new(|cx| Self::new(workspace, prompt_store, window, cx));
- if let Some(serialized_panel) = &serialized_panel {
- panel.update(cx, |panel, cx| {
+ panel.update(cx, |panel, cx| {
+ let is_via_collab = panel.project.read(cx).is_via_collab();
+
+ // Only apply a non-native global fallback to local projects.
+ // Collab workspaces only support NativeAgent, so inheriting a
+ // custom agent would cause set_active → new_agent_thread_inner
+ // to bypass the collab guard in external_thread.
+ let global_fallback = global_last_used_agent
+ .filter(|agent| !is_via_collab || agent.is_native());
+
+ if let Some(serialized_panel) = &serialized_panel {
if let Some(selected_agent) = serialized_panel.selected_agent.clone() {
panel.selected_agent = selected_agent;
+ } else if let Some(agent) = global_fallback {
+ panel.selected_agent = agent;
}
if let Some(start_thread_in) = serialized_panel.start_thread_in {
let is_worktree_flag_enabled =
@@ -739,9 +819,11 @@ impl AgentPanel {
);
}
}
- cx.notify();
- });
- }
+ } else if let Some(agent) = global_fallback {
+ panel.selected_agent = agent;
+ }
+ cx.notify();
+ });
if let Some(thread_info) = last_active_thread {
let agent = thread_info.agent_type.clone();
@@ -1074,85 +1156,30 @@ impl AgentPanel {
let workspace = self.workspace.clone();
let project = self.project.clone();
let fs = self.fs.clone();
- let is_via_collab = self.project.read(cx).is_via_collab();
-
- const LAST_USED_EXTERNAL_AGENT_KEY: &str = "agent_panel__last_used_external_agent";
-
- #[derive(Serialize, Deserialize)]
- struct LastUsedExternalAgent {
- agent: crate::Agent,
- }
-
let thread_store = self.thread_store.clone();
- let kvp = KeyValueStore::global(cx);
-
- if let Some(agent) = agent_choice {
- cx.background_spawn({
- let agent = agent.clone();
- let kvp = kvp;
- async move {
- if let Some(serialized) =
- serde_json::to_string(&LastUsedExternalAgent { agent }).log_err()
- {
- kvp.write_kvp(LAST_USED_EXTERNAL_AGENT_KEY.to_string(), serialized)
- .await
- .log_err();
- }
- }
- })
- .detach();
-
- let server = agent.server(fs, thread_store);
- self.create_agent_thread(
- server,
- resume_session_id,
- work_dirs,
- title,
- initial_content,
- workspace,
- project,
- agent,
- focus,
- window,
- cx,
- );
- } else {
- cx.spawn_in(window, async move |this, cx| {
- let ext_agent = if is_via_collab {
- Agent::NativeAgent
- } else {
- cx.background_spawn(async move { kvp.read_kvp(LAST_USED_EXTERNAL_AGENT_KEY) })
- .await
- .log_err()
- .flatten()
- .and_then(|value| {
- serde_json::from_str::(&value).log_err()
- })
- .map(|agent| agent.agent)
- .unwrap_or(Agent::NativeAgent)
- };
- let server = ext_agent.server(fs, thread_store);
- this.update_in(cx, |agent_panel, window, cx| {
- agent_panel.create_agent_thread(
- server,
- resume_session_id,
- work_dirs,
- title,
- initial_content,
- workspace,
- project,
- ext_agent,
- focus,
- window,
- cx,
- );
- })?;
+ let agent = agent_choice.unwrap_or_else(|| {
+ if self.project.read(cx).is_via_collab() {
+ Agent::NativeAgent
+ } else {
+ self.selected_agent.clone()
+ }
+ });
- anyhow::Ok(())
- })
- .detach_and_log_err(cx);
- }
+ let server = agent.server(fs, thread_store);
+ self.create_agent_thread(
+ server,
+ resume_session_id,
+ work_dirs,
+ title,
+ initial_content,
+ workspace,
+ project,
+ agent,
+ focus,
+ window,
+ cx,
+ );
}
fn deploy_rules_library(
@@ -2107,15 +2134,25 @@ impl AgentPanel {
initial_content: Option,
workspace: WeakEntity,
project: Entity,
- ext_agent: Agent,
+ agent: Agent,
focus: bool,
window: &mut Window,
cx: &mut Context,
) {
- if self.selected_agent != ext_agent {
- self.selected_agent = ext_agent.clone();
+ if self.selected_agent != agent {
+ self.selected_agent = agent.clone();
self.serialize(cx);
}
+
+ cx.background_spawn({
+ let kvp = KeyValueStore::global(cx);
+ let agent = agent.clone();
+ async move {
+ write_global_last_used_agent(kvp, agent).await;
+ }
+ })
+ .detach();
+
let thread_store = server
.clone()
.downcast::()
@@ -2128,7 +2165,7 @@ impl AgentPanel {
crate::ConversationView::new(
server,
connection_store,
- ext_agent,
+ agent,
resume_session_id,
work_dirs,
title,
@@ -5616,4 +5653,211 @@ mod tests {
"Thread A work_dirs should revert to only /project_a after removing /project_b"
);
}
+
+ #[gpui::test]
+ async fn test_new_workspace_inherits_global_last_used_agent(cx: &mut TestAppContext) {
+ init_test(cx);
+ cx.update(|cx| {
+ cx.update_flags(true, vec!["agent-v2".to_string()]);
+ agent::ThreadStore::init_global(cx);
+ language_model::LanguageModelRegistry::test(cx);
+ // Use an isolated DB so parallel tests can't overwrite our global key.
+ cx.set_global(db::AppDatabase::test_new());
+ });
+
+ let custom_agent = Agent::Custom {
+ id: "my-preferred-agent".into(),
+ };
+
+ // Write a known agent to the global KVP to simulate a user who has
+ // previously used this agent in another workspace.
+ let kvp = cx.update(|cx| KeyValueStore::global(cx));
+ write_global_last_used_agent(kvp, custom_agent.clone()).await;
+
+ let fs = FakeFs::new(cx.executor());
+ let project = Project::test(fs.clone(), [], cx).await;
+
+ let multi_workspace =
+ cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
+
+ let workspace = multi_workspace
+ .read_with(cx, |multi_workspace, _cx| {
+ multi_workspace.workspace().clone()
+ })
+ .unwrap();
+
+ workspace.update(cx, |workspace, _cx| {
+ workspace.set_random_database_id();
+ });
+
+ let cx = &mut VisualTestContext::from_window(multi_workspace.into(), cx);
+
+ // Load the panel via `load()`, which reads the global fallback
+ // asynchronously when no per-workspace state exists.
+ let async_cx = cx.update(|window, cx| window.to_async(cx));
+ let panel = AgentPanel::load(workspace.downgrade(), async_cx)
+ .await
+ .expect("panel load should succeed");
+ cx.run_until_parked();
+
+ panel.read_with(cx, |panel, _cx| {
+ assert_eq!(
+ panel.selected_agent, custom_agent,
+ "new workspace should inherit the global last-used agent"
+ );
+ });
+ }
+
+ #[gpui::test]
+ async fn test_workspaces_maintain_independent_agent_selection(cx: &mut TestAppContext) {
+ init_test(cx);
+ cx.update(|cx| {
+ cx.update_flags(true, vec!["agent-v2".to_string()]);
+ agent::ThreadStore::init_global(cx);
+ language_model::LanguageModelRegistry::test(cx);
+ });
+
+ let fs = FakeFs::new(cx.executor());
+ let project_a = Project::test(fs.clone(), [], cx).await;
+ let project_b = Project::test(fs, [], cx).await;
+
+ let multi_workspace =
+ cx.add_window(|window, cx| MultiWorkspace::test_new(project_a.clone(), window, cx));
+
+ let workspace_a = multi_workspace
+ .read_with(cx, |multi_workspace, _cx| {
+ multi_workspace.workspace().clone()
+ })
+ .unwrap();
+
+ let workspace_b = multi_workspace
+ .update(cx, |multi_workspace, window, cx| {
+ multi_workspace.test_add_workspace(project_b.clone(), window, cx)
+ })
+ .unwrap();
+
+ workspace_a.update(cx, |workspace, _cx| {
+ workspace.set_random_database_id();
+ });
+ workspace_b.update(cx, |workspace, _cx| {
+ workspace.set_random_database_id();
+ });
+
+ let cx = &mut VisualTestContext::from_window(multi_workspace.into(), cx);
+
+ let agent_a = Agent::Custom {
+ id: "agent-alpha".into(),
+ };
+ let agent_b = Agent::Custom {
+ id: "agent-beta".into(),
+ };
+
+ // Set up workspace A with agent_a
+ let panel_a = workspace_a.update_in(cx, |workspace, window, cx| {
+ cx.new(|cx| AgentPanel::new(workspace, None, window, cx))
+ });
+ panel_a.update(cx, |panel, _cx| {
+ panel.selected_agent = agent_a.clone();
+ });
+
+ // Set up workspace B with agent_b
+ let panel_b = workspace_b.update_in(cx, |workspace, window, cx| {
+ cx.new(|cx| AgentPanel::new(workspace, None, window, cx))
+ });
+ panel_b.update(cx, |panel, _cx| {
+ panel.selected_agent = agent_b.clone();
+ });
+
+ // Serialize both panels
+ panel_a.update(cx, |panel, cx| panel.serialize(cx));
+ panel_b.update(cx, |panel, cx| panel.serialize(cx));
+ cx.run_until_parked();
+
+ // Load fresh panels from serialized state and verify independence
+ let async_cx = cx.update(|window, cx| window.to_async(cx));
+ let loaded_a = AgentPanel::load(workspace_a.downgrade(), async_cx)
+ .await
+ .expect("panel A load should succeed");
+ cx.run_until_parked();
+
+ let async_cx = cx.update(|window, cx| window.to_async(cx));
+ let loaded_b = AgentPanel::load(workspace_b.downgrade(), async_cx)
+ .await
+ .expect("panel B load should succeed");
+ cx.run_until_parked();
+
+ loaded_a.read_with(cx, |panel, _cx| {
+ assert_eq!(
+ panel.selected_agent, agent_a,
+ "workspace A should restore agent-alpha, not agent-beta"
+ );
+ });
+
+ loaded_b.read_with(cx, |panel, _cx| {
+ assert_eq!(
+ panel.selected_agent, agent_b,
+ "workspace B should restore agent-beta, not agent-alpha"
+ );
+ });
+ }
+
+ #[gpui::test]
+ async fn test_new_thread_uses_workspace_selected_agent(cx: &mut TestAppContext) {
+ init_test(cx);
+ cx.update(|cx| {
+ cx.update_flags(true, vec!["agent-v2".to_string()]);
+ agent::ThreadStore::init_global(cx);
+ language_model::LanguageModelRegistry::test(cx);
+ });
+
+ let fs = FakeFs::new(cx.executor());
+ let project = Project::test(fs.clone(), [], cx).await;
+
+ let multi_workspace =
+ cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
+
+ let workspace = multi_workspace
+ .read_with(cx, |multi_workspace, _cx| {
+ multi_workspace.workspace().clone()
+ })
+ .unwrap();
+
+ workspace.update(cx, |workspace, _cx| {
+ workspace.set_random_database_id();
+ });
+
+ let cx = &mut VisualTestContext::from_window(multi_workspace.into(), cx);
+
+ let custom_agent = Agent::Custom {
+ id: "my-custom-agent".into(),
+ };
+
+ let panel = workspace.update_in(cx, |workspace, window, cx| {
+ let panel = cx.new(|cx| AgentPanel::new(workspace, None, window, cx));
+ workspace.add_panel(panel.clone(), window, cx);
+ panel
+ });
+
+ // Set selected_agent to a custom agent
+ panel.update(cx, |panel, _cx| {
+ panel.selected_agent = custom_agent.clone();
+ });
+
+ // Call new_thread, which internally calls external_thread(None, ...)
+ // This resolves the agent from self.selected_agent
+ panel.update_in(cx, |panel, window, cx| {
+ panel.new_thread(&NewThread, window, cx);
+ });
+
+ panel.read_with(cx, |panel, _cx| {
+ assert_eq!(
+ panel.selected_agent, custom_agent,
+ "selected_agent should remain the custom agent after new_thread"
+ );
+ assert!(
+ panel.active_conversation_view().is_some(),
+ "a thread should have been created"
+ );
+ });
+ }
}
diff --git a/crates/agent_ui/src/completion_provider.rs b/crates/agent_ui/src/completion_provider.rs
index b6be6502b152847822a79bc8c486195345c0a195..6259269834b0add5b87fd9d397e17671d30adb9f 100644
--- a/crates/agent_ui/src/completion_provider.rs
+++ b/crates/agent_ui/src/completion_provider.rs
@@ -28,7 +28,7 @@ use prompt_store::{PromptStore, UserPromptId};
use rope::Point;
use settings::{Settings, TerminalDockPosition};
use terminal::terminal_settings::TerminalSettings;
-use terminal_view::terminal_panel::TerminalPanel;
+use terminal_view::{TerminalView, terminal_panel::TerminalPanel};
use text::{Anchor, ToOffset as _, ToPoint as _};
use ui::IconName;
use ui::prelude::*;
@@ -562,8 +562,7 @@ impl PromptCompletionProvider {
.collect();
// Collect terminal selections from all terminal views if the terminal panel is visible
- let terminal_selections: Vec =
- terminal_selections_if_panel_open(workspace, cx);
+ let terminal_selections: Vec = terminal_selections(workspace, cx);
const EDITOR_PLACEHOLDER: &str = "selection ";
const TERMINAL_PLACEHOLDER: &str = "terminal ";
@@ -1198,7 +1197,7 @@ impl PromptCompletionProvider {
})
});
- let has_terminal_selection = !terminal_selections_if_panel_open(workspace, cx).is_empty();
+ let has_terminal_selection = !terminal_selections(workspace, cx).is_empty();
if has_editor_selection || has_terminal_selection {
entries.push(PromptContextEntry::Action(
@@ -2169,28 +2168,45 @@ fn build_code_label_for_path(
label.build()
}
-/// Returns terminal selections from all terminal views if the terminal panel is open.
-fn terminal_selections_if_panel_open(workspace: &Entity, cx: &App) -> Vec {
- let Some(panel) = workspace.read(cx).panel::(cx) else {
- return Vec::new();
- };
+fn terminal_selections(workspace: &Entity, cx: &App) -> Vec {
+ let mut selections = Vec::new();
- // Check if the dock containing this panel is open
- let position = match TerminalSettings::get_global(cx).dock {
- TerminalDockPosition::Left => DockPosition::Left,
- TerminalDockPosition::Bottom => DockPosition::Bottom,
- TerminalDockPosition::Right => DockPosition::Right,
- };
- let dock_is_open = workspace
+ // Check if the active item is a terminal (in a panel or not)
+ if let Some(terminal_view) = workspace
.read(cx)
- .dock_at_position(position)
- .read(cx)
- .is_open();
- if !dock_is_open {
- return Vec::new();
+ .active_item(cx)
+ .and_then(|item| item.act_as::(cx))
+ {
+ if let Some(text) = terminal_view
+ .read(cx)
+ .terminal()
+ .read(cx)
+ .last_content
+ .selection_text
+ .clone()
+ .filter(|text| !text.is_empty())
+ {
+ selections.push(text);
+ }
}
- panel.read(cx).terminal_selections(cx)
+ if let Some(panel) = workspace.read(cx).panel::(cx) {
+ let position = match TerminalSettings::get_global(cx).dock {
+ TerminalDockPosition::Left => DockPosition::Left,
+ TerminalDockPosition::Bottom => DockPosition::Bottom,
+ TerminalDockPosition::Right => DockPosition::Right,
+ };
+ let dock_is_open = workspace
+ .read(cx)
+ .dock_at_position(position)
+ .read(cx)
+ .is_open();
+ if dock_is_open {
+ selections.extend(panel.read(cx).terminal_selections(cx));
+ }
+ }
+
+ selections
}
fn selection_ranges(
@@ -2213,17 +2229,8 @@ fn selection_ranges(
selections
.into_iter()
- .map(|s| {
- let (start, end) = if s.is_empty() {
- let row = multi_buffer::MultiBufferRow(s.start.row);
- let line_start = text::Point::new(s.start.row, 0);
- let line_end = text::Point::new(s.start.row, snapshot.line_len(row));
- (line_start, line_end)
- } else {
- (s.start, s.end)
- };
- snapshot.anchor_after(start)..snapshot.anchor_before(end)
- })
+ .filter(|s| !s.is_empty())
+ .map(|s| snapshot.anchor_after(s.start)..snapshot.anchor_before(s.end))
.flat_map(|range| {
let (start_buffer, start) = buffer.text_anchor_for_position(range.start, cx)?;
let (end_buffer, end) = buffer.text_anchor_for_position(range.end, cx)?;
diff --git a/crates/agent_ui/src/config_options.rs b/crates/agent_ui/src/config_options.rs
index b8cf7e5d57921c7710392911829fc2b5045a0f90..44c0baa232222c0ba7c1d54acdecaabacfa85f12 100644
--- a/crates/agent_ui/src/config_options.rs
+++ b/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 {
diff --git a/crates/agent_ui/src/conversation_view/thread_view.rs b/crates/agent_ui/src/conversation_view/thread_view.rs
index 63aa8b8529655a26b99ba74062f8d0a6a4812c5f..b25769eadbe31c35a6261cc9433349a2943617be 100644
--- a/crates/agent_ui/src/conversation_view/thread_view.rs
+++ b/crates/agent_ui/src/conversation_view/thread_view.rs
@@ -5152,9 +5152,12 @@ impl ThreadView {
}
pub(crate) fn auto_expand_streaming_thought(&mut self, cx: &mut Context) {
- // Only auto-expand thinking blocks in Automatic mode.
- // AlwaysExpanded shows them open by default; AlwaysCollapsed keeps them closed.
- if AgentSettings::get_global(cx).thinking_display != ThinkingBlockDisplay::Automatic {
+ let thinking_display = AgentSettings::get_global(cx).thinking_display;
+
+ if !matches!(
+ thinking_display,
+ ThinkingBlockDisplay::Auto | ThinkingBlockDisplay::Preview
+ ) {
return;
}
@@ -5183,6 +5186,13 @@ impl ThreadView {
cx.notify();
}
} else if self.auto_expanded_thinking_block.is_some() {
+ if thinking_display == ThinkingBlockDisplay::Auto {
+ if let Some(key) = self.auto_expanded_thinking_block {
+ if !self.user_toggled_thinking_blocks.contains(&key) {
+ self.expanded_thinking_blocks.remove(&key);
+ }
+ }
+ }
self.auto_expanded_thinking_block = None;
cx.notify();
}
@@ -5196,7 +5206,16 @@ impl ThreadView {
let thinking_display = AgentSettings::get_global(cx).thinking_display;
match thinking_display {
- ThinkingBlockDisplay::Automatic => {
+ ThinkingBlockDisplay::Auto => {
+ if self.expanded_thinking_blocks.contains(&key) {
+ self.expanded_thinking_blocks.remove(&key);
+ self.user_toggled_thinking_blocks.insert(key);
+ } else {
+ self.expanded_thinking_blocks.insert(key);
+ self.user_toggled_thinking_blocks.insert(key);
+ }
+ }
+ ThinkingBlockDisplay::Preview => {
let is_user_expanded = self.user_toggled_thinking_blocks.contains(&key);
let is_in_expanded_set = self.expanded_thinking_blocks.contains(&key);
@@ -5249,7 +5268,11 @@ impl ThreadView {
let is_in_expanded_set = self.expanded_thinking_blocks.contains(&key);
let (is_open, is_constrained) = match thinking_display {
- ThinkingBlockDisplay::Automatic => {
+ ThinkingBlockDisplay::Auto => {
+ let is_open = is_user_toggled || is_in_expanded_set;
+ (is_open, false)
+ }
+ ThinkingBlockDisplay::Preview => {
let is_open = is_user_toggled || is_in_expanded_set;
let is_constrained = is_in_expanded_set && !is_user_toggled;
(is_open, is_constrained)
diff --git a/crates/agent_ui/src/thread_import.rs b/crates/agent_ui/src/thread_import.rs
index 9dd6b5efa0ae1cd3bc19dc6ae6a287218de8c668..f5fc89d3df4991ff5186e2af6d73ad6a840c09a1 100644
--- a/crates/agent_ui/src/thread_import.rs
+++ b/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) {
- 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) {
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) -> 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::>();
- 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| {
diff --git a/crates/agent_ui/src/thread_metadata_store.rs b/crates/agent_ui/src/thread_metadata_store.rs
index d4e8e6d37aabe98dc41bf39575b77fd28a3bed08..4c66d57bcfafe98432319a173e7736a581f1d986 100644
--- a/crates/agent_ui/src/thread_metadata_store.rs
+++ b/crates/agent_ui/src/thread_metadata_store.rs
@@ -55,7 +55,7 @@ fn migrate_thread_metadata(cx: &mut App) {
.read(cx)
.entries()
.filter_map(|entry| {
- if existing_entries.contains(&entry.id.0) || entry.folder_paths.is_empty() {
+ if existing_entries.contains(&entry.id.0) {
return None;
}
@@ -81,6 +81,9 @@ fn migrate_thread_metadata(cx: &mut App) {
if is_first_migration {
let mut per_project: HashMap> = HashMap::default();
for entry in &mut to_migrate {
+ if entry.folder_paths.is_empty() {
+ continue;
+ }
per_project
.entry(entry.folder_paths.clone())
.or_default()
@@ -316,6 +319,25 @@ impl ThreadMetadataStore {
.log_err();
}
+ pub fn update_working_directories(
+ &mut self,
+ session_id: &acp::SessionId,
+ work_dirs: PathList,
+ cx: &mut Context,
+ ) {
+ if !cx.has_flag::() {
+ return;
+ }
+
+ if let Some(thread) = self.threads.get(session_id) {
+ self.save_internal(ThreadMetadata {
+ folder_paths: work_dirs,
+ ..thread.clone()
+ });
+ cx.notify();
+ }
+ }
+
pub fn archive(&mut self, session_id: &acp::SessionId, cx: &mut Context) {
self.update_archived(session_id, true, cx);
}
@@ -495,7 +517,13 @@ impl ThreadMetadataStore {
PathList::new(&paths)
};
- let archived = existing_thread.map(|t| t.archived).unwrap_or(false);
+ // Threads without a folder path (e.g. started in an empty
+ // window) are archived by default so they don't get lost,
+ // because they won't show up in the sidebar. Users can reload
+ // them from the archive.
+ let archived = existing_thread
+ .map(|t| t.archived)
+ .unwrap_or(folder_paths.is_empty());
let metadata = ThreadMetadata {
session_id,
@@ -994,7 +1022,7 @@ mod tests {
store.read(cx).entries().cloned().collect::>()
});
- assert_eq!(list.len(), 3);
+ assert_eq!(list.len(), 4);
assert!(
list.iter()
.all(|metadata| metadata.agent_id.as_ref() == agent::ZED_AGENT_ID.as_ref())
@@ -1013,17 +1041,12 @@ mod tests {
.collect::>();
assert!(migrated_session_ids.contains(&"a-session-1"));
assert!(migrated_session_ids.contains(&"b-session-0"));
- assert!(!migrated_session_ids.contains(&"projectless"));
+ assert!(migrated_session_ids.contains(&"projectless"));
let migrated_entries = list
.iter()
.filter(|metadata| metadata.session_id.0.as_ref() != "a-session-0")
.collect::>();
- assert!(
- migrated_entries
- .iter()
- .all(|metadata| !metadata.folder_paths.is_empty())
- );
assert!(migrated_entries.iter().all(|metadata| metadata.archived));
}
@@ -1269,6 +1292,84 @@ mod tests {
assert_eq!(metadata_ids, vec![session_id]);
}
+ #[gpui::test]
+ async fn test_threads_without_project_association_are_archived_by_default(
+ cx: &mut TestAppContext,
+ ) {
+ init_test(cx);
+
+ let fs = FakeFs::new(cx.executor());
+ let project_without_worktree = Project::test(fs.clone(), None::<&Path>, cx).await;
+ let project_with_worktree = Project::test(fs, [Path::new("/project-a")], cx).await;
+ let connection = Rc::new(StubAgentConnection::new());
+
+ let thread_without_worktree = cx
+ .update(|cx| {
+ connection.clone().new_session(
+ project_without_worktree.clone(),
+ PathList::default(),
+ cx,
+ )
+ })
+ .await
+ .unwrap();
+ let session_without_worktree =
+ cx.read(|cx| thread_without_worktree.read(cx).session_id().clone());
+
+ cx.update(|cx| {
+ thread_without_worktree.update(cx, |thread, cx| {
+ thread.set_title("No Project Thread".into(), cx).detach();
+ });
+ });
+ cx.run_until_parked();
+
+ let thread_with_worktree = cx
+ .update(|cx| {
+ connection.clone().new_session(
+ project_with_worktree.clone(),
+ PathList::default(),
+ cx,
+ )
+ })
+ .await
+ .unwrap();
+ let session_with_worktree =
+ cx.read(|cx| thread_with_worktree.read(cx).session_id().clone());
+
+ cx.update(|cx| {
+ thread_with_worktree.update(cx, |thread, cx| {
+ thread.set_title("Project Thread".into(), cx).detach();
+ });
+ });
+ cx.run_until_parked();
+
+ cx.update(|cx| {
+ let store = ThreadMetadataStore::global(cx);
+ let store = store.read(cx);
+
+ let without_worktree = store
+ .entry(&session_without_worktree)
+ .expect("missing metadata for thread without project association");
+ assert!(without_worktree.folder_paths.is_empty());
+ assert!(
+ without_worktree.archived,
+ "expected thread without project association to be archived"
+ );
+
+ let with_worktree = store
+ .entry(&session_with_worktree)
+ .expect("missing metadata for thread with project association");
+ assert_eq!(
+ with_worktree.folder_paths,
+ PathList::new(&[Path::new("/project-a")])
+ );
+ assert!(
+ !with_worktree.archived,
+ "expected thread with project association to remain unarchived"
+ );
+ });
+ }
+
#[gpui::test]
async fn test_subagent_threads_excluded_from_sidebar_metadata(cx: &mut TestAppContext) {
init_test(cx);
diff --git a/crates/agent_ui/src/threads_archive_view.rs b/crates/agent_ui/src/threads_archive_view.rs
index 445d86c9ad4e37fa8b2502a754a5264cd1d4dc45..9aca31e1edbe729fccecfc0dd8f0530d2aed2564 100644
--- a/crates/agent_ui/src/threads_archive_view.rs
+++ b/crates/agent_ui/src/threads_archive_view.rs
@@ -1,5 +1,8 @@
+use std::collections::HashSet;
+use std::sync::Arc;
+
use crate::agent_connection_store::AgentConnectionStore;
-use crate::thread_import::{AcpThreadImportOnboarding, ThreadImportModal};
+
use crate::thread_metadata_store::{ThreadMetadata, ThreadMetadataStore};
use crate::{Agent, RemoveSelectedThread};
@@ -9,21 +12,32 @@ use agent_settings::AgentSettings;
use chrono::{DateTime, Datelike as _, Local, NaiveDate, TimeDelta, Utc};
use editor::Editor;
use fs::Fs;
+use fuzzy::{StringMatch, StringMatchCandidate};
use gpui::{
- AnyElement, App, Context, Entity, EventEmitter, FocusHandle, Focusable, ListState, Render,
- SharedString, Subscription, Task, WeakEntity, Window, list, prelude::*, px,
+ AnyElement, App, Context, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable,
+ ListState, Render, SharedString, Subscription, Task, WeakEntity, Window, list, prelude::*, px,
};
use itertools::Itertools as _;
use menu::{Confirm, SelectFirst, SelectLast, SelectNext, SelectPrevious};
-use project::{AgentId, AgentRegistryStore, AgentServerStore};
+use picker::{
+ Picker, PickerDelegate,
+ highlighted_match_with_paths::{HighlightedMatch, HighlightedMatchWithPaths},
+};
+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,
+ Divider, KeyBinding, ListItem, ListItemSpacing, ListSubHeader, Tooltip, WithScrollbar,
+ prelude::*, utils::platform_title_bar_height,
};
+use ui_input::ErasedEditor;
use util::ResultExt;
-use workspace::{MultiWorkspace, Workspace};
+use util::paths::PathExt;
+use workspace::{
+ ModalView, PathList, SerializedWorkspaceLocation, Workspace, WorkspaceDb, WorkspaceId,
+ resolve_worktree_workspaces,
+};
use zed_actions::agents_sidebar::FocusSidebarFilter;
use zed_actions::editor::{MoveDown, MoveUp};
@@ -112,20 +126,16 @@ pub struct ThreadsArchiveView {
filter_editor: Entity,
_subscriptions: Vec,
_refresh_history_task: Task<()>,
+ workspace: WeakEntity,
agent_connection_store: WeakEntity,
agent_server_store: WeakEntity,
- agent_registry_store: WeakEntity,
- workspace: WeakEntity,
- multi_workspace: WeakEntity,
}
impl ThreadsArchiveView {
pub fn new(
+ workspace: WeakEntity,
agent_connection_store: WeakEntity,
agent_server_store: WeakEntity,
- agent_registry_store: WeakEntity,
- workspace: WeakEntity,
- multi_workspace: WeakEntity,
window: &mut Window,
cx: &mut Context,
) -> Self {
@@ -184,11 +194,9 @@ impl ThreadsArchiveView {
thread_metadata_store_subscription,
],
_refresh_history_task: Task::ready(()),
- agent_registry_store,
+ workspace,
agent_connection_store,
agent_server_store,
- workspace,
- multi_workspace,
};
this.update_items(cx);
@@ -265,7 +273,14 @@ impl ThreadsArchiveView {
self.list_state.reset(items.len());
self.items = items;
- self.hovered_index = None;
+
+ if !preserve {
+ self.hovered_index = None;
+ } else if let Some(ix) = self.hovered_index {
+ if ix >= self.items.len() || !self.is_selectable_item(ix) {
+ self.hovered_index = None;
+ }
+ }
if let Some(scroll_top) = saved_scroll {
self.list_state.scroll_to(scroll_top);
@@ -299,11 +314,57 @@ impl ThreadsArchiveView {
window: &mut Window,
cx: &mut Context,
) {
+ if thread.folder_paths.is_empty() {
+ self.show_project_picker_for_thread(thread, window, cx);
+ return;
+ }
+
self.selection = None;
self.reset_filter_editor_text(window, cx);
cx.emit(ThreadsArchiveViewEvent::Unarchive { thread });
}
+ fn show_project_picker_for_thread(
+ &mut self,
+ thread: ThreadMetadata,
+ window: &mut Window,
+ cx: &mut Context,
+ ) {
+ let Some(workspace) = self.workspace.upgrade() else {
+ return;
+ };
+
+ let archive_view = cx.weak_entity();
+ let fs = workspace.read(cx).app_state().fs.clone();
+ let current_workspace_id = workspace.read(cx).database_id();
+ let sibling_workspace_ids: HashSet = workspace
+ .read(cx)
+ .multi_workspace()
+ .and_then(|mw| mw.upgrade())
+ .map(|mw| {
+ mw.read(cx)
+ .workspaces()
+ .iter()
+ .filter_map(|ws| ws.read(cx).database_id())
+ .collect()
+ })
+ .unwrap_or_default();
+
+ workspace.update(cx, |workspace, cx| {
+ workspace.toggle_modal(window, cx, |window, cx| {
+ ProjectPickerModal::new(
+ thread,
+ fs,
+ archive_view,
+ current_workspace_id,
+ sibling_workspace_ids,
+ window,
+ cx,
+ )
+ });
+ });
+ }
+
fn is_selectable_item(&self, ix: usize) -> bool {
matches!(self.items.get(ix), Some(ArchiveListItem::Entry { .. }))
}
@@ -391,10 +452,6 @@ impl ThreadsArchiveView {
return;
};
- if thread.folder_paths.is_empty() {
- return;
- }
-
self.unarchive_thread(thread.clone(), window, cx);
}
@@ -482,6 +539,7 @@ impl ThreadsArchiveView {
let agent = thread.agent_id.clone();
let session_id = thread.session_id.clone();
cx.listener(move |this, _, _, cx| {
+ this.preserve_selection_on_next_update = true;
this.delete_thread(session_id.clone(), agent.clone(), cx);
cx.stop_propagation();
})
@@ -550,43 +608,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) {
- 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) -> impl IntoElement {
let has_query = !self.filter_editor.read(cx).text(cx).is_empty();
let sidebar_on_left = matches!(
@@ -729,28 +750,536 @@ 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)
+ }
+}
+
+struct ProjectPickerModal {
+ picker: Entity>,
+ _subscription: Subscription,
+}
+
+impl ProjectPickerModal {
+ fn new(
+ thread: ThreadMetadata,
+ fs: Arc,
+ archive_view: WeakEntity,
+ current_workspace_id: Option,
+ sibling_workspace_ids: HashSet,
+ window: &mut Window,
+ cx: &mut Context,
+ ) -> Self {
+ let delegate = ProjectPickerDelegate {
+ thread,
+ archive_view,
+ workspaces: Vec::new(),
+ filtered_entries: Vec::new(),
+ selected_index: 0,
+ current_workspace_id,
+ sibling_workspace_ids,
+ focus_handle: cx.focus_handle(),
+ };
+
+ let picker = cx.new(|cx| {
+ Picker::list(delegate, window, cx)
+ .list_measure_all()
+ .modal(false)
+ });
+
+ let picker_focus_handle = picker.focus_handle(cx);
+ picker.update(cx, |picker, _| {
+ picker.delegate.focus_handle = picker_focus_handle;
+ });
+
+ let _subscription =
+ cx.subscribe(&picker, |_this: &mut Self, _, _event: &DismissEvent, cx| {
+ cx.emit(DismissEvent);
+ });
+
+ let db = WorkspaceDb::global(cx);
+ cx.spawn_in(window, async move |this, cx| {
+ let workspaces = db
+ .recent_workspaces_on_disk(fs.as_ref())
+ .await
+ .log_err()
+ .unwrap_or_default();
+ let workspaces = resolve_worktree_workspaces(workspaces, fs.as_ref()).await;
+ this.update_in(cx, move |this, window, cx| {
+ this.picker.update(cx, move |picker, cx| {
+ picker.delegate.workspaces = workspaces;
+ picker.update_matches(picker.query(cx), window, cx)
+ })
+ })
+ .ok();
+ })
+ .detach();
+
+ picker.focus_handle(cx).focus(window, cx);
+
+ Self {
+ picker,
+ _subscription,
+ }
+ }
+}
+
+impl EventEmitter for ProjectPickerModal {}
+
+impl Focusable for ProjectPickerModal {
+ fn focus_handle(&self, cx: &App) -> FocusHandle {
+ self.picker.focus_handle(cx)
+ }
+}
+
+impl ModalView for ProjectPickerModal {}
+
+impl Render for ProjectPickerModal {
+ fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement {
+ v_flex()
+ .key_context("ProjectPickerModal")
+ .elevation_3(cx)
+ .w(rems(34.))
+ .on_action(cx.listener(|this, _: &workspace::Open, window, cx| {
+ this.picker.update(cx, |picker, cx| {
+ picker.delegate.open_local_folder(window, cx)
+ })
+ }))
+ .child(self.picker.clone())
+ }
+}
+
+enum ProjectPickerEntry {
+ Header(SharedString),
+ Workspace(StringMatch),
+}
+
+struct ProjectPickerDelegate {
+ thread: ThreadMetadata,
+ archive_view: WeakEntity,
+ current_workspace_id: Option,
+ sibling_workspace_ids: HashSet,
+ workspaces: Vec<(
+ WorkspaceId,
+ SerializedWorkspaceLocation,
+ PathList,
+ DateTime,
+ )>,
+ filtered_entries: Vec,
+ selected_index: usize,
+ focus_handle: FocusHandle,
+}
+
+impl ProjectPickerDelegate {
+ fn update_working_directories_and_unarchive(
+ &mut self,
+ paths: PathList,
+ window: &mut Window,
+ cx: &mut Context>,
+ ) {
+ self.thread.folder_paths = paths.clone();
+ ThreadMetadataStore::global(cx).update(cx, |store, cx| {
+ store.update_working_directories(&self.thread.session_id, paths, cx);
+ });
+
+ self.archive_view
+ .update(cx, |view, cx| {
+ view.selection = None;
+ view.reset_filter_editor_text(window, cx);
+ cx.emit(ThreadsArchiveViewEvent::Unarchive {
+ thread: self.thread.clone(),
+ });
+ })
+ .log_err();
+ }
+
+ fn is_current_workspace(&self, workspace_id: WorkspaceId) -> bool {
+ self.current_workspace_id == Some(workspace_id)
+ }
+
+ fn is_sibling_workspace(&self, workspace_id: WorkspaceId) -> bool {
+ self.sibling_workspace_ids.contains(&workspace_id)
+ && !self.is_current_workspace(workspace_id)
+ }
+
+ fn selected_match(&self) -> Option<&StringMatch> {
+ match self.filtered_entries.get(self.selected_index)? {
+ ProjectPickerEntry::Workspace(hit) => Some(hit),
+ ProjectPickerEntry::Header(_) => None,
+ }
+ }
+
+ fn open_local_folder(&mut self, window: &mut Window, cx: &mut Context>) {
+ let paths_receiver = cx.prompt_for_paths(gpui::PathPromptOptions {
+ files: false,
+ directories: true,
+ multiple: false,
+ prompt: None,
+ });
+ cx.spawn_in(window, async move |this, cx| {
+ let Ok(Ok(Some(paths))) = paths_receiver.await else {
+ return;
+ };
+ if paths.is_empty() {
+ return;
+ }
+
+ let work_dirs = PathList::new(&paths);
+
+ this.update_in(cx, |this, window, cx| {
+ this.delegate
+ .update_working_directories_and_unarchive(work_dirs, window, cx);
+ cx.emit(DismissEvent);
+ })
+ .log_err();
+ })
+ .detach();
+ }
+}
+
+impl EventEmitter for ProjectPickerDelegate {}
+
+impl PickerDelegate for ProjectPickerDelegate {
+ type ListItem = AnyElement;
+
+ fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc {
+ format!("Associate the \"{}\" thread with...", self.thread.title).into()
+ }
+
+ fn render_editor(
+ &self,
+ editor: &Arc,
+ window: &mut Window,
+ cx: &mut Context>,
+ ) -> Div {
+ h_flex()
+ .flex_none()
+ .h_9()
+ .px_2p5()
+ .justify_between()
+ .border_b_1()
+ .border_color(cx.theme().colors().border_variant)
+ .child(editor.render(window, cx))
+ }
+
+ fn match_count(&self) -> usize {
+ self.filtered_entries.len()
+ }
+
+ fn selected_index(&self) -> usize {
+ self.selected_index
+ }
+
+ fn set_selected_index(
+ &mut self,
+ ix: usize,
+ _window: &mut Window,
+ _cx: &mut Context>,
+ ) {
+ self.selected_index = ix;
+ }
+
+ fn can_select(&self, ix: usize, _window: &mut Window, _cx: &mut Context>) -> bool {
+ matches!(
+ self.filtered_entries.get(ix),
+ Some(ProjectPickerEntry::Workspace(_))
+ )
+ }
+
+ fn update_matches(
+ &mut self,
+ query: String,
+ _window: &mut Window,
+ cx: &mut Context>,
+ ) -> Task<()> {
+ let query = query.trim_start();
+ let smart_case = query.chars().any(|c| c.is_uppercase());
+ let is_empty_query = query.is_empty();
+
+ let sibling_candidates: Vec<_> = self
+ .workspaces
+ .iter()
+ .enumerate()
+ .filter(|(_, (id, _, _, _))| self.is_sibling_workspace(*id))
+ .map(|(id, (_, _, paths, _))| {
+ let combined_string = paths
+ .ordered_paths()
+ .map(|path| path.compact().to_string_lossy().into_owned())
+ .collect::>()
+ .join("");
+ StringMatchCandidate::new(id, &combined_string)
+ })
+ .collect();
+
+ let mut sibling_matches = smol::block_on(fuzzy::match_strings(
+ &sibling_candidates,
+ query,
+ smart_case,
+ true,
+ 100,
+ &Default::default(),
+ cx.background_executor().clone(),
+ ));
+
+ sibling_matches.sort_unstable_by(|a, b| {
+ b.score
+ .partial_cmp(&a.score)
+ .unwrap_or(std::cmp::Ordering::Equal)
+ .then_with(|| a.candidate_id.cmp(&b.candidate_id))
+ });
+
+ let recent_candidates: Vec<_> = self
+ .workspaces
+ .iter()
+ .enumerate()
+ .filter(|(_, (id, _, _, _))| {
+ !self.is_current_workspace(*id) && !self.is_sibling_workspace(*id)
+ })
+ .map(|(id, (_, _, paths, _))| {
+ let combined_string = paths
+ .ordered_paths()
+ .map(|path| path.compact().to_string_lossy().into_owned())
+ .collect::>()
+ .join("");
+ StringMatchCandidate::new(id, &combined_string)
+ })
+ .collect();
+
+ let mut recent_matches = smol::block_on(fuzzy::match_strings(
+ &recent_candidates,
+ query,
+ smart_case,
+ true,
+ 100,
+ &Default::default(),
+ cx.background_executor().clone(),
+ ));
+
+ recent_matches.sort_unstable_by(|a, b| {
+ b.score
+ .partial_cmp(&a.score)
+ .unwrap_or(std::cmp::Ordering::Equal)
+ .then_with(|| a.candidate_id.cmp(&b.candidate_id))
+ });
+
+ let mut entries = Vec::new();
+
+ let has_siblings_to_show = if is_empty_query {
+ !sibling_candidates.is_empty()
+ } else {
+ !sibling_matches.is_empty()
+ };
+
+ if has_siblings_to_show {
+ entries.push(ProjectPickerEntry::Header("This Window".into()));
+
+ if is_empty_query {
+ for (id, (workspace_id, _, _, _)) in self.workspaces.iter().enumerate() {
+ if self.is_sibling_workspace(*workspace_id) {
+ entries.push(ProjectPickerEntry::Workspace(StringMatch {
+ candidate_id: id,
+ score: 0.0,
+ positions: Vec::new(),
+ string: String::new(),
+ }));
+ }
+ }
+ } else {
+ for m in sibling_matches {
+ entries.push(ProjectPickerEntry::Workspace(m));
+ }
+ }
+ }
+
+ let has_recent_to_show = if is_empty_query {
+ !recent_candidates.is_empty()
+ } else {
+ !recent_matches.is_empty()
+ };
+
+ if has_recent_to_show {
+ entries.push(ProjectPickerEntry::Header("Recent Projects".into()));
+
+ if is_empty_query {
+ for (id, (workspace_id, _, _, _)) in self.workspaces.iter().enumerate() {
+ if !self.is_current_workspace(*workspace_id)
+ && !self.is_sibling_workspace(*workspace_id)
+ {
+ entries.push(ProjectPickerEntry::Workspace(StringMatch {
+ candidate_id: id,
+ score: 0.0,
+ positions: Vec::new(),
+ string: String::new(),
+ }));
+ }
+ }
+ } else {
+ for m in recent_matches {
+ entries.push(ProjectPickerEntry::Workspace(m));
+ }
+ }
+ }
+
+ self.filtered_entries = entries;
+
+ self.selected_index = self
+ .filtered_entries
+ .iter()
+ .position(|e| matches!(e, ProjectPickerEntry::Workspace(_)))
+ .unwrap_or(0);
+
+ Task::ready(())
+ }
+
+ fn confirm(&mut self, _secondary: bool, window: &mut Window, cx: &mut Context>) {
+ let candidate_id = match self.filtered_entries.get(self.selected_index) {
+ Some(ProjectPickerEntry::Workspace(hit)) => hit.candidate_id,
+ _ => return,
+ };
+ let Some((_workspace_id, _location, paths, _)) = self.workspaces.get(candidate_id) else {
+ return;
+ };
+
+ self.update_working_directories_and_unarchive(paths.clone(), window, cx);
+ cx.emit(DismissEvent);
+ }
+
+ fn dismissed(&mut self, _window: &mut Window, _cx: &mut Context>) {}
+
+ fn no_matches_text(&self, _window: &mut Window, _cx: &mut App) -> Option {
+ let text = if self.workspaces.is_empty() {
+ "No recent projects found"
+ } else {
+ "No matches"
+ };
+ Some(text.into())
+ }
+
+ fn render_match(
+ &self,
+ ix: usize,
+ selected: bool,
+ window: &mut Window,
+ cx: &mut Context>,
+ ) -> Option {
+ match self.filtered_entries.get(ix)? {
+ ProjectPickerEntry::Header(title) => Some(
+ v_flex()
+ .w_full()
+ .gap_1()
+ .when(ix > 0, |this| this.mt_1().child(Divider::horizontal()))
+ .child(ListSubHeader::new(title.clone()).inset(true))
+ .into_any_element(),
+ ),
+ ProjectPickerEntry::Workspace(hit) => {
+ let (_, location, paths, _) = self.workspaces.get(hit.candidate_id)?;
+
+ let ordered_paths: Vec<_> = paths
+ .ordered_paths()
+ .map(|p| p.compact().to_string_lossy().to_string())
+ .collect();
+
+ let tooltip_path: SharedString = ordered_paths.join("\n").into();
+
+ let mut path_start_offset = 0;
+ let match_labels: Vec<_> = paths
+ .ordered_paths()
+ .map(|p| p.compact())
+ .map(|path| {
+ let path_string = path.to_string_lossy();
+ let path_text = path_string.to_string();
+ let path_byte_len = path_text.len();
+
+ let path_positions: Vec = hit
+ .positions
+ .iter()
+ .copied()
+ .skip_while(|pos| *pos < path_start_offset)
+ .take_while(|pos| *pos < path_start_offset + path_byte_len)
+ .map(|pos| pos - path_start_offset)
+ .collect();
+
+ let file_name_match = path.file_name().map(|file_name| {
+ let file_name_text = file_name.to_string_lossy().into_owned();
+ let file_name_start = path_byte_len - file_name_text.len();
+ let highlight_positions: Vec = path_positions
+ .iter()
+ .copied()
+ .skip_while(|pos| *pos < file_name_start)
+ .take_while(|pos| *pos < file_name_start + file_name_text.len())
+ .map(|pos| pos - file_name_start)
+ .collect();
+ HighlightedMatch {
+ text: file_name_text,
+ highlight_positions,
+ color: Color::Default,
+ }
+ });
+
+ path_start_offset += path_byte_len;
+ file_name_match
+ })
+ .collect();
+
+ let highlighted_match = HighlightedMatchWithPaths {
+ prefix: match location {
+ SerializedWorkspaceLocation::Remote(options) => {
+ Some(SharedString::from(options.display_name()))
+ }
+ _ => None,
+ },
+ match_label: HighlightedMatch::join(match_labels.into_iter().flatten(), ", "),
+ paths: Vec::new(),
+ };
+
+ Some(
+ ListItem::new(ix)
+ .toggle_state(selected)
+ .inset(true)
+ .spacing(ListItemSpacing::Sparse)
.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);
- })),
- ),
+ h_flex()
+ .gap_3()
+ .flex_grow()
+ .child(highlighted_match.render(window, cx)),
+ )
+ .tooltip(Tooltip::text(tooltip_path))
+ .into_any_element(),
)
- })
+ }
+ }
+ }
+
+ fn render_footer(&self, _: &mut Window, cx: &mut Context>) -> Option {
+ let has_selection = self.selected_match().is_some();
+ let focus_handle = self.focus_handle.clone();
+
+ Some(
+ h_flex()
+ .flex_1()
+ .p_1p5()
+ .gap_1()
+ .justify_end()
+ .border_t_1()
+ .border_color(cx.theme().colors().border_variant)
+ .child(
+ Button::new("open_local_folder", "Choose from Local Folders")
+ .key_binding(KeyBinding::for_action_in(
+ &workspace::Open::default(),
+ &focus_handle,
+ cx,
+ ))
+ .on_click(cx.listener(|this, _, window, cx| {
+ this.delegate.open_local_folder(window, cx);
+ })),
+ )
+ .child(
+ Button::new("select_project", "Select")
+ .disabled(!has_selection)
+ .key_binding(KeyBinding::for_action_in(&menu::Confirm, &focus_handle, cx))
+ .on_click(cx.listener(move |picker, _, window, cx| {
+ picker.delegate.confirm(false, window, cx);
+ })),
+ )
+ .into_any(),
+ )
}
}
diff --git a/crates/agent_ui/src/ui.rs b/crates/agent_ui/src/ui.rs
index 16732951ce67d76ca8d65259e309c4b81df30c3b..d43b7e4b043bcd1b155699c5eea3ca695585b94b 100644
--- a/crates/agent_ui/src/ui.rs
+++ b/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::*;
diff --git a/crates/agent_ui/src/ui/claude_agent_onboarding_modal.rs b/crates/agent_ui/src/ui/claude_agent_onboarding_modal.rs
deleted file mode 100644
index 5b7e58eb4fd79a5075446dad997c2642fedf32a6..0000000000000000000000000000000000000000
--- a/crates/agent_ui/src/ui/claude_agent_onboarding_modal.rs
+++ /dev/null
@@ -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,
-}
-
-impl ClaudeCodeOnboardingModal {
- pub fn toggle(workspace: &mut Workspace, window: &mut Window, cx: &mut Context) {
- 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.workspace.update(cx, |workspace, cx| {
- workspace.focus_panel::(window, cx);
-
- if let Some(panel) = workspace.panel::(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) {
- 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) {
- cx.emit(DismissEvent);
- }
-}
-
-impl EventEmitter 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) -> impl IntoElement {
- let illustration_element = |icon: IconName, label: Option, 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)
- }
-}
diff --git a/crates/agent_ui/src/ui/model_selector_components.rs b/crates/agent_ui/src/ui/model_selector_components.rs
index 01ba6c4511854e83b97b1fc053e41e5d0e82ff1e..88bf546a0e7beef53c8043fd04f8e6e9e5e92c88 100644
--- a/crates/agent_ui/src/ui/model_selector_components.rs
+++ b/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")
diff --git a/crates/csv_preview/src/csv_preview.rs b/crates/csv_preview/src/csv_preview.rs
index b0b6ad4186758fd33693d5ee29bd2f0d4d28b816..c38cefb2456b3f44e3cac61b02294ab1ed1e79f4 100644
--- a/crates/csv_preview/src/csv_preview.rs
+++ b/crates/csv_preview/src/csv_preview.rs
@@ -9,7 +9,10 @@ use std::{
};
use crate::table_data_engine::TableDataEngine;
-use ui::{SharedString, TableColumnWidths, TableInteractionState, prelude::*};
+use ui::{
+ AbsoluteLength, DefiniteLength, RedistributableColumnsState, SharedString,
+ TableInteractionState, TableResizeBehavior, prelude::*,
+};
use workspace::{Item, SplitDirection, Workspace};
use crate::{parser::EditorState, settings::CsvPreviewSettings, types::TableLikeContent};
@@ -52,6 +55,32 @@ pub fn init(cx: &mut App) {
}
impl CsvPreviewView {
+ pub(crate) fn sync_column_widths(&self, cx: &mut Context) {
+ // plus 1 for the rows column
+ let cols = self.engine.contents.headers.cols() + 1;
+ let remaining_col_number = cols.saturating_sub(1);
+ let fraction = if remaining_col_number > 0 {
+ 1. / remaining_col_number as f32
+ } else {
+ 1.
+ };
+ let mut widths = vec![DefiniteLength::Fraction(fraction); cols];
+ let line_number_width = self.calculate_row_identifier_column_width();
+ widths[0] = DefiniteLength::Absolute(AbsoluteLength::Pixels(line_number_width.into()));
+
+ let mut resize_behaviors = vec![TableResizeBehavior::Resizable; cols];
+ resize_behaviors[0] = TableResizeBehavior::None;
+
+ self.column_widths.widths.update(cx, |state, _cx| {
+ if state.cols() != cols
+ || state.initial_widths().as_slice() != widths.as_slice()
+ || state.resize_behavior().as_slice() != resize_behaviors.as_slice()
+ {
+ *state = RedistributableColumnsState::new(cols, widths, resize_behaviors);
+ }
+ });
+ }
+
pub fn register(workspace: &mut Workspace) {
workspace.register_action_renderer(|div, _, _, cx| {
div.when(cx.has_flag::(), |div| {
@@ -286,18 +315,19 @@ impl PerformanceMetrics {
/// Holds state of column widths for a table component in CSV preview.
pub(crate) struct ColumnWidths {
- pub widths: Entity,
+ pub widths: Entity,
}
impl ColumnWidths {
pub(crate) fn new(cx: &mut Context, cols: usize) -> Self {
Self {
- widths: cx.new(|cx| TableColumnWidths::new(cols, cx)),
+ widths: cx.new(|_cx| {
+ RedistributableColumnsState::new(
+ cols,
+ vec![ui::DefiniteLength::Fraction(1.0 / cols as f32); cols],
+ vec![ui::TableResizeBehavior::Resizable; cols],
+ )
+ }),
}
}
- /// Replace the current `TableColumnWidths` entity with a new one for the given column count.
- pub(crate) fn replace(&self, cx: &mut Context, cols: usize) {
- self.widths
- .update(cx, |entity, cx| *entity = TableColumnWidths::new(cols, cx));
- }
}
diff --git a/crates/csv_preview/src/parser.rs b/crates/csv_preview/src/parser.rs
index b087404e0ebbd13cdaf20cab692f5470ea6ce292..efa3573d7aa53d97e2801ff00feb4665072830f4 100644
--- a/crates/csv_preview/src/parser.rs
+++ b/crates/csv_preview/src/parser.rs
@@ -80,11 +80,8 @@ impl CsvPreviewView {
.insert("Parsing", (parse_duration, Instant::now()));
log::debug!("Parsed {} rows", parsed_csv.rows.len());
- // Update table width so it can be rendered properly
- let cols = parsed_csv.headers.cols();
- view.column_widths.replace(cx, cols + 1); // Add 1 for the line number column
-
view.engine.contents = parsed_csv;
+ view.sync_column_widths(cx);
view.last_parse_end_time = Some(parse_end_time);
view.apply_filter_sort();
diff --git a/crates/csv_preview/src/renderer/render_table.rs b/crates/csv_preview/src/renderer/render_table.rs
index 0cc3bc3c46fb24570b3c99c9121dff3860c6b820..fb3d7e5fc603ba5b109319cfb19466dc3ad7652f 100644
--- a/crates/csv_preview/src/renderer/render_table.rs
+++ b/crates/csv_preview/src/renderer/render_table.rs
@@ -1,11 +1,9 @@
use crate::types::TableCell;
use gpui::{AnyElement, Entity};
use std::ops::Range;
-use ui::Table;
-use ui::TableColumnWidths;
-use ui::TableResizeBehavior;
-use ui::UncheckedTableRow;
-use ui::{DefiniteLength, div, prelude::*};
+use ui::{
+ ColumnWidthConfig, RedistributableColumnsState, Table, UncheckedTableRow, div, prelude::*,
+};
use crate::{
CsvPreviewView,
@@ -15,44 +13,22 @@ use crate::{
impl CsvPreviewView {
/// Creates a new table.
- /// Column number is derived from the `TableColumnWidths` entity.
+ /// Column number is derived from the `RedistributableColumnsState` entity.
pub(crate) fn create_table(
&self,
- current_widths: &Entity,
+ current_widths: &Entity,
cx: &mut Context,
) -> AnyElement {
- let cols = current_widths.read(cx).cols();
- let remaining_col_number = cols - 1;
- let fraction = if remaining_col_number > 0 {
- 1. / remaining_col_number as f32
- } else {
- 1. // only column with line numbers is present. Put 100%, but it will be overwritten anyways :D
- };
- let mut widths = vec![DefiniteLength::Fraction(fraction); cols];
- let line_number_width = self.calculate_row_identifier_column_width();
- widths[0] = DefiniteLength::Absolute(AbsoluteLength::Pixels(line_number_width.into()));
-
- let mut resize_behaviors = vec![TableResizeBehavior::Resizable; cols];
- resize_behaviors[0] = TableResizeBehavior::None;
-
- self.create_table_inner(
- self.engine.contents.rows.len(),
- widths,
- resize_behaviors,
- current_widths,
- cx,
- )
+ self.create_table_inner(self.engine.contents.rows.len(), current_widths, cx)
}
fn create_table_inner(
&self,
row_count: usize,
- widths: UncheckedTableRow,
- resize_behaviors: UncheckedTableRow,
- current_widths: &Entity,
+ current_widths: &Entity,
cx: &mut Context,
) -> AnyElement {
- let cols = widths.len();
+ let cols = current_widths.read(cx).cols();
// Create headers array with interactive elements
let mut headers = Vec::with_capacity(cols);
@@ -78,8 +54,7 @@ impl CsvPreviewView {
Table::new(cols)
.interactable(&self.table_interaction_state)
.striped()
- .column_widths(widths)
- .resizable_columns(resize_behaviors, current_widths, cx)
+ .width_config(ColumnWidthConfig::redistributable(current_widths.clone()))
.header(headers)
.disable_base_style()
.map(|table| {
diff --git a/crates/csv_preview/src/renderer/row_identifiers.rs b/crates/csv_preview/src/renderer/row_identifiers.rs
index a122aa9bf3d803b9deb9c6211e117ba4aa593d93..fc8bf68845fd41917e7d60bf5f9276295534c902 100644
--- a/crates/csv_preview/src/renderer/row_identifiers.rs
+++ b/crates/csv_preview/src/renderer/row_identifiers.rs
@@ -139,6 +139,7 @@ impl CsvPreviewView {
RowIdentifiers::SrcLines => RowIdentifiers::RowNum,
RowIdentifiers::RowNum => RowIdentifiers::SrcLines,
};
+ this.sync_column_widths(cx);
cx.notify();
});
}),
diff --git a/crates/csv_preview/src/renderer/table_cell.rs b/crates/csv_preview/src/renderer/table_cell.rs
index 32900ab77708936e218e9af10a4de5fba796e6a7..733488110fbcdb39761b150a74c135426ca6514a 100644
--- a/crates/csv_preview/src/renderer/table_cell.rs
+++ b/crates/csv_preview/src/renderer/table_cell.rs
@@ -53,7 +53,6 @@ fn create_table_cell(
.px_1()
.bg(cx.theme().colors().editor_background)
.border_b_1()
- .border_r_1()
.border_color(cx.theme().colors().border_variant)
.map(|div| match vertical_alignment {
VerticalAlignment::Top => div.items_start(),
diff --git a/crates/diagnostics/src/diagnostic_renderer.rs b/crates/diagnostics/src/diagnostic_renderer.rs
index 27e1cbbac9c779056ecd9da00dd7a56ff3536f17..62b7f4eadf322da1c57a9f1da60b412d7b0dcd68 100644
--- a/crates/diagnostics/src/diagnostic_renderer.rs
+++ b/crates/diagnostics/src/diagnostic_renderer.rs
@@ -8,7 +8,7 @@ use editor::{
use gpui::{AppContext, Entity, Focusable, WeakEntity};
use language::{BufferId, Diagnostic, DiagnosticEntryRef, LanguageRegistry};
use lsp::DiagnosticSeverity;
-use markdown::{Markdown, MarkdownElement};
+use markdown::{CopyButtonVisibility, Markdown, MarkdownElement};
use settings::Settings;
use text::{AnchorRangeExt, Point};
use theme_settings::ThemeSettings;
@@ -239,8 +239,7 @@ impl DiagnosticBlock {
diagnostics_markdown_style(bcx.window, cx),
)
.code_block_renderer(markdown::CodeBlockRenderer::Default {
- copy_button: false,
- copy_button_on_hover: false,
+ copy_button_visibility: CopyButtonVisibility::Hidden,
border: false,
})
.on_url_click({
diff --git a/crates/editor/src/code_context_menus.rs b/crates/editor/src/code_context_menus.rs
index 3fc6080b4da8ca85d258d04de29d603ea7097623..5d6c037d9b67034423dda9f119a1e78fb1e5b9b2 100644
--- a/crates/editor/src/code_context_menus.rs
+++ b/crates/editor/src/code_context_menus.rs
@@ -9,7 +9,7 @@ use itertools::Itertools;
use language::CodeLabel;
use language::{Buffer, LanguageName, LanguageRegistry};
use lsp::CompletionItemTag;
-use markdown::{Markdown, MarkdownElement};
+use markdown::{CopyButtonVisibility, Markdown, MarkdownElement};
use multi_buffer::{Anchor, ExcerptId};
use ordered_float::OrderedFloat;
use project::lsp_store::CompletionDocumentation;
@@ -1118,8 +1118,7 @@ impl CompletionsMenu {
div().child(
MarkdownElement::new(markdown, hover_markdown_style(window, cx))
.code_block_renderer(markdown::CodeBlockRenderer::Default {
- copy_button: false,
- copy_button_on_hover: false,
+ copy_button_visibility: CopyButtonVisibility::Hidden,
border: false,
})
.on_url_click(open_markdown_url),
diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs
index 65a872e6035565bb01fdd78e00d6cf0f35d35ef8..7e397507eda0d800ee9ed6b204ed95e71d50234b 100644
--- a/crates/editor/src/editor_tests.rs
+++ b/crates/editor/src/editor_tests.rs
@@ -32309,6 +32309,91 @@ async fn test_scroll_by_clicking_sticky_header(cx: &mut TestAppContext) {
assert_eq!(selections, vec![empty_range(4, 5)]);
}
+#[gpui::test]
+async fn test_clicking_sticky_header_sets_character_select_mode(cx: &mut TestAppContext) {
+ init_test(cx, |_| {});
+ cx.update(|cx| {
+ SettingsStore::update_global(cx, |store, cx| {
+ store.update_user_settings(cx, |settings| {
+ settings.editor.sticky_scroll = Some(settings::StickyScrollContent {
+ enabled: Some(true),
+ })
+ });
+ });
+ });
+ let mut cx = EditorTestContext::new(cx).await;
+
+ let line_height = cx.update_editor(|editor, window, cx| {
+ editor
+ .style(cx)
+ .text
+ .line_height_in_pixels(window.rem_size())
+ });
+
+ let buffer = indoc! {"
+ fn foo() {
+ let abc = 123;
+ }
+ ˇstruct Bar;
+ "};
+ cx.set_state(&buffer);
+
+ cx.update_editor(|editor, _, cx| {
+ editor
+ .buffer()
+ .read(cx)
+ .as_singleton()
+ .unwrap()
+ .update(cx, |buffer, cx| {
+ buffer.set_language(Some(rust_lang()), cx);
+ })
+ });
+
+ let text_origin_x = cx.update_editor(|editor, _, _| {
+ editor
+ .last_position_map
+ .as_ref()
+ .unwrap()
+ .text_hitbox
+ .bounds
+ .origin
+ .x
+ });
+
+ cx.update_editor(|editor, window, cx| {
+ // Double click on `struct` to select it
+ editor.begin_selection(DisplayPoint::new(DisplayRow(3), 1), false, 2, window, cx);
+ editor.end_selection(window, cx);
+
+ // Scroll down one row to make `fn foo() {` a sticky header
+ editor.scroll(gpui::Point { x: 0., y: 1. }, None, window, cx);
+ });
+ cx.run_until_parked();
+
+ // Click at the start of the `fn foo() {` sticky header
+ cx.simulate_click(
+ gpui::Point {
+ x: text_origin_x,
+ y: 0.5 * line_height,
+ },
+ Modifiers::none(),
+ );
+ cx.run_until_parked();
+
+ // Shift-click at the end of `fn foo() {` to select the whole row
+ cx.update_editor(|editor, window, cx| {
+ editor.extend_selection(DisplayPoint::new(DisplayRow(0), 10), 1, window, cx);
+ editor.end_selection(window, cx);
+ });
+ cx.run_until_parked();
+
+ let selections = cx.update_editor(|editor, _, cx| display_ranges(editor, cx));
+ assert_eq!(
+ selections,
+ vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 10)]
+ );
+}
+
#[gpui::test]
async fn test_next_prev_reference(cx: &mut TestAppContext) {
const CYCLE_POSITIONS: &[&'static str] = &[
diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs
index 9ce080c87bf82ec1098e2a4b1db6bc6a65d22828..2fdb2686ee00ea2fc27881b0c18a54fa85466d9a 100644
--- a/crates/editor/src/element.rs
+++ b/crates/editor/src/element.rs
@@ -1289,7 +1289,9 @@ impl EditorElement {
cx.notify();
}
- if let Some((bounds, buffer_id, blame_entry)) = &position_map.inline_blame_bounds {
+ if text_hovered
+ && let Some((bounds, buffer_id, blame_entry)) = &position_map.inline_blame_bounds
+ {
let mouse_over_inline_blame = bounds.contains(&event.position);
let mouse_over_popover = editor
.inline_blame_popover
@@ -6732,7 +6734,13 @@ impl EditorElement {
SelectionEffects::scroll(Autoscroll::top_relative(line_index)),
window,
cx,
- |selections| selections.select_ranges([anchor..anchor]),
+ |selections| {
+ selections.clear_disjoint();
+ selections.set_pending_anchor_range(
+ anchor..anchor,
+ crate::SelectMode::Character,
+ );
+ },
);
cx.stop_propagation();
});
diff --git a/crates/editor/src/hover_popover.rs b/crates/editor/src/hover_popover.rs
index 9b127a8f1bc089d9cee28254c6b8ffc181677765..3bad6c97b6bcba4015331257a5b9a476dd0d3fd3 100644
--- a/crates/editor/src/hover_popover.rs
+++ b/crates/editor/src/hover_popover.rs
@@ -17,7 +17,7 @@ use gpui::{
use itertools::Itertools;
use language::{DiagnosticEntry, Language, LanguageRegistry};
use lsp::DiagnosticSeverity;
-use markdown::{Markdown, MarkdownElement, MarkdownStyle};
+use markdown::{CopyButtonVisibility, Markdown, MarkdownElement, MarkdownStyle};
use multi_buffer::{MultiBufferOffset, ToOffset, ToPoint};
use project::{HoverBlock, HoverBlockKind, InlayHintLabelPart};
use settings::Settings;
@@ -1040,8 +1040,7 @@ impl InfoPopover {
.child(
MarkdownElement::new(markdown, hover_markdown_style(window, cx))
.code_block_renderer(markdown::CodeBlockRenderer::Default {
- copy_button: false,
- copy_button_on_hover: false,
+ copy_button_visibility: CopyButtonVisibility::Hidden,
border: false,
})
.on_url_click(open_markdown_url)
@@ -1155,8 +1154,7 @@ impl DiagnosticPopover {
diagnostics_markdown_style(window, cx),
)
.code_block_renderer(markdown::CodeBlockRenderer::Default {
- copy_button: false,
- copy_button_on_hover: false,
+ copy_button_visibility: CopyButtonVisibility::Hidden,
border: false,
})
.on_url_click(
diff --git a/crates/editor/src/signature_help.rs b/crates/editor/src/signature_help.rs
index 27c26d4691686c16bcbafbf74bba6b5f1156b835..6305fc73e44d745e943c1d4c8ec573e0cce7d9ed 100644
--- a/crates/editor/src/signature_help.rs
+++ b/crates/editor/src/signature_help.rs
@@ -7,7 +7,7 @@ use gpui::{
};
use language::BufferSnapshot;
-use markdown::{Markdown, MarkdownElement};
+use markdown::{CopyButtonVisibility, Markdown, MarkdownElement};
use multi_buffer::{Anchor, MultiBufferOffset, ToOffset};
use settings::Settings;
use std::ops::Range;
@@ -408,9 +408,8 @@ impl SignatureHelpPopover {
hover_markdown_style(window, cx),
)
.code_block_renderer(markdown::CodeBlockRenderer::Default {
- copy_button: false,
+ copy_button_visibility: CopyButtonVisibility::Hidden,
border: false,
- copy_button_on_hover: false,
})
.on_url_click(open_markdown_url),
)
@@ -421,9 +420,8 @@ impl SignatureHelpPopover {
.child(
MarkdownElement::new(description, hover_markdown_style(window, cx))
.code_block_renderer(markdown::CodeBlockRenderer::Default {
- copy_button: false,
+ copy_button_visibility: CopyButtonVisibility::Hidden,
border: false,
- copy_button_on_hover: false,
})
.on_url_click(open_markdown_url),
)
diff --git a/crates/extension_host/src/wasm_host.rs b/crates/extension_host/src/wasm_host.rs
index 286639cdd67d716b1137290baf269670ecddebe7..87a2032e831fc942f6848428a901a9fe3f613fc8 100644
--- a/crates/extension_host/src/wasm_host.rs
+++ b/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 {
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