diff --git a/.github/ISSUE_TEMPLATE/07_bug_windows_alpha.yml b/.github/ISSUE_TEMPLATE/07_bug_windows_beta.yml
similarity index 86%
rename from .github/ISSUE_TEMPLATE/07_bug_windows_alpha.yml
rename to .github/ISSUE_TEMPLATE/07_bug_windows_beta.yml
index 826c2b8027144d4b658108e09c79e40490c3005d..b2b2a0f9dfcd5ddaa0dda41650864b053c5bb933 100644
--- a/.github/ISSUE_TEMPLATE/07_bug_windows_alpha.yml
+++ b/.github/ISSUE_TEMPLATE/07_bug_windows_beta.yml
@@ -1,8 +1,8 @@
-name: Bug Report (Windows Alpha)
-description: Zed Windows Alpha Related Bugs
+name: Bug Report (Windows Beta)
+description: Zed Windows Beta Related Bugs
type: "Bug"
labels: ["windows"]
-title: "Windows Alpha: "
+title: "Windows Beta: "
body:
- type: textarea
attributes:
diff --git a/.github/workflows/community_release_actions.yml b/.github/workflows/community_release_actions.yml
index 37ade90574e76cd95755aad6b5601a43946a271c..0f7a73649e9e1180c78a66ddf54055bf66f243f9 100644
--- a/.github/workflows/community_release_actions.yml
+++ b/.github/workflows/community_release_actions.yml
@@ -1,3 +1,6 @@
+# IF YOU UPDATE THE NAME OF ANY GITHUB SECRET, YOU MUST CHERRY PICK THE COMMIT
+# TO BOTH STABLE AND PREVIEW CHANNELS
+
name: Release Actions
on:
@@ -13,9 +16,9 @@ jobs:
id: get-release-url
run: |
if [ "${{ github.event.release.prerelease }}" == "true" ]; then
- URL="https://zed.dev/releases/preview/latest"
+ URL="https://zed.dev/releases/preview"
else
- URL="https://zed.dev/releases/stable/latest"
+ URL="https://zed.dev/releases/stable"
fi
echo "URL=$URL" >> "$GITHUB_OUTPUT"
diff --git a/.rules b/.rules
index 2f2b9cd705d95775bedf092bc4e6254136da6117..82d15eb9e88299ee7c7fe6c717b2da2646e676a7 100644
--- a/.rules
+++ b/.rules
@@ -59,7 +59,7 @@ Trying to update an entity while it's already being updated must be avoided as t
When `read_with`, `update`, or `update_in` are used with an async context, the closure's return value is wrapped in an `anyhow::Result`.
-`WeakEntity` is a weak handle. It has `read_with`, `update`, and `update_in` methods that work the same, but always return an `anyhow::Result` so that they can fail if the entity no longer exists. This can be useful to avoid memory leaks - if entities have mutually recursive handles to eachother they will never be dropped.
+`WeakEntity` is a weak handle. It has `read_with`, `update`, and `update_in` methods that work the same, but always return an `anyhow::Result` so that they can fail if the entity no longer exists. This can be useful to avoid memory leaks - if entities have mutually recursive handles to each other they will never be dropped.
## Concurrency
diff --git a/Cargo.lock b/Cargo.lock
index f7ef8f5ccc67f504da11872b7171817a72b6a116..da94c746e7fc528e12dce31b547c13d248055d66 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -39,7 +39,6 @@ dependencies = [
"util",
"uuid",
"watch",
- "which 6.0.3",
"workspace-hack",
]
@@ -196,12 +195,13 @@ dependencies = [
[[package]]
name = "agent-client-protocol"
-version = "0.2.1"
+version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "003fb91bf1b8d6e15f72c45fb9171839af8241e81e3839fbb73536af113b7a79"
+checksum = "cc2526e80463b9742afed4829aedd6ae5632d6db778c6cc1fecb80c960c3521b"
dependencies = [
"anyhow",
"async-broadcast",
+ "async-trait",
"futures 0.3.31",
"log",
"parking_lot",
@@ -294,6 +294,7 @@ dependencies = [
"agent-client-protocol",
"agent_settings",
"anyhow",
+ "async-trait",
"client",
"collections",
"env_logger 0.11.8",
@@ -301,6 +302,7 @@ dependencies = [
"futures 0.3.31",
"gpui",
"gpui_tokio",
+ "http_client",
"indoc",
"language",
"language_model",
@@ -416,7 +418,6 @@ dependencies = [
"serde_json",
"serde_json_lenient",
"settings",
- "shlex",
"smol",
"streaming_diff",
"task",
@@ -690,6 +691,9 @@ name = "arbitrary"
version = "1.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dde20b3d026af13f561bdd0f15edf01fc734f0dafcedbaf42bba506a9517f223"
+dependencies = [
+ "derive_arbitrary",
+]
[[package]]
name = "arc-swap"
@@ -898,7 +902,6 @@ version = "0.1.0"
dependencies = [
"anyhow",
"assistant_slash_command",
- "cargo_toml",
"chrono",
"collections",
"context_server",
@@ -921,7 +924,6 @@ dependencies = [
"settings",
"smol",
"text",
- "toml 0.8.20",
"ui",
"util",
"workspace",
@@ -1025,7 +1027,6 @@ dependencies = [
"util",
"watch",
"web_search",
- "which 6.0.3",
"workspace",
"workspace-hack",
"zlog",
@@ -2688,6 +2689,53 @@ dependencies = [
"serde",
]
+[[package]]
+name = "candle-core"
+version = "0.9.1"
+source = "git+https://github.com/zed-industries/candle?branch=9.1-patched#724d75eb3deebefe83f2a7381a45d4fac6eda383"
+dependencies = [
+ "byteorder",
+ "float8",
+ "gemm 0.17.1",
+ "half",
+ "memmap2",
+ "num-traits",
+ "num_cpus",
+ "rand 0.9.1",
+ "rand_distr",
+ "rayon",
+ "safetensors",
+ "thiserror 1.0.69",
+ "ug",
+ "yoke",
+ "zip 1.1.4",
+]
+
+[[package]]
+name = "candle-nn"
+version = "0.9.1"
+source = "git+https://github.com/zed-industries/candle?branch=9.1-patched#724d75eb3deebefe83f2a7381a45d4fac6eda383"
+dependencies = [
+ "candle-core",
+ "half",
+ "libc",
+ "num-traits",
+ "rayon",
+ "safetensors",
+ "serde",
+ "thiserror 1.0.69",
+]
+
+[[package]]
+name = "candle-onnx"
+version = "0.9.1"
+source = "git+https://github.com/zed-industries/candle?branch=9.1-patched#724d75eb3deebefe83f2a7381a45d4fac6eda383"
+dependencies = [
+ "candle-core",
+ "candle-nn",
+ "prost 0.12.6",
+]
+
[[package]]
name = "cap-fs-ext"
version = "3.4.4"
@@ -2930,7 +2978,7 @@ dependencies = [
"num-traits",
"serde",
"wasm-bindgen",
- "windows-link",
+ "windows-link 0.1.1",
]
[[package]]
@@ -4674,6 +4722,20 @@ version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "da692b8d1080ea3045efaab14434d40468c3d8657e42abddfffca87b428f4c1b"
+[[package]]
+name = "denoise"
+version = "0.1.0"
+dependencies = [
+ "candle-core",
+ "candle-onnx",
+ "log",
+ "realfft",
+ "rodio",
+ "rustfft",
+ "thiserror 2.0.12",
+ "workspace-hack",
+]
+
[[package]]
name = "der"
version = "0.6.1"
@@ -4705,6 +4767,17 @@ dependencies = [
"serde",
]
+[[package]]
+name = "derive_arbitrary"
+version = "1.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.101",
+]
+
[[package]]
name = "derive_more"
version = "0.99.19"
@@ -5018,6 +5091,25 @@ version = "1.0.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1c7a8fb8a9fbf66c1f703fe16184d10ca0ee9d23be5b4436400408ba54a95005"
+[[package]]
+name = "dyn-stack"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "56e53799688f5632f364f8fb387488dd05db9fe45db7011be066fc20e7027f8b"
+dependencies = [
+ "bytemuck",
+ "reborrow",
+]
+
+[[package]]
+name = "dyn-stack"
+version = "0.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "490bd48eb68fffcfed519b4edbfd82c69cbe741d175b84f0e0cbe8c57cbe0bdd"
+dependencies = [
+ "bytemuck",
+]
+
[[package]]
name = "ec4rs"
version = "1.2.0"
@@ -5079,6 +5171,30 @@ dependencies = [
"zeta",
]
+[[package]]
+name = "edit_prediction_context"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "arrayvec",
+ "collections",
+ "futures 0.3.31",
+ "gpui",
+ "indoc",
+ "language",
+ "log",
+ "pretty_assertions",
+ "project",
+ "serde_json",
+ "settings",
+ "slotmap",
+ "text",
+ "tree-sitter",
+ "util",
+ "workspace-hack",
+ "zlog",
+]
+
[[package]]
name = "editor"
version = "0.1.0"
@@ -5262,6 +5378,18 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a3d8a32ae18130a3c84dd492d4215c3d913c3b07c6b63c2eb3eb7ff1101ab7bf"
+[[package]]
+name = "enum-as-inner"
+version = "0.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a1e6a265c649f3f5979b601d26f1d05ada116434c87741c9493cb56218f76cbc"
+dependencies = [
+ "heck 0.5.0",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.101",
+]
+
[[package]]
name = "enumflags2"
version = "0.7.11"
@@ -5892,6 +6020,18 @@ version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ce81f49ae8a0482e4c55ea62ebbd7e5a686af544c00b9d090bba3ff9be97b3d"
+[[package]]
+name = "float8"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4203231de188ebbdfb85c11f3c20ca2b063945710de04e7b59268731e728b462"
+dependencies = [
+ "half",
+ "num-traits",
+ "rand 0.9.1",
+ "rand_distr",
+]
+
[[package]]
name = "float_next_after"
version = "1.0.0"
@@ -6346,6 +6486,243 @@ dependencies = [
"thread_local",
]
+[[package]]
+name = "gemm"
+version = "0.17.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6ab24cc62135b40090e31a76a9b2766a501979f3070fa27f689c27ec04377d32"
+dependencies = [
+ "dyn-stack 0.10.0",
+ "gemm-c32 0.17.1",
+ "gemm-c64 0.17.1",
+ "gemm-common 0.17.1",
+ "gemm-f16 0.17.1",
+ "gemm-f32 0.17.1",
+ "gemm-f64 0.17.1",
+ "num-complex",
+ "num-traits",
+ "paste",
+ "raw-cpuid 10.7.0",
+ "seq-macro",
+]
+
+[[package]]
+name = "gemm"
+version = "0.18.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ab96b703d31950f1aeddded248bc95543c9efc7ac9c4a21fda8703a83ee35451"
+dependencies = [
+ "dyn-stack 0.13.0",
+ "gemm-c32 0.18.2",
+ "gemm-c64 0.18.2",
+ "gemm-common 0.18.2",
+ "gemm-f16 0.18.2",
+ "gemm-f32 0.18.2",
+ "gemm-f64 0.18.2",
+ "num-complex",
+ "num-traits",
+ "paste",
+ "raw-cpuid 11.6.0",
+ "seq-macro",
+]
+
+[[package]]
+name = "gemm-c32"
+version = "0.17.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b9c030d0b983d1e34a546b86e08f600c11696fde16199f971cd46c12e67512c0"
+dependencies = [
+ "dyn-stack 0.10.0",
+ "gemm-common 0.17.1",
+ "num-complex",
+ "num-traits",
+ "paste",
+ "raw-cpuid 10.7.0",
+ "seq-macro",
+]
+
+[[package]]
+name = "gemm-c32"
+version = "0.18.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f6db9fd9f40421d00eea9dd0770045a5603b8d684654816637732463f4073847"
+dependencies = [
+ "dyn-stack 0.13.0",
+ "gemm-common 0.18.2",
+ "num-complex",
+ "num-traits",
+ "paste",
+ "raw-cpuid 11.6.0",
+ "seq-macro",
+]
+
+[[package]]
+name = "gemm-c64"
+version = "0.17.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fbb5f2e79fefb9693d18e1066a557b4546cd334b226beadc68b11a8f9431852a"
+dependencies = [
+ "dyn-stack 0.10.0",
+ "gemm-common 0.17.1",
+ "num-complex",
+ "num-traits",
+ "paste",
+ "raw-cpuid 10.7.0",
+ "seq-macro",
+]
+
+[[package]]
+name = "gemm-c64"
+version = "0.18.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dfcad8a3d35a43758330b635d02edad980c1e143dc2f21e6fd25f9e4eada8edf"
+dependencies = [
+ "dyn-stack 0.13.0",
+ "gemm-common 0.18.2",
+ "num-complex",
+ "num-traits",
+ "paste",
+ "raw-cpuid 11.6.0",
+ "seq-macro",
+]
+
+[[package]]
+name = "gemm-common"
+version = "0.17.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a2e7ea062c987abcd8db95db917b4ffb4ecdfd0668471d8dc54734fdff2354e8"
+dependencies = [
+ "bytemuck",
+ "dyn-stack 0.10.0",
+ "half",
+ "num-complex",
+ "num-traits",
+ "once_cell",
+ "paste",
+ "pulp 0.18.22",
+ "raw-cpuid 10.7.0",
+ "rayon",
+ "seq-macro",
+ "sysctl 0.5.5",
+]
+
+[[package]]
+name = "gemm-common"
+version = "0.18.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a352d4a69cbe938b9e2a9cb7a3a63b7e72f9349174a2752a558a8a563510d0f3"
+dependencies = [
+ "bytemuck",
+ "dyn-stack 0.13.0",
+ "half",
+ "libm",
+ "num-complex",
+ "num-traits",
+ "once_cell",
+ "paste",
+ "pulp 0.21.5",
+ "raw-cpuid 11.6.0",
+ "rayon",
+ "seq-macro",
+ "sysctl 0.6.0",
+]
+
+[[package]]
+name = "gemm-f16"
+version = "0.17.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7ca4c06b9b11952071d317604acb332e924e817bd891bec8dfb494168c7cedd4"
+dependencies = [
+ "dyn-stack 0.10.0",
+ "gemm-common 0.17.1",
+ "gemm-f32 0.17.1",
+ "half",
+ "num-complex",
+ "num-traits",
+ "paste",
+ "raw-cpuid 10.7.0",
+ "rayon",
+ "seq-macro",
+]
+
+[[package]]
+name = "gemm-f16"
+version = "0.18.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cff95ae3259432f3c3410eaa919033cd03791d81cebd18018393dc147952e109"
+dependencies = [
+ "dyn-stack 0.13.0",
+ "gemm-common 0.18.2",
+ "gemm-f32 0.18.2",
+ "half",
+ "num-complex",
+ "num-traits",
+ "paste",
+ "raw-cpuid 11.6.0",
+ "rayon",
+ "seq-macro",
+]
+
+[[package]]
+name = "gemm-f32"
+version = "0.17.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e9a69f51aaefbd9cf12d18faf273d3e982d9d711f60775645ed5c8047b4ae113"
+dependencies = [
+ "dyn-stack 0.10.0",
+ "gemm-common 0.17.1",
+ "num-complex",
+ "num-traits",
+ "paste",
+ "raw-cpuid 10.7.0",
+ "seq-macro",
+]
+
+[[package]]
+name = "gemm-f32"
+version = "0.18.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bc8d3d4385393304f407392f754cd2dc4b315d05063f62cf09f47b58de276864"
+dependencies = [
+ "dyn-stack 0.13.0",
+ "gemm-common 0.18.2",
+ "num-complex",
+ "num-traits",
+ "paste",
+ "raw-cpuid 11.6.0",
+ "seq-macro",
+]
+
+[[package]]
+name = "gemm-f64"
+version = "0.17.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "aa397a48544fadf0b81ec8741e5c0fba0043008113f71f2034def1935645d2b0"
+dependencies = [
+ "dyn-stack 0.10.0",
+ "gemm-common 0.17.1",
+ "num-complex",
+ "num-traits",
+ "paste",
+ "raw-cpuid 10.7.0",
+ "seq-macro",
+]
+
+[[package]]
+name = "gemm-f64"
+version = "0.18.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "35b2a4f76ce4b8b16eadc11ccf2e083252d8237c1b589558a49b0183545015bd"
+dependencies = [
+ "dyn-stack 0.13.0",
+ "gemm-common 0.18.2",
+ "num-complex",
+ "num-traits",
+ "paste",
+ "raw-cpuid 11.6.0",
+ "seq-macro",
+]
+
[[package]]
name = "generator"
version = "0.8.5"
@@ -7621,9 +7998,12 @@ version = "2.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "459196ed295495a68f7d7fe1d84f6c4b7ff0e21fe3017b2f283c6fac3ad803c9"
dependencies = [
+ "bytemuck",
"cfg-if",
"crunchy",
"num-traits",
+ "rand 0.9.1",
+ "rand_distr",
]
[[package]]
@@ -9236,6 +9616,7 @@ dependencies = [
"credentials_provider",
"deepseek",
"editor",
+ "fs",
"futures 0.3.31",
"google_ai",
"gpui",
@@ -9269,6 +9650,7 @@ dependencies = [
"vercel",
"workspace-hack",
"x_ai",
+ "zed_env_vars",
]
[[package]]
@@ -9362,6 +9744,7 @@ dependencies = [
"pet-fs",
"pet-poetry",
"pet-reporter",
+ "pet-virtualenv",
"pretty_assertions",
"project",
"regex",
@@ -10231,6 +10614,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fd3f7eed9d3848f8b98834af67102b720745c4ec028fcd0aa0239277e7de374f"
dependencies = [
"libc",
+ "stable_deref_trait",
]
[[package]]
@@ -10497,12 +10881,6 @@ version = "0.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e5ce46fe64a9d73be07dcbe690a38ce1b293be448fd8ce1e6c1b8062c9f72c6a"
-[[package]]
-name = "multimap"
-version = "0.10.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "defc4c55412d89136f966bbb339008b474350e5e6e78d2714439c386b3137a03"
-
[[package]]
name = "naga"
version = "25.0.1"
@@ -10876,6 +11254,7 @@ version = "0.4.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495"
dependencies = [
+ "bytemuck",
"num-traits",
]
@@ -12572,6 +12951,15 @@ dependencies = [
"syn 2.0.101",
]
+[[package]]
+name = "primal-check"
+version = "0.3.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dc0d895b311e3af9902528fbb8f928688abbd95872819320517cc24ca6b2bd08"
+dependencies = [
+ "num-integer",
+]
+
[[package]]
name = "proc-macro-crate"
version = "3.3.0"
@@ -12880,7 +13268,7 @@ dependencies = [
"itertools 0.10.5",
"lazy_static",
"log",
- "multimap 0.8.3",
+ "multimap",
"petgraph",
"prost 0.9.0",
"prost-types 0.9.0",
@@ -12899,7 +13287,7 @@ dependencies = [
"heck 0.5.0",
"itertools 0.12.1",
"log",
- "multimap 0.10.0",
+ "multimap",
"once_cell",
"petgraph",
"prettyplease",
@@ -13071,6 +13459,32 @@ dependencies = [
"wasmtime-math",
]
+[[package]]
+name = "pulp"
+version = "0.18.22"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a0a01a0dc67cf4558d279f0c25b0962bd08fc6dec0137699eae304103e882fe6"
+dependencies = [
+ "bytemuck",
+ "libm",
+ "num-complex",
+ "reborrow",
+]
+
+[[package]]
+name = "pulp"
+version = "0.21.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "96b86df24f0a7ddd5e4b95c94fc9ed8a98f1ca94d3b01bdce2824097e7835907"
+dependencies = [
+ "bytemuck",
+ "cfg-if",
+ "libm",
+ "num-complex",
+ "reborrow",
+ "version_check",
+]
+
[[package]]
name = "qoi"
version = "0.4.1"
@@ -13247,6 +13661,16 @@ dependencies = [
"getrandom 0.3.2",
]
+[[package]]
+name = "rand_distr"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6a8615d50dcf34fa31f7ab52692afec947c4dd0ab803cc87cb3b0b4570ff7463"
+dependencies = [
+ "num-traits",
+ "rand 0.9.1",
+]
+
[[package]]
name = "range-map"
version = "0.2.0"
@@ -13312,6 +13736,24 @@ dependencies = [
"rgb",
]
+[[package]]
+name = "raw-cpuid"
+version = "10.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6c297679cb867470fa8c9f67dbba74a78d78e3e98d7cf2b08d6d71540f797332"
+dependencies = [
+ "bitflags 1.3.2",
+]
+
+[[package]]
+name = "raw-cpuid"
+version = "11.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "498cd0dc59d73224351ee52a95fee0f1a617a2eae0e7d9d720cc622c73a54186"
+dependencies = [
+ "bitflags 2.9.0",
+]
+
[[package]]
name = "raw-window-handle"
version = "0.6.2"
@@ -13360,6 +13802,21 @@ dependencies = [
"font-types",
]
+[[package]]
+name = "realfft"
+version = "3.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f821338fddb99d089116342c46e9f1fbf3828dba077674613e734e01d6ea8677"
+dependencies = [
+ "rustfft",
+]
+
+[[package]]
+name = "reborrow"
+version = "0.5.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "03251193000f4bd3b042892be858ee50e8b3719f2b08e5833ac4353724632430"
+
[[package]]
name = "recent_projects"
version = "0.1.0"
@@ -13373,6 +13830,7 @@ dependencies = [
"futures 0.3.31",
"fuzzy",
"gpui",
+ "indoc",
"language",
"log",
"markdown",
@@ -13393,6 +13851,7 @@ dependencies = [
"theme",
"ui",
"util",
+ "windows-registry 0.6.0",
"workspace",
"workspace-hack",
"zed_actions",
@@ -14177,6 +14636,20 @@ dependencies = [
"semver",
]
+[[package]]
+name = "rustfft"
+version = "6.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c6f140db74548f7c9d7cce60912c9ac414e74df5e718dc947d514b051b42f3f4"
+dependencies = [
+ "num-complex",
+ "num-integer",
+ "num-traits",
+ "primal-check",
+ "strength_reduce",
+ "transpose",
+]
+
[[package]]
name = "rustix"
version = "0.38.44"
@@ -14401,6 +14874,16 @@ version = "1.0.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
+[[package]]
+name = "safetensors"
+version = "0.4.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "44560c11236a6130a46ce36c836a62936dc81ebf8c36a37947423571be0e55b6"
+dependencies = [
+ "serde",
+ "serde_json",
+]
+
[[package]]
name = "salsa20"
version = "0.10.2"
@@ -14794,6 +15277,12 @@ dependencies = [
"serde",
]
+[[package]]
+name = "seq-macro"
+version = "0.3.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1bc711410fbe7399f390ca1c3b60ad0f53f80e95c5eb935e52268a0e2cd49acc"
+
[[package]]
name = "serde"
version = "1.0.221"
@@ -15760,6 +16249,12 @@ dependencies = [
"workspace-hack",
]
+[[package]]
+name = "strength_reduce"
+version = "0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fe895eb47f22e2ddd4dabc02bce419d2e643c8e3b585c78158b349195bc24d82"
+
[[package]]
name = "strict-num"
version = "0.1.1"
@@ -16250,6 +16745,34 @@ dependencies = [
"libc",
]
+[[package]]
+name = "sysctl"
+version = "0.5.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ec7dddc5f0fee506baf8b9fdb989e242f17e4b11c61dfbb0635b705217199eea"
+dependencies = [
+ "bitflags 2.9.0",
+ "byteorder",
+ "enum-as-inner",
+ "libc",
+ "thiserror 1.0.69",
+ "walkdir",
+]
+
+[[package]]
+name = "sysctl"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "01198a2debb237c62b6826ec7081082d951f46dbb64b0e8c7649a452230d1dfc"
+dependencies = [
+ "bitflags 2.9.0",
+ "byteorder",
+ "enum-as-inner",
+ "libc",
+ "thiserror 1.0.69",
+ "walkdir",
+]
+
[[package]]
name = "sysinfo"
version = "0.31.4"
@@ -17345,6 +17868,16 @@ dependencies = [
"syn 2.0.101",
]
+[[package]]
+name = "transpose"
+version = "0.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1ad61aed86bc3faea4300c7aee358b4c6d0c8d6ccc36524c96e4c92ccf26e77e"
+dependencies = [
+ "num-integer",
+ "strength_reduce",
+]
+
[[package]]
name = "tree-sitter"
version = "0.25.6"
@@ -17706,6 +18239,27 @@ dependencies = [
"winapi",
]
+[[package]]
+name = "ug"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "90b70b37e9074642bc5f60bb23247fd072a84314ca9e71cdf8527593406a0dd3"
+dependencies = [
+ "gemm 0.18.2",
+ "half",
+ "libloading",
+ "memmap2",
+ "num",
+ "num-traits",
+ "num_cpus",
+ "rayon",
+ "safetensors",
+ "serde",
+ "thiserror 1.0.69",
+ "tracing",
+ "yoke",
+]
+
[[package]]
name = "ui"
version = "0.1.0"
@@ -18972,7 +19526,7 @@ dependencies = [
"reqwest 0.11.27",
"scratch",
"semver",
- "zip",
+ "zip 0.6.6",
]
[[package]]
@@ -19145,7 +19699,7 @@ dependencies = [
"windows-collections",
"windows-core 0.61.0",
"windows-future",
- "windows-link",
+ "windows-link 0.1.1",
"windows-numerics",
]
@@ -19215,7 +19769,7 @@ checksum = "4763c1de310c86d75a878046489e2e5ba02c649d185f21c67d4cf8a56d098980"
dependencies = [
"windows-implement 0.60.0",
"windows-interface 0.59.1",
- "windows-link",
+ "windows-link 0.1.1",
"windows-result 0.3.2",
"windows-strings 0.4.0",
]
@@ -19227,7 +19781,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a1d6bbefcb7b60acd19828e1bc965da6fcf18a7e39490c5f8be71e54a19ba32"
dependencies = [
"windows-core 0.61.0",
- "windows-link",
+ "windows-link 0.1.1",
]
[[package]]
@@ -19302,6 +19856,12 @@ version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38"
+[[package]]
+name = "windows-link"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "45e46c0661abb7180e7b9c281db115305d49ca1709ab8242adf09666d2173c65"
+
[[package]]
name = "windows-numerics"
version = "0.2.0"
@@ -19309,7 +19869,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1"
dependencies = [
"windows-core 0.61.0",
- "windows-link",
+ "windows-link 0.1.1",
]
[[package]]
@@ -19329,11 +19889,22 @@ version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ad1da3e436dc7653dfdf3da67332e22bff09bb0e28b0239e1624499c7830842e"
dependencies = [
- "windows-link",
+ "windows-link 0.1.1",
"windows-result 0.3.2",
"windows-strings 0.4.0",
]
+[[package]]
+name = "windows-registry"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3f91f87ce112ffb7275000ea98eb1940912c21c1567c9312fde20261f3eadd29"
+dependencies = [
+ "windows-link 0.2.0",
+ "windows-result 0.4.0",
+ "windows-strings 0.5.0",
+]
+
[[package]]
name = "windows-result"
version = "0.1.2"
@@ -19358,7 +19929,16 @@ version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c64fd11a4fd95df68efcfee5f44a294fe71b8bc6a91993e2791938abcc712252"
dependencies = [
- "windows-link",
+ "windows-link 0.1.1",
+]
+
+[[package]]
+name = "windows-result"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7084dcc306f89883455a206237404d3eaf961e5bd7e0f312f7c91f57eb44167f"
+dependencies = [
+ "windows-link 0.2.0",
]
[[package]]
@@ -19377,7 +19957,7 @@ version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "87fa48cc5d406560701792be122a10132491cff9d0aeb23583cc2dcafc847319"
dependencies = [
- "windows-link",
+ "windows-link 0.1.1",
]
[[package]]
@@ -19386,7 +19966,16 @@ version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a2ba9642430ee452d5a7aa78d72907ebe8cfda358e8cb7918a2050581322f97"
dependencies = [
- "windows-link",
+ "windows-link 0.1.1",
+]
+
+[[package]]
+name = "windows-strings"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7218c655a553b0bed4426cf54b20d7ba363ef543b52d515b3e48d7fd55318dda"
+dependencies = [
+ "windows-link 0.2.0",
]
[[package]]
@@ -20131,6 +20720,7 @@ dependencies = [
"lyon_path",
"md-5",
"memchr",
+ "memmap2",
"mime_guess",
"miniz_oxide",
"mio 1.0.3",
@@ -20139,8 +20729,10 @@ dependencies = [
"nix 0.29.0",
"nix 0.30.1",
"nom 7.1.3",
+ "num",
"num-bigint",
"num-bigint-dig",
+ "num-complex",
"num-integer",
"num-iter",
"num-rational",
@@ -20156,6 +20748,7 @@ dependencies = [
"phf_shared",
"prettyplease",
"proc-macro2",
+ "prost 0.12.6",
"prost 0.9.0",
"prost-types 0.9.0",
"quote",
@@ -20163,6 +20756,7 @@ dependencies = [
"rand 0.9.1",
"rand_chacha 0.3.1",
"rand_core 0.6.4",
+ "rand_distr",
"regalloc2",
"regex",
"regex-automata",
@@ -20192,6 +20786,7 @@ dependencies = [
"sqlx-macros-core",
"sqlx-postgres",
"sqlx-sqlite",
+ "stable_deref_trait",
"strum 0.26.3",
"subtle",
"syn 1.0.109",
@@ -20225,6 +20820,7 @@ dependencies = [
"windows-sys 0.48.0",
"windows-sys 0.52.0",
"windows-sys 0.59.0",
+ "windows-sys 0.60.2",
"winnow",
"zeroize",
"zvariant",
@@ -20594,7 +21190,7 @@ dependencies = [
[[package]]
name = "zed"
-version = "0.205.0"
+version = "0.206.0"
dependencies = [
"acp_tools",
"activity_indicator",
@@ -20760,6 +21356,7 @@ dependencies = [
name = "zed_env_vars"
version = "0.1.0"
dependencies = [
+ "gpui",
"workspace-hack",
]
@@ -21073,6 +21670,21 @@ dependencies = [
"zstd",
]
+[[package]]
+name = "zip"
+version = "1.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9cc23c04387f4da0374be4533ad1208cbb091d5c11d070dfef13676ad6497164"
+dependencies = [
+ "arbitrary",
+ "crc32fast",
+ "crossbeam-utils",
+ "displaydoc",
+ "indexmap 2.9.0",
+ "num_enum",
+ "thiserror 1.0.69",
+]
+
[[package]]
name = "zlib-rs"
version = "0.5.0"
diff --git a/Cargo.toml b/Cargo.toml
index 846b0e32ee61662efa2026c116b8beee87495bcf..08a9b41315c36d7facd7b9d1751b949a2577395c 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -52,10 +52,12 @@ members = [
"crates/debugger_tools",
"crates/debugger_ui",
"crates/deepseek",
+ "crates/denoise",
"crates/diagnostics",
"crates/docs_preprocessor",
"crates/edit_prediction",
"crates/edit_prediction_button",
+ "crates/edit_prediction_context",
"crates/editor",
"crates/eval",
"crates/explorer_command_injector",
@@ -311,6 +313,7 @@ icons = { path = "crates/icons" }
image_viewer = { path = "crates/image_viewer" }
edit_prediction = { path = "crates/edit_prediction" }
edit_prediction_button = { path = "crates/edit_prediction_button" }
+edit_prediction_context = { path = "crates/edit_prediction_context" }
inspector_ui = { path = "crates/inspector_ui" }
install_cli = { path = "crates/install_cli" }
jj = { path = "crates/jj" }
@@ -433,7 +436,7 @@ zlog_settings = { path = "crates/zlog_settings" }
# External crates
#
-agent-client-protocol = { version = "0.2.1", features = ["unstable"] }
+agent-client-protocol = { version = "0.4.0", features = ["unstable"] }
aho-corasick = "1.1"
alacritty_terminal = { git = "https://github.com/zed-industries/alacritty.git", branch = "add-hush-login-flag" }
any_vec = "0.14"
@@ -581,6 +584,7 @@ pet-fs = { git = "https://github.com/microsoft/python-environment-tools.git", re
pet-pixi = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "845945b830297a50de0e24020b980a65e4820559" }
pet-poetry = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "845945b830297a50de0e24020b980a65e4820559" }
pet-reporter = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "845945b830297a50de0e24020b980a65e4820559" }
+pet-virtualenv = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "845945b830297a50de0e24020b980a65e4820559" }
portable-pty = "0.9.0"
postage = { version = "0.5", features = ["futures-traits"] }
pretty_assertions = { version = "1.3.0", features = ["unstable"] }
@@ -630,6 +634,7 @@ sha2 = "0.10"
shellexpand = "2.1.0"
shlex = "1.3.0"
simplelog = "0.12.2"
+slotmap = "1.0.6"
smallvec = { version = "1.6", features = ["union"] }
smol = "2.0"
sqlformat = "0.2"
diff --git a/assets/icons/linux.svg b/assets/icons/linux.svg
new file mode 100644
index 0000000000000000000000000000000000000000..fc76742a3f236650cb8c514c8263ec2c3b2d4521
--- /dev/null
+++ b/assets/icons/linux.svg
@@ -0,0 +1,11 @@
+
diff --git a/assets/keymaps/default-linux.json b/assets/keymaps/default-linux.json
index 6b4c4e0fac95cf751c21cfaa0770d1279a35adcc..8ca0a5d42094db8b4b37c7e6919da0f7a6bd41db 100644
--- a/assets/keymaps/default-linux.json
+++ b/assets/keymaps/default-linux.json
@@ -462,8 +462,8 @@
"ctrl-k ctrl-w": "workspace::CloseAllItemsAndPanes",
"back": "pane::GoBack",
"ctrl-alt--": "pane::GoBack",
- "ctrl-alt-_": "pane::GoForward",
"forward": "pane::GoForward",
+ "ctrl-alt-_": "pane::GoForward",
"ctrl-alt-g": "search::SelectNextMatch",
"f3": "search::SelectNextMatch",
"ctrl-alt-shift-g": "search::SelectPreviousMatch",
diff --git a/assets/keymaps/default-windows.json b/assets/keymaps/default-windows.json
index e5839964ad545f3994d675da817a5f4571b88db4..78d5e4e698daefee5a57b04d6a8548fb948233b1 100644
--- a/assets/keymaps/default-windows.json
+++ b/assets/keymaps/default-windows.json
@@ -497,6 +497,8 @@
"shift-alt-down": "editor::DuplicateLineDown",
"shift-alt-right": "editor::SelectLargerSyntaxNode", // Expand selection
"shift-alt-left": "editor::SelectSmallerSyntaxNode", // Shrink selection
+ "ctrl-shift-right": "editor::SelectLargerSyntaxNode", // Expand selection (VSCode version)
+ "ctrl-shift-left": "editor::SelectSmallerSyntaxNode", // Shrink selection (VSCode version)
"ctrl-shift-l": "editor::SelectAllMatches", // Select all occurrences of current selection
"ctrl-f2": "editor::SelectAllMatches", // Select all occurrences of current word
"ctrl-d": ["editor::SelectNext", { "replace_newest": false }], // editor.action.addSelectionToNextFindMatch / find_under_expand
diff --git a/assets/settings/default.json b/assets/settings/default.json
index 78fdc3d38d2e33febcb186e1edf68c3a40b01d66..e3e81b83b0ac809e7bc557796ee0879ee9d33cf8 100644
--- a/assets/settings/default.json
+++ b/assets/settings/default.json
@@ -311,7 +311,7 @@
// bracket, brace, single or double quote characters.
// For example, when you select text and type (, Zed will surround the text with ().
"use_auto_surround": true,
- /// Whether indentation should be adjusted based on the context whilst typing.
+ // Whether indentation should be adjusted based on the context whilst typing.
"auto_indent": true,
// Whether indentation of pasted content should be adjusted based on the context.
"auto_indent_on_paste": true,
@@ -409,18 +409,18 @@
"show_menus": false
},
"audio": {
- /// Opt into the new audio system.
+ // Opt into the new audio system.
"experimental.rodio_audio": false,
- /// Requires 'rodio_audio: true'
- ///
- /// Use the new audio systems automatic gain control for your microphone.
- /// This affects how loud you sound to others.
+ // Requires 'rodio_audio: true'
+ //
+ // Use the new audio systems automatic gain control for your microphone.
+ // This affects how loud you sound to others.
"experimental.control_input_volume": false,
- /// Requires 'rodio_audio: true'
- ///
- /// Use the new audio systems automatic gain control on everyone in the
- /// call. This makes call members who are too quite louder and those who are
- /// too loud quieter. This only affects how things sound for you.
+ // Requires 'rodio_audio: true'
+ //
+ // Use the new audio systems automatic gain control on everyone in the
+ // call. This makes call members who are too quite louder and those who are
+ // too loud quieter. This only affects how things sound for you.
"experimental.control_output_volume": false
},
// Scrollbar related settings
@@ -812,7 +812,7 @@
"agent": {
// Whether the agent is enabled.
"enabled": true,
- /// What completion mode to start new threads in, if available. Can be 'normal' or 'burn'.
+ // What completion mode to start new threads in, if available. Can be 'normal' or 'burn'.
"preferred_completion_mode": "normal",
// Whether to show the agent panel button in the status bar.
"button": true,
@@ -925,18 +925,22 @@
// Default: false
"play_sound_when_agent_done": false,
- /// Whether to have edit cards in the agent panel expanded, showing a preview of the full diff.
- ///
- /// Default: true
+ // Whether to have edit cards in the agent panel expanded, showing a preview of the full diff.
+ //
+ // Default: true
"expand_edit_card": true,
- /// Whether to have terminal cards in the agent panel expanded, showing the whole command output.
- ///
- /// Default: true
+ // Whether to have terminal cards in the agent panel expanded, showing the whole command output.
+ //
+ // Default: true
"expand_terminal_card": true,
- /// Whether to always use cmd-enter (or ctrl-enter on Linux or Windows) to send messages in the agent panel.
- ///
- /// Default: false
- "use_modifier_to_send": false
+ // Whether to always use cmd-enter (or ctrl-enter on Linux or Windows) to send messages in the agent panel.
+ //
+ // Default: false
+ "use_modifier_to_send": false,
+ // Minimum number of lines to display in the agent message editor.
+ //
+ // Default: 4
+ "message_editor_min_lines": 4
},
// Whether the screen sharing icon is shown in the os status bar.
"show_call_status_icon": true,
@@ -1821,12 +1825,12 @@
"zed.dev": {}
},
"session": {
- /// Whether or not to restore unsaved buffers on restart.
- ///
- /// If this is true, user won't be prompted whether to save/discard
- /// dirty files when closing the application.
- ///
- /// Default: true
+ // Whether or not to restore unsaved buffers on restart.
+ //
+ // If this is true, user won't be prompted whether to save/discard
+ // dirty files when closing the application.
+ //
+ // Default: true
"restore_unsaved_buffers": true
},
// Zed's Prettier integration settings.
@@ -2012,9 +2016,9 @@
// }
"profiles": [],
- /// A map of log scopes to the desired log level.
- /// Useful for filtering out noisy logs or enabling more verbose logging.
- ///
- /// Example: {"log": {"client": "warn"}}
+ // A map of log scopes to the desired log level.
+ // Useful for filtering out noisy logs or enabling more verbose logging.
+ //
+ // Example: {"log": {"client": "warn"}}
"log": {}
}
diff --git a/crates/acp_thread/Cargo.toml b/crates/acp_thread/Cargo.toml
index a0bbda848f9ec761aebdf66b644a8b2926685122..ac24a6ed0f41c75d5c4dcd9b9b4122336022ddf3 100644
--- a/crates/acp_thread/Cargo.toml
+++ b/crates/acp_thread/Cargo.toml
@@ -45,7 +45,6 @@ url.workspace = true
util.workspace = true
uuid.workspace = true
watch.workspace = true
-which.workspace = true
workspace-hack.workspace = true
[dev-dependencies]
diff --git a/crates/acp_thread/src/acp_thread.rs b/crates/acp_thread/src/acp_thread.rs
index afbb4781f61d5ccf1ea753df1fd0379e533e8e46..68e5266f06aa8bddfaa252bdc1cf5b21891c7f10 100644
--- a/crates/acp_thread/src/acp_thread.rs
+++ b/crates/acp_thread/src/acp_thread.rs
@@ -7,12 +7,12 @@ use agent_settings::AgentSettings;
use collections::HashSet;
pub use connection::*;
pub use diff::*;
-use futures::future::Shared;
use language::language_settings::FormatOnSave;
pub use mention::*;
use project::lsp_store::{FormatTrigger, LspFormatTarget};
use serde::{Deserialize, Serialize};
use settings::Settings as _;
+use task::{Shell, ShellBuilder};
pub use terminal::*;
use action_log::ActionLog;
@@ -34,7 +34,7 @@ use std::rc::Rc;
use std::time::{Duration, Instant};
use std::{fmt::Display, mem, path::PathBuf, sync::Arc};
use ui::App;
-use util::{ResultExt, get_system_shell};
+use util::{ResultExt, get_default_system_shell};
use uuid::Uuid;
#[derive(Debug)]
@@ -786,7 +786,6 @@ pub struct AcpThread {
token_usage: Option,
prompt_capabilities: acp::PromptCapabilities,
_observe_prompt_capabilities: Task>,
- determine_shell: Shared>,
terminals: HashMap>,
}
@@ -873,20 +872,6 @@ impl AcpThread {
}
});
- let determine_shell = cx
- .background_spawn(async move {
- if cfg!(windows) {
- return get_system_shell();
- }
-
- if which::which("bash").is_ok() {
- "bash".into()
- } else {
- get_system_shell()
- }
- })
- .shared();
-
Self {
action_log,
shared_buffers: Default::default(),
@@ -901,7 +886,6 @@ impl AcpThread {
prompt_capabilities,
_observe_prompt_capabilities: task,
terminals: HashMap::default(),
- determine_shell,
}
}
@@ -1127,9 +1111,33 @@ impl AcpThread {
let update = update.into();
let languages = self.project.read(cx).languages().clone();
- let ix = self
- .index_for_tool_call(update.id())
- .context("Tool call not found")?;
+ let ix = match self.index_for_tool_call(update.id()) {
+ Some(ix) => ix,
+ None => {
+ // Tool call not found - create a failed tool call entry
+ let failed_tool_call = ToolCall {
+ id: update.id().clone(),
+ label: cx.new(|cx| Markdown::new("Tool call not found".into(), None, None, cx)),
+ kind: acp::ToolKind::Fetch,
+ content: vec![ToolCallContent::ContentBlock(ContentBlock::new(
+ acp::ContentBlock::Text(acp::TextContent {
+ text: "Tool call not found".to_string(),
+ annotations: None,
+ meta: None,
+ }),
+ &languages,
+ cx,
+ ))],
+ status: ToolCallStatus::Failed,
+ locations: Vec::new(),
+ resolved_locations: Vec::new(),
+ raw_input: None,
+ raw_output: None,
+ };
+ self.push_entry(AgentThreadEntry::ToolCall(failed_tool_call), cx);
+ return Ok(());
+ }
+ };
let AgentThreadEntry::ToolCall(call) = &mut self.entries[ix] else {
unreachable!()
};
@@ -1940,28 +1948,13 @@ impl AcpThread {
pub fn create_terminal(
&self,
- mut command: String,
+ command: String,
args: Vec,
extra_env: Vec,
cwd: Option,
output_byte_limit: Option,
cx: &mut Context,
) -> Task>> {
- for arg in args {
- command.push(' ');
- command.push_str(&arg);
- }
-
- let shell_command = if cfg!(windows) {
- format!("$null | & {{{}}}", command.replace("\"", "'"))
- } else if let Some(cwd) = cwd.as_ref().and_then(|cwd| cwd.as_os_str().to_str()) {
- // Make sure once we're *inside* the shell, we cd into `cwd`
- format!("(cd {cwd}; {}) self.project.update(cx, |project, cx| {
project.directory_environment(dir.as_path().into(), cx)
@@ -1982,20 +1975,30 @@ impl AcpThread {
let project = self.project.clone();
let language_registry = project.read(cx).languages().clone();
- let determine_shell = self.determine_shell.clone();
let terminal_id = acp::TerminalId(Uuid::new_v4().to_string().into());
let terminal_task = cx.spawn({
let terminal_id = terminal_id.clone();
async move |_this, cx| {
- let program = determine_shell.await;
let env = env.await;
+ let (command, args) = ShellBuilder::new(
+ project
+ .update(cx, |project, cx| {
+ project
+ .remote_client()
+ .and_then(|r| r.read(cx).default_system_shell())
+ })?
+ .as_deref(),
+ &Shell::Program(get_default_system_shell()),
+ )
+ .redirect_stdin_to_dev_null()
+ .build(Some(command), &args);
let terminal = project
.update(cx, |project, cx| {
project.create_terminal_task(
task::SpawnInTerminal {
- command: Some(program),
- args,
+ command: Some(command.clone()),
+ args: args.clone(),
cwd: cwd.clone(),
env,
..Default::default()
@@ -2008,7 +2011,7 @@ impl AcpThread {
cx.new(|cx| {
Terminal::new(
terminal_id,
- command,
+ &format!("{} {}", command, args.join(" ")),
cwd,
output_byte_limit.map(|l| l as usize),
terminal,
@@ -3181,4 +3184,65 @@ mod tests {
Task::ready(Ok(()))
}
}
+
+ #[gpui::test]
+ async fn test_tool_call_not_found_creates_failed_entry(cx: &mut TestAppContext) {
+ init_test(cx);
+
+ let fs = FakeFs::new(cx.executor());
+ let project = Project::test(fs, [], cx).await;
+ let connection = Rc::new(FakeAgentConnection::new());
+ let thread = cx
+ .update(|cx| connection.new_thread(project, Path::new(path!("/test")), cx))
+ .await
+ .unwrap();
+
+ // Try to update a tool call that doesn't exist
+ let nonexistent_id = acp::ToolCallId("nonexistent-tool-call".into());
+ thread.update(cx, |thread, cx| {
+ let result = thread.handle_session_update(
+ acp::SessionUpdate::ToolCallUpdate(acp::ToolCallUpdate {
+ id: nonexistent_id.clone(),
+ fields: acp::ToolCallUpdateFields {
+ status: Some(acp::ToolCallStatus::Completed),
+ ..Default::default()
+ },
+ meta: None,
+ }),
+ cx,
+ );
+
+ // The update should succeed (not return an error)
+ assert!(result.is_ok());
+
+ // There should now be exactly one entry in the thread
+ assert_eq!(thread.entries.len(), 1);
+
+ // The entry should be a failed tool call
+ if let AgentThreadEntry::ToolCall(tool_call) = &thread.entries[0] {
+ assert_eq!(tool_call.id, nonexistent_id);
+ assert!(matches!(tool_call.status, ToolCallStatus::Failed));
+ assert_eq!(tool_call.kind, acp::ToolKind::Fetch);
+
+ // Check that the content contains the error message
+ assert_eq!(tool_call.content.len(), 1);
+ if let ToolCallContent::ContentBlock(content_block) = &tool_call.content[0] {
+ match content_block {
+ ContentBlock::Markdown { markdown } => {
+ let markdown_text = markdown.read(cx).source();
+ assert!(markdown_text.contains("Tool call not found"));
+ }
+ ContentBlock::Empty => panic!("Expected markdown content, got empty"),
+ ContentBlock::ResourceLink { .. } => {
+ panic!("Expected markdown content, got resource link")
+ }
+ }
+ } else {
+ panic!("Expected ContentBlock, got: {:?}", tool_call.content[0]);
+ }
+ } else {
+ panic!("Expected ToolCall entry, got: {:?}", thread.entries[0]);
+ }
+ });
+ }
}
diff --git a/crates/acp_thread/src/terminal.rs b/crates/acp_thread/src/terminal.rs
index a927083b0bd576f1580ba261d4028407fcea7a5c..888c7698c3d2270769f3afbe712ecba7d08b055f 100644
--- a/crates/acp_thread/src/terminal.rs
+++ b/crates/acp_thread/src/terminal.rs
@@ -28,7 +28,7 @@ pub struct TerminalOutput {
impl Terminal {
pub fn new(
id: acp::TerminalId,
- command: String,
+ command_label: &str,
working_dir: Option,
output_byte_limit: Option,
terminal: Entity,
@@ -40,7 +40,7 @@ impl Terminal {
id,
command: cx.new(|cx| {
Markdown::new(
- format!("```\n{}\n```", command).into(),
+ format!("```\n{}\n```", command_label).into(),
Some(language_registry.clone()),
None,
cx,
diff --git a/crates/activity_indicator/src/activity_indicator.rs b/crates/activity_indicator/src/activity_indicator.rs
index 1870ab74db214b518bb0b543166067e636f14965..f35b2ad17879c57b15ac8579e6b50a26110ff21d 100644
--- a/crates/activity_indicator/src/activity_indicator.rs
+++ b/crates/activity_indicator/src/activity_indicator.rs
@@ -1,4 +1,4 @@
-use auto_update::{AutoUpdateStatus, AutoUpdater, DismissErrorMessage, VersionCheckType};
+use auto_update::{AutoUpdateStatus, AutoUpdater, DismissMessage, VersionCheckType};
use editor::Editor;
use extension_host::{ExtensionOperation, ExtensionStore};
use futures::StreamExt;
@@ -280,18 +280,13 @@ impl ActivityIndicator {
});
}
- fn dismiss_error_message(
- &mut self,
- _: &DismissErrorMessage,
- _: &mut Window,
- cx: &mut Context,
- ) {
- let error_dismissed = if let Some(updater) = &self.auto_updater {
- updater.update(cx, |updater, cx| updater.dismiss_error(cx))
+ fn dismiss_message(&mut self, _: &DismissMessage, _: &mut Window, cx: &mut Context) {
+ let dismissed = if let Some(updater) = &self.auto_updater {
+ updater.update(cx, |updater, cx| updater.dismiss(cx))
} else {
false
};
- if error_dismissed {
+ if dismissed {
return;
}
@@ -513,7 +508,7 @@ impl ActivityIndicator {
on_click: Some(Arc::new(move |this, window, cx| {
this.statuses
.retain(|status| !downloading.contains(&status.name));
- this.dismiss_error_message(&DismissErrorMessage, window, cx)
+ this.dismiss_message(&DismissMessage, window, cx)
})),
tooltip_message: None,
});
@@ -542,7 +537,7 @@ impl ActivityIndicator {
on_click: Some(Arc::new(move |this, window, cx| {
this.statuses
.retain(|status| !checking_for_update.contains(&status.name));
- this.dismiss_error_message(&DismissErrorMessage, window, cx)
+ this.dismiss_message(&DismissMessage, window, cx)
})),
tooltip_message: None,
});
@@ -650,13 +645,14 @@ impl ActivityIndicator {
.and_then(|updater| match &updater.read(cx).status() {
AutoUpdateStatus::Checking => Some(Content {
icon: Some(
- Icon::new(IconName::Download)
+ Icon::new(IconName::LoadCircle)
.size(IconSize::Small)
+ .with_rotate_animation(3)
.into_any_element(),
),
message: "Checking for Zed updates…".to_string(),
on_click: Some(Arc::new(|this, window, cx| {
- this.dismiss_error_message(&DismissErrorMessage, window, cx)
+ this.dismiss_message(&DismissMessage, window, cx)
})),
tooltip_message: None,
}),
@@ -668,19 +664,20 @@ impl ActivityIndicator {
),
message: "Downloading Zed update…".to_string(),
on_click: Some(Arc::new(|this, window, cx| {
- this.dismiss_error_message(&DismissErrorMessage, window, cx)
+ this.dismiss_message(&DismissMessage, window, cx)
})),
tooltip_message: Some(Self::version_tooltip_message(version)),
}),
AutoUpdateStatus::Installing { version } => Some(Content {
icon: Some(
- Icon::new(IconName::Download)
+ Icon::new(IconName::LoadCircle)
.size(IconSize::Small)
+ .with_rotate_animation(3)
.into_any_element(),
),
message: "Installing Zed update…".to_string(),
on_click: Some(Arc::new(|this, window, cx| {
- this.dismiss_error_message(&DismissErrorMessage, window, cx)
+ this.dismiss_message(&DismissMessage, window, cx)
})),
tooltip_message: Some(Self::version_tooltip_message(version)),
}),
@@ -690,17 +687,18 @@ impl ActivityIndicator {
on_click: Some(Arc::new(move |_, _, cx| workspace::reload(cx))),
tooltip_message: Some(Self::version_tooltip_message(version)),
}),
- AutoUpdateStatus::Errored => Some(Content {
+ AutoUpdateStatus::Errored { error } => Some(Content {
icon: Some(
Icon::new(IconName::Warning)
.size(IconSize::Small)
.into_any_element(),
),
- message: "Auto update failed".to_string(),
+ message: "Failed to update Zed".to_string(),
on_click: Some(Arc::new(|this, window, cx| {
- this.dismiss_error_message(&DismissErrorMessage, window, cx)
+ window.dispatch_action(Box::new(workspace::OpenLog), cx);
+ this.dismiss_message(&DismissMessage, window, cx);
})),
- tooltip_message: None,
+ tooltip_message: Some(format!("{error}")),
}),
AutoUpdateStatus::Idle => None,
})
@@ -738,7 +736,7 @@ impl ActivityIndicator {
})),
message,
on_click: Some(Arc::new(|this, window, cx| {
- this.dismiss_error_message(&Default::default(), window, cx)
+ this.dismiss_message(&Default::default(), window, cx)
})),
tooltip_message: None,
})
@@ -777,7 +775,7 @@ impl Render for ActivityIndicator {
let result = h_flex()
.id("activity-indicator")
.on_action(cx.listener(Self::show_error_message))
- .on_action(cx.listener(Self::dismiss_error_message));
+ .on_action(cx.listener(Self::dismiss_message));
let Some(content) = self.content_to_render(cx) else {
return result;
};
diff --git a/crates/agent_servers/Cargo.toml b/crates/agent_servers/Cargo.toml
index bb3fe6ff9078535b500e28f4beeab957929546a5..ca6db6c663ddb2132c05d716e5b935c5855bccdb 100644
--- a/crates/agent_servers/Cargo.toml
+++ b/crates/agent_servers/Cargo.toml
@@ -23,6 +23,7 @@ action_log.workspace = true
agent-client-protocol.workspace = true
agent_settings.workspace = true
anyhow.workspace = true
+async-trait.workspace = true
client.workspace = true
collections.workspace = true
env_logger = { workspace = true, optional = true }
@@ -30,6 +31,7 @@ fs.workspace = true
futures.workspace = true
gpui.workspace = true
gpui_tokio = { workspace = true, optional = true }
+http_client.workspace = true
indoc.workspace = true
language.workspace = true
language_model.workspace = true
diff --git a/crates/agent_servers/src/acp.rs b/crates/agent_servers/src/acp.rs
index cc897d85e7b4de149a0dca84df84d2b8c2c5bc98..b8c75a01a2e2965c255e32bd3c0746b26d78ecab 100644
--- a/crates/agent_servers/src/acp.rs
+++ b/crates/agent_servers/src/acp.rs
@@ -13,7 +13,7 @@ use util::ResultExt as _;
use std::path::PathBuf;
use std::{any::Any, cell::RefCell};
-use std::{path::Path, rc::Rc, sync::Arc};
+use std::{path::Path, rc::Rc};
use thiserror::Error;
use anyhow::{Context as _, Result};
@@ -505,6 +505,7 @@ struct ClientDelegate {
cx: AsyncApp,
}
+#[async_trait::async_trait(?Send)]
impl acp::Client for ClientDelegate {
async fn request_permission(
&self,
@@ -638,19 +639,11 @@ impl acp::Client for ClientDelegate {
Ok(Default::default())
}
- async fn ext_method(
- &self,
- _name: Arc,
- _params: Arc,
- ) -> Result, acp::Error> {
+ async fn ext_method(&self, _args: acp::ExtRequest) -> Result {
Err(acp::Error::method_not_found())
}
- async fn ext_notification(
- &self,
- _name: Arc,
- _params: Arc,
- ) -> Result<(), acp::Error> {
+ async fn ext_notification(&self, _args: acp::ExtNotification) -> Result<(), acp::Error> {
Err(acp::Error::method_not_found())
}
diff --git a/crates/agent_servers/src/agent_servers.rs b/crates/agent_servers/src/agent_servers.rs
index 2c2900cb79328249355704606652c54d08f072e5..b9751d7f63053bf073bcc8181f0cc2f8211d5c9f 100644
--- a/crates/agent_servers/src/agent_servers.rs
+++ b/crates/agent_servers/src/agent_servers.rs
@@ -7,15 +7,19 @@ mod gemini;
pub mod e2e_tests;
pub use claude::*;
+use client::ProxySettings;
+use collections::HashMap;
pub use custom::*;
use fs::Fs;
pub use gemini::*;
+use http_client::read_no_proxy_from_env;
use project::agent_server_store::AgentServerStore;
use acp_thread::AgentConnection;
use anyhow::Result;
-use gpui::{App, Entity, SharedString, Task};
+use gpui::{App, AppContext, Entity, SharedString, Task};
use project::Project;
+use settings::SettingsStore;
use std::{any::Any, path::Path, rc::Rc, sync::Arc};
pub use acp::AcpConnection;
@@ -77,3 +81,25 @@ impl dyn AgentServer {
self.into_any().downcast().ok()
}
}
+
+/// Load the default proxy environment variables to pass through to the agent
+pub fn load_proxy_env(cx: &mut App) -> HashMap {
+ let proxy_url = cx
+ .read_global(|settings: &SettingsStore, _| settings.get::(None).proxy_url());
+ let mut env = HashMap::default();
+
+ if let Some(proxy_url) = &proxy_url {
+ let env_var = if proxy_url.scheme() == "https" {
+ "HTTPS_PROXY"
+ } else {
+ "HTTP_PROXY"
+ };
+ env.insert(env_var.to_owned(), proxy_url.to_string());
+ }
+
+ if let Some(no_proxy) = read_no_proxy_from_env() {
+ env.insert("NO_PROXY".to_owned(), no_proxy);
+ }
+
+ env
+}
diff --git a/crates/agent_servers/src/claude.rs b/crates/agent_servers/src/claude.rs
index 489839d82244fe76f6e9d1e9ea025a7b7c4a3bf7..4646b2e8259fa2cd63c0daa67b47f66b5e78af05 100644
--- a/crates/agent_servers/src/claude.rs
+++ b/crates/agent_servers/src/claude.rs
@@ -10,7 +10,7 @@ use anyhow::{Context as _, Result};
use gpui::{App, AppContext as _, SharedString, Task};
use project::agent_server_store::{AllAgentServersSettings, CLAUDE_CODE_NAME};
-use crate::{AgentServer, AgentServerDelegate};
+use crate::{AgentServer, AgentServerDelegate, load_proxy_env};
use acp_thread::AgentConnection;
#[derive(Clone)]
@@ -65,6 +65,7 @@ impl AgentServer for ClaudeCode {
let root_dir = root_dir.map(|root_dir| root_dir.to_string_lossy().to_string());
let is_remote = delegate.project.read(cx).is_via_remote_server();
let store = delegate.store.downgrade();
+ let extra_env = load_proxy_env(cx);
let default_mode = self.default_mode(cx);
cx.spawn(async move |cx| {
@@ -75,7 +76,7 @@ impl AgentServer for ClaudeCode {
.context("Claude Code is not registered")?;
anyhow::Ok(agent.get_command(
root_dir.as_deref(),
- Default::default(),
+ extra_env,
delegate.status_tx,
delegate.new_version_available,
&mut cx.to_async(),
diff --git a/crates/agent_servers/src/custom.rs b/crates/agent_servers/src/custom.rs
index aa2bbc0868dc64c5b415c445d19a357eb4b2ea85..cb9a6dba3c6376fa5030c21523c86853c9b6d761 100644
--- a/crates/agent_servers/src/custom.rs
+++ b/crates/agent_servers/src/custom.rs
@@ -1,4 +1,4 @@
-use crate::AgentServerDelegate;
+use crate::{AgentServerDelegate, load_proxy_env};
use acp_thread::AgentConnection;
use agent_client_protocol as acp;
use anyhow::{Context as _, Result};
@@ -71,6 +71,7 @@ impl crate::AgentServer for CustomAgentServer {
let is_remote = delegate.project.read(cx).is_via_remote_server();
let default_mode = self.default_mode(cx);
let store = delegate.store.downgrade();
+ let extra_env = load_proxy_env(cx);
cx.spawn(async move |cx| {
let (command, root_dir, login) = store
@@ -82,7 +83,7 @@ impl crate::AgentServer for CustomAgentServer {
})?;
anyhow::Ok(agent.get_command(
root_dir.as_deref(),
- Default::default(),
+ extra_env,
delegate.status_tx,
delegate.new_version_available,
&mut cx.to_async(),
diff --git a/crates/agent_servers/src/gemini.rs b/crates/agent_servers/src/gemini.rs
index 01f15557899e1c7826e91d1555320996eccd0f45..9407a42e68d34e38e78f2103b29f980f874fb3db 100644
--- a/crates/agent_servers/src/gemini.rs
+++ b/crates/agent_servers/src/gemini.rs
@@ -1,15 +1,12 @@
use std::rc::Rc;
use std::{any::Any, path::Path};
-use crate::{AgentServer, AgentServerDelegate};
+use crate::{AgentServer, AgentServerDelegate, load_proxy_env};
use acp_thread::AgentConnection;
use anyhow::{Context as _, Result};
-use client::ProxySettings;
-use collections::HashMap;
-use gpui::{App, AppContext, SharedString, Task};
+use gpui::{App, SharedString, Task};
use language_models::provider::google::GoogleLanguageModelProvider;
use project::agent_server_store::GEMINI_NAME;
-use settings::SettingsStore;
#[derive(Clone)]
pub struct Gemini;
@@ -37,17 +34,20 @@ impl AgentServer for Gemini {
let root_dir = root_dir.map(|root_dir| root_dir.to_string_lossy().to_string());
let is_remote = delegate.project.read(cx).is_via_remote_server();
let store = delegate.store.downgrade();
- let proxy_url = cx.read_global(|settings: &SettingsStore, _| {
- settings.get::(None).proxy.clone()
- });
+ let mut extra_env = load_proxy_env(cx);
let default_mode = self.default_mode(cx);
cx.spawn(async move |cx| {
- let mut extra_env = HashMap::default();
- if let Some(api_key) = cx.update(GoogleLanguageModelProvider::api_key)?.await.ok() {
- extra_env.insert("GEMINI_API_KEY".into(), api_key.key);
+ extra_env.insert("SURFACE".to_owned(), "zed".to_owned());
+
+ if let Some(api_key) = cx
+ .update(GoogleLanguageModelProvider::api_key_for_gemini_cli)?
+ .await
+ .ok()
+ {
+ extra_env.insert("GEMINI_API_KEY".into(), api_key);
}
- let (mut command, root_dir, login) = store
+ let (command, root_dir, login) = store
.update(cx, |store, cx| {
let agent = store
.get_external_agent(&GEMINI_NAME.into())
@@ -62,14 +62,6 @@ impl AgentServer for Gemini {
})??
.await?;
- // Add proxy flag if proxy settings are configured in Zed and not in the args
- if let Some(proxy_url_value) = &proxy_url
- && !command.args.iter().any(|arg| arg.contains("--proxy"))
- {
- command.args.push("--proxy".into());
- command.args.push(proxy_url_value.clone());
- }
-
let connection = crate::acp::connect(
name,
command,
diff --git a/crates/agent_servers/src/settings.rs b/crates/agent_servers/src/settings.rs
deleted file mode 100644
index 9a610465be5516664dafd9cd4cb46be96ad89c8b..0000000000000000000000000000000000000000
--- a/crates/agent_servers/src/settings.rs
+++ /dev/null
@@ -1,125 +0,0 @@
-use agent_client_protocol as acp;
-use std::path::PathBuf;
-
-use crate::AgentServerCommand;
-use anyhow::Result;
-use collections::HashMap;
-use gpui::{App, SharedString};
-use schemars::JsonSchema;
-use serde::{Deserialize, Serialize};
-use settings::{Settings, SettingsKey, SettingsSources, SettingsUi};
-
-pub fn init(cx: &mut App) {
- AllAgentServersSettings::register(cx);
-}
-
-#[derive(Default, Deserialize, Serialize, Clone, JsonSchema, Debug, SettingsUi, SettingsKey)]
-#[settings_key(key = "agent_servers")]
-pub struct AllAgentServersSettings {
- pub gemini: Option,
- pub claude: Option,
-
- /// Custom agent servers configured by the user
- #[serde(flatten)]
- pub custom: HashMap,
-}
-
-#[derive(Default, Deserialize, Serialize, Clone, JsonSchema, Debug, PartialEq)]
-pub struct BuiltinAgentServerSettings {
- /// Absolute path to a binary to be used when launching this agent.
- ///
- /// This can be used to run a specific binary without automatic downloads or searching `$PATH`.
- #[serde(rename = "command")]
- pub path: Option,
- /// If a binary is specified in `command`, it will be passed these arguments.
- pub args: Option>,
- /// If a binary is specified in `command`, it will be passed these environment variables.
- pub env: Option>,
- /// Whether to skip searching `$PATH` for an agent server binary when
- /// launching this agent.
- ///
- /// This has no effect if a `command` is specified. Otherwise, when this is
- /// `false`, Zed will search `$PATH` for an agent server binary and, if one
- /// is found, use it for threads with this agent. If no agent binary is
- /// found on `$PATH`, Zed will automatically install and use its own binary.
- /// When this is `true`, Zed will not search `$PATH`, and will always use
- /// its own binary.
- ///
- /// Default: true
- pub ignore_system_version: Option,
- /// The default mode for new threads.
- ///
- /// Note: Not all agents support modes.
- ///
- /// Default: None
- #[serde(skip_serializing_if = "Option::is_none")]
- pub default_mode: Option,
-}
-
-impl BuiltinAgentServerSettings {
- pub(crate) fn custom_command(self) -> Option {
- self.path.map(|path| AgentServerCommand {
- path,
- args: self.args.unwrap_or_default(),
- env: self.env,
- })
- }
-}
-
-impl From for BuiltinAgentServerSettings {
- fn from(value: AgentServerCommand) -> Self {
- BuiltinAgentServerSettings {
- path: Some(value.path),
- args: Some(value.args),
- env: value.env,
- ..Default::default()
- }
- }
-}
-
-#[derive(Deserialize, Serialize, Clone, JsonSchema, Debug, PartialEq)]
-pub struct CustomAgentServerSettings {
- #[serde(flatten)]
- pub command: AgentServerCommand,
- /// The default mode for new threads.
- ///
- /// Note: Not all agents support modes.
- ///
- /// Default: None
- #[serde(skip_serializing_if = "Option::is_none")]
- pub default_mode: Option,
-}
-
-impl settings::Settings for AllAgentServersSettings {
- type FileContent = Self;
-
- fn load(sources: SettingsSources, _: &mut App) -> Result {
- let mut settings = AllAgentServersSettings::default();
-
- for AllAgentServersSettings {
- gemini,
- claude,
- custom,
- } in sources.defaults_and_customizations()
- {
- if gemini.is_some() {
- settings.gemini = gemini.clone();
- }
- if claude.is_some() {
- settings.claude = claude.clone();
- }
-
- // Merge custom agents
- for (name, config) in custom {
- // Skip built-in agent names to avoid conflicts
- if name != "gemini" && name != "claude" {
- settings.custom.insert(name.clone(), config.clone());
- }
- }
- }
-
- Ok(settings)
- }
-
- fn import_from_vscode(_vscode: &settings::VsCodeSettings, _current: &mut Self::FileContent) {}
-}
diff --git a/crates/agent_settings/src/agent_settings.rs b/crates/agent_settings/src/agent_settings.rs
index e5744458601b7fd208bd97c11da7d136cb329f05..e0389a47ce015f0644f7ebfe0025b8c0d74fdcd0 100644
--- a/crates/agent_settings/src/agent_settings.rs
+++ b/crates/agent_settings/src/agent_settings.rs
@@ -50,6 +50,7 @@ pub struct AgentSettings {
pub expand_edit_card: bool,
pub expand_terminal_card: bool,
pub use_modifier_to_send: bool,
+ pub message_editor_min_lines: usize,
}
impl AgentSettings {
@@ -91,6 +92,10 @@ impl AgentSettings {
model,
});
}
+
+ pub fn set_message_editor_max_lines(&self) -> usize {
+ self.message_editor_min_lines * 2
+ }
}
#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Default)]
@@ -175,6 +180,7 @@ impl Settings for AgentSettings {
expand_edit_card: agent.expand_edit_card.unwrap(),
expand_terminal_card: agent.expand_terminal_card.unwrap(),
use_modifier_to_send: agent.use_modifier_to_send.unwrap(),
+ message_editor_min_lines: agent.message_editor_min_lines.unwrap(),
}
}
@@ -224,6 +230,8 @@ impl Settings for AgentSettings {
self.model_parameters
.extend_from_slice(&value.model_parameters);
+ self.message_editor_min_lines
+ .merge_from(&value.message_editor_min_lines);
if let Some(profiles) = value.profiles.as_ref() {
self.profiles.extend(
diff --git a/crates/agent_ui/Cargo.toml b/crates/agent_ui/Cargo.toml
index 028db95c10a8c7a319bb05927dcabd0564a14683..47d9f6d6a27a2ad5102e831094912208e66a9b43 100644
--- a/crates/agent_ui/Cargo.toml
+++ b/crates/agent_ui/Cargo.toml
@@ -80,7 +80,6 @@ serde.workspace = true
serde_json.workspace = true
serde_json_lenient.workspace = true
settings.workspace = true
-shlex.workspace = true
smol.workspace = true
streaming_diff.workspace = true
task.workspace = true
diff --git a/crates/agent_ui/src/acp/message_editor.rs b/crates/agent_ui/src/acp/message_editor.rs
index ab4e8d680925c96e64fdb2e7707bea9c1e177b5c..2734726ddbe1608356bcc34af5c42f479d5a8e8a 100644
--- a/crates/agent_ui/src/acp/message_editor.rs
+++ b/crates/agent_ui/src/acp/message_editor.rs
@@ -1099,11 +1099,16 @@ impl MessageEditor {
}
pub fn insert_selections(&mut self, window: &mut Window, cx: &mut Context) {
- let buffer = self.editor.read(cx).buffer().clone();
- let Some(buffer) = buffer.read(cx).as_singleton() else {
+ let editor = self.editor.read(cx);
+ let editor_buffer = editor.buffer().read(cx);
+ let Some(buffer) = editor_buffer.as_singleton() else {
return;
};
- let anchor = buffer.update(cx, |buffer, _cx| buffer.anchor_before(buffer.len()));
+ let cursor_anchor = editor.selections.newest_anchor().head();
+ let cursor_offset = cursor_anchor.to_offset(&editor_buffer.snapshot(cx));
+ let anchor = buffer.update(cx, |buffer, _cx| {
+ buffer.anchor_before(cursor_offset.min(buffer.len()))
+ });
let Some(workspace) = self.workspace.upgrade() else {
return;
};
@@ -1117,13 +1122,7 @@ impl MessageEditor {
return;
};
self.editor.update(cx, |message_editor, cx| {
- message_editor.edit(
- [(
- multi_buffer::Anchor::max()..multi_buffer::Anchor::max(),
- completion.new_text,
- )],
- cx,
- );
+ message_editor.edit([(cursor_anchor..cursor_anchor, completion.new_text)], cx);
});
if let Some(confirm) = completion.confirm {
confirm(CompletionIntent::Complete, window, cx);
diff --git a/crates/agent_ui/src/acp/mode_selector.rs b/crates/agent_ui/src/acp/mode_selector.rs
index d4d424f41a36652881a50b65bc3dbe00c18fdcde..410874126665b7d622c7cf45e81596dce7f96823 100644
--- a/crates/agent_ui/src/acp/mode_selector.rs
+++ b/crates/agent_ui/src/acp/mode_selector.rs
@@ -107,13 +107,15 @@ impl ModeSelector {
.text_sm()
.text_color(Color::Muted.color(cx))
.child("Hold")
- .child(div().pt_0p5().children(ui::render_modifiers(
- &gpui::Modifiers::secondary_key(),
- PlatformStyle::platform(),
- None,
- Some(ui::TextSize::Default.rems(cx).into()),
- true,
- )))
+ .child(h_flex().flex_shrink_0().children(
+ ui::render_modifiers(
+ &gpui::Modifiers::secondary_key(),
+ PlatformStyle::platform(),
+ None,
+ Some(ui::TextSize::Default.rems(cx).into()),
+ true,
+ ),
+ ))
.child(div().map(|this| {
if is_default {
this.child("to also unset as default")
diff --git a/crates/agent_ui/src/acp/thread_history.rs b/crates/agent_ui/src/acp/thread_history.rs
index ed508ea18da7df3426fc13b137b97f37267ed283..cd696f33fa44976e0784c79d1945b548feb20a50 100644
--- a/crates/agent_ui/src/acp/thread_history.rs
+++ b/crates/agent_ui/src/acp/thread_history.rs
@@ -500,20 +500,24 @@ impl Render for AcpThreadHistory {
),
)
} else {
- view.pr_5()
- .child(
- uniform_list(
- "thread-history",
- self.visible_items.len(),
- cx.processor(|this, range: Range, window, cx| {
- this.render_list_items(range, window, cx)
- }),
- )
- .p_1()
- .track_scroll(self.scroll_handle.clone())
- .flex_grow(),
+ view.child(
+ uniform_list(
+ "thread-history",
+ self.visible_items.len(),
+ cx.processor(|this, range: Range, window, cx| {
+ this.render_list_items(range, window, cx)
+ }),
)
- .vertical_scrollbar_for(self.scroll_handle.clone(), window, cx)
+ .p_1()
+ .pr_4()
+ .track_scroll(self.scroll_handle.clone())
+ .flex_grow(),
+ )
+ .vertical_scrollbar_for(
+ self.scroll_handle.clone(),
+ window,
+ cx,
+ )
}
})
}
diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs
index fd848d2c42ab6ae4bea9255876793dba22022760..ac84fd36f24a850330a5f20b979bcc0f8fc442ad 100644
--- a/crates/agent_ui/src/acp/thread_view.rs
+++ b/crates/agent_ui/src/acp/thread_view.rs
@@ -9,7 +9,7 @@ use agent_client_protocol::{self as acp, PromptCapabilities};
use agent_servers::{AgentServer, AgentServerDelegate};
use agent_settings::{AgentProfileId, AgentSettings, CompletionMode};
use agent2::{DbThreadMetadata, HistoryEntry, HistoryEntryId, HistoryStore, NativeAgentServer};
-use anyhow::{Context as _, Result, anyhow, bail};
+use anyhow::{Result, anyhow, bail};
use arrayvec::ArrayVec;
use audio::{Audio, Sound};
use buffer_diff::BufferDiff;
@@ -71,9 +71,6 @@ use crate::{
RejectOnce, ToggleBurnMode, ToggleProfileSelector,
};
-pub const MIN_EDITOR_LINES: usize = 4;
-pub const MAX_EDITOR_LINES: usize = 8;
-
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
enum ThreadFeedback {
Positive,
@@ -357,8 +354,8 @@ impl AcpThreadView {
agent.name(),
&placeholder,
editor::EditorMode::AutoHeight {
- min_lines: MIN_EDITOR_LINES,
- max_lines: Some(MAX_EDITOR_LINES),
+ min_lines: AgentSettings::get_global(cx).message_editor_min_lines,
+ max_lines: Some(AgentSettings::get_global(cx).set_message_editor_max_lines()),
},
window,
cx,
@@ -857,10 +854,11 @@ impl AcpThreadView {
cx,
)
} else {
+ let agent_settings = AgentSettings::get_global(cx);
editor.set_mode(
EditorMode::AutoHeight {
- min_lines: MIN_EDITOR_LINES,
- max_lines: Some(MAX_EDITOR_LINES),
+ min_lines: agent_settings.message_editor_min_lines,
+ max_lines: Some(agent_settings.set_message_editor_max_lines()),
},
cx,
)
@@ -1584,19 +1582,6 @@ impl AcpThreadView {
window.spawn(cx, async move |cx| {
let mut task = login.clone();
- task.command = task
- .command
- .map(|command| anyhow::Ok(shlex::try_quote(&command)?.to_string()))
- .transpose()?;
- task.args = task
- .args
- .iter()
- .map(|arg| {
- Ok(shlex::try_quote(arg)
- .context("Failed to quote argument")?
- .to_string())
- })
- .collect::>>()?;
task.full_label = task.label.clone();
task.id = task::TaskId(format!("external-agent-{}-login", task.label));
task.command_label = task.label.clone();
@@ -3197,10 +3182,14 @@ impl AcpThreadView {
};
Button::new(SharedString::from(method_id.clone()), name)
- .when(ix == 0, |el| {
- el.style(ButtonStyle::Tinted(ui::TintColor::Warning))
- })
.label_size(LabelSize::Small)
+ .map(|this| {
+ if ix == 0 {
+ this.style(ButtonStyle::Tinted(TintColor::Warning))
+ } else {
+ this.style(ButtonStyle::Outlined)
+ }
+ })
.on_click({
cx.listener(move |this, _, window, cx| {
telemetry::event!(
@@ -5680,6 +5669,23 @@ pub(crate) mod tests {
});
}
+ #[gpui::test]
+ async fn test_spawn_external_agent_login_handles_spaces(cx: &mut TestAppContext) {
+ init_test(cx);
+
+ // Verify paths with spaces aren't pre-quoted
+ let path_with_spaces = "/Users/test/Library/Application Support/Zed/cli.js";
+ let login_task = task::SpawnInTerminal {
+ command: Some("node".to_string()),
+ args: vec![path_with_spaces.to_string(), "/login".to_string()],
+ ..Default::default()
+ };
+
+ // Args should be passed as-is, not pre-quoted
+ assert!(!login_task.args[0].starts_with('"'));
+ assert!(!login_task.args[0].starts_with('\''));
+ }
+
#[gpui::test]
async fn test_notification_for_tool_authorization(cx: &mut TestAppContext) {
init_test(cx);
diff --git a/crates/agent_ui/src/agent_configuration.rs b/crates/agent_ui/src/agent_configuration.rs
index ea1ecb10e55241368bacc636b75d182b64210bc7..382a9db2573a21bcb74e75d15a6a87c0aa412588 100644
--- a/crates/agent_ui/src/agent_configuration.rs
+++ b/crates/agent_ui/src/agent_configuration.rs
@@ -272,13 +272,28 @@ impl AgentConfiguration {
*is_expanded = !*is_expanded;
}
})),
- )
- .when(provider.is_authenticated(cx), |parent| {
+ ),
+ )
+ .child(
+ v_flex()
+ .w_full()
+ .px_2()
+ .gap_1()
+ .when(is_expanded, |parent| match configuration_view {
+ Some(configuration_view) => parent.child(configuration_view),
+ None => parent.child(Label::new(format!(
+ "No configuration view for {provider_name}",
+ ))),
+ })
+ .when(is_expanded && provider.is_authenticated(cx), |parent| {
parent.child(
Button::new(
SharedString::from(format!("new-thread-{provider_id}")),
"Start New Thread",
)
+ .full_width()
+ .style(ButtonStyle::Filled)
+ .layer(ElevationIndex::ModalSurface)
.icon_position(IconPosition::Start)
.icon(IconName::Thread)
.icon_size(IconSize::Small)
@@ -295,17 +310,6 @@ impl AgentConfiguration {
)
}),
)
- .child(
- div()
- .w_full()
- .px_2()
- .when(is_expanded, |parent| match configuration_view {
- Some(configuration_view) => parent.child(configuration_view),
- None => parent.child(Label::new(format!(
- "No configuration view for {provider_name}",
- ))),
- }),
- )
}
fn render_provider_configuration_section(
@@ -562,11 +566,28 @@ impl AgentConfiguration {
.color(Color::Muted),
),
)
- .children(
- context_server_ids.into_iter().map(|context_server_id| {
- self.render_context_server(context_server_id, window, cx)
- }),
- )
+ .map(|parent| {
+ if context_server_ids.is_empty() {
+ parent.child(
+ h_flex()
+ .p_4()
+ .justify_center()
+ .border_1()
+ .border_dashed()
+ .border_color(cx.theme().colors().border.opacity(0.6))
+ .rounded_sm()
+ .child(
+ Label::new("No MCP servers added yet.")
+ .color(Color::Muted)
+ .size(LabelSize::Small),
+ ),
+ )
+ } else {
+ parent.children(context_server_ids.into_iter().map(|context_server_id| {
+ self.render_context_server(context_server_id, window, cx)
+ }))
+ }
+ })
.child(
h_flex()
.justify_between()
@@ -819,6 +840,8 @@ impl AgentConfiguration {
)
.child(
h_flex()
+ .flex_1()
+ .min_w_0()
.child(
Disclosure::new(
"tool-list-disclosure",
@@ -842,17 +865,19 @@ impl AgentConfiguration {
.id(SharedString::from(format!("tooltip-{}", item_id)))
.h_full()
.w_3()
- .mx_1()
+ .ml_1()
+ .mr_1p5()
.justify_center()
.tooltip(Tooltip::text(tooltip_text))
.child(status_indicator),
)
- .child(Label::new(item_id).ml_0p5())
+ .child(Label::new(item_id).truncate())
.child(
div()
.id("extension-source")
.mt_0p5()
.mx_1()
+ .flex_none()
.tooltip(Tooltip::text(source_tooltip))
.child(
Icon::new(source_icon)
@@ -874,7 +899,8 @@ impl AgentConfiguration {
)
.child(
h_flex()
- .gap_1()
+ .gap_0p5()
+ .flex_none()
.child(context_server_configuration_menu)
.child(
Switch::new("context-server-switch", is_running.into())
@@ -1110,6 +1136,7 @@ impl AgentConfiguration {
SharedString::from(format!("start_acp_thread-{name}")),
"Start New Thread",
)
+ .layer(ElevationIndex::ModalSurface)
.label_size(LabelSize::Small)
.icon(IconName::Thread)
.icon_position(IconPosition::Start)
diff --git a/crates/agent_ui/src/agent_ui.rs b/crates/agent_ui/src/agent_ui.rs
index f4c3fe1069eb42ad53c5771d4a511f88ff780664..e34789d62d2c95b06f5c4f03b93b60f01c6dbf6a 100644
--- a/crates/agent_ui/src/agent_ui.rs
+++ b/crates/agent_ui/src/agent_ui.rs
@@ -14,7 +14,6 @@ mod message_editor;
mod profile_selector;
mod slash_command;
mod slash_command_picker;
-mod slash_command_settings;
mod terminal_codegen;
mod terminal_inline_assistant;
mod text_thread_editor;
@@ -46,7 +45,6 @@ use std::any::TypeId;
use crate::agent_configuration::{ConfigureContextServerModal, ManageProfilesModal};
pub use crate::agent_panel::{AgentPanel, ConcreteAssistantPanelDelegate};
pub use crate::inline_assistant::InlineAssistant;
-use crate::slash_command_settings::SlashCommandSettings;
pub use agent_diff::{AgentDiffPane, AgentDiffToolbar};
pub use text_thread_editor::{AgentPanelDelegate, TextThreadEditor};
use zed_actions;
@@ -257,7 +255,6 @@ pub fn init(
cx: &mut App,
) {
AgentSettings::register(cx);
- SlashCommandSettings::register(cx);
assistant_context::init(client.clone(), cx);
rules_library::init(cx);
@@ -413,8 +410,6 @@ fn register_slash_commands(cx: &mut App) {
slash_command_registry.register_command(assistant_slash_commands::DeltaSlashCommand, true);
slash_command_registry.register_command(assistant_slash_commands::OutlineSlashCommand, true);
slash_command_registry.register_command(assistant_slash_commands::TabSlashCommand, true);
- slash_command_registry
- .register_command(assistant_slash_commands::CargoWorkspaceSlashCommand, true);
slash_command_registry.register_command(assistant_slash_commands::PromptSlashCommand, true);
slash_command_registry.register_command(assistant_slash_commands::SelectionCommand, true);
slash_command_registry.register_command(assistant_slash_commands::DefaultSlashCommand, false);
@@ -434,21 +429,4 @@ fn register_slash_commands(cx: &mut App) {
}
})
.detach();
-
- update_slash_commands_from_settings(cx);
- cx.observe_global::(update_slash_commands_from_settings)
- .detach();
-}
-
-fn update_slash_commands_from_settings(cx: &mut App) {
- let slash_command_registry = SlashCommandRegistry::global(cx);
- let settings = SlashCommandSettings::get_global(cx);
-
- if settings.cargo_workspace.enabled {
- slash_command_registry
- .register_command(assistant_slash_commands::CargoWorkspaceSlashCommand, true);
- } else {
- slash_command_registry
- .unregister_command(assistant_slash_commands::CargoWorkspaceSlashCommand);
- }
}
diff --git a/crates/agent_ui/src/context_picker/completion_provider.rs b/crates/agent_ui/src/context_picker/completion_provider.rs
index c9cd69bf8e49b2e4f20148640cd029caea51264f..01a7a51316eee4709eaf9c17c8840e3cd637a62b 100644
--- a/crates/agent_ui/src/context_picker/completion_provider.rs
+++ b/crates/agent_ui/src/context_picker/completion_provider.rs
@@ -743,15 +743,15 @@ impl CompletionProvider for ContextPickerCompletionProvider {
_window: &mut Window,
cx: &mut Context,
) -> Task>> {
- let state = buffer.update(cx, |buffer, _cx| {
- let position = buffer_position.to_point(buffer);
- let line_start = Point::new(position.row, 0);
- let offset_to_line = buffer.point_to_offset(line_start);
- let mut lines = buffer.text_for_range(line_start..position).lines();
- let line = lines.next()?;
- MentionCompletion::try_parse(line, offset_to_line)
- });
- let Some(state) = state else {
+ let snapshot = buffer.read(cx).snapshot();
+ let position = buffer_position.to_point(&snapshot);
+ let line_start = Point::new(position.row, 0);
+ let offset_to_line = snapshot.point_to_offset(line_start);
+ let mut lines = snapshot.text_for_range(line_start..position).lines();
+ let Some(line) = lines.next() else {
+ return Task::ready(Ok(Vec::new()));
+ };
+ let Some(state) = MentionCompletion::try_parse(line, offset_to_line) else {
return Task::ready(Ok(Vec::new()));
};
@@ -761,7 +761,6 @@ impl CompletionProvider for ContextPickerCompletionProvider {
return Task::ready(Ok(Vec::new()));
};
- let snapshot = buffer.read(cx).snapshot();
let source_range = snapshot.anchor_before(state.source_range.start)
..snapshot.anchor_after(state.source_range.end);
diff --git a/crates/agent_ui/src/inline_assistant.rs b/crates/agent_ui/src/inline_assistant.rs
index 4ac88e6daa3d3623580e206c2759f27b218d1bac..79e092b709dd2778c89a79e1d6ce36802c853eb6 100644
--- a/crates/agent_ui/src/inline_assistant.rs
+++ b/crates/agent_ui/src/inline_assistant.rs
@@ -744,19 +744,14 @@ impl InlineAssistant {
.update(cx, |editor, cx| {
let scroll_top = editor.scroll_position(cx).y;
let scroll_bottom = scroll_top + editor.visible_line_count().unwrap_or(0.);
- let prompt_row = editor
+ editor_assists.scroll_lock = editor
.row_for_block(decorations.prompt_block_id, cx)
- .unwrap()
- .0 as f32;
-
- if (scroll_top..scroll_bottom).contains(&prompt_row) {
- editor_assists.scroll_lock = Some(InlineAssistScrollLock {
+ .map(|row| row.0 as f32)
+ .filter(|prompt_row| (scroll_top..scroll_bottom).contains(&prompt_row))
+ .map(|prompt_row| InlineAssistScrollLock {
assist_id,
distance_from_top: prompt_row - scroll_top,
});
- } else {
- editor_assists.scroll_lock = None;
- }
})
.ok();
}
@@ -917,14 +912,12 @@ impl InlineAssistant {
editor.update(cx, |editor, cx| {
let scroll_position = editor.scroll_position(cx);
- let target_scroll_top = editor
- .row_for_block(decorations.prompt_block_id, cx)
- .unwrap()
- .0 as f32
+ let target_scroll_top = editor.row_for_block(decorations.prompt_block_id, cx)?.0 as f32
- scroll_lock.distance_from_top;
if target_scroll_top != scroll_position.y {
editor.set_scroll_position(point(scroll_position.x, target_scroll_top), window, cx);
}
+ Some(())
});
}
@@ -968,14 +961,14 @@ impl InlineAssistant {
if let Some(decorations) = assist.decorations.as_ref() {
let distance_from_top = editor.update(cx, |editor, cx| {
let scroll_top = editor.scroll_position(cx).y;
- let prompt_row = editor
- .row_for_block(decorations.prompt_block_id, cx)
- .unwrap()
- .0 as f32;
- prompt_row - scroll_top
+ let prompt_row =
+ editor.row_for_block(decorations.prompt_block_id, cx)?.0 as f32;
+ Some(prompt_row - scroll_top)
});
- if distance_from_top != scroll_lock.distance_from_top {
+ if distance_from_top.is_none_or(|distance_from_top| {
+ distance_from_top != scroll_lock.distance_from_top
+ }) {
editor_assists.scroll_lock = None;
}
}
diff --git a/crates/agent_ui/src/slash_command_settings.rs b/crates/agent_ui/src/slash_command_settings.rs
deleted file mode 100644
index f0a04c6b49984ae94d629f1bbfa96c6de4e01606..0000000000000000000000000000000000000000
--- a/crates/agent_ui/src/slash_command_settings.rs
+++ /dev/null
@@ -1,27 +0,0 @@
-use gpui::App;
-use settings::Settings;
-
-/// Settings for slash commands.
-#[derive(Debug, Default, Clone)]
-pub struct SlashCommandSettings {
- /// Settings for the `/cargo-workspace` slash command.
- pub cargo_workspace: CargoWorkspaceCommandSettings,
-}
-
-/// Settings for the `/cargo-workspace` slash command.
-#[derive(Debug, Default, Clone)]
-pub struct CargoWorkspaceCommandSettings {
- /// Whether `/cargo-workspace` is enabled.
- pub enabled: bool,
-}
-
-// todo!() I think this setting is bogus... default.json has "slash_commands": {"project"}
-impl Settings for SlashCommandSettings {
- fn from_defaults(_content: &settings::SettingsContent, _cx: &mut App) -> Self {
- Self {
- cargo_workspace: CargoWorkspaceCommandSettings { enabled: false },
- }
- }
-
- fn refine(&mut self, _content: &settings::SettingsContent, _cx: &mut App) {}
-}
diff --git a/crates/agent_ui/src/text_thread_editor.rs b/crates/agent_ui/src/text_thread_editor.rs
index b40b996ae74f93220d88c97e8ae7d99dd8576cf1..dadb6263c765a10fedc01b25ae5dd3dded19b877 100644
--- a/crates/agent_ui/src/text_thread_editor.rs
+++ b/crates/agent_ui/src/text_thread_editor.rs
@@ -485,7 +485,7 @@ impl TextThreadEditor {
return;
}
- let selections = self.editor.read(cx).selections.disjoint_anchors();
+ let selections = self.editor.read(cx).selections.disjoint_anchors_arc();
let mut commands_by_range = HashMap::default();
let workspace = self.workspace.clone();
self.context.update(cx, |context, cx| {
@@ -1831,7 +1831,7 @@ impl TextThreadEditor {
fn split(&mut self, _: &Split, _window: &mut Window, cx: &mut Context) {
self.context.update(cx, |context, cx| {
- let selections = self.editor.read(cx).selections.disjoint_anchors();
+ let selections = self.editor.read(cx).selections.disjoint_anchors_arc();
for selection in selections.as_ref() {
let buffer = self.editor.read(cx).buffer().read(cx).snapshot(cx);
let range = selection
diff --git a/crates/assistant_slash_commands/Cargo.toml b/crates/assistant_slash_commands/Cargo.toml
index c054c3ced84825bcd131bdd76644c00595c4c4a9..f151515d4235b7ecb150539aceb1c5478960517b 100644
--- a/crates/assistant_slash_commands/Cargo.toml
+++ b/crates/assistant_slash_commands/Cargo.toml
@@ -14,7 +14,6 @@ path = "src/assistant_slash_commands.rs"
[dependencies]
anyhow.workspace = true
assistant_slash_command.workspace = true
-cargo_toml.workspace = true
chrono.workspace = true
collections.workspace = true
context_server.workspace = true
@@ -35,7 +34,6 @@ serde.workspace = true
serde_json.workspace = true
smol.workspace = true
text.workspace = true
-toml.workspace = true
ui.workspace = true
util.workspace = true
workspace.workspace = true
diff --git a/crates/assistant_slash_commands/src/assistant_slash_commands.rs b/crates/assistant_slash_commands/src/assistant_slash_commands.rs
index fb00a912197e07942a67ad92418b85c4920ad66b..2bf2573e99d7a5a0140c1972967ec68523b0b56a 100644
--- a/crates/assistant_slash_commands/src/assistant_slash_commands.rs
+++ b/crates/assistant_slash_commands/src/assistant_slash_commands.rs
@@ -1,4 +1,3 @@
-mod cargo_workspace_command;
mod context_server_command;
mod default_command;
mod delta_command;
@@ -12,7 +11,6 @@ mod streaming_example_command;
mod symbols_command;
mod tab_command;
-pub use crate::cargo_workspace_command::*;
pub use crate::context_server_command::*;
pub use crate::default_command::*;
pub use crate::delta_command::*;
diff --git a/crates/assistant_slash_commands/src/cargo_workspace_command.rs b/crates/assistant_slash_commands/src/cargo_workspace_command.rs
deleted file mode 100644
index d58b2edc4c3dffd799dd9eb1c104686dc6488687..0000000000000000000000000000000000000000
--- a/crates/assistant_slash_commands/src/cargo_workspace_command.rs
+++ /dev/null
@@ -1,158 +0,0 @@
-use anyhow::{Context as _, Result, anyhow};
-use assistant_slash_command::{
- ArgumentCompletion, SlashCommand, SlashCommandOutput, SlashCommandOutputSection,
- SlashCommandResult,
-};
-use fs::Fs;
-use gpui::{App, Entity, Task, WeakEntity};
-use language::{BufferSnapshot, LspAdapterDelegate};
-use project::{Project, ProjectPath};
-use std::{
- fmt::Write,
- path::Path,
- sync::{Arc, atomic::AtomicBool},
-};
-use ui::prelude::*;
-use workspace::Workspace;
-
-pub struct CargoWorkspaceSlashCommand;
-
-impl CargoWorkspaceSlashCommand {
- async fn build_message(fs: Arc, path_to_cargo_toml: &Path) -> Result {
- let buffer = fs.load(path_to_cargo_toml).await?;
- let cargo_toml: cargo_toml::Manifest = toml::from_str(&buffer)?;
-
- let mut message = String::new();
- writeln!(message, "You are in a Rust project.")?;
-
- if let Some(workspace) = cargo_toml.workspace {
- writeln!(
- message,
- "The project is a Cargo workspace with the following members:"
- )?;
- for member in workspace.members {
- writeln!(message, "- {member}")?;
- }
-
- if !workspace.default_members.is_empty() {
- writeln!(message, "The default members are:")?;
- for member in workspace.default_members {
- writeln!(message, "- {member}")?;
- }
- }
-
- if !workspace.dependencies.is_empty() {
- writeln!(
- message,
- "The following workspace dependencies are installed:"
- )?;
- for dependency in workspace.dependencies.keys() {
- writeln!(message, "- {dependency}")?;
- }
- }
- } else if let Some(package) = cargo_toml.package {
- writeln!(
- message,
- "The project name is \"{name}\".",
- name = package.name
- )?;
-
- let description = package
- .description
- .as_ref()
- .and_then(|description| description.get().ok().cloned());
- if let Some(description) = description.as_ref() {
- writeln!(message, "It describes itself as \"{description}\".")?;
- }
-
- if !cargo_toml.dependencies.is_empty() {
- writeln!(message, "The following dependencies are installed:")?;
- for dependency in cargo_toml.dependencies.keys() {
- writeln!(message, "- {dependency}")?;
- }
- }
- }
-
- Ok(message)
- }
-
- fn path_to_cargo_toml(project: Entity, cx: &mut App) -> Option> {
- let worktree = project.read(cx).worktrees(cx).next()?;
- let worktree = worktree.read(cx);
- let entry = worktree.entry_for_path("Cargo.toml")?;
- let path = ProjectPath {
- worktree_id: worktree.id(),
- path: entry.path.clone(),
- };
- Some(Arc::from(
- project.read(cx).absolute_path(&path, cx)?.as_path(),
- ))
- }
-}
-
-impl SlashCommand for CargoWorkspaceSlashCommand {
- fn name(&self) -> String {
- "cargo-workspace".into()
- }
-
- fn description(&self) -> String {
- "insert project workspace metadata".into()
- }
-
- fn menu_text(&self) -> String {
- "Insert Project Workspace Metadata".into()
- }
-
- fn complete_argument(
- self: Arc,
- _arguments: &[String],
- _cancel: Arc,
- _workspace: Option>,
- _window: &mut Window,
- _cx: &mut App,
- ) -> Task>> {
- Task::ready(Err(anyhow!("this command does not require argument")))
- }
-
- fn requires_argument(&self) -> bool {
- false
- }
-
- fn run(
- self: Arc,
- _arguments: &[String],
- _context_slash_command_output_sections: &[SlashCommandOutputSection],
- _context_buffer: BufferSnapshot,
- workspace: WeakEntity,
- _delegate: Option>,
- _window: &mut Window,
- cx: &mut App,
- ) -> Task {
- let output = workspace.update(cx, |workspace, cx| {
- let project = workspace.project().clone();
- let fs = workspace.project().read(cx).fs().clone();
- let path = Self::path_to_cargo_toml(project, cx);
- let output = cx.background_spawn(async move {
- let path = path.with_context(|| "Cargo.toml not found")?;
- Self::build_message(fs, &path).await
- });
-
- cx.foreground_executor().spawn(async move {
- let text = output.await?;
- let range = 0..text.len();
- Ok(SlashCommandOutput {
- text,
- sections: vec![SlashCommandOutputSection {
- range,
- icon: IconName::FileTree,
- label: "Project".into(),
- metadata: None,
- }],
- run_commands_in_text: false,
- }
- .into_event_stream())
- })
- });
- output.unwrap_or_else(|error| Task::ready(Err(error)))
- }
-}
diff --git a/crates/assistant_tools/Cargo.toml b/crates/assistant_tools/Cargo.toml
index 5a8ca8a5e995fd2c738eb3b309f2bb4ebe9595a1..9b9b8196d1c342c536d605306a1a062e73768c56 100644
--- a/crates/assistant_tools/Cargo.toml
+++ b/crates/assistant_tools/Cargo.toml
@@ -63,7 +63,6 @@ ui.workspace = true
util.workspace = true
watch.workspace = true
web_search.workspace = true
-which.workspace = true
workspace-hack.workspace = true
workspace.workspace = true
diff --git a/crates/assistant_tools/src/assistant_tools.rs b/crates/assistant_tools/src/assistant_tools.rs
index ce3b639cb2c46d3f736490c0b2153260f970963c..17e2ba12f706387859ca3393aa44f5c05570e50a 100644
--- a/crates/assistant_tools/src/assistant_tools.rs
+++ b/crates/assistant_tools/src/assistant_tools.rs
@@ -52,7 +52,7 @@ pub fn init(http_client: Arc, cx: &mut App) {
assistant_tool::init(cx);
let registry = ToolRegistry::global(cx);
- registry.register_tool(TerminalTool::new(cx));
+ registry.register_tool(TerminalTool);
registry.register_tool(CreateDirectoryTool);
registry.register_tool(CopyPathTool);
registry.register_tool(DeletePathTool);
diff --git a/crates/assistant_tools/src/edit_agent/create_file_parser.rs b/crates/assistant_tools/src/edit_agent/create_file_parser.rs
index 5126f9c6b1fe4ee5cc600ae93b7300b7af09451f..2272434d796a92e53b741f8ed5f4303d94f88489 100644
--- a/crates/assistant_tools/src/edit_agent/create_file_parser.rs
+++ b/crates/assistant_tools/src/edit_agent/create_file_parser.rs
@@ -160,7 +160,7 @@ mod tests {
&mut parser,
&mut rng
),
- // This output is marlformed, so we're doing our best effort
+ // This output is malformed, so we're doing our best effort
"Hello world\n```\n\nThe end\n".to_string()
);
}
@@ -182,7 +182,7 @@ mod tests {
&mut parser,
&mut rng
),
- // This output is marlformed, so we're doing our best effort
+ // This output is malformed, so we're doing our best effort
"```\nHello world\n```\n".to_string()
);
}
diff --git a/crates/assistant_tools/src/edit_agent/evals/fixtures/use_wasi_sdk_in_compile_parser_to_wasm/before.rs b/crates/assistant_tools/src/edit_agent/evals/fixtures/use_wasi_sdk_in_compile_parser_to_wasm/before.rs
index b51c74c798d88b3f84303ffe41f4ac2590e7f236..cfa28fe1ad6091c9adda22f610e1cf13166f8dfb 100644
--- a/crates/assistant_tools/src/edit_agent/evals/fixtures/use_wasi_sdk_in_compile_parser_to_wasm/before.rs
+++ b/crates/assistant_tools/src/edit_agent/evals/fixtures/use_wasi_sdk_in_compile_parser_to_wasm/before.rs
@@ -916,7 +916,7 @@ impl Loader {
if !found_non_static {
found_non_static = true;
eprintln!(
- "Warning: Found non-static non-tree-sitter functions in the external scannner"
+ "Warning: Found non-static non-tree-sitter functions in the external scanner"
);
}
eprintln!(" `{function_name}`");
diff --git a/crates/assistant_tools/src/terminal_tool.rs b/crates/assistant_tools/src/terminal_tool.rs
index 1605003671621b90e58a5f62e521c0aba2c990c6..8014a39e23137ad71b91e5c24d5d79699b530e5d 100644
--- a/crates/assistant_tools/src/terminal_tool.rs
+++ b/crates/assistant_tools/src/terminal_tool.rs
@@ -6,7 +6,7 @@ use action_log::ActionLog;
use agent_settings;
use anyhow::{Context as _, Result, anyhow};
use assistant_tool::{Tool, ToolCard, ToolResult, ToolUseStatus};
-use futures::{FutureExt as _, future::Shared};
+use futures::FutureExt as _;
use gpui::{
AnyWindowHandle, App, AppContext, Empty, Entity, EntityId, Task, TextStyleRefinement,
WeakEntity, Window,
@@ -26,11 +26,12 @@ use std::{
sync::Arc,
time::{Duration, Instant},
};
+use task::{Shell, ShellBuilder};
use terminal_view::TerminalView;
use theme::ThemeSettings;
use ui::{CommonAnimationExt, Disclosure, Tooltip, prelude::*};
use util::{
- ResultExt, get_system_shell, markdown::MarkdownInlineCode, size::format_file_size,
+ ResultExt, get_default_system_shell, markdown::MarkdownInlineCode, size::format_file_size,
time::duration_alt_display,
};
use workspace::Workspace;
@@ -45,29 +46,10 @@ pub struct TerminalToolInput {
cd: String,
}
-pub struct TerminalTool {
- determine_shell: Shared>,
-}
+pub struct TerminalTool;
impl TerminalTool {
pub const NAME: &str = "terminal";
-
- pub(crate) fn new(cx: &mut App) -> Self {
- let determine_shell = cx.background_spawn(async move {
- if cfg!(windows) {
- return get_system_shell();
- }
-
- if which::which("bash").is_ok() {
- "bash".into()
- } else {
- get_system_shell()
- }
- });
- Self {
- determine_shell: determine_shell.shared(),
- }
- }
}
impl Tool for TerminalTool {
@@ -135,19 +117,6 @@ impl Tool for TerminalTool {
Ok(dir) => dir,
Err(err) => return Task::ready(Err(err)).into(),
};
- let program = self.determine_shell.clone();
- let command = if cfg!(windows) {
- format!("$null | & {{{}}}", input.command.replace("\"", "'"))
- } else if let Some(cwd) = working_dir
- .as_ref()
- .and_then(|cwd| cwd.as_os_str().to_str())
- {
- // Make sure once we're *inside* the shell, we cd into `cwd`
- format!("(cd {cwd}; {}) Task::ready(None).shared(),
};
+ let remote_shell = project.update(cx, |project, cx| {
+ project
+ .remote_client()
+ .and_then(|r| r.read(cx).default_system_shell())
+ });
let env = cx.spawn(async move |_| {
let mut env = env.await.unwrap_or_default();
@@ -171,8 +145,13 @@ impl Tool for TerminalTool {
let task = cx.background_spawn(async move {
let env = env.await;
let pty_system = native_pty_system();
- let program = program.await;
- let mut cmd = CommandBuilder::new(program);
+ let (command, args) = ShellBuilder::new(
+ remote_shell.as_deref(),
+ &Shell::Program(get_default_system_shell()),
+ )
+ .redirect_stdin_to_dev_null()
+ .build(Some(input.command.clone()), &[]);
+ let mut cmd = CommandBuilder::new(command);
cmd.args(args);
for (k, v) in env {
cmd.env(k, v);
@@ -208,16 +187,22 @@ impl Tool for TerminalTool {
};
};
+ let command = input.command.clone();
let terminal = cx.spawn({
let project = project.downgrade();
async move |cx| {
- let program = program.await;
+ let (command, args) = ShellBuilder::new(
+ remote_shell.as_deref(),
+ &Shell::Program(get_default_system_shell()),
+ )
+ .redirect_stdin_to_dev_null()
+ .build(Some(input.command), &[]);
let env = env.await;
project
.update(cx, |project, cx| {
project.create_terminal_task(
task::SpawnInTerminal {
- command: Some(program),
+ command: Some(command),
args,
cwd,
env,
@@ -230,14 +215,8 @@ impl Tool for TerminalTool {
}
});
- let command_markdown = cx.new(|cx| {
- Markdown::new(
- format!("```bash\n{}\n```", input.command).into(),
- None,
- None,
- cx,
- )
- });
+ let command_markdown =
+ cx.new(|cx| Markdown::new(format!("```bash\n{}\n```", command).into(), None, None, cx));
let card = cx.new(|cx| {
TerminalToolCard::new(
@@ -288,7 +267,7 @@ impl Tool for TerminalTool {
let previous_len = content.len();
let (processed_content, finished_with_empty_output) = process_content(
&content,
- &input.command,
+ &command,
exit_status.map(portable_pty::ExitStatus::from),
);
@@ -740,7 +719,6 @@ mod tests {
if cfg!(windows) {
return;
}
-
init_test(&executor, cx);
let fs = Arc::new(RealFs::new(None, executor));
@@ -763,7 +741,7 @@ mod tests {
};
let result = cx.update(|cx| {
TerminalTool::run(
- Arc::new(TerminalTool::new(cx)),
+ Arc::new(TerminalTool),
serde_json::to_value(input).unwrap(),
Arc::default(),
project.clone(),
@@ -783,7 +761,6 @@ mod tests {
if cfg!(windows) {
return;
}
-
init_test(&executor, cx);
let fs = Arc::new(RealFs::new(None, executor));
@@ -798,7 +775,7 @@ mod tests {
let check = |input, expected, cx: &mut App| {
let headless_result = TerminalTool::run(
- Arc::new(TerminalTool::new(cx)),
+ Arc::new(TerminalTool),
serde_json::to_value(input).unwrap(),
Arc::default(),
project.clone(),
diff --git a/crates/audio/src/audio.rs b/crates/audio/src/audio.rs
index ab8d85cdaa6bab7ed1be3fdab8a66b42f883533b..f60ddb87b9615d2da9c2be248ab397c19a463616 100644
--- a/crates/audio/src/audio.rs
+++ b/crates/audio/src/audio.rs
@@ -211,7 +211,7 @@ impl Audio {
agc_source.set_enabled(LIVE_SETTINGS.control_input_volume.load(Ordering::Relaxed));
})
.replayable(REPLAY_DURATION)
- .expect("REPLAY_DURATION is longer then 100ms");
+ .expect("REPLAY_DURATION is longer than 100ms");
cx.update_default_global(|this: &mut Self, _cx| {
let output_mixer = this
diff --git a/crates/audio/src/rodio_ext.rs b/crates/audio/src/rodio_ext.rs
index ba4e4ff0554dd3c9bc2a7e2691de270c0d00908b..e80b00e15a8fdbd3fc438b78a9ca45d0902dcef1 100644
--- a/crates/audio/src/rodio_ext.rs
+++ b/crates/audio/src/rodio_ext.rs
@@ -57,7 +57,7 @@ impl RodioExt for S {
/// replay is being read
///
/// # Errors
- /// If duration is smaller then 100ms
+ /// If duration is smaller than 100ms
fn replayable(
self,
duration: Duration,
@@ -151,7 +151,7 @@ impl Source for TakeSamples {
struct ReplayQueue {
inner: ArrayQueue>,
normal_chunk_len: usize,
- /// The last chunk in the queue may be smaller then
+ /// The last chunk in the queue may be smaller than
/// the normal chunk size. This is always equal to the
/// size of the last element in the queue.
/// (so normally chunk_size)
@@ -535,7 +535,7 @@ mod tests {
let (mut replay, mut source) = input
.replayable(Duration::from_secs(3))
- .expect("longer then 100ms");
+ .expect("longer than 100ms");
source.by_ref().take(3).count();
let yielded: Vec = replay.by_ref().take(3).collect();
@@ -552,7 +552,7 @@ mod tests {
let (mut replay, mut source) = input
.replayable(Duration::from_secs(2))
- .expect("longer then 100ms");
+ .expect("longer than 100ms");
source.by_ref().take(5).count(); // get all items but do not end the source
let yielded: Vec = replay.by_ref().take(2).collect();
@@ -567,7 +567,7 @@ mod tests {
let (replay, mut source) = input
.replayable(Duration::from_secs(2))
- .expect("longer then 100ms");
+ .expect("longer than 100ms");
// exhaust but do not yet end source
source.by_ref().take(40_000).count();
@@ -586,7 +586,7 @@ mod tests {
let input = StaticSamplesBuffer::new(nz!(1), nz!(16_000), &[0.0; 40_000]);
let (mut replay, source) = input
.replayable(Duration::from_secs(2))
- .expect("longer then 100ms");
+ .expect("longer than 100ms");
assert_eq!(replay.by_ref().samples_ready(), 0);
source.take(8000).count(); // half a second
diff --git a/crates/auto_update/Cargo.toml b/crates/auto_update/Cargo.toml
index 1a772710c98f8437932d6e8918df65d003d7962e..35cef84f0366b16d9e31b2416e7a6a10173ff5ef 100644
--- a/crates/auto_update/Cargo.toml
+++ b/crates/auto_update/Cargo.toml
@@ -32,3 +32,6 @@ workspace-hack.workspace = true
[target.'cfg(not(target_os = "windows"))'.dependencies]
which.workspace = true
+
+[dev-dependencies]
+gpui = { workspace = true, "features" = ["test-support"] }
diff --git a/crates/auto_update/src/auto_update.rs b/crates/auto_update/src/auto_update.rs
index f5b211bf8f7099c6e29f5a5e68c49e335426b668..4e0348575e687c2b4e36fcde7df83b8f329733d0 100644
--- a/crates/auto_update/src/auto_update.rs
+++ b/crates/auto_update/src/auto_update.rs
@@ -33,7 +33,7 @@ actions!(
/// Checks for available updates.
Check,
/// Dismisses the update error message.
- DismissErrorMessage,
+ DismissMessage,
/// Opens the release notes for the current version in a browser.
ViewReleaseNotes,
]
@@ -54,14 +54,14 @@ pub enum VersionCheckType {
Semantic(SemanticVersion),
}
-#[derive(Clone, PartialEq, Eq)]
+#[derive(Clone)]
pub enum AutoUpdateStatus {
Idle,
Checking,
Downloading { version: VersionCheckType },
Installing { version: VersionCheckType },
Updated { version: VersionCheckType },
- Errored,
+ Errored { error: Arc },
}
impl AutoUpdateStatus {
@@ -362,7 +362,9 @@ impl AutoUpdater {
}
UpdateCheckType::Manual => {
log::error!("auto-update failed: error:{:?}", error);
- AutoUpdateStatus::Errored
+ AutoUpdateStatus::Errored {
+ error: Arc::new(error),
+ }
}
};
@@ -381,8 +383,8 @@ impl AutoUpdater {
self.status.clone()
}
- pub fn dismiss_error(&mut self, cx: &mut Context) -> bool {
- if self.status == AutoUpdateStatus::Idle {
+ pub fn dismiss(&mut self, cx: &mut Context) -> bool {
+ if let AutoUpdateStatus::Idle = self.status {
return false;
}
self.status = AutoUpdateStatus::Idle;
@@ -971,8 +973,27 @@ pub fn finalize_auto_update_on_quit() {
#[cfg(test)]
mod tests {
+ use gpui::TestAppContext;
+ use settings::default_settings;
+
use super::*;
+ #[gpui::test]
+ fn test_auto_update_defaults_to_true(cx: &mut TestAppContext) {
+ cx.update(|cx| {
+ let mut store = SettingsStore::new(cx, &settings::default_settings());
+ store
+ .set_default_settings(&default_settings(), cx)
+ .expect("Unable to set default settings");
+ store
+ .set_user_settings("{}", cx)
+ .expect("Unable to set user settings");
+ cx.set_global(store);
+ AutoUpdateSetting::register(cx);
+ assert!(AutoUpdateSetting::get_global(cx).0);
+ });
+ }
+
#[test]
fn test_stable_does_not_update_when_fetched_version_is_not_higher() {
let release_channel = ReleaseChannel::Stable;
diff --git a/crates/cli/src/main.rs b/crates/cli/src/main.rs
index d4b4a350f61b5bd1249b33ff3925dd281e9d529c..d0d30f72d7aea7d7f6cf0355caf12b1f2a36eedb 100644
--- a/crates/cli/src/main.rs
+++ b/crates/cli/src/main.rs
@@ -339,59 +339,70 @@ fn main() -> Result<()> {
"Dev servers were removed in v0.157.x please upgrade to SSH remoting: https://zed.dev/docs/remote-development"
);
- let sender: JoinHandle> = thread::spawn({
- let exit_status = exit_status.clone();
- let user_data_dir_for_thread = user_data_dir.clone();
- move || {
- let (_, handshake) = server.accept().context("Handshake after Zed spawn")?;
- let (tx, rx) = (handshake.requests, handshake.responses);
-
- #[cfg(target_os = "windows")]
- let wsl = args.wsl;
- #[cfg(not(target_os = "windows"))]
- let wsl = None;
-
- tx.send(CliRequest::Open {
- paths,
- urls,
- diff_paths,
- wsl,
- wait: args.wait,
- open_new_workspace,
- env,
- user_data_dir: user_data_dir_for_thread,
- })?;
-
- while let Ok(response) = rx.recv() {
- match response {
- CliResponse::Ping => {}
- CliResponse::Stdout { message } => println!("{message}"),
- CliResponse::Stderr { message } => eprintln!("{message}"),
- CliResponse::Exit { status } => {
- exit_status.lock().replace(status);
- return Ok(());
+ let sender: JoinHandle> = thread::Builder::new()
+ .name("CliReceiver".to_string())
+ .spawn({
+ let exit_status = exit_status.clone();
+ let user_data_dir_for_thread = user_data_dir.clone();
+ move || {
+ let (_, handshake) = server.accept().context("Handshake after Zed spawn")?;
+ let (tx, rx) = (handshake.requests, handshake.responses);
+
+ #[cfg(target_os = "windows")]
+ let wsl = args.wsl;
+ #[cfg(not(target_os = "windows"))]
+ let wsl = None;
+
+ tx.send(CliRequest::Open {
+ paths,
+ urls,
+ diff_paths,
+ wsl,
+ wait: args.wait,
+ open_new_workspace,
+ env,
+ user_data_dir: user_data_dir_for_thread,
+ })?;
+
+ while let Ok(response) = rx.recv() {
+ match response {
+ CliResponse::Ping => {}
+ CliResponse::Stdout { message } => println!("{message}"),
+ CliResponse::Stderr { message } => eprintln!("{message}"),
+ CliResponse::Exit { status } => {
+ exit_status.lock().replace(status);
+ return Ok(());
+ }
}
}
- }
- Ok(())
- }
- });
+ Ok(())
+ }
+ })
+ .unwrap();
let stdin_pipe_handle: Option>> =
stdin_tmp_file.map(|mut tmp_file| {
- thread::spawn(move || {
- let mut stdin = std::io::stdin().lock();
- if !io::IsTerminal::is_terminal(&stdin) {
- io::copy(&mut stdin, &mut tmp_file)?;
- }
- Ok(())
- })
+ thread::Builder::new()
+ .name("CliStdin".to_string())
+ .spawn(move || {
+ let mut stdin = std::io::stdin().lock();
+ if !io::IsTerminal::is_terminal(&stdin) {
+ io::copy(&mut stdin, &mut tmp_file)?;
+ }
+ Ok(())
+ })
+ .unwrap()
});
let anonymous_fd_pipe_handles: Vec<_> = anonymous_fd_tmp_files
.into_iter()
- .map(|(mut file, mut tmp_file)| thread::spawn(move || io::copy(&mut file, &mut tmp_file)))
+ .map(|(mut file, mut tmp_file)| {
+ thread::Builder::new()
+ .name("CliAnonymousFd".to_string())
+ .spawn(move || io::copy(&mut file, &mut tmp_file))
+ .unwrap()
+ })
.collect();
if args.foreground {
diff --git a/crates/client/src/client.rs b/crates/client/src/client.rs
index a1ac476bbba40d97d611c3016c0f06a6cb08f2ae..237eaa11d954db7c95eaa513e8e921a1000faac6 100644
--- a/crates/client/src/client.rs
+++ b/crates/client/src/client.rs
@@ -22,7 +22,7 @@ use futures::{
channel::oneshot, future::BoxFuture,
};
use gpui::{App, AsyncApp, Entity, Global, Task, WeakEntity, actions};
-use http_client::{HttpClient, HttpClientWithUrl, http};
+use http_client::{HttpClient, HttpClientWithUrl, http, read_proxy_from_env};
use parking_lot::RwLock;
use postage::watch;
use proxy::connect_proxy_stream;
@@ -136,6 +136,20 @@ pub struct ProxySettings {
pub proxy: Option,
}
+impl ProxySettings {
+ pub fn proxy_url(&self) -> Option {
+ self.proxy
+ .as_ref()
+ .and_then(|input| {
+ input
+ .parse::()
+ .inspect_err(|e| log::error!("Error parsing proxy settings: {}", e))
+ .ok()
+ })
+ .or_else(read_proxy_from_env)
+ }
+}
+
impl Settings for ProxySettings {
fn from_defaults(content: &settings::SettingsContent, _cx: &mut App) -> Self {
Self {
diff --git a/crates/client/src/user.rs b/crates/client/src/user.rs
index 63626e8ce1f3b25c742f227a56556545762367c3..de0668b406c512eabfc70f4702466f013eb8c515 100644
--- a/crates/client/src/user.rs
+++ b/crates/client/src/user.rs
@@ -754,6 +754,10 @@ impl UserStore {
}
pub fn model_request_usage(&self) -> Option {
+ if self.plan().is_some_and(|plan| plan.is_v2()) {
+ return None;
+ }
+
self.model_request_usage
}
diff --git a/crates/cloud_llm_client/src/cloud_llm_client.rs b/crates/cloud_llm_client/src/cloud_llm_client.rs
index 16267d86d806387140016dc0a25021ad92607ff2..24923a318441afeaa2521064b4f433ab9ee1e55f 100644
--- a/crates/cloud_llm_client/src/cloud_llm_client.rs
+++ b/crates/cloud_llm_client/src/cloud_llm_client.rs
@@ -39,7 +39,7 @@ pub const EDIT_PREDICTIONS_RESOURCE_HEADER_VALUE: &str = "edit_predictions";
/// The name of the header used to indicate that the maximum number of consecutive tool uses has been reached.
pub const TOOL_USE_LIMIT_REACHED_HEADER_NAME: &str = "x-zed-tool-use-limit-reached";
-/// The name of the header used to indicate the the minimum required Zed version.
+/// The name of the header used to indicate the minimum required Zed version.
///
/// This can be used to force a Zed upgrade in order to continue communicating
/// with the LLM service.
@@ -321,8 +321,8 @@ pub struct LanguageModel {
#[derive(Debug, Serialize, Deserialize)]
pub struct ListModelsResponse {
pub models: Vec,
- pub default_model: LanguageModelId,
- pub default_fast_model: LanguageModelId,
+ pub default_model: Option,
+ pub default_fast_model: Option,
pub recommended_models: Vec,
}
diff --git a/crates/collab/k8s/collab.template.yml b/crates/collab/k8s/collab.template.yml
index 214b550ac20499b8b03cfafeefab9b45d51fcc24..1476e5890283c62cee3563a327fcdd5ee84842e7 100644
--- a/crates/collab/k8s/collab.template.yml
+++ b/crates/collab/k8s/collab.template.yml
@@ -226,12 +226,6 @@ spec:
secretKeyRef:
name: supermaven
key: api_key
- - name: USER_BACKFILLER_GITHUB_ACCESS_TOKEN
- valueFrom:
- secretKeyRef:
- name: user-backfiller
- key: github_access_token
- optional: true
- name: INVITE_LINK_PREFIX
value: ${INVITE_LINK_PREFIX}
- name: RUST_BACKTRACE
diff --git a/crates/collab/src/db/queries/users.rs b/crates/collab/src/db/queries/users.rs
index 4b0f66fcbe09d23af58b0a30ffebf68455651fd8..89211130b88c69d4bf524bba25ae116790321d3e 100644
--- a/crates/collab/src/db/queries/users.rs
+++ b/crates/collab/src/db/queries/users.rs
@@ -342,79 +342,6 @@ impl Database {
result
}
- /// Returns all feature flags.
- pub async fn list_feature_flags(&self) -> Result> {
- self.transaction(|tx| async move { Ok(feature_flag::Entity::find().all(&*tx).await?) })
- .await
- }
-
- /// Creates a new feature flag.
- pub async fn create_user_flag(&self, flag: &str, enabled_for_all: bool) -> Result {
- self.transaction(|tx| async move {
- let flag = feature_flag::Entity::insert(feature_flag::ActiveModel {
- flag: ActiveValue::set(flag.to_string()),
- enabled_for_all: ActiveValue::set(enabled_for_all),
- ..Default::default()
- })
- .exec(&*tx)
- .await?
- .last_insert_id;
-
- Ok(flag)
- })
- .await
- }
-
- /// Add the given user to the feature flag
- pub async fn add_user_flag(&self, user: UserId, flag: FlagId) -> Result<()> {
- self.transaction(|tx| async move {
- user_feature::Entity::insert(user_feature::ActiveModel {
- user_id: ActiveValue::set(user),
- feature_id: ActiveValue::set(flag),
- })
- .exec(&*tx)
- .await?;
-
- Ok(())
- })
- .await
- }
-
- /// Returns the active flags for the user.
- pub async fn get_user_flags(&self, user: UserId) -> Result> {
- self.transaction(|tx| async move {
- #[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)]
- enum QueryAs {
- Flag,
- }
-
- let flags_enabled_for_all = feature_flag::Entity::find()
- .filter(feature_flag::Column::EnabledForAll.eq(true))
- .select_only()
- .column(feature_flag::Column::Flag)
- .into_values::<_, QueryAs>()
- .all(&*tx)
- .await?;
-
- let flags_enabled_for_user = user::Model {
- id: user,
- ..Default::default()
- }
- .find_linked(user::UserFlags)
- .select_only()
- .column(feature_flag::Column::Flag)
- .into_values::<_, QueryAs>()
- .all(&*tx)
- .await?;
-
- let mut all_flags = HashSet::from_iter(flags_enabled_for_all);
- all_flags.extend(flags_enabled_for_user);
-
- Ok(all_flags.into_iter().collect())
- })
- .await
- }
-
pub async fn get_users_missing_github_user_created_at(&self) -> Result> {
self.transaction(|tx| async move {
Ok(user::Entity::find()
diff --git a/crates/collab/src/db/tables.rs b/crates/collab/src/db/tables.rs
index 0082a9fb030a27e4be13af725f08ea9c82217377..32c4570af5893b503f0fcfdaa1759616cf9be387 100644
--- a/crates/collab/src/db/tables.rs
+++ b/crates/collab/src/db/tables.rs
@@ -13,7 +13,6 @@ pub mod contributor;
pub mod embedding;
pub mod extension;
pub mod extension_version;
-pub mod feature_flag;
pub mod follower;
pub mod language_server;
pub mod notification;
@@ -29,7 +28,6 @@ pub mod room_participant;
pub mod server;
pub mod signup;
pub mod user;
-pub mod user_feature;
pub mod worktree;
pub mod worktree_diagnostic_summary;
pub mod worktree_entry;
diff --git a/crates/collab/src/db/tables/feature_flag.rs b/crates/collab/src/db/tables/feature_flag.rs
deleted file mode 100644
index 5bbfedd71e70b7f1cc58219475c49c28bc62ff3d..0000000000000000000000000000000000000000
--- a/crates/collab/src/db/tables/feature_flag.rs
+++ /dev/null
@@ -1,41 +0,0 @@
-use sea_orm::entity::prelude::*;
-
-use crate::db::FlagId;
-
-#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
-#[sea_orm(table_name = "feature_flags")]
-pub struct Model {
- #[sea_orm(primary_key)]
- pub id: FlagId,
- pub flag: String,
- pub enabled_for_all: bool,
-}
-
-#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
-pub enum Relation {
- #[sea_orm(has_many = "super::user_feature::Entity")]
- UserFeature,
-}
-
-impl Related for Entity {
- fn to() -> RelationDef {
- Relation::UserFeature.def()
- }
-}
-
-impl ActiveModelBehavior for ActiveModel {}
-
-pub struct FlaggedUsers;
-
-impl Linked for FlaggedUsers {
- type FromEntity = Entity;
-
- type ToEntity = super::user::Entity;
-
- fn link(&self) -> Vec {
- vec![
- super::user_feature::Relation::Flag.def().rev(),
- super::user_feature::Relation::User.def(),
- ]
- }
-}
diff --git a/crates/collab/src/db/tables/user.rs b/crates/collab/src/db/tables/user.rs
index af43fe300a6cc1224487541ca72af9d887a6fae3..8e8c03fafc92127f8754f473e04dfab39592ea14 100644
--- a/crates/collab/src/db/tables/user.rs
+++ b/crates/collab/src/db/tables/user.rs
@@ -35,8 +35,6 @@ pub enum Relation {
HostedProjects,
#[sea_orm(has_many = "super::channel_member::Entity")]
ChannelMemberships,
- #[sea_orm(has_many = "super::user_feature::Entity")]
- UserFeatures,
#[sea_orm(has_one = "super::contributor::Entity")]
Contributor,
}
@@ -84,25 +82,4 @@ impl Related for Entity {
}
}
-impl Related for Entity {
- fn to() -> RelationDef {
- Relation::UserFeatures.def()
- }
-}
-
impl ActiveModelBehavior for ActiveModel {}
-
-pub struct UserFlags;
-
-impl Linked for UserFlags {
- type FromEntity = Entity;
-
- type ToEntity = super::feature_flag::Entity;
-
- fn link(&self) -> Vec {
- vec![
- super::user_feature::Relation::User.def().rev(),
- super::user_feature::Relation::Flag.def(),
- ]
- }
-}
diff --git a/crates/collab/src/db/tables/user_feature.rs b/crates/collab/src/db/tables/user_feature.rs
deleted file mode 100644
index cc24b5e796342f7733f59933362d46a0df2be112..0000000000000000000000000000000000000000
--- a/crates/collab/src/db/tables/user_feature.rs
+++ /dev/null
@@ -1,42 +0,0 @@
-use sea_orm::entity::prelude::*;
-
-use crate::db::{FlagId, UserId};
-
-#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
-#[sea_orm(table_name = "user_features")]
-pub struct Model {
- #[sea_orm(primary_key)]
- pub user_id: UserId,
- #[sea_orm(primary_key)]
- pub feature_id: FlagId,
-}
-
-#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
-pub enum Relation {
- #[sea_orm(
- belongs_to = "super::feature_flag::Entity",
- from = "Column::FeatureId",
- to = "super::feature_flag::Column::Id"
- )]
- Flag,
- #[sea_orm(
- belongs_to = "super::user::Entity",
- from = "Column::UserId",
- to = "super::user::Column::Id"
- )]
- User,
-}
-
-impl Related for Entity {
- fn to() -> RelationDef {
- Relation::Flag.def()
- }
-}
-
-impl Related for Entity {
- fn to() -> RelationDef {
- Relation::User.def()
- }
-}
-
-impl ActiveModelBehavior for ActiveModel {}
diff --git a/crates/collab/src/db/tests.rs b/crates/collab/src/db/tests.rs
index 25e03f1320a25455ede347b43477761d591fbd57..141262d5e94a4bf1d4d897e78f6281ab9ee3ccfc 100644
--- a/crates/collab/src/db/tests.rs
+++ b/crates/collab/src/db/tests.rs
@@ -6,7 +6,6 @@ mod db_tests;
#[cfg(target_os = "macos")]
mod embedding_tests;
mod extension_tests;
-mod feature_flag_tests;
mod user_tests;
use crate::migrations::run_database_migrations;
diff --git a/crates/collab/src/db/tests/feature_flag_tests.rs b/crates/collab/src/db/tests/feature_flag_tests.rs
deleted file mode 100644
index 0e68dcc941cdb2488c3822548dada56746667bc2..0000000000000000000000000000000000000000
--- a/crates/collab/src/db/tests/feature_flag_tests.rs
+++ /dev/null
@@ -1,66 +0,0 @@
-use crate::{
- db::{Database, NewUserParams},
- test_both_dbs,
-};
-use pretty_assertions::assert_eq;
-use std::sync::Arc;
-
-test_both_dbs!(
- test_get_user_flags,
- test_get_user_flags_postgres,
- test_get_user_flags_sqlite
-);
-
-async fn test_get_user_flags(db: &Arc) {
- let user_1 = db
- .create_user(
- "user1@example.com",
- None,
- false,
- NewUserParams {
- github_login: "user1".to_string(),
- github_user_id: 1,
- },
- )
- .await
- .unwrap()
- .user_id;
-
- let user_2 = db
- .create_user(
- "user2@example.com",
- None,
- false,
- NewUserParams {
- github_login: "user2".to_string(),
- github_user_id: 2,
- },
- )
- .await
- .unwrap()
- .user_id;
-
- const FEATURE_FLAG_ONE: &str = "brand-new-ux";
- const FEATURE_FLAG_TWO: &str = "cool-feature";
- const FEATURE_FLAG_THREE: &str = "feature-enabled-for-everyone";
-
- let feature_flag_one = db.create_user_flag(FEATURE_FLAG_ONE, false).await.unwrap();
- let feature_flag_two = db.create_user_flag(FEATURE_FLAG_TWO, false).await.unwrap();
- db.create_user_flag(FEATURE_FLAG_THREE, true).await.unwrap();
-
- db.add_user_flag(user_1, feature_flag_one).await.unwrap();
- db.add_user_flag(user_1, feature_flag_two).await.unwrap();
-
- db.add_user_flag(user_2, feature_flag_one).await.unwrap();
-
- let mut user_1_flags = db.get_user_flags(user_1).await.unwrap();
- user_1_flags.sort();
- assert_eq!(
- user_1_flags,
- &[FEATURE_FLAG_ONE, FEATURE_FLAG_TWO, FEATURE_FLAG_THREE]
- );
-
- let mut user_2_flags = db.get_user_flags(user_2).await.unwrap();
- user_2_flags.sort();
- assert_eq!(user_2_flags, &[FEATURE_FLAG_ONE, FEATURE_FLAG_THREE]);
-}
diff --git a/crates/collab/src/lib.rs b/crates/collab/src/lib.rs
index 191025df3770db78df3a12bc16d5c8f32d54571c..f1de0cdc7ff79cd25c8ef7b0b2b21d9e0b45d332 100644
--- a/crates/collab/src/lib.rs
+++ b/crates/collab/src/lib.rs
@@ -7,7 +7,6 @@ pub mod llm;
pub mod migrations;
pub mod rpc;
pub mod seed;
-pub mod user_backfiller;
#[cfg(test)]
mod tests;
@@ -157,7 +156,6 @@ pub struct Config {
pub slack_panics_webhook: Option,
pub auto_join_channel_id: Option,
pub supermaven_admin_api_key: Option>,
- pub user_backfiller_github_access_token: Option>,
}
impl Config {
@@ -211,7 +209,6 @@ impl Config {
migrations_path: None,
seed_path: None,
supermaven_admin_api_key: None,
- user_backfiller_github_access_token: None,
kinesis_region: None,
kinesis_access_key: None,
kinesis_secret_key: None,
diff --git a/crates/collab/src/main.rs b/crates/collab/src/main.rs
index cb6f6cad1dd483c463bcda5d8a4ff914f4bf10aa..6b94459910647c1e48ee69f2b0dd38afd3723821 100644
--- a/crates/collab/src/main.rs
+++ b/crates/collab/src/main.rs
@@ -11,7 +11,6 @@ use collab::ServiceMode;
use collab::api::CloudflareIpCountryHeader;
use collab::llm::db::LlmDatabase;
use collab::migrations::run_database_migrations;
-use collab::user_backfiller::spawn_user_backfiller;
use collab::{
AppState, Config, Result, api::fetch_extensions_from_blob_store_periodically, db, env,
executor::Executor, rpc::ResultExt,
@@ -114,7 +113,6 @@ async fn main() -> Result<()> {
if mode.is_api() {
fetch_extensions_from_blob_store_periodically(state.clone());
- spawn_user_backfiller(state.clone());
app = app
.merge(collab::api::events::router())
diff --git a/crates/collab/src/seed.rs b/crates/collab/src/seed.rs
index 2d070b30abada79dc177b2b600d9ecc40aa472e1..5f5779e1e4990d1a03461bb3ec2222e82d9f544e 100644
--- a/crates/collab/src/seed.rs
+++ b/crates/collab/src/seed.rs
@@ -46,27 +46,6 @@ pub async fn seed(config: &Config, db: &Database, force: bool) -> anyhow::Result
let mut first_user = None;
let mut others = vec![];
- let flag_names = ["language-models"];
- let mut flags = Vec::new();
-
- let existing_feature_flags = db.list_feature_flags().await?;
-
- for flag_name in flag_names {
- if existing_feature_flags
- .iter()
- .any(|flag| flag.flag == flag_name)
- {
- log::info!("Flag {flag_name:?} already exists");
- continue;
- }
-
- let flag = db
- .create_user_flag(flag_name, false)
- .await
- .unwrap_or_else(|err| panic!("failed to create flag: '{flag_name}': {err}"));
- flags.push(flag);
- }
-
for admin_login in seed_config.admins {
let user = fetch_github::(
&client,
@@ -90,15 +69,6 @@ pub async fn seed(config: &Config, db: &Database, force: bool) -> anyhow::Result
} else {
others.push(user.user_id)
}
-
- for flag in &flags {
- db.add_user_flag(user.user_id, *flag)
- .await
- .context(format!(
- "Unable to enable flag '{}' for user '{}'",
- flag, user.user_id
- ))?;
- }
}
for channel in seed_config.channels {
@@ -126,24 +96,16 @@ pub async fn seed(config: &Config, db: &Database, force: bool) -> anyhow::Result
for github_user in github_users {
log::info!("Seeding {:?} from GitHub", github_user.login);
- let user = db
- .update_or_create_user_by_github_account(
- &github_user.login,
- github_user.id,
- github_user.email.as_deref(),
- github_user.name.as_deref(),
- github_user.created_at,
- None,
- )
- .await
- .expect("failed to insert user");
-
- for flag in &flags {
- db.add_user_flag(user.id, *flag).await.context(format!(
- "Unable to enable flag '{}' for user '{}'",
- flag, user.id
- ))?;
- }
+ db.update_or_create_user_by_github_account(
+ &github_user.login,
+ github_user.id,
+ github_user.email.as_deref(),
+ github_user.name.as_deref(),
+ github_user.created_at,
+ None,
+ )
+ .await
+ .expect("failed to insert user");
}
Ok(())
diff --git a/crates/collab/src/tests/test_server.rs b/crates/collab/src/tests/test_server.rs
index eb7df28478158a10a0c2d52c3560cad391937383..5e99cc192ad080c1a79913c79fbbaae9d8b6d951 100644
--- a/crates/collab/src/tests/test_server.rs
+++ b/crates/collab/src/tests/test_server.rs
@@ -604,7 +604,6 @@ impl TestServer {
migrations_path: None,
seed_path: None,
supermaven_admin_api_key: None,
- user_backfiller_github_access_token: None,
kinesis_region: None,
kinesis_stream: None,
kinesis_access_key: None,
diff --git a/crates/collab/src/user_backfiller.rs b/crates/collab/src/user_backfiller.rs
deleted file mode 100644
index fdb9ef67c2f1d04bf0a1919045f91d75a14ef834..0000000000000000000000000000000000000000
--- a/crates/collab/src/user_backfiller.rs
+++ /dev/null
@@ -1,165 +0,0 @@
-use std::sync::Arc;
-
-use anyhow::{Context as _, Result};
-use chrono::{DateTime, Utc};
-use util::ResultExt;
-
-use crate::db::Database;
-use crate::executor::Executor;
-use crate::{AppState, Config};
-
-pub fn spawn_user_backfiller(app_state: Arc) {
- let Some(user_backfiller_github_access_token) =
- app_state.config.user_backfiller_github_access_token.clone()
- else {
- log::info!("no USER_BACKFILLER_GITHUB_ACCESS_TOKEN set; not spawning user backfiller");
- return;
- };
-
- let executor = app_state.executor.clone();
- executor.spawn_detached({
- let executor = executor.clone();
- async move {
- let user_backfiller = UserBackfiller::new(
- app_state.config.clone(),
- user_backfiller_github_access_token,
- app_state.db.clone(),
- executor,
- );
-
- log::info!("backfilling users");
-
- user_backfiller
- .backfill_github_user_created_at()
- .await
- .log_err();
- }
- });
-}
-
-const GITHUB_REQUESTS_PER_HOUR_LIMIT: usize = 5_000;
-const SLEEP_DURATION_BETWEEN_USERS: std::time::Duration = std::time::Duration::from_millis(
- (GITHUB_REQUESTS_PER_HOUR_LIMIT as f64 / 60. / 60. * 1000.) as u64,
-);
-
-struct UserBackfiller {
- config: Config,
- github_access_token: Arc,
- db: Arc,
- http_client: reqwest::Client,
- executor: Executor,
-}
-
-impl UserBackfiller {
- fn new(
- config: Config,
- github_access_token: Arc,
- db: Arc,
- executor: Executor,
- ) -> Self {
- Self {
- config,
- github_access_token,
- db,
- http_client: reqwest::Client::new(),
- executor,
- }
- }
-
- async fn backfill_github_user_created_at(&self) -> Result<()> {
- let initial_channel_id = self.config.auto_join_channel_id;
-
- let users_missing_github_user_created_at =
- self.db.get_users_missing_github_user_created_at().await?;
-
- for user in users_missing_github_user_created_at {
- match self
- .fetch_github_user(&format!(
- "https://api.github.com/user/{}",
- user.github_user_id
- ))
- .await
- {
- Ok(github_user) => {
- self.db
- .update_or_create_user_by_github_account(
- &user.github_login,
- github_user.id,
- user.email_address.as_deref(),
- user.name.as_deref(),
- github_user.created_at,
- initial_channel_id,
- )
- .await?;
-
- log::info!("backfilled user: {}", user.github_login);
- }
- Err(err) => {
- log::error!("failed to fetch GitHub user {}: {err}", user.github_login);
- }
- }
-
- self.executor.sleep(SLEEP_DURATION_BETWEEN_USERS).await;
- }
-
- Ok(())
- }
-
- async fn fetch_github_user(&self, url: &str) -> Result {
- let response = self
- .http_client
- .get(url)
- .header(
- "authorization",
- format!("Bearer {}", self.github_access_token),
- )
- .header("user-agent", "zed")
- .send()
- .await
- .with_context(|| format!("failed to fetch '{url}'"))?;
-
- let rate_limit_remaining = response
- .headers()
- .get("x-ratelimit-remaining")
- .and_then(|value| value.to_str().ok())
- .and_then(|value| value.parse::().ok());
- let rate_limit_reset = response
- .headers()
- .get("x-ratelimit-reset")
- .and_then(|value| value.to_str().ok())
- .and_then(|value| value.parse::().ok())
- .and_then(|value| DateTime::from_timestamp(value, 0));
-
- if rate_limit_remaining == Some(0)
- && let Some(reset_at) = rate_limit_reset
- {
- let now = Utc::now();
- if reset_at > now {
- let sleep_duration = reset_at - now;
- log::info!(
- "rate limit reached. Sleeping for {} seconds",
- sleep_duration.num_seconds()
- );
- self.executor.sleep(sleep_duration.to_std().unwrap()).await;
- }
- }
-
- response
- .error_for_status()
- .context("fetching GitHub user")?
- .json()
- .await
- .with_context(|| format!("failed to deserialize GitHub user from '{url}'"))
- }
-}
-
-#[derive(serde::Deserialize)]
-struct GithubUser {
- id: i32,
- created_at: DateTime,
- #[expect(
- unused,
- reason = "This field was found to be unused with serde library bump; it's left as is due to insufficient context on PO's side, but it *may* be fine to remove"
- )]
- name: Option,
-}
diff --git a/crates/crashes/src/crashes.rs b/crates/crashes/src/crashes.rs
index 98db4bfc73f157994f3f7286c0764cfb0778e4a4..8312638e2a811767ee245f53c356eca15ef852f1 100644
--- a/crates/crashes/src/crashes.rs
+++ b/crates/crashes/src/crashes.rs
@@ -321,16 +321,19 @@ pub fn crash_server(socket: &Path) {
let shutdown = Arc::new(AtomicBool::new(false));
let has_connection = Arc::new(AtomicBool::new(false));
- std::thread::spawn({
- let shutdown = shutdown.clone();
- let has_connection = has_connection.clone();
- move || {
- std::thread::sleep(CRASH_HANDLER_CONNECT_TIMEOUT);
- if !has_connection.load(Ordering::SeqCst) {
- shutdown.store(true, Ordering::SeqCst);
+ thread::Builder::new()
+ .name("CrashServerTimeout".to_owned())
+ .spawn({
+ let shutdown = shutdown.clone();
+ let has_connection = has_connection.clone();
+ move || {
+ std::thread::sleep(CRASH_HANDLER_CONNECT_TIMEOUT);
+ if !has_connection.load(Ordering::SeqCst) {
+ shutdown.store(true, Ordering::SeqCst);
+ }
}
- }
- });
+ })
+ .unwrap();
server
.run(
diff --git a/crates/denoise/Cargo.toml b/crates/denoise/Cargo.toml
new file mode 100644
index 0000000000000000000000000000000000000000..a2f43cdfee72a64fbd7e6e60b9414c691c3adfcd
--- /dev/null
+++ b/crates/denoise/Cargo.toml
@@ -0,0 +1,21 @@
+[package]
+name = "denoise"
+version = "0.1.0"
+edition.workspace = true
+publish.workspace = true
+license = "GPL-3.0-or-later"
+
+[lints]
+workspace = true
+
+[dependencies]
+candle-core = { version = "0.9.1", git ="https://github.com/zed-industries/candle", branch = "9.1-patched" }
+candle-onnx = { version = "0.9.1", git ="https://github.com/zed-industries/candle", branch = "9.1-patched" }
+log.workspace = true
+
+rodio = { workspace = true, features = ["wav_output"] }
+
+rustfft = { version = "6.2.0", features = ["avx"] }
+realfft = "3.4.0"
+thiserror.workspace = true
+workspace-hack.workspace = true
diff --git a/crates/denoise/LICENSE-GPL b/crates/denoise/LICENSE-GPL
new file mode 120000
index 0000000000000000000000000000000000000000..89e542f750cd3860a0598eff0dc34b56d7336dc4
--- /dev/null
+++ b/crates/denoise/LICENSE-GPL
@@ -0,0 +1 @@
+../../LICENSE-GPL
\ No newline at end of file
diff --git a/crates/denoise/README.md b/crates/denoise/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..d7486da36e9078f2f99c2a6a8226dbce499cae8b
--- /dev/null
+++ b/crates/denoise/README.md
@@ -0,0 +1,20 @@
+Real time streaming audio denoising using a [Dual-Signal Transformation LSTM Network for Real-Time Noise Suppression](https://arxiv.org/abs/2005.07551).
+
+Trivial to build as it uses the native rust Candle crate for inference. Easy to integrate into any Rodio pipeline.
+
+```rust
+ # use rodio::{nz, source::UniformSourceIterator, wav_to_file};
+ let file = std::fs::File::open("clips_airconditioning.wav")?;
+ let decoder = rodio::Decoder::try_from(file)?;
+ let resampled = UniformSourceIterator::new(decoder, nz!(1), nz!(16_000));
+
+ let mut denoised = denoise::Denoiser::try_new(resampled)?;
+ wav_to_file(&mut denoised, "denoised.wav")?;
+ Result::Ok<(), Box>
+```
+
+## Acknowledgements & License
+
+The trained models in this repo are optimized versions of the models in the [breizhn/DTLN](https://github.com/breizhn/DTLN?tab=readme-ov-file#model-conversion-and-real-time-processing-with-onnx). These are licensed under MIT.
+
+The FFT code was adapted from Datadog's [dtln-rs Repo](https://github.com/DataDog/dtln-rs/tree/main) also licensed under MIT.
diff --git a/crates/denoise/examples/denoise.rs b/crates/denoise/examples/denoise.rs
new file mode 100644
index 0000000000000000000000000000000000000000..a4d89d7e517e7b35d0f87adbd218cd34b75a4789
--- /dev/null
+++ b/crates/denoise/examples/denoise.rs
@@ -0,0 +1,11 @@
+use rodio::{nz, source::UniformSourceIterator, wav_to_file};
+
+fn main() -> Result<(), Box> {
+ let file = std::fs::File::open("airconditioning.wav")?;
+ let decoder = rodio::Decoder::try_from(file)?;
+ let resampled = UniformSourceIterator::new(decoder, nz!(1), nz!(16_000));
+
+ let mut denoised = denoise::Denoiser::try_new(resampled)?;
+ wav_to_file(&mut denoised, "denoised.wav")?;
+ Ok(())
+}
diff --git a/crates/denoise/examples/enable_disable.rs b/crates/denoise/examples/enable_disable.rs
new file mode 100644
index 0000000000000000000000000000000000000000..1cffadbce2b0e58cdf56b291cb68a13fc6556b22
--- /dev/null
+++ b/crates/denoise/examples/enable_disable.rs
@@ -0,0 +1,23 @@
+use std::time::Duration;
+
+use rodio::Source;
+use rodio::wav_to_file;
+use rodio::{nz, source::UniformSourceIterator};
+
+fn main() -> Result<(), Box> {
+ let file = std::fs::File::open("clips_airconditioning.wav")?;
+ let decoder = rodio::Decoder::try_from(file)?;
+ let resampled = UniformSourceIterator::new(decoder, nz!(1), nz!(16_000));
+
+ let mut enabled = true;
+ let denoised = denoise::Denoiser::try_new(resampled)?.periodic_access(
+ Duration::from_secs(2),
+ |denoised| {
+ enabled = !enabled;
+ denoised.set_enabled(enabled);
+ },
+ );
+
+ wav_to_file(denoised, "processed.wav")?;
+ Ok(())
+}
diff --git a/crates/denoise/models/model_1_converted_simplified.onnx b/crates/denoise/models/model_1_converted_simplified.onnx
new file mode 100644
index 0000000000000000000000000000000000000000..821cb73bd76b1470c0ee814d07bd03c47a613643
Binary files /dev/null and b/crates/denoise/models/model_1_converted_simplified.onnx differ
diff --git a/crates/denoise/models/model_2_converted_simplified.onnx b/crates/denoise/models/model_2_converted_simplified.onnx
new file mode 100644
index 0000000000000000000000000000000000000000..a83023ab22748fb60f8186c3c5b0161531337082
Binary files /dev/null and b/crates/denoise/models/model_2_converted_simplified.onnx differ
diff --git a/crates/denoise/src/engine.rs b/crates/denoise/src/engine.rs
new file mode 100644
index 0000000000000000000000000000000000000000..5196b70b5ba02f665385c022a0dfa9cd22c1db9c
--- /dev/null
+++ b/crates/denoise/src/engine.rs
@@ -0,0 +1,204 @@
+/// use something like https://netron.app/ to inspect the models and understand
+/// the flow
+use std::collections::HashMap;
+
+use candle_core::{Device, IndexOp, Tensor};
+use candle_onnx::onnx::ModelProto;
+use candle_onnx::prost::Message;
+use realfft::RealFftPlanner;
+use rustfft::num_complex::Complex;
+
+pub struct Engine {
+ spectral_model: ModelProto,
+ signal_model: ModelProto,
+
+ fft_planner: RealFftPlanner,
+ fft_scratch: Vec>,
+ spectrum: [Complex; FFT_OUT_SIZE],
+ signal: [f32; BLOCK_LEN],
+
+ in_magnitude: [f32; FFT_OUT_SIZE],
+ in_phase: [f32; FFT_OUT_SIZE],
+
+ spectral_memory: Tensor,
+ signal_memory: Tensor,
+
+ in_buffer: [f32; BLOCK_LEN],
+ out_buffer: [f32; BLOCK_LEN],
+}
+
+// 32 ms @ 16khz per DTLN docs: https://github.com/breizhn/DTLN
+pub const BLOCK_LEN: usize = 512;
+// 8 ms @ 16khz per DTLN docs.
+pub const BLOCK_SHIFT: usize = 128;
+pub const FFT_OUT_SIZE: usize = BLOCK_LEN / 2 + 1;
+
+impl Engine {
+ pub fn new() -> Self {
+ let mut fft_planner = RealFftPlanner::new();
+ let fft_planned = fft_planner.plan_fft_forward(BLOCK_LEN);
+ let scratch_len = fft_planned.get_scratch_len();
+ Self {
+ // Models are 1.5MB and 2.5MB respectively. Its worth the binary
+ // size increase not to have to distribute the models separately.
+ spectral_model: ModelProto::decode(
+ include_bytes!("../models/model_1_converted_simplified.onnx").as_slice(),
+ )
+ .expect("The model should decode"),
+ signal_model: ModelProto::decode(
+ include_bytes!("../models/model_2_converted_simplified.onnx").as_slice(),
+ )
+ .expect("The model should decode"),
+ fft_planner,
+ fft_scratch: vec![Complex::ZERO; scratch_len],
+ spectrum: [Complex::ZERO; FFT_OUT_SIZE],
+ signal: [0f32; BLOCK_LEN],
+
+ in_magnitude: [0f32; FFT_OUT_SIZE],
+ in_phase: [0f32; FFT_OUT_SIZE],
+
+ spectral_memory: Tensor::from_slice::<_, f32>(
+ &[0f32; 512],
+ (1, 2, BLOCK_SHIFT, 2),
+ &Device::Cpu,
+ )
+ .expect("Tensor has the correct dimensions"),
+ signal_memory: Tensor::from_slice::<_, f32>(
+ &[0f32; 512],
+ (1, 2, BLOCK_SHIFT, 2),
+ &Device::Cpu,
+ )
+ .expect("Tensor has the correct dimensions"),
+ out_buffer: [0f32; BLOCK_LEN],
+ in_buffer: [0f32; BLOCK_LEN],
+ }
+ }
+
+ /// Add a clunk of samples and get the denoised chunk 4 feeds later
+ pub fn feed(&mut self, samples: &[f32]) -> [f32; BLOCK_SHIFT] {
+ /// The name of the output node of the onnx network
+ /// [Dual-Signal Transformation LSTM Network for Real-Time Noise Suppression](https://arxiv.org/abs/2005.07551).
+ const MEMORY_OUTPUT: &'static str = "Identity_1";
+
+ debug_assert_eq!(samples.len(), BLOCK_SHIFT);
+
+ // place new samples at the end of the `in_buffer`
+ self.in_buffer.copy_within(BLOCK_SHIFT.., 0);
+ self.in_buffer[(BLOCK_LEN - BLOCK_SHIFT)..].copy_from_slice(&samples);
+
+ // run inference
+ let inputs = self.spectral_inputs();
+ let mut spectral_outputs = candle_onnx::simple_eval(&self.spectral_model, inputs)
+ .expect("The embedded file must be valid");
+ self.spectral_memory = spectral_outputs
+ .remove(MEMORY_OUTPUT)
+ .expect("The model has an output named Identity_1");
+ let inputs = self.signal_inputs(spectral_outputs);
+ let mut signal_outputs = candle_onnx::simple_eval(&self.signal_model, inputs)
+ .expect("The embedded file must be valid");
+ self.signal_memory = signal_outputs
+ .remove(MEMORY_OUTPUT)
+ .expect("The model has an output named Identity_1");
+ let model_output = model_outputs(signal_outputs);
+
+ // place processed samples at the start of the `out_buffer`
+ // shift the rest left, fill the end with zeros. Zeros are needed as
+ // the out buffer is part of the input of the network
+ self.out_buffer.copy_within(BLOCK_SHIFT.., 0);
+ self.out_buffer[BLOCK_LEN - BLOCK_SHIFT..].fill(0f32);
+ for (a, b) in self.out_buffer.iter_mut().zip(model_output) {
+ *a += b;
+ }
+
+ // samples at the front of the `out_buffer` are now denoised
+ self.out_buffer[..BLOCK_SHIFT]
+ .try_into()
+ .expect("len is correct")
+ }
+
+ fn spectral_inputs(&mut self) -> HashMap {
+ // Prepare FFT input
+ let fft = self.fft_planner.plan_fft_forward(BLOCK_LEN);
+
+ // Perform real-to-complex FFT
+ let mut fft_in = self.in_buffer;
+ fft.process_with_scratch(&mut fft_in, &mut self.spectrum, &mut self.fft_scratch)
+ .expect("The fft should run, there is enough scratch space");
+
+ // Generate magnitude and phase
+ for ((magnitude, phase), complex) in self
+ .in_magnitude
+ .iter_mut()
+ .zip(self.in_phase.iter_mut())
+ .zip(self.spectrum)
+ {
+ *magnitude = complex.norm();
+ *phase = complex.arg();
+ }
+
+ const SPECTRUM_INPUT: &str = "input_2";
+ const MEMORY_INPUT: &str = "input_3";
+ let memory_input =
+ Tensor::from_slice::<_, f32>(&self.in_magnitude, (1, 1, FFT_OUT_SIZE), &Device::Cpu)
+ .expect("the in magnitude has enough elements to fill the Tensor");
+
+ let inputs = HashMap::from([
+ (MEMORY_INPUT.to_string(), memory_input),
+ (SPECTRUM_INPUT.to_string(), self.spectral_memory.clone()),
+ ]);
+ inputs
+ }
+
+ fn signal_inputs(&mut self, outputs: HashMap) -> HashMap {
+ let magnitude_weight = model_outputs(outputs);
+
+ // Apply mask and reconstruct complex spectrum
+ let mut spectrum = [Complex::I; FFT_OUT_SIZE];
+ for i in 0..FFT_OUT_SIZE {
+ let magnitude = self.in_magnitude[i] * magnitude_weight[i];
+ let phase = self.in_phase[i];
+ let real = magnitude * phase.cos();
+ let imag = magnitude * phase.sin();
+ spectrum[i] = Complex::new(real, imag);
+ }
+
+ // Handle DC component (i = 0)
+ let magnitude = self.in_magnitude[0] * magnitude_weight[0];
+ spectrum[0] = Complex::new(magnitude, 0.0);
+
+ // Handle Nyquist component (i = N/2)
+ let magnitude = self.in_magnitude[FFT_OUT_SIZE - 1] * magnitude_weight[FFT_OUT_SIZE - 1];
+ spectrum[FFT_OUT_SIZE - 1] = Complex::new(magnitude, 0.0);
+
+ // Perform complex-to-real IFFT
+ let ifft = self.fft_planner.plan_fft_inverse(BLOCK_LEN);
+ ifft.process_with_scratch(&mut spectrum, &mut self.signal, &mut self.fft_scratch)
+ .expect("The fft should run, there is enough scratch space");
+
+ // Normalize the IFFT output
+ for real in &mut self.signal {
+ *real /= BLOCK_LEN as f32;
+ }
+
+ const SIGNAL_INPUT: &str = "input_4";
+ const SIGNAL_MEMORY: &str = "input_5";
+ let signal_input =
+ Tensor::from_slice::<_, f32>(&self.signal, (1, 1, BLOCK_LEN), &Device::Cpu).unwrap();
+
+ HashMap::from([
+ (SIGNAL_INPUT.to_string(), signal_input),
+ (SIGNAL_MEMORY.to_string(), self.signal_memory.clone()),
+ ])
+ }
+}
+
+// Both models put their outputs in the same location
+fn model_outputs(mut outputs: HashMap) -> Vec {
+ const NON_MEMORY_OUTPUT: &str = "Identity";
+ outputs
+ .remove(NON_MEMORY_OUTPUT)
+ .expect("The model has this output")
+ .i((0, 0))
+ .and_then(|tensor| tensor.to_vec1())
+ .expect("The tensor has the correct dimensions")
+}
diff --git a/crates/denoise/src/lib.rs b/crates/denoise/src/lib.rs
new file mode 100644
index 0000000000000000000000000000000000000000..1422c81a4b915d571d35585447165c04d3695b73
--- /dev/null
+++ b/crates/denoise/src/lib.rs
@@ -0,0 +1,273 @@
+mod engine;
+
+use core::fmt;
+use std::{collections::VecDeque, sync::mpsc, thread};
+
+pub use engine::Engine;
+use rodio::{ChannelCount, Sample, SampleRate, Source, nz};
+
+use crate::engine::BLOCK_SHIFT;
+
+const SUPPORTED_SAMPLE_RATE: SampleRate = nz!(16_000);
+const SUPPORTED_CHANNEL_COUNT: ChannelCount = nz!(1);
+
+pub struct Denoiser {
+ inner: S,
+ input_tx: mpsc::Sender<[Sample; BLOCK_SHIFT]>,
+ denoised_rx: mpsc::Receiver<[Sample; BLOCK_SHIFT]>,
+ ready: [Sample; BLOCK_SHIFT],
+ next: usize,
+ state: IterState,
+ // When disabled instead of reading denoised sub-blocks from the engine through
+ // `denoised_rx` we read unprocessed from this queue. This maintains the same
+ // latency so we can 'trivially' re-enable
+ queued: Queue,
+}
+
+impl fmt::Debug for Denoiser {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ f.debug_struct("Denoiser")
+ .field("state", &self.state)
+ .finish_non_exhaustive()
+ }
+}
+
+struct Queue(VecDeque<[Sample; BLOCK_SHIFT]>);
+
+impl Queue {
+ fn new() -> Self {
+ Self(VecDeque::new())
+ }
+ fn push(&mut self, block: [Sample; BLOCK_SHIFT]) {
+ self.0.push_back(block);
+ self.0.resize(4, [0f32; BLOCK_SHIFT]);
+ }
+ fn pop(&mut self) -> [Sample; BLOCK_SHIFT] {
+ debug_assert!(self.0.len() == 4);
+ self.0.pop_front().expect(
+ "There is no State where the queue is popped while there are less then 4 entries",
+ )
+ }
+}
+
+#[derive(Debug, Clone, Copy)]
+pub enum IterState {
+ Enabled,
+ StartingMidAudio { fed_to_denoiser: usize },
+ Disabled,
+ Startup { enabled: bool },
+}
+
+#[derive(Debug, thiserror::Error)]
+pub enum DenoiserError {
+ #[error("This denoiser only works on sources with samplerate 16000")]
+ UnsupportedSampleRate,
+ #[error("This denoiser only works on mono sources (1 channel)")]
+ UnsupportedChannelCount,
+}
+
+// todo dvdsk needs constant source upstream in rodio
+impl Denoiser {
+ pub fn try_new(source: S) -> Result {
+ if source.sample_rate() != SUPPORTED_SAMPLE_RATE {
+ return Err(DenoiserError::UnsupportedSampleRate);
+ }
+ if source.channels() != SUPPORTED_CHANNEL_COUNT {
+ return Err(DenoiserError::UnsupportedChannelCount);
+ }
+
+ let (input_tx, input_rx) = mpsc::channel();
+ let (denoised_tx, denoised_rx) = mpsc::channel();
+
+ thread::Builder::new()
+ .name("NeuralDenoiser".to_owned())
+ .spawn(move || {
+ run_neural_denoiser(denoised_tx, input_rx);
+ })
+ .unwrap();
+
+ Ok(Self {
+ inner: source,
+ input_tx,
+ denoised_rx,
+ ready: [0.0; BLOCK_SHIFT],
+ state: IterState::Startup { enabled: true },
+ next: BLOCK_SHIFT,
+ queued: Queue::new(),
+ })
+ }
+
+ pub fn set_enabled(&mut self, enabled: bool) {
+ self.state = match (enabled, self.state) {
+ (false, IterState::StartingMidAudio { .. }) | (false, IterState::Enabled) => {
+ IterState::Disabled
+ }
+ (false, IterState::Startup { enabled: true }) => IterState::Startup { enabled: false },
+ (true, IterState::Disabled) => IterState::StartingMidAudio { fed_to_denoiser: 0 },
+ (_, state) => state,
+ };
+ }
+
+ fn feed(&self, sub_block: [f32; BLOCK_SHIFT]) {
+ self.input_tx.send(sub_block).unwrap();
+ }
+}
+
+fn run_neural_denoiser(
+ denoised_tx: mpsc::Sender<[f32; BLOCK_SHIFT]>,
+ input_rx: mpsc::Receiver<[f32; BLOCK_SHIFT]>,
+) {
+ let mut engine = Engine::new();
+ loop {
+ let Ok(sub_block) = input_rx.recv() else {
+ // tx must have dropped, stop thread
+ break;
+ };
+
+ let denoised_sub_block = engine.feed(&sub_block);
+ if denoised_tx.send(denoised_sub_block).is_err() {
+ break;
+ }
+ }
+}
+
+impl Source for Denoiser {
+ fn current_span_len(&self) -> Option {
+ self.inner.current_span_len()
+ }
+
+ fn channels(&self) -> rodio::ChannelCount {
+ self.inner.channels()
+ }
+
+ fn sample_rate(&self) -> rodio::SampleRate {
+ self.inner.sample_rate()
+ }
+
+ fn total_duration(&self) -> Option {
+ self.inner.total_duration()
+ }
+}
+
+impl Iterator for Denoiser {
+ type Item = Sample;
+
+ #[inline]
+ fn next(&mut self) -> Option {
+ self.next += 1;
+ if self.next < self.ready.len() {
+ let sample = self.ready[self.next];
+ return Some(sample);
+ }
+
+ // This is a separate function to prevent it from being inlined
+ // as this code only runs once every 128 samples
+ self.prepare_next_ready()
+ .inspect_err(|_| {
+ log::error!("Denoise engine crashed");
+ })
+ .ok()
+ .flatten()
+ }
+}
+
+#[derive(Debug, thiserror::Error)]
+#[error("Could not send or receive from denoise thread. It must have crashed")]
+struct DenoiseEngineCrashed;
+
+impl Denoiser {
+ #[cold]
+ fn prepare_next_ready(&mut self) -> Result