diff --git a/.github/actions/build_docs/action.yml b/.github/actions/build_docs/action.yml
index 9a2d7e1ec7..a7effad247 100644
--- a/.github/actions/build_docs/action.yml
+++ b/.github/actions/build_docs/action.yml
@@ -19,7 +19,7 @@ runs:
shell: bash -euxo pipefail {0}
run: ./script/linux
- - name: Check for broken links
+ - name: Check for broken links (in MD)
uses: lycheeverse/lychee-action@82202e5e9c2f4ef1a55a3d02563e1cb6041e5332 # v2.4.1
with:
args: --no-progress --exclude '^http' './docs/src/**/*'
@@ -30,3 +30,9 @@ runs:
run: |
mkdir -p target/deploy
mdbook build ./docs --dest-dir=../target/deploy/docs/
+
+ - name: Check for broken links (in HTML)
+ uses: lycheeverse/lychee-action@82202e5e9c2f4ef1a55a3d02563e1cb6041e5332 # v2.4.1
+ with:
+ args: --no-progress --exclude '^http' 'target/deploy/docs/'
+ fail: true
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 009fcc8337..7dfc33e0d2 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -771,7 +771,8 @@ jobs:
timeout-minutes: 120
name: Create a Windows installer
runs-on: [self-hosted, Windows, X64]
- if: (startsWith(github.ref, 'refs/tags/v') || contains(github.event.pull_request.labels.*.name, 'run-bundling'))
+ if: contains(github.event.pull_request.labels.*.name, 'run-bundling')
+ # if: (startsWith(github.ref, 'refs/tags/v') || contains(github.event.pull_request.labels.*.name, 'run-bundling'))
needs: [windows_tests]
env:
AZURE_TENANT_ID: ${{ secrets.AZURE_SIGNING_TENANT_ID }}
diff --git a/Cargo.lock b/Cargo.lock
index f31ecdef99..767d9a4c9a 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -114,7 +114,6 @@ dependencies = [
"pretty_assertions",
"project",
"prompt_store",
- "proto",
"rand 0.8.5",
"ref-cast",
"rope",
@@ -357,10 +356,10 @@ name = "ai_onboarding"
version = "0.1.0"
dependencies = [
"client",
+ "cloud_llm_client",
"component",
"gpui",
"language_model",
- "proto",
"serde",
"smallvec",
"telemetry",
@@ -1077,17 +1076,6 @@ dependencies = [
"tracing",
]
-[[package]]
-name = "async-recursion"
-version = "0.3.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d7d78656ba01f1b93024b7c3a0467f1608e4be67d725749fdcd7d2c7678fd7a2"
-dependencies = [
- "proc-macro2",
- "quote",
- "syn 1.0.109",
-]
-
[[package]]
name = "async-recursion"
version = "1.1.1"
@@ -2973,11 +2961,11 @@ name = "client"
version = "0.1.0"
dependencies = [
"anyhow",
- "async-recursion 0.3.2",
"async-tungstenite",
"base64 0.22.1",
"chrono",
"clock",
+ "cloud_api_client",
"cloud_llm_client",
"cocoa 0.26.0",
"collections",
@@ -3033,6 +3021,31 @@ dependencies = [
"workspace-hack",
]
+[[package]]
+name = "cloud_api_client"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "cloud_api_types",
+ "futures 0.3.31",
+ "http_client",
+ "parking_lot",
+ "serde_json",
+ "workspace-hack",
+]
+
+[[package]]
+name = "cloud_api_types"
+version = "0.1.0"
+dependencies = [
+ "chrono",
+ "cloud_llm_client",
+ "pretty_assertions",
+ "serde",
+ "serde_json",
+ "workspace-hack",
+]
+
[[package]]
name = "cloud_llm_client"
version = "0.1.0"
@@ -4271,41 +4284,6 @@ dependencies = [
"workspace-hack",
]
-[[package]]
-name = "darling"
-version = "0.20.11"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee"
-dependencies = [
- "darling_core",
- "darling_macro",
-]
-
-[[package]]
-name = "darling_core"
-version = "0.20.11"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e"
-dependencies = [
- "fnv",
- "ident_case",
- "proc-macro2",
- "quote",
- "strsim",
- "syn 2.0.101",
-]
-
-[[package]]
-name = "darling_macro"
-version = "0.20.11"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead"
-dependencies = [
- "darling_core",
- "quote",
- "syn 2.0.101",
-]
-
[[package]]
name = "dashmap"
version = "5.5.3"
@@ -4521,37 +4499,6 @@ dependencies = [
"serde",
]
-[[package]]
-name = "derive_builder"
-version = "0.20.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "507dfb09ea8b7fa618fcf76e953f4f5e192547945816d5358edffe39f6f94947"
-dependencies = [
- "derive_builder_macro",
-]
-
-[[package]]
-name = "derive_builder_core"
-version = "0.20.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2d5bcf7b024d6835cfb3d473887cd966994907effbe9227e8c8219824d06c4e8"
-dependencies = [
- "darling",
- "proc-macro2",
- "quote",
- "syn 2.0.101",
-]
-
-[[package]]
-name = "derive_builder_macro"
-version = "0.20.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ab63b0e2bf4d5928aff72e83a7dace85d7bba5fe12dcc3c5a572d78caffd3f3c"
-dependencies = [
- "derive_builder_core",
- "syn 2.0.101",
-]
-
[[package]]
name = "derive_more"
version = "0.99.19"
@@ -4962,6 +4909,7 @@ dependencies = [
"theme",
"time",
"tree-sitter-bash",
+ "tree-sitter-c",
"tree-sitter-html",
"tree-sitter-python",
"tree-sitter-rust",
@@ -5930,7 +5878,7 @@ dependencies = [
"ignore",
"libc",
"log",
- "notify",
+ "notify 8.0.0",
"objc",
"parking_lot",
"paths",
@@ -7369,9 +7317,8 @@ dependencies = [
"wayland-backend",
"wayland-client",
"wayland-cursor",
- "wayland-protocols 0.31.2",
+ "wayland-protocols",
"wayland-protocols-plasma",
- "wayland-protocols-wlr",
"windows 0.61.1",
"windows-core 0.61.0",
"windows-numerics",
@@ -7488,18 +7435,16 @@ dependencies = [
[[package]]
name = "handlebars"
-version = "6.3.2"
+version = "5.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "759e2d5aea3287cb1190c8ec394f42866cb5bf74fcbf213f354e3c856ea26098"
+checksum = "d08485b96a0e6393e9e4d1b8d48cf74ad6c063cd905eb33f42c1ce3f0377539b"
dependencies = [
- "derive_builder",
"log",
- "num-order",
"pest",
"pest_derive",
"serde",
"serde_json",
- "thiserror 2.0.12",
+ "thiserror 1.0.69",
]
[[package]]
@@ -7680,12 +7625,6 @@ version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
-[[package]]
-name = "hex-literal"
-version = "1.0.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bcaaec4551594c969335c98c903c1397853d4198408ea609190f420500f6be71"
-
[[package]]
name = "hexf-parse"
version = "0.2.1"
@@ -7863,6 +7802,7 @@ dependencies = [
"http 1.3.1",
"http-body 1.0.1",
"log",
+ "parking_lot",
"serde",
"serde_json",
"url",
@@ -8170,12 +8110,6 @@ version = "2.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "25a2bc672d1148e28034f176e01fffebb08b35768468cc954630da77a1449005"
-[[package]]
-name = "ident_case"
-version = "1.0.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
-
[[package]]
name = "idna"
version = "1.0.3"
@@ -8394,6 +8328,17 @@ dependencies = [
"zeta",
]
+[[package]]
+name = "inotify"
+version = "0.9.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f8069d3ec154eb856955c1c0fbffefbf5f3c40a104ec912d4797314c1801abff"
+dependencies = [
+ "bitflags 1.3.2",
+ "inotify-sys",
+ "libc",
+]
+
[[package]]
name = "inotify"
version = "0.11.0"
@@ -8547,7 +8492,7 @@ dependencies = [
"fnv",
"lazy_static",
"libc",
- "mio",
+ "mio 1.0.3",
"rand 0.8.5",
"serde",
"tempfile",
@@ -9129,7 +9074,6 @@ dependencies = [
"open_router",
"partial-json-fixer",
"project",
- "proto",
"release_channel",
"schemars",
"serde",
@@ -9401,7 +9345,7 @@ dependencies = [
[[package]]
name = "libwebrtc"
version = "0.3.10"
-source = "git+https://github.com/zed-industries/livekit-rust-sdks?rev=383e5377f8b7de1f8627ee16f0cf11c5293337bd#383e5377f8b7de1f8627ee16f0cf11c5293337bd"
+source = "git+https://github.com/zed-industries/livekit-rust-sdks?rev=5f04705ac3f356350ae31534ffbc476abc9ea83d#5f04705ac3f356350ae31534ffbc476abc9ea83d"
dependencies = [
"cxx",
"jni",
@@ -9481,7 +9425,7 @@ checksum = "23fb14cb19457329c82206317a5663005a4d404783dc74f4252769b0d5f42856"
[[package]]
name = "livekit"
version = "0.7.8"
-source = "git+https://github.com/zed-industries/livekit-rust-sdks?rev=383e5377f8b7de1f8627ee16f0cf11c5293337bd#383e5377f8b7de1f8627ee16f0cf11c5293337bd"
+source = "git+https://github.com/zed-industries/livekit-rust-sdks?rev=5f04705ac3f356350ae31534ffbc476abc9ea83d#5f04705ac3f356350ae31534ffbc476abc9ea83d"
dependencies = [
"chrono",
"futures-util",
@@ -9504,7 +9448,7 @@ dependencies = [
[[package]]
name = "livekit-api"
version = "0.4.2"
-source = "git+https://github.com/zed-industries/livekit-rust-sdks?rev=383e5377f8b7de1f8627ee16f0cf11c5293337bd#383e5377f8b7de1f8627ee16f0cf11c5293337bd"
+source = "git+https://github.com/zed-industries/livekit-rust-sdks?rev=5f04705ac3f356350ae31534ffbc476abc9ea83d#5f04705ac3f356350ae31534ffbc476abc9ea83d"
dependencies = [
"futures-util",
"http 0.2.12",
@@ -9528,7 +9472,7 @@ dependencies = [
[[package]]
name = "livekit-protocol"
version = "0.3.9"
-source = "git+https://github.com/zed-industries/livekit-rust-sdks?rev=383e5377f8b7de1f8627ee16f0cf11c5293337bd#383e5377f8b7de1f8627ee16f0cf11c5293337bd"
+source = "git+https://github.com/zed-industries/livekit-rust-sdks?rev=5f04705ac3f356350ae31534ffbc476abc9ea83d#5f04705ac3f356350ae31534ffbc476abc9ea83d"
dependencies = [
"futures-util",
"livekit-runtime",
@@ -9545,7 +9489,7 @@ dependencies = [
[[package]]
name = "livekit-runtime"
version = "0.4.0"
-source = "git+https://github.com/zed-industries/livekit-rust-sdks?rev=383e5377f8b7de1f8627ee16f0cf11c5293337bd#383e5377f8b7de1f8627ee16f0cf11c5293337bd"
+source = "git+https://github.com/zed-industries/livekit-rust-sdks?rev=5f04705ac3f356350ae31534ffbc476abc9ea83d#5f04705ac3f356350ae31534ffbc476abc9ea83d"
dependencies = [
"tokio",
"tokio-stream",
@@ -9867,7 +9811,7 @@ name = "markdown_preview"
version = "0.1.0"
dependencies = [
"anyhow",
- "async-recursion 1.1.1",
+ "async-recursion",
"collections",
"editor",
"fs",
@@ -9987,9 +9931,9 @@ dependencies = [
[[package]]
name = "mdbook"
-version = "0.4.48"
+version = "0.4.40"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8b6fbb4ac2d9fd7aa987c3510309ea3c80004a968d063c42f0d34fea070817c1"
+checksum = "b45a38e19bd200220ef07c892b0157ad3d2365e5b5a267ca01ad12182491eea5"
dependencies = [
"ammonia",
"anyhow",
@@ -9999,12 +9943,11 @@ dependencies = [
"elasticlunr-rs",
"env_logger 0.11.8",
"futures-util",
- "handlebars 6.3.2",
- "hex",
+ "handlebars 5.1.2",
"ignore",
"log",
"memchr",
- "notify",
+ "notify 6.1.1",
"notify-debouncer-mini",
"once_cell",
"opener",
@@ -10013,7 +9956,6 @@ dependencies = [
"regex",
"serde",
"serde_json",
- "sha2",
"shlex",
"tempfile",
"tokio",
@@ -10156,6 +10098,18 @@ version = "0.5.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e53debba6bda7a793e5f99b8dacf19e626084f525f7829104ba9898f367d85ff"
+[[package]]
+name = "mio"
+version = "0.8.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c"
+dependencies = [
+ "libc",
+ "log",
+ "wasi 0.11.0+wasi-snapshot-preview1",
+ "windows-sys 0.48.0",
+]
+
[[package]]
name = "mio"
version = "1.0.3"
@@ -10502,6 +10456,25 @@ dependencies = [
"zed_actions",
]
+[[package]]
+name = "notify"
+version = "6.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6205bd8bb1e454ad2e27422015fb5e4f2bcc7e08fa8f27058670d208324a4d2d"
+dependencies = [
+ "bitflags 2.9.0",
+ "crossbeam-channel",
+ "filetime",
+ "fsevent-sys 4.1.0",
+ "inotify 0.9.6",
+ "kqueue",
+ "libc",
+ "log",
+ "mio 0.8.11",
+ "walkdir",
+ "windows-sys 0.48.0",
+]
+
[[package]]
name = "notify"
version = "8.0.0"
@@ -10510,11 +10483,11 @@ dependencies = [
"bitflags 2.9.0",
"filetime",
"fsevent-sys 4.1.0",
- "inotify",
+ "inotify 0.11.0",
"kqueue",
"libc",
"log",
- "mio",
+ "mio 1.0.3",
"notify-types",
"walkdir",
"windows-sys 0.59.0",
@@ -10522,14 +10495,13 @@ dependencies = [
[[package]]
name = "notify-debouncer-mini"
-version = "0.6.0"
+version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a689eb4262184d9a1727f9087cd03883ea716682ab03ed24efec57d7716dccb8"
+checksum = "5d40b221972a1fc5ef4d858a2f671fb34c75983eb385463dff3780eeff6a9d43"
dependencies = [
+ "crossbeam-channel",
"log",
- "notify",
- "notify-types",
- "tempfile",
+ "notify 6.1.1",
]
[[package]]
@@ -10669,21 +10641,6 @@ dependencies = [
"num-traits",
]
-[[package]]
-name = "num-modular"
-version = "0.6.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "17bb261bf36fa7d83f4c294f834e91256769097b3cb505d44831e0a179ac647f"
-
-[[package]]
-name = "num-order"
-version = "1.2.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "537b596b97c40fcf8056d153049eb22f481c17ebce72a513ec9286e4986d1bb6"
-dependencies = [
- "num-modular",
-]
-
[[package]]
name = "num-rational"
version = "0.4.2"
@@ -10954,21 +10911,33 @@ dependencies = [
name = "onboarding"
version = "0.1.0"
dependencies = [
+ "ai_onboarding",
"anyhow",
+ "client",
"command_palette_hooks",
+ "component",
"db",
+ "documented",
"editor",
"feature_flags",
"fs",
"gpui",
+ "itertools 0.14.0",
"language",
+ "language_model",
+ "menu",
"project",
+ "schemars",
+ "serde",
"settings",
"theme",
"ui",
+ "util",
+ "vim_mode_setting",
"workspace",
"workspace-hack",
"zed_actions",
+ "zlog",
]
[[package]]
@@ -14732,6 +14701,27 @@ dependencies = [
"zlog",
]
+[[package]]
+name = "settings_profile_selector"
+version = "0.1.0"
+dependencies = [
+ "client",
+ "editor",
+ "fuzzy",
+ "gpui",
+ "language",
+ "menu",
+ "picker",
+ "project",
+ "serde_json",
+ "settings",
+ "theme",
+ "ui",
+ "workspace",
+ "workspace-hack",
+ "zed_actions",
+]
+
[[package]]
name = "settings_ui"
version = "0.1.0"
@@ -14754,7 +14744,6 @@ dependencies = [
"notifications",
"paths",
"project",
- "schemars",
"search",
"serde",
"serde_json",
@@ -16191,7 +16180,7 @@ version = "0.1.0"
dependencies = [
"anyhow",
"assistant_slash_command",
- "async-recursion 1.1.1",
+ "async-recursion",
"breadcrumbs",
"client",
"collections",
@@ -16540,6 +16529,7 @@ dependencies = [
"call",
"chrono",
"client",
+ "cloud_llm_client",
"collections",
"db",
"gpui",
@@ -16575,7 +16565,7 @@ dependencies = [
"backtrace",
"bytes 1.10.1",
"libc",
- "mio",
+ "mio 1.0.3",
"parking_lot",
"pin-project-lite",
"signal-hook-registry",
@@ -18388,9 +18378,9 @@ dependencies = [
[[package]]
name = "wayland-backend"
-version = "0.3.10"
+version = "0.3.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fe770181423e5fc79d3e2a7f4410b7799d5aab1de4372853de3c6aa13ca24121"
+checksum = "b7208998eaa3870dad37ec8836979581506e0c5c64c20c9e79e9d2a10d6f47bf"
dependencies = [
"cc",
"downcast-rs",
@@ -18402,9 +18392,9 @@ dependencies = [
[[package]]
name = "wayland-client"
-version = "0.31.10"
+version = "0.31.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "978fa7c67b0847dbd6a9f350ca2569174974cd4082737054dbb7fbb79d7d9a61"
+checksum = "c2120de3d33638aaef5b9f4472bff75f07c56379cf76ea320bd3a3d65ecaf73f"
dependencies = [
"bitflags 2.9.0",
"rustix 0.38.44",
@@ -18435,18 +18425,6 @@ dependencies = [
"wayland-scanner",
]
-[[package]]
-name = "wayland-protocols"
-version = "0.32.8"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "779075454e1e9a521794fed15886323ea0feda3f8b0fc1390f5398141310422a"
-dependencies = [
- "bitflags 2.9.0",
- "wayland-backend",
- "wayland-client",
- "wayland-scanner",
-]
-
[[package]]
name = "wayland-protocols-plasma"
version = "0.2.0"
@@ -18456,20 +18434,7 @@ dependencies = [
"bitflags 2.9.0",
"wayland-backend",
"wayland-client",
- "wayland-protocols 0.31.2",
- "wayland-scanner",
-]
-
-[[package]]
-name = "wayland-protocols-wlr"
-version = "0.3.8"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1cb6cdc73399c0e06504c437fe3cf886f25568dd5454473d565085b36d6a8bbf"
-dependencies = [
- "bitflags 2.9.0",
- "wayland-backend",
- "wayland-client",
- "wayland-protocols 0.32.8",
+ "wayland-protocols",
"wayland-scanner",
]
@@ -18578,7 +18543,7 @@ dependencies = [
[[package]]
name = "webrtc-sys"
version = "0.3.7"
-source = "git+https://github.com/zed-industries/livekit-rust-sdks?rev=383e5377f8b7de1f8627ee16f0cf11c5293337bd#383e5377f8b7de1f8627ee16f0cf11c5293337bd"
+source = "git+https://github.com/zed-industries/livekit-rust-sdks?rev=5f04705ac3f356350ae31534ffbc476abc9ea83d#5f04705ac3f356350ae31534ffbc476abc9ea83d"
dependencies = [
"cc",
"cxx",
@@ -18591,15 +18556,13 @@ dependencies = [
[[package]]
name = "webrtc-sys-build"
version = "0.3.6"
-source = "git+https://github.com/zed-industries/livekit-rust-sdks?rev=383e5377f8b7de1f8627ee16f0cf11c5293337bd#383e5377f8b7de1f8627ee16f0cf11c5293337bd"
+source = "git+https://github.com/zed-industries/livekit-rust-sdks?rev=5f04705ac3f356350ae31534ffbc476abc9ea83d#5f04705ac3f356350ae31534ffbc476abc9ea83d"
dependencies = [
"fs2",
- "hex-literal",
"regex",
"reqwest 0.11.27",
"scratch",
"semver",
- "sha2",
"zip",
]
@@ -18628,7 +18591,6 @@ dependencies = [
"serde",
"settings",
"telemetry",
- "theme",
"ui",
"util",
"vim_mode_setting",
@@ -19643,7 +19605,7 @@ version = "0.1.0"
dependencies = [
"any_vec",
"anyhow",
- "async-recursion 1.1.1",
+ "async-recursion",
"bincode",
"call",
"client",
@@ -19777,7 +19739,7 @@ dependencies = [
"md-5",
"memchr",
"miniz_oxide",
- "mio",
+ "mio 1.0.3",
"naga",
"nix 0.29.0",
"nom",
@@ -20168,7 +20130,7 @@ dependencies = [
"async-io",
"async-lock",
"async-process",
- "async-recursion 1.1.1",
+ "async-recursion",
"async-task",
"async-trait",
"blocking",
@@ -20221,7 +20183,7 @@ dependencies = [
[[package]]
name = "zed"
-version = "0.198.0"
+version = "0.199.0"
dependencies = [
"activity_indicator",
"agent",
@@ -20324,6 +20286,7 @@ dependencies = [
"serde_json",
"session",
"settings",
+ "settings_profile_selector",
"settings_ui",
"shellexpand 2.1.2",
"smol",
@@ -20600,6 +20563,7 @@ dependencies = [
"call",
"client",
"clock",
+ "cloud_api_types",
"cloud_llm_client",
"collections",
"command_palette_hooks",
@@ -20620,7 +20584,6 @@ dependencies = [
"menu",
"postage",
"project",
- "proto",
"regex",
"release_channel",
"reqwest_client",
@@ -20645,6 +20608,42 @@ dependencies = [
"zlog",
]
+[[package]]
+name = "zeta_cli"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "clap",
+ "client",
+ "debug_adapter_extension",
+ "extension",
+ "fs",
+ "futures 0.3.31",
+ "gpui",
+ "gpui_tokio",
+ "language",
+ "language_extension",
+ "language_model",
+ "language_models",
+ "languages",
+ "node_runtime",
+ "paths",
+ "project",
+ "prompt_store",
+ "release_channel",
+ "reqwest_client",
+ "serde",
+ "serde_json",
+ "settings",
+ "shellexpand 2.1.2",
+ "smol",
+ "terminal_view",
+ "util",
+ "watch",
+ "workspace-hack",
+ "zeta",
+]
+
[[package]]
name = "zip"
version = "0.6.6"
diff --git a/Cargo.toml b/Cargo.toml
index df1e1d7467..902c7e8d19 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,13 +1,13 @@
[workspace]
resolver = "2"
members = [
- "crates/activity_indicator",
"crates/acp_thread",
- "crates/agent_ui",
+ "crates/activity_indicator",
"crates/agent",
- "crates/agent_settings",
- "crates/ai_onboarding",
"crates/agent_servers",
+ "crates/agent_settings",
+ "crates/agent_ui",
+ "crates/ai_onboarding",
"crates/anthropic",
"crates/askpass",
"crates/assets",
@@ -29,6 +29,8 @@ members = [
"crates/cli",
"crates/client",
"crates/clock",
+ "crates/cloud_api_client",
+ "crates/cloud_api_types",
"crates/cloud_llm_client",
"crates/collab",
"crates/collab_ui",
@@ -49,8 +51,8 @@ members = [
"crates/diagnostics",
"crates/docs_preprocessor",
"crates/editor",
- "crates/explorer_command_injector",
"crates/eval",
+ "crates/explorer_command_injector",
"crates/extension",
"crates/extension_api",
"crates/extension_cli",
@@ -99,7 +101,6 @@ members = [
"crates/markdown_preview",
"crates/media",
"crates/menu",
- "crates/svg_preview",
"crates/migrator",
"crates/mistral",
"crates/multi_buffer",
@@ -140,6 +141,7 @@ members = [
"crates/semantic_version",
"crates/session",
"crates/settings",
+ "crates/settings_profile_selector",
"crates/settings_ui",
"crates/snippet",
"crates/snippet_provider",
@@ -152,6 +154,7 @@ members = [
"crates/sum_tree",
"crates/supermaven",
"crates/supermaven_api",
+ "crates/svg_preview",
"crates/tab_switcher",
"crates/task",
"crates/tasks_ui",
@@ -186,6 +189,7 @@ members = [
"crates/zed",
"crates/zed_actions",
"crates/zeta",
+ "crates/zeta_cli",
"crates/zlog",
"crates/zlog_settings",
@@ -251,6 +255,8 @@ channel = { path = "crates/channel" }
cli = { path = "crates/cli" }
client = { path = "crates/client" }
clock = { path = "crates/clock" }
+cloud_api_client = { path = "crates/cloud_api_client" }
+cloud_api_types = { path = "crates/cloud_api_types" }
cloud_llm_client = { path = "crates/cloud_llm_client" }
collab = { path = "crates/collab" }
collab_ui = { path = "crates/collab_ui" }
@@ -338,6 +344,7 @@ picker = { path = "crates/picker" }
plugin = { path = "crates/plugin" }
plugin_macros = { path = "crates/plugin_macros" }
prettier = { path = "crates/prettier" }
+settings_profile_selector = { path = "crates/settings_profile_selector" }
project = { path = "crates/project" }
project_panel = { path = "crates/project_panel" }
project_symbols = { path = "crates/project_symbols" }
@@ -672,14 +679,16 @@ features = [
"UI_ViewManagement",
"Wdk_System_SystemServices",
"Win32_Globalization",
- "Win32_Graphics_Direct2D",
- "Win32_Graphics_Direct2D_Common",
+ "Win32_Graphics_Direct3D",
+ "Win32_Graphics_Direct3D11",
+ "Win32_Graphics_Direct3D_Fxc",
+ "Win32_Graphics_DirectComposition",
"Win32_Graphics_DirectWrite",
"Win32_Graphics_Dwm",
+ "Win32_Graphics_Dxgi",
"Win32_Graphics_Dxgi_Common",
"Win32_Graphics_Gdi",
"Win32_Graphics_Imaging",
- "Win32_Graphics_Imaging_D2D",
"Win32_Networking_WinSock",
"Win32_Security",
"Win32_Security_Credentials",
diff --git a/assets/icons/editor_atom.svg b/assets/icons/editor_atom.svg
new file mode 100644
index 0000000000..cc5fa83843
--- /dev/null
+++ b/assets/icons/editor_atom.svg
@@ -0,0 +1,3 @@
+
diff --git a/assets/icons/editor_cursor.svg b/assets/icons/editor_cursor.svg
new file mode 100644
index 0000000000..338697be8a
--- /dev/null
+++ b/assets/icons/editor_cursor.svg
@@ -0,0 +1,9 @@
+
diff --git a/assets/icons/editor_emacs.svg b/assets/icons/editor_emacs.svg
new file mode 100644
index 0000000000..951d7b2be1
--- /dev/null
+++ b/assets/icons/editor_emacs.svg
@@ -0,0 +1,10 @@
+
diff --git a/assets/icons/editor_jet_brains.svg b/assets/icons/editor_jet_brains.svg
new file mode 100644
index 0000000000..7d9cf0c65c
--- /dev/null
+++ b/assets/icons/editor_jet_brains.svg
@@ -0,0 +1,3 @@
+
diff --git a/assets/icons/editor_sublime.svg b/assets/icons/editor_sublime.svg
new file mode 100644
index 0000000000..95a04f6b54
--- /dev/null
+++ b/assets/icons/editor_sublime.svg
@@ -0,0 +1,5 @@
+
diff --git a/assets/icons/editor_vs_code.svg b/assets/icons/editor_vs_code.svg
new file mode 100644
index 0000000000..2a71ad52af
--- /dev/null
+++ b/assets/icons/editor_vs_code.svg
@@ -0,0 +1,3 @@
+
diff --git a/assets/icons/shield_check.svg b/assets/icons/shield_check.svg
new file mode 100644
index 0000000000..6e58c31468
--- /dev/null
+++ b/assets/icons/shield_check.svg
@@ -0,0 +1,4 @@
+
diff --git a/assets/keymaps/default-linux.json b/assets/keymaps/default-linux.json
index e36e093e22..ef5354e82d 100644
--- a/assets/keymaps/default-linux.json
+++ b/assets/keymaps/default-linux.json
@@ -232,7 +232,7 @@
"ctrl-n": "agent::NewThread",
"ctrl-alt-n": "agent::NewTextThread",
"ctrl-shift-h": "agent::OpenHistory",
- "ctrl-alt-c": "agent::OpenConfiguration",
+ "ctrl-alt-c": "agent::OpenSettings",
"ctrl-alt-p": "agent::OpenRulesLibrary",
"ctrl-i": "agent::ToggleProfileSelector",
"ctrl-alt-/": "agent::ToggleModelSelector",
@@ -598,6 +598,7 @@
"ctrl-shift-t": "pane::ReopenClosedItem",
"ctrl-k ctrl-s": "zed::OpenKeymapEditor",
"ctrl-k ctrl-t": "theme_selector::Toggle",
+ "ctrl-alt-super-p": "settings_profile_selector::Toggle",
"ctrl-t": "project_symbols::Toggle",
"ctrl-p": "file_finder::Toggle",
"ctrl-tab": "tab_switcher::Toggle",
@@ -1167,5 +1168,14 @@
"up": "menu::SelectPrevious",
"down": "menu::SelectNext"
}
+ },
+ {
+ "context": "Onboarding",
+ "use_key_equivalents": true,
+ "bindings": {
+ "ctrl-1": "onboarding::ActivateBasicsPage",
+ "ctrl-2": "onboarding::ActivateEditingPage",
+ "ctrl-3": "onboarding::ActivateAISetupPage"
+ }
}
]
diff --git a/assets/keymaps/default-macos.json b/assets/keymaps/default-macos.json
index 0114e2da1d..3287e50acb 100644
--- a/assets/keymaps/default-macos.json
+++ b/assets/keymaps/default-macos.json
@@ -272,7 +272,7 @@
"cmd-n": "agent::NewThread",
"cmd-alt-n": "agent::NewTextThread",
"cmd-shift-h": "agent::OpenHistory",
- "cmd-alt-c": "agent::OpenConfiguration",
+ "cmd-alt-c": "agent::OpenSettings",
"cmd-alt-p": "agent::OpenRulesLibrary",
"cmd-i": "agent::ToggleProfileSelector",
"cmd-alt-/": "agent::ToggleModelSelector",
@@ -665,6 +665,7 @@
"cmd-shift-t": "pane::ReopenClosedItem",
"cmd-k cmd-s": "zed::OpenKeymapEditor",
"cmd-k cmd-t": "theme_selector::Toggle",
+ "ctrl-alt-cmd-p": "settings_profile_selector::Toggle",
"cmd-t": "project_symbols::Toggle",
"cmd-p": "file_finder::Toggle",
"ctrl-tab": "tab_switcher::Toggle",
@@ -1269,5 +1270,14 @@
"up": "menu::SelectPrevious",
"down": "menu::SelectNext"
}
+ },
+ {
+ "context": "Onboarding",
+ "use_key_equivalents": true,
+ "bindings": {
+ "cmd-1": "onboarding::ActivateBasicsPage",
+ "cmd-2": "onboarding::ActivateEditingPage",
+ "cmd-3": "onboarding::ActivateAISetupPage"
+ }
}
]
diff --git a/assets/keymaps/linux/cursor.json b/assets/keymaps/linux/cursor.json
index 347b7885fc..1c381b0cf0 100644
--- a/assets/keymaps/linux/cursor.json
+++ b/assets/keymaps/linux/cursor.json
@@ -8,7 +8,7 @@
"ctrl-shift-i": "agent::ToggleFocus",
"ctrl-l": "agent::ToggleFocus",
"ctrl-shift-l": "agent::ToggleFocus",
- "ctrl-shift-j": "agent::OpenConfiguration"
+ "ctrl-shift-j": "agent::OpenSettings"
}
},
{
diff --git a/assets/keymaps/linux/jetbrains.json b/assets/keymaps/linux/jetbrains.json
index f81f363ae0..9bc1f24bfb 100644
--- a/assets/keymaps/linux/jetbrains.json
+++ b/assets/keymaps/linux/jetbrains.json
@@ -95,7 +95,7 @@
"ctrl-shift-r": ["pane::DeploySearch", { "replace_enabled": true }],
"alt-shift-f10": "task::Spawn",
"ctrl-e": "file_finder::Toggle",
- "ctrl-k": "git_panel::ToggleFocus", // bug: This should also focus commit editor
+ // "ctrl-k": "git_panel::ToggleFocus", // bug: This should also focus commit editor
"ctrl-shift-n": "file_finder::Toggle",
"ctrl-shift-a": "command_palette::Toggle",
"shift shift": "command_palette::Toggle",
diff --git a/assets/keymaps/macos/cursor.json b/assets/keymaps/macos/cursor.json
index b1d39bef9e..fdf9c437cf 100644
--- a/assets/keymaps/macos/cursor.json
+++ b/assets/keymaps/macos/cursor.json
@@ -8,7 +8,7 @@
"cmd-shift-i": "agent::ToggleFocus",
"cmd-l": "agent::ToggleFocus",
"cmd-shift-l": "agent::ToggleFocus",
- "cmd-shift-j": "agent::OpenConfiguration"
+ "cmd-shift-j": "agent::OpenSettings"
}
},
{
diff --git a/assets/keymaps/macos/jetbrains.json b/assets/keymaps/macos/jetbrains.json
index 5795d2ac7e..b1cd51a338 100644
--- a/assets/keymaps/macos/jetbrains.json
+++ b/assets/keymaps/macos/jetbrains.json
@@ -97,7 +97,7 @@
"cmd-shift-r": ["pane::DeploySearch", { "replace_enabled": true }],
"ctrl-alt-r": "task::Spawn",
"cmd-e": "file_finder::Toggle",
- "cmd-k": "git_panel::ToggleFocus", // bug: This should also focus commit editor
+ // "cmd-k": "git_panel::ToggleFocus", // bug: This should also focus commit editor
"cmd-shift-o": "file_finder::Toggle",
"cmd-shift-a": "command_palette::Toggle",
"shift shift": "command_palette::Toggle",
diff --git a/assets/settings/default.json b/assets/settings/default.json
index 3a7a48efc2..4734b5d118 100644
--- a/assets/settings/default.json
+++ b/assets/settings/default.json
@@ -1877,5 +1877,25 @@
"save_breakpoints": true,
"dock": "bottom",
"button": true
- }
+ },
+ // Configures any number of settings profiles that are temporarily applied on
+ // top of your existing user settings when selected from
+ // `settings profile selector: toggle`.
+ // Examples:
+ // "profiles": {
+ // "Presenting": {
+ // "agent_font_size": 20.0,
+ // "buffer_font_size": 20.0,
+ // "theme": "One Light",
+ // "ui_font_size": 20.0
+ // },
+ // "Python (ty)": {
+ // "languages": {
+ // "Python": {
+ // "language_servers": ["ty"]
+ // }
+ // }
+ // }
+ // }
+ "profiles": []
}
diff --git a/crates/acp_thread/src/acp_thread.rs b/crates/acp_thread/src/acp_thread.rs
index 3bf6134862..d10fecdb28 100644
--- a/crates/acp_thread/src/acp_thread.rs
+++ b/crates/acp_thread/src/acp_thread.rs
@@ -580,6 +580,9 @@ pub struct AcpThread {
pub enum AcpThreadEvent {
NewEntry,
EntryUpdated(usize),
+ ToolAuthorizationRequired,
+ Stopped,
+ Error,
}
impl EventEmitter for AcpThread {}
@@ -677,6 +680,18 @@ impl AcpThread {
false
}
+ pub fn used_tools_since_last_user_message(&self) -> bool {
+ for entry in self.entries.iter().rev() {
+ match entry {
+ AgentThreadEntry::UserMessage(..) => return false,
+ AgentThreadEntry::AssistantMessage(..) => continue,
+ AgentThreadEntry::ToolCall(..) => return true,
+ }
+ }
+
+ false
+ }
+
pub fn handle_session_update(
&mut self,
update: acp::SessionUpdate,
@@ -880,6 +895,7 @@ impl AcpThread {
};
self.upsert_tool_call_inner(tool_call, status, cx);
+ cx.emit(AcpThreadEvent::ToolAuthorizationRequired);
rx
}
@@ -1015,12 +1031,18 @@ impl AcpThread {
.log_err();
}));
- async move {
- match rx.await {
- Ok(Err(e)) => Err(e)?,
- _ => Ok(()),
+ cx.spawn(async move |this, cx| match rx.await {
+ Ok(Err(e)) => {
+ this.update(cx, |_, cx| cx.emit(AcpThreadEvent::Error))
+ .log_err();
+ Err(e)?
}
- }
+ _ => {
+ this.update(cx, |_, cx| cx.emit(AcpThreadEvent::Stopped))
+ .log_err();
+ Ok(())
+ }
+ })
.boxed()
}
diff --git a/crates/agent/Cargo.toml b/crates/agent/Cargo.toml
index c89a7f3303..7bc0e82cad 100644
--- a/crates/agent/Cargo.toml
+++ b/crates/agent/Cargo.toml
@@ -47,7 +47,6 @@ paths.workspace = true
postage.workspace = true
project.workspace = true
prompt_store.workspace = true
-proto.workspace = true
ref-cast.workspace = true
rope.workspace = true
schemars.workspace = true
diff --git a/crates/agent/src/thread.rs b/crates/agent/src/thread.rs
index 0e5da2d43b..8558dd528d 100644
--- a/crates/agent/src/thread.rs
+++ b/crates/agent/src/thread.rs
@@ -13,7 +13,7 @@ use anyhow::{Result, anyhow};
use assistant_tool::{ActionLog, AnyToolCard, Tool, ToolWorkingSet};
use chrono::{DateTime, Utc};
use client::{ModelRequestUsage, RequestUsage};
-use cloud_llm_client::{CompletionIntent, CompletionRequestStatus, UsageLimit};
+use cloud_llm_client::{CompletionIntent, CompletionRequestStatus, Plan, UsageLimit};
use collections::HashMap;
use feature_flags::{self, FeatureFlagAppExt};
use futures::{FutureExt, StreamExt as _, future::Shared};
@@ -37,7 +37,6 @@ use project::{
git_store::{GitStore, GitStoreCheckpoint, RepositoryState},
};
use prompt_store::{ModelContext, PromptBuilder};
-use proto::Plan;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use settings::Settings;
@@ -3255,8 +3254,10 @@ impl Thread {
}
fn update_model_request_usage(&self, amount: u32, limit: UsageLimit, cx: &mut Context) {
- self.project.update(cx, |project, cx| {
- project.user_store().update(cx, |user_store, cx| {
+ self.project
+ .read(cx)
+ .user_store()
+ .update(cx, |user_store, cx| {
user_store.update_model_request_usage(
ModelRequestUsage(RequestUsage {
amount: amount as i32,
@@ -3264,8 +3265,7 @@ impl Thread {
}),
cx,
)
- })
- });
+ });
}
pub fn deny_tool_use(
diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs
index 17575e42db..167f7e6136 100644
--- a/crates/agent_ui/src/acp/thread_view.rs
+++ b/crates/agent_ui/src/acp/thread_view.rs
@@ -1,5 +1,7 @@
use acp_thread::{AgentConnection, Plan};
use agent_servers::AgentServer;
+use agent_settings::{AgentSettings, NotifyWhenAgentWaiting};
+use audio::{Audio, Sound};
use std::cell::RefCell;
use std::collections::BTreeMap;
use std::path::Path;
@@ -18,10 +20,10 @@ use editor::{
use file_icons::FileIcons;
use gpui::{
Action, Animation, AnimationExt, App, BorderStyle, EdgesRefinement, Empty, Entity, EntityId,
- FocusHandle, Focusable, Hsla, Length, ListOffset, ListState, SharedString, StyleRefinement,
- Subscription, Task, TextStyle, TextStyleRefinement, Transformation, UnderlineStyle, WeakEntity,
- Window, div, linear_color_stop, linear_gradient, list, percentage, point, prelude::*,
- pulsating_between,
+ FocusHandle, Focusable, Hsla, Length, ListOffset, ListState, PlatformDisplay, SharedString,
+ StyleRefinement, Subscription, Task, TextStyle, TextStyleRefinement, Transformation,
+ UnderlineStyle, WeakEntity, Window, WindowHandle, div, linear_color_stop, linear_gradient,
+ list, percentage, point, prelude::*, pulsating_between,
};
use language::language_settings::SoftWrap;
use language::{Buffer, Language};
@@ -45,7 +47,10 @@ use crate::acp::completion_provider::{ContextPickerCompletionProvider, MentionSe
use crate::acp::message_history::MessageHistory;
use crate::agent_diff::AgentDiff;
use crate::message_editor::{MAX_EDITOR_LINES, MIN_EDITOR_LINES};
-use crate::{AgentDiffPane, ExpandMessageEditor, Follow, KeepAll, OpenAgentDiff, RejectAll};
+use crate::ui::{AgentNotification, AgentNotificationEvent};
+use crate::{
+ AgentDiffPane, AgentPanel, ExpandMessageEditor, Follow, KeepAll, OpenAgentDiff, RejectAll,
+};
const RESPONSE_PADDING_X: Pixels = px(19.);
@@ -59,6 +64,8 @@ pub struct AcpThreadView {
message_set_from_history: bool,
_message_editor_subscription: Subscription,
mention_set: Arc>,
+ notifications: Vec>,
+ notification_subscriptions: HashMap, Vec>,
last_error: Option>,
list_state: ListState,
auth_task: Option>,
@@ -174,6 +181,8 @@ impl AcpThreadView {
message_set_from_history: false,
_message_editor_subscription: message_editor_subscription,
mention_set,
+ notifications: Vec::new(),
+ notification_subscriptions: HashMap::default(),
diff_editors: Default::default(),
list_state: list_state,
last_error: None,
@@ -382,7 +391,9 @@ impl AcpThreadView {
return;
}
- let Some(thread) = self.thread() else { return };
+ let Some(thread) = self.thread() else {
+ return;
+ };
let task = thread.update(cx, |thread, cx| thread.send(chunks.clone(), cx));
cx.spawn(async move |this, cx| {
@@ -565,6 +576,30 @@ impl AcpThreadView {
self.sync_thread_entry_view(index, window, cx);
self.list_state.splice(index..index + 1, 1);
}
+ AcpThreadEvent::ToolAuthorizationRequired => {
+ self.notify_with_sound("Waiting for tool confirmation", IconName::Info, window, cx);
+ }
+ AcpThreadEvent::Stopped => {
+ let used_tools = thread.read(cx).used_tools_since_last_user_message();
+ self.notify_with_sound(
+ if used_tools {
+ "Finished running tools"
+ } else {
+ "New message"
+ },
+ IconName::ZedAssistant,
+ window,
+ cx,
+ );
+ }
+ AcpThreadEvent::Error => {
+ self.notify_with_sound(
+ "Agent stopped due to an error",
+ IconName::Warning,
+ window,
+ cx,
+ );
+ }
}
cx.notify();
}
@@ -2166,6 +2201,154 @@ impl AcpThreadView {
self.list_state.scroll_to(ListOffset::default());
cx.notify();
}
+
+ fn notify_with_sound(
+ &mut self,
+ caption: impl Into,
+ icon: IconName,
+ window: &mut Window,
+ cx: &mut Context,
+ ) {
+ self.play_notification_sound(window, cx);
+ self.show_notification(caption, icon, window, cx);
+ }
+
+ fn play_notification_sound(&self, window: &Window, cx: &mut App) {
+ let settings = AgentSettings::get_global(cx);
+ if settings.play_sound_when_agent_done && !window.is_window_active() {
+ Audio::play_sound(Sound::AgentDone, cx);
+ }
+ }
+
+ fn show_notification(
+ &mut self,
+ caption: impl Into,
+ icon: IconName,
+ window: &mut Window,
+ cx: &mut Context,
+ ) {
+ if window.is_window_active() || !self.notifications.is_empty() {
+ return;
+ }
+
+ let title = self.title(cx);
+
+ match AgentSettings::get_global(cx).notify_when_agent_waiting {
+ NotifyWhenAgentWaiting::PrimaryScreen => {
+ if let Some(primary) = cx.primary_display() {
+ self.pop_up(icon, caption.into(), title, window, primary, cx);
+ }
+ }
+ NotifyWhenAgentWaiting::AllScreens => {
+ let caption = caption.into();
+ for screen in cx.displays() {
+ self.pop_up(icon, caption.clone(), title.clone(), window, screen, cx);
+ }
+ }
+ NotifyWhenAgentWaiting::Never => {
+ // Don't show anything
+ }
+ }
+ }
+
+ fn pop_up(
+ &mut self,
+ icon: IconName,
+ caption: SharedString,
+ title: SharedString,
+ window: &mut Window,
+ screen: Rc,
+ cx: &mut Context,
+ ) {
+ let options = AgentNotification::window_options(screen, cx);
+
+ let project_name = self.workspace.upgrade().and_then(|workspace| {
+ workspace
+ .read(cx)
+ .project()
+ .read(cx)
+ .visible_worktrees(cx)
+ .next()
+ .map(|worktree| worktree.read(cx).root_name().to_string())
+ });
+
+ if let Some(screen_window) = cx
+ .open_window(options, |_, cx| {
+ cx.new(|_| {
+ AgentNotification::new(title.clone(), caption.clone(), icon, project_name)
+ })
+ })
+ .log_err()
+ {
+ if let Some(pop_up) = screen_window.entity(cx).log_err() {
+ self.notification_subscriptions
+ .entry(screen_window)
+ .or_insert_with(Vec::new)
+ .push(cx.subscribe_in(&pop_up, window, {
+ |this, _, event, window, cx| match event {
+ AgentNotificationEvent::Accepted => {
+ let handle = window.window_handle();
+ cx.activate(true);
+
+ let workspace_handle = this.workspace.clone();
+
+ // If there are multiple Zed windows, activate the correct one.
+ cx.defer(move |cx| {
+ handle
+ .update(cx, |_view, window, _cx| {
+ window.activate_window();
+
+ if let Some(workspace) = workspace_handle.upgrade() {
+ workspace.update(_cx, |workspace, cx| {
+ workspace.focus_panel::(window, cx);
+ });
+ }
+ })
+ .log_err();
+ });
+
+ this.dismiss_notifications(cx);
+ }
+ AgentNotificationEvent::Dismissed => {
+ this.dismiss_notifications(cx);
+ }
+ }
+ }));
+
+ self.notifications.push(screen_window);
+
+ // If the user manually refocuses the original window, dismiss the popup.
+ self.notification_subscriptions
+ .entry(screen_window)
+ .or_insert_with(Vec::new)
+ .push({
+ let pop_up_weak = pop_up.downgrade();
+
+ cx.observe_window_activation(window, move |_, window, cx| {
+ if window.is_window_active() {
+ if let Some(pop_up) = pop_up_weak.upgrade() {
+ pop_up.update(cx, |_, cx| {
+ cx.emit(AgentNotificationEvent::Dismissed);
+ });
+ }
+ }
+ })
+ });
+ }
+ }
+ }
+
+ fn dismiss_notifications(&mut self, cx: &mut Context) {
+ for window in self.notifications.drain(..) {
+ window
+ .update(cx, |_, window, _| {
+ window.remove_window();
+ })
+ .ok();
+
+ self.notification_subscriptions.remove(&window);
+ }
+ }
}
impl Focusable for AcpThreadView {
@@ -2451,3 +2634,331 @@ fn plan_label_markdown_style(
..default_md_style
}
}
+
+#[cfg(test)]
+mod tests {
+ use agent_client_protocol::SessionId;
+ use editor::EditorSettings;
+ use fs::FakeFs;
+ use futures::future::try_join_all;
+ use gpui::{SemanticVersion, TestAppContext, VisualTestContext};
+ use rand::Rng;
+ use settings::SettingsStore;
+
+ use super::*;
+
+ #[gpui::test]
+ async fn test_notification_for_stop_event(cx: &mut TestAppContext) {
+ init_test(cx);
+
+ let (thread_view, cx) = setup_thread_view(StubAgentServer::default(), cx).await;
+
+ let message_editor = cx.read(|cx| thread_view.read(cx).message_editor.clone());
+ message_editor.update_in(cx, |editor, window, cx| {
+ editor.set_text("Hello", window, cx);
+ });
+
+ cx.deactivate_window();
+
+ thread_view.update_in(cx, |thread_view, window, cx| {
+ thread_view.chat(&Chat, window, cx);
+ });
+
+ cx.run_until_parked();
+
+ assert!(
+ cx.windows()
+ .iter()
+ .any(|window| window.downcast::().is_some())
+ );
+ }
+
+ #[gpui::test]
+ async fn test_notification_for_error(cx: &mut TestAppContext) {
+ init_test(cx);
+
+ let (thread_view, cx) =
+ setup_thread_view(StubAgentServer::new(SaboteurAgentConnection), cx).await;
+
+ let message_editor = cx.read(|cx| thread_view.read(cx).message_editor.clone());
+ message_editor.update_in(cx, |editor, window, cx| {
+ editor.set_text("Hello", window, cx);
+ });
+
+ cx.deactivate_window();
+
+ thread_view.update_in(cx, |thread_view, window, cx| {
+ thread_view.chat(&Chat, window, cx);
+ });
+
+ cx.run_until_parked();
+
+ assert!(
+ cx.windows()
+ .iter()
+ .any(|window| window.downcast::().is_some())
+ );
+ }
+
+ #[gpui::test]
+ async fn test_notification_for_tool_authorization(cx: &mut TestAppContext) {
+ init_test(cx);
+
+ let tool_call_id = acp::ToolCallId("1".into());
+ let tool_call = acp::ToolCall {
+ id: tool_call_id.clone(),
+ label: "Label".into(),
+ kind: acp::ToolKind::Edit,
+ status: acp::ToolCallStatus::Pending,
+ content: vec!["hi".into()],
+ locations: vec![],
+ raw_input: None,
+ };
+ let connection = StubAgentConnection::new(vec![acp::SessionUpdate::ToolCall(tool_call)])
+ .with_permission_requests(HashMap::from_iter([(
+ tool_call_id,
+ vec![acp::PermissionOption {
+ id: acp::PermissionOptionId("1".into()),
+ label: "Allow".into(),
+ kind: acp::PermissionOptionKind::AllowOnce,
+ }],
+ )]));
+ let (thread_view, cx) = setup_thread_view(StubAgentServer::new(connection), cx).await;
+
+ let message_editor = cx.read(|cx| thread_view.read(cx).message_editor.clone());
+ message_editor.update_in(cx, |editor, window, cx| {
+ editor.set_text("Hello", window, cx);
+ });
+
+ cx.deactivate_window();
+
+ thread_view.update_in(cx, |thread_view, window, cx| {
+ thread_view.chat(&Chat, window, cx);
+ });
+
+ cx.run_until_parked();
+
+ assert!(
+ cx.windows()
+ .iter()
+ .any(|window| window.downcast::().is_some())
+ );
+ }
+
+ async fn setup_thread_view(
+ agent: impl AgentServer + 'static,
+ cx: &mut TestAppContext,
+ ) -> (Entity, &mut VisualTestContext) {
+ let fs = FakeFs::new(cx.executor());
+ let project = Project::test(fs, [], cx).await;
+ let (workspace, cx) =
+ cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
+
+ let thread_view = cx.update(|window, cx| {
+ cx.new(|cx| {
+ AcpThreadView::new(
+ Rc::new(agent),
+ workspace.downgrade(),
+ project,
+ Rc::new(RefCell::new(MessageHistory::default())),
+ 1,
+ None,
+ window,
+ cx,
+ )
+ })
+ });
+ cx.run_until_parked();
+ (thread_view, cx)
+ }
+
+ struct StubAgentServer {
+ connection: C,
+ }
+
+ impl StubAgentServer {
+ fn new(connection: C) -> Self {
+ Self { connection }
+ }
+ }
+
+ impl StubAgentServer {
+ fn default() -> Self {
+ Self::new(StubAgentConnection::default())
+ }
+ }
+
+ impl AgentServer for StubAgentServer
+ where
+ C: 'static + AgentConnection + Send + Clone,
+ {
+ fn logo(&self) -> ui::IconName {
+ unimplemented!()
+ }
+
+ fn name(&self) -> &'static str {
+ unimplemented!()
+ }
+
+ fn empty_state_headline(&self) -> &'static str {
+ unimplemented!()
+ }
+
+ fn empty_state_message(&self) -> &'static str {
+ unimplemented!()
+ }
+
+ fn connect(
+ &self,
+ _root_dir: &Path,
+ _project: &Entity,
+ _cx: &mut App,
+ ) -> Task>> {
+ Task::ready(Ok(Rc::new(self.connection.clone())))
+ }
+ }
+
+ #[derive(Clone, Default)]
+ struct StubAgentConnection {
+ sessions: Arc>>>,
+ permission_requests: HashMap>,
+ updates: Vec,
+ }
+
+ impl StubAgentConnection {
+ fn new(updates: Vec) -> Self {
+ Self {
+ updates,
+ permission_requests: HashMap::default(),
+ sessions: Arc::default(),
+ }
+ }
+
+ fn with_permission_requests(
+ mut self,
+ permission_requests: HashMap>,
+ ) -> Self {
+ self.permission_requests = permission_requests;
+ self
+ }
+ }
+
+ impl AgentConnection for StubAgentConnection {
+ fn name(&self) -> &'static str {
+ "StubAgentConnection"
+ }
+
+ fn new_thread(
+ self: Rc,
+ project: Entity,
+ _cwd: &Path,
+ cx: &mut gpui::AsyncApp,
+ ) -> Task>> {
+ let session_id = SessionId(
+ rand::thread_rng()
+ .sample_iter(&rand::distributions::Alphanumeric)
+ .take(7)
+ .map(char::from)
+ .collect::()
+ .into(),
+ );
+ let thread = cx
+ .new(|cx| AcpThread::new(self.clone(), project, session_id.clone(), cx))
+ .unwrap();
+ self.sessions.lock().insert(session_id, thread.downgrade());
+ Task::ready(Ok(thread))
+ }
+
+ fn authenticate(&self, _cx: &mut App) -> Task> {
+ unimplemented!()
+ }
+
+ fn prompt(&self, params: acp::PromptArguments, cx: &mut App) -> Task> {
+ let sessions = self.sessions.lock();
+ let thread = sessions.get(¶ms.session_id).unwrap();
+ let mut tasks = vec![];
+ for update in &self.updates {
+ let thread = thread.clone();
+ let update = update.clone();
+ let permission_request = if let acp::SessionUpdate::ToolCall(tool_call) = &update
+ && let Some(options) = self.permission_requests.get(&tool_call.id)
+ {
+ Some((tool_call.clone(), options.clone()))
+ } else {
+ None
+ };
+ let task = cx.spawn(async move |cx| {
+ if let Some((tool_call, options)) = permission_request {
+ let permission = thread.update(cx, |thread, cx| {
+ thread.request_tool_call_permission(
+ tool_call.clone(),
+ options.clone(),
+ cx,
+ )
+ })?;
+ permission.await?;
+ }
+ thread.update(cx, |thread, cx| {
+ thread.handle_session_update(update.clone(), cx).unwrap();
+ })?;
+ anyhow::Ok(())
+ });
+ tasks.push(task);
+ }
+ cx.spawn(async move |_| {
+ try_join_all(tasks).await?;
+ Ok(())
+ })
+ }
+
+ fn cancel(&self, _session_id: &acp::SessionId, _cx: &mut App) {
+ unimplemented!()
+ }
+ }
+
+ #[derive(Clone)]
+ struct SaboteurAgentConnection;
+
+ impl AgentConnection for SaboteurAgentConnection {
+ fn name(&self) -> &'static str {
+ "SaboteurAgentConnection"
+ }
+
+ fn new_thread(
+ self: Rc,
+ project: Entity,
+ _cwd: &Path,
+ cx: &mut gpui::AsyncApp,
+ ) -> Task>> {
+ Task::ready(Ok(cx
+ .new(|cx| AcpThread::new(self, project, SessionId("test".into()), cx))
+ .unwrap()))
+ }
+
+ fn authenticate(&self, _cx: &mut App) -> Task> {
+ unimplemented!()
+ }
+
+ fn prompt(&self, _params: acp::PromptArguments, _cx: &mut App) -> Task> {
+ Task::ready(Err(anyhow::anyhow!("Error prompting")))
+ }
+
+ fn cancel(&self, _session_id: &acp::SessionId, _cx: &mut App) {
+ unimplemented!()
+ }
+ }
+
+ fn init_test(cx: &mut TestAppContext) {
+ cx.update(|cx| {
+ let settings_store = SettingsStore::test(cx);
+ cx.set_global(settings_store);
+ language::init(cx);
+ Project::init_settings(cx);
+ AgentSettings::register(cx);
+ workspace::init_settings(cx);
+ ThemeSettings::register(cx);
+ release_channel::init(SemanticVersion::default(), cx);
+ EditorSettings::register(cx);
+ });
+ }
+}
diff --git a/crates/agent_ui/src/agent_configuration.rs b/crates/agent_ui/src/agent_configuration.rs
index fae04188eb..dad930be9e 100644
--- a/crates/agent_ui/src/agent_configuration.rs
+++ b/crates/agent_ui/src/agent_configuration.rs
@@ -7,6 +7,7 @@ use std::{sync::Arc, time::Duration};
use agent_settings::AgentSettings;
use assistant_tool::{ToolSource, ToolWorkingSet};
+use cloud_llm_client::Plan;
use collections::HashMap;
use context_server::ContextServerId;
use extension::ExtensionManifest;
@@ -25,7 +26,6 @@ use project::{
context_server_store::{ContextServerConfiguration, ContextServerStatus, ContextServerStore},
project_settings::{ContextServerSettings, ProjectSettings},
};
-use proto::Plan;
use settings::{Settings, update_settings_file};
use ui::{
Chip, ContextMenu, Disclosure, Divider, DividerColor, ElevationIndex, Indicator, PopoverMenu,
@@ -180,7 +180,7 @@ impl AgentConfiguration {
let current_plan = if is_zed_provider {
self.workspace
.upgrade()
- .and_then(|workspace| workspace.read(cx).user_store().read(cx).current_plan())
+ .and_then(|workspace| workspace.read(cx).user_store().read(cx).plan())
} else {
None
};
@@ -406,7 +406,9 @@ impl AgentConfiguration {
SwitchField::new(
"always-allow-tool-actions-switch",
"Allow running commands without asking for confirmation",
- "The agent can perform potentially destructive actions without asking for your confirmation.",
+ Some(
+ "The agent can perform potentially destructive actions without asking for your confirmation.".into(),
+ ),
always_allow_tool_actions,
move |state, _window, cx| {
let allow = state == &ToggleState::Selected;
@@ -424,7 +426,7 @@ impl AgentConfiguration {
SwitchField::new(
"single-file-review",
"Enable single-file agent reviews",
- "Agent edits are also displayed in single-file editors for review.",
+ Some("Agent edits are also displayed in single-file editors for review.".into()),
single_file_review,
move |state, _window, cx| {
let allow = state == &ToggleState::Selected;
@@ -442,7 +444,9 @@ impl AgentConfiguration {
SwitchField::new(
"sound-notification",
"Play sound when finished generating",
- "Hear a notification sound when the agent is done generating changes or needs your input.",
+ Some(
+ "Hear a notification sound when the agent is done generating changes or needs your input.".into(),
+ ),
play_sound_when_agent_done,
move |state, _window, cx| {
let allow = state == &ToggleState::Selected;
@@ -460,7 +464,9 @@ impl AgentConfiguration {
SwitchField::new(
"modifier-send",
"Use modifier to submit a message",
- "Make a modifier (cmd-enter on macOS, ctrl-enter on Linux) required to send messages.",
+ Some(
+ "Make a modifier (cmd-enter on macOS, ctrl-enter on Linux) required to send messages.".into(),
+ ),
use_modifier_to_send,
move |state, _window, cx| {
let allow = state == &ToggleState::Selected;
@@ -502,7 +508,7 @@ impl AgentConfiguration {
.blend(cx.theme().colors().text_accent.opacity(0.2));
let (plan_name, label_color, bg_color) = match plan {
- Plan::Free => ("Free", Color::Default, free_chip_bg),
+ Plan::ZedFree => ("Free", Color::Default, free_chip_bg),
Plan::ZedProTrial => ("Pro Trial", Color::Accent, pro_chip_bg),
Plan::ZedPro => ("Pro", Color::Accent, pro_chip_bg),
};
diff --git a/crates/agent_ui/src/agent_diff.rs b/crates/agent_ui/src/agent_diff.rs
index ec0a11f86b..c4dc359093 100644
--- a/crates/agent_ui/src/agent_diff.rs
+++ b/crates/agent_ui/src/agent_diff.rs
@@ -1521,6 +1521,9 @@ impl AgentDiff {
self.update_reviewing_editors(workspace, window, cx);
}
}
+ AcpThreadEvent::Stopped
+ | AcpThreadEvent::ToolAuthorizationRequired
+ | AcpThreadEvent::Error => {}
}
}
diff --git a/crates/agent_ui/src/agent_panel.rs b/crates/agent_ui/src/agent_panel.rs
index 875320372d..a09c669769 100644
--- a/crates/agent_ui/src/agent_panel.rs
+++ b/crates/agent_ui/src/agent_panel.rs
@@ -44,7 +44,7 @@ use assistant_context::{AssistantContext, ContextEvent, ContextSummary};
use assistant_slash_command::SlashCommandWorkingSet;
use assistant_tool::ToolWorkingSet;
use client::{DisableAiSettings, UserStore, zed_urls};
-use cloud_llm_client::{CompletionIntent, UsageLimit};
+use cloud_llm_client::{CompletionIntent, Plan, UsageLimit};
use editor::{Anchor, AnchorRangeExt as _, Editor, EditorEvent, MultiBuffer};
use feature_flags::{self, FeatureFlagAppExt};
use fs::Fs;
@@ -60,7 +60,6 @@ use language_model::{
};
use project::{Project, ProjectPath, Worktree};
use prompt_store::{PromptBuilder, PromptStore, UserPromptId};
-use proto::Plan;
use rules_library::{RulesLibrary, open_rules_library};
use search::{BufferSearchBar, buffer_search};
use settings::{Settings, update_settings_file};
@@ -78,7 +77,7 @@ use workspace::{
};
use zed_actions::{
DecreaseBufferFontSize, IncreaseBufferFontSize, ResetBufferFontSize,
- agent::{OpenConfiguration, OpenOnboardingModal, ResetOnboarding, ToggleModelSelector},
+ agent::{OpenOnboardingModal, OpenSettings, ResetOnboarding, ToggleModelSelector},
assistant::{OpenRulesLibrary, ToggleFocus},
};
@@ -105,7 +104,7 @@ pub fn init(cx: &mut App) {
panel.update(cx, |panel, cx| panel.open_history(window, cx));
}
})
- .register_action(|workspace, _: &OpenConfiguration, window, cx| {
+ .register_action(|workspace, _: &OpenSettings, window, cx| {
if let Some(panel) = workspace.panel::(cx) {
workspace.focus_panel::(window, cx);
panel.update(cx, |panel, cx| panel.open_configuration(window, cx));
@@ -579,7 +578,6 @@ impl AgentPanel {
MessageEditor::new(
fs.clone(),
workspace.clone(),
- user_store.clone(),
message_editor_context_store.clone(),
prompt_store.clone(),
thread_store.downgrade(),
@@ -848,7 +846,6 @@ impl AgentPanel {
MessageEditor::new(
self.fs.clone(),
self.workspace.clone(),
- self.user_store.clone(),
context_store.clone(),
self.prompt_store.clone(),
self.thread_store.downgrade(),
@@ -1122,7 +1119,6 @@ impl AgentPanel {
MessageEditor::new(
self.fs.clone(),
self.workspace.clone(),
- self.user_store.clone(),
context_store,
self.prompt_store.clone(),
self.thread_store.downgrade(),
@@ -2074,7 +2070,7 @@ impl AgentPanel {
menu = menu
.action("Rules…", Box::new(OpenRulesLibrary::default()))
- .action("Settings", Box::new(OpenConfiguration))
+ .action("Settings", Box::new(OpenSettings))
.action(zoom_in_label, Box::new(ToggleZoom));
menu
}))
@@ -2279,10 +2275,10 @@ impl AgentPanel {
| ActiveView::Configuration => return false,
}
- let plan = self.user_store.read(cx).current_plan();
+ let plan = self.user_store.read(cx).plan();
let has_previous_trial = self.user_store.read(cx).trial_started_at().is_some();
- matches!(plan, Some(Plan::Free)) && has_previous_trial
+ matches!(plan, Some(Plan::ZedFree)) && has_previous_trial
}
fn should_render_onboarding(&self, cx: &mut Context) -> bool {
@@ -2468,14 +2464,14 @@ impl AgentPanel {
.icon_color(Color::Muted)
.full_width()
.key_binding(KeyBinding::for_action_in(
- &OpenConfiguration,
+ &OpenSettings,
&focus_handle,
window,
cx,
))
.on_click(|_event, window, cx| {
window.dispatch_action(
- OpenConfiguration.boxed_clone(),
+ OpenSettings.boxed_clone(),
cx,
)
}),
@@ -2680,16 +2676,11 @@ impl AgentPanel {
.style(ButtonStyle::Tinted(ui::TintColor::Warning))
.label_size(LabelSize::Small)
.key_binding(
- KeyBinding::for_action_in(
- &OpenConfiguration,
- &focus_handle,
- window,
- cx,
- )
- .map(|kb| kb.size(rems_from_px(12.))),
+ KeyBinding::for_action_in(&OpenSettings, &focus_handle, window, cx)
+ .map(|kb| kb.size(rems_from_px(12.))),
)
.on_click(|_event, window, cx| {
- window.dispatch_action(OpenConfiguration.boxed_clone(), cx)
+ window.dispatch_action(OpenSettings.boxed_clone(), cx)
}),
),
ConfigurationError::ProviderPendingTermsAcceptance(provider) => {
@@ -2883,7 +2874,7 @@ impl AgentPanel {
) -> AnyElement {
let error_message = match plan {
Plan::ZedPro => "Upgrade to usage-based billing for more prompts.",
- Plan::ZedProTrial | Plan::Free => "Upgrade to Zed Pro for more prompts.",
+ Plan::ZedProTrial | Plan::ZedFree => "Upgrade to Zed Pro for more prompts.",
};
let icon = Icon::new(IconName::XCircle)
@@ -3193,7 +3184,7 @@ impl Render for AgentPanel {
.on_action(cx.listener(|this, _: &OpenHistory, window, cx| {
this.open_history(window, cx);
}))
- .on_action(cx.listener(|this, _: &OpenConfiguration, window, cx| {
+ .on_action(cx.listener(|this, _: &OpenSettings, window, cx| {
this.open_configuration(window, cx);
}))
.on_action(cx.listener(Self::open_active_thread_as_markdown))
diff --git a/crates/agent_ui/src/agent_ui.rs b/crates/agent_ui/src/agent_ui.rs
index 6ae78585de..c5574c2371 100644
--- a/crates/agent_ui/src/agent_ui.rs
+++ b/crates/agent_ui/src/agent_ui.rs
@@ -263,8 +263,8 @@ fn update_command_palette_filter(cx: &mut App) {
filter.hide_namespace("agent");
filter.hide_namespace("assistant");
filter.hide_namespace("copilot");
+ filter.hide_namespace("supermaven");
filter.hide_namespace("zed_predict_onboarding");
-
filter.hide_namespace("edit_prediction");
use editor::actions::{
diff --git a/crates/agent_ui/src/inline_assistant.rs b/crates/agent_ui/src/inline_assistant.rs
index 44ec050ae2..ffa654d12b 100644
--- a/crates/agent_ui/src/inline_assistant.rs
+++ b/crates/agent_ui/src/inline_assistant.rs
@@ -48,7 +48,7 @@ use text::{OffsetRangeExt, ToPoint as _};
use ui::prelude::*;
use util::{RangeExt, ResultExt, maybe};
use workspace::{ItemHandle, Toast, Workspace, dock::Panel, notifications::NotificationId};
-use zed_actions::agent::OpenConfiguration;
+use zed_actions::agent::OpenSettings;
pub fn init(
fs: Arc,
@@ -345,7 +345,7 @@ impl InlineAssistant {
if let Some(answer) = answer {
if answer == 0 {
cx.update(|window, cx| {
- window.dispatch_action(Box::new(OpenConfiguration), cx)
+ window.dispatch_action(Box::new(OpenSettings), cx)
})
.ok();
}
diff --git a/crates/agent_ui/src/language_model_selector.rs b/crates/agent_ui/src/language_model_selector.rs
index 655e87d7cd..7121624c87 100644
--- a/crates/agent_ui/src/language_model_selector.rs
+++ b/crates/agent_ui/src/language_model_selector.rs
@@ -576,7 +576,7 @@ impl PickerDelegate for LanguageModelPickerDelegate {
.icon_position(IconPosition::Start)
.on_click(|_, window, cx| {
window.dispatch_action(
- zed_actions::agent::OpenConfiguration.boxed_clone(),
+ zed_actions::agent::OpenSettings.boxed_clone(),
cx,
);
}),
diff --git a/crates/agent_ui/src/message_editor.rs b/crates/agent_ui/src/message_editor.rs
index 082d1dfb51..2185885347 100644
--- a/crates/agent_ui/src/message_editor.rs
+++ b/crates/agent_ui/src/message_editor.rs
@@ -17,7 +17,6 @@ use agent::{
use agent_settings::{AgentSettings, CompletionMode};
use ai_onboarding::ApiKeysWithProviders;
use buffer_diff::BufferDiff;
-use client::UserStore;
use cloud_llm_client::CompletionIntent;
use collections::{HashMap, HashSet};
use editor::actions::{MoveUp, Paste};
@@ -43,7 +42,6 @@ use language_model::{
use multi_buffer;
use project::Project;
use prompt_store::PromptStore;
-use proto::Plan;
use settings::Settings;
use std::time::Duration;
use theme::ThemeSettings;
@@ -79,7 +77,6 @@ pub struct MessageEditor {
editor: Entity,
workspace: WeakEntity,
project: Entity,
- user_store: Entity,
context_store: Entity,
prompt_store: Option>,
history_store: Option>,
@@ -159,7 +156,6 @@ impl MessageEditor {
pub fn new(
fs: Arc,
workspace: WeakEntity,
- user_store: Entity,
context_store: Entity,
prompt_store: Option>,
thread_store: WeakEntity,
@@ -231,7 +227,6 @@ impl MessageEditor {
Self {
editor: editor.clone(),
project: thread.read(cx).project().clone(),
- user_store,
thread,
incompatible_tools_state: incompatible_tools.clone(),
workspace,
@@ -1287,24 +1282,12 @@ impl MessageEditor {
return None;
}
- let user_store = self.user_store.read(cx);
-
- let ubb_enable = user_store
- .usage_based_billing_enabled()
- .map_or(false, |enabled| enabled);
-
- if ubb_enable {
+ let user_store = self.project.read(cx).user_store().read(cx);
+ if user_store.is_usage_based_billing_enabled() {
return None;
}
- let plan = user_store
- .current_plan()
- .map(|plan| match plan {
- Plan::Free => cloud_llm_client::Plan::ZedFree,
- Plan::ZedPro => cloud_llm_client::Plan::ZedPro,
- Plan::ZedProTrial => cloud_llm_client::Plan::ZedProTrial,
- })
- .unwrap_or(cloud_llm_client::Plan::ZedFree);
+ let plan = user_store.plan().unwrap_or(cloud_llm_client::Plan::ZedFree);
let usage = user_store.model_request_usage()?;
@@ -1769,7 +1752,6 @@ impl AgentPreview for MessageEditor {
) -> Option {
if let Some(workspace) = workspace.upgrade() {
let fs = workspace.read(cx).app_state().fs.clone();
- let user_store = workspace.read(cx).app_state().user_store.clone();
let project = workspace.read(cx).project().clone();
let weak_project = project.downgrade();
let context_store = cx.new(|_cx| ContextStore::new(weak_project, None));
@@ -1782,7 +1764,6 @@ impl AgentPreview for MessageEditor {
MessageEditor::new(
fs,
workspace.downgrade(),
- user_store,
context_store,
None,
thread_store.downgrade(),
diff --git a/crates/ai_onboarding/Cargo.toml b/crates/ai_onboarding/Cargo.toml
index 9031e14e29..95a45b1a6f 100644
--- a/crates/ai_onboarding/Cargo.toml
+++ b/crates/ai_onboarding/Cargo.toml
@@ -16,10 +16,10 @@ default = []
[dependencies]
client.workspace = true
+cloud_llm_client.workspace = true
component.workspace = true
gpui.workspace = true
language_model.workspace = true
-proto.workspace = true
serde.workspace = true
smallvec.workspace = true
telemetry.workspace = true
diff --git a/crates/ai_onboarding/src/agent_api_keys_onboarding.rs b/crates/ai_onboarding/src/agent_api_keys_onboarding.rs
index 5f56e4d26e..e86568fe7a 100644
--- a/crates/ai_onboarding/src/agent_api_keys_onboarding.rs
+++ b/crates/ai_onboarding/src/agent_api_keys_onboarding.rs
@@ -136,10 +136,7 @@ impl RenderOnce for ApiKeysWithoutProviders {
.full_width()
.style(ButtonStyle::Outlined)
.on_click(move |_, window, cx| {
- window.dispatch_action(
- zed_actions::agent::OpenConfiguration.boxed_clone(),
- cx,
- );
+ window.dispatch_action(zed_actions::agent::OpenSettings.boxed_clone(), cx);
}),
)
}
diff --git a/crates/ai_onboarding/src/agent_panel_onboarding_content.rs b/crates/ai_onboarding/src/agent_panel_onboarding_content.rs
index e8a62f7ff2..f1629eeff8 100644
--- a/crates/ai_onboarding/src/agent_panel_onboarding_content.rs
+++ b/crates/ai_onboarding/src/agent_panel_onboarding_content.rs
@@ -1,6 +1,7 @@
use std::sync::Arc;
use client::{Client, UserStore};
+use cloud_llm_client::Plan;
use gpui::{Entity, IntoElement, ParentElement};
use language_model::{LanguageModelRegistry, ZED_CLOUD_PROVIDER_ID};
use ui::prelude::*;
@@ -56,15 +57,8 @@ impl AgentPanelOnboarding {
impl Render for AgentPanelOnboarding {
fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement {
- let enrolled_in_trial = matches!(
- self.user_store.read(cx).current_plan(),
- Some(proto::Plan::ZedProTrial)
- );
-
- let is_pro_user = matches!(
- self.user_store.read(cx).current_plan(),
- Some(proto::Plan::ZedPro)
- );
+ let enrolled_in_trial = self.user_store.read(cx).plan() == Some(Plan::ZedProTrial);
+ let is_pro_user = self.user_store.read(cx).plan() == Some(Plan::ZedPro);
AgentPanelOnboardingCard::new()
.child(
diff --git a/crates/ai_onboarding/src/ai_onboarding.rs b/crates/ai_onboarding/src/ai_onboarding.rs
index 3aec9c62cd..c252b65f20 100644
--- a/crates/ai_onboarding/src/ai_onboarding.rs
+++ b/crates/ai_onboarding/src/ai_onboarding.rs
@@ -9,6 +9,7 @@ pub use agent_api_keys_onboarding::{ApiKeysWithProviders, ApiKeysWithoutProvider
pub use agent_panel_onboarding_card::AgentPanelOnboardingCard;
pub use agent_panel_onboarding_content::AgentPanelOnboarding;
pub use ai_upsell_card::AiUpsellCard;
+use cloud_llm_client::Plan;
pub use edit_prediction_onboarding_content::EditPredictionOnboarding;
pub use young_account_banner::YoungAccountBanner;
@@ -79,7 +80,7 @@ impl From for SignInStatus {
pub struct ZedAiOnboarding {
pub sign_in_status: SignInStatus,
pub has_accepted_terms_of_service: bool,
- pub plan: Option,
+ pub plan: Option,
pub account_too_young: bool,
pub continue_with_zed_ai: Arc,
pub sign_in: Arc,
@@ -99,8 +100,8 @@ impl ZedAiOnboarding {
Self {
sign_in_status: status.into(),
- has_accepted_terms_of_service: store.current_user_has_accepted_terms().unwrap_or(false),
- plan: store.current_plan(),
+ has_accepted_terms_of_service: store.has_accepted_terms_of_service(),
+ plan: store.plan(),
account_too_young: store.account_too_young(),
continue_with_zed_ai,
accept_terms_of_service: Arc::new({
@@ -113,11 +114,9 @@ impl ZedAiOnboarding {
sign_in: Arc::new(move |_window, cx| {
cx.spawn({
let client = client.clone();
- async move |cx| {
- client.authenticate_and_connect(true, cx).await;
- }
+ async move |cx| client.sign_in_with_optional_connect(true, cx).await
})
- .detach();
+ .detach_and_log_err(cx);
}),
dismiss_onboarding: None,
}
@@ -411,9 +410,9 @@ impl RenderOnce for ZedAiOnboarding {
if matches!(self.sign_in_status, SignInStatus::SignedIn) {
if self.has_accepted_terms_of_service {
match self.plan {
- None | Some(proto::Plan::Free) => self.render_free_plan_state(cx),
- Some(proto::Plan::ZedProTrial) => self.render_trial_state(cx),
- Some(proto::Plan::ZedPro) => self.render_pro_plan_state(cx),
+ None | Some(Plan::ZedFree) => self.render_free_plan_state(cx),
+ Some(Plan::ZedProTrial) => self.render_trial_state(cx),
+ Some(Plan::ZedPro) => self.render_pro_plan_state(cx),
}
} else {
self.render_accept_terms_of_service()
@@ -433,7 +432,7 @@ impl Component for ZedAiOnboarding {
fn onboarding(
sign_in_status: SignInStatus,
has_accepted_terms_of_service: bool,
- plan: Option,
+ plan: Option,
account_too_young: bool,
) -> AnyElement {
ZedAiOnboarding {
@@ -468,25 +467,15 @@ impl Component for ZedAiOnboarding {
),
single_example(
"Free Plan",
- onboarding(SignInStatus::SignedIn, true, Some(proto::Plan::Free), false),
+ onboarding(SignInStatus::SignedIn, true, Some(Plan::ZedFree), false),
),
single_example(
"Pro Trial",
- onboarding(
- SignInStatus::SignedIn,
- true,
- Some(proto::Plan::ZedProTrial),
- false,
- ),
+ onboarding(SignInStatus::SignedIn, true, Some(Plan::ZedProTrial), false),
),
single_example(
"Pro Plan",
- onboarding(
- SignInStatus::SignedIn,
- true,
- Some(proto::Plan::ZedPro),
- false,
- ),
+ onboarding(SignInStatus::SignedIn, true, Some(Plan::ZedPro), false),
),
])
.into_any_element(),
diff --git a/crates/ai_onboarding/src/ai_upsell_card.rs b/crates/ai_onboarding/src/ai_upsell_card.rs
index 041e0d87ec..2408b6aa37 100644
--- a/crates/ai_onboarding/src/ai_upsell_card.rs
+++ b/crates/ai_onboarding/src/ai_upsell_card.rs
@@ -1,6 +1,7 @@
use std::sync::Arc;
use client::{Client, zed_urls};
+use cloud_llm_client::Plan;
use gpui::{AnyElement, App, IntoElement, RenderOnce, Window};
use ui::{Divider, List, Vector, VectorName, prelude::*};
@@ -10,22 +11,22 @@ use crate::{BulletItem, SignInStatus};
pub struct AiUpsellCard {
pub sign_in_status: SignInStatus,
pub sign_in: Arc,
+ pub user_plan: Option,
}
impl AiUpsellCard {
- pub fn new(client: Arc) -> Self {
+ pub fn new(client: Arc, user_plan: Option) -> Self {
let status = *client.status().borrow();
Self {
+ user_plan,
sign_in_status: status.into(),
sign_in: Arc::new(move |_window, cx| {
cx.spawn({
let client = client.clone();
- async move |cx| {
- client.authenticate_and_connect(true, cx).await;
- }
+ async move |cx| client.sign_in_with_optional_connect(true, cx).await
})
- .detach();
+ .detach_and_log_err(cx);
}),
}
}
@@ -34,6 +35,7 @@ impl AiUpsellCard {
impl RenderOnce for AiUpsellCard {
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
let pro_section = v_flex()
+ .flex_grow()
.w_full()
.gap_1()
.child(
@@ -56,6 +58,7 @@ impl RenderOnce for AiUpsellCard {
);
let free_section = v_flex()
+ .flex_grow()
.w_full()
.gap_1()
.child(
@@ -71,7 +74,7 @@ impl RenderOnce for AiUpsellCard {
)
.child(
List::new()
- .child(BulletItem::new("50 prompts with the Claude models"))
+ .child(BulletItem::new("50 prompts with Claude models"))
.child(BulletItem::new("2,000 accepted edit predictions")),
);
@@ -132,22 +135,28 @@ impl RenderOnce for AiUpsellCard {
v_flex()
.relative()
- .p_6()
- .pt_4()
+ .p_4()
+ .pt_3()
.border_1()
.border_color(cx.theme().colors().border)
.rounded_lg()
.overflow_hidden()
.child(grid_bg)
.child(gradient_bg)
- .child(Headline::new("Try Zed AI"))
- .child(Label::new(DESCRIPTION).color(Color::Muted).mb_2())
+ .child(Label::new("Try Zed AI").size(LabelSize::Large))
+ .child(
+ div()
+ .max_w_3_4()
+ .mb_2()
+ .child(Label::new(DESCRIPTION).color(Color::Muted)),
+ )
.child(
h_flex()
+ .w_full()
.mt_1p5()
.mb_2p5()
.items_start()
- .gap_12()
+ .gap_6()
.child(free_section)
.child(pro_section),
)
@@ -183,6 +192,7 @@ impl Component for AiUpsellCard {
AiUpsellCard {
sign_in_status: SignInStatus::SignedOut,
sign_in: Arc::new(|_, _| {}),
+ user_plan: None,
}
.into_any_element(),
),
@@ -191,6 +201,7 @@ impl Component for AiUpsellCard {
AiUpsellCard {
sign_in_status: SignInStatus::SignedIn,
sign_in: Arc::new(|_, _| {}),
+ user_plan: None,
}
.into_any_element(),
),
diff --git a/crates/channel/src/channel_store.rs b/crates/channel/src/channel_store.rs
index b7ba811421..4ad156b9fb 100644
--- a/crates/channel/src/channel_store.rs
+++ b/crates/channel/src/channel_store.rs
@@ -126,7 +126,7 @@ impl ChannelMembership {
proto::channel_member::Kind::Member => 0,
proto::channel_member::Kind::Invitee => 1,
},
- username_order: self.user.github_login.as_str(),
+ username_order: &self.user.github_login,
}
}
}
diff --git a/crates/channel/src/channel_store_tests.rs b/crates/channel/src/channel_store_tests.rs
index f8f5de3c39..c92226eeeb 100644
--- a/crates/channel/src/channel_store_tests.rs
+++ b/crates/channel/src/channel_store_tests.rs
@@ -259,20 +259,6 @@ async fn test_channel_messages(cx: &mut TestAppContext) {
assert_channels(&channel_store, &[(0, "the-channel".to_string())], cx);
});
- let get_users = server.receive::().await.unwrap();
- assert_eq!(get_users.payload.user_ids, vec![5]);
- server.respond(
- get_users.receipt(),
- proto::UsersResponse {
- users: vec![proto::User {
- id: 5,
- github_login: "nathansobo".into(),
- avatar_url: "http://avatar.com/nathansobo".into(),
- name: None,
- }],
- },
- );
-
// Join a channel and populate its existing messages.
let channel = channel_store.update(cx, |store, cx| {
let channel_id = store.ordered_channels().next().unwrap().1.id;
@@ -334,7 +320,7 @@ async fn test_channel_messages(cx: &mut TestAppContext) {
.map(|message| (message.sender.github_login.clone(), message.body.clone()))
.collect::>(),
&[
- ("nathansobo".into(), "a".into()),
+ ("user-5".into(), "a".into()),
("maxbrunsfeld".into(), "b".into())
]
);
@@ -437,7 +423,7 @@ async fn test_channel_messages(cx: &mut TestAppContext) {
.map(|message| (message.sender.github_login.clone(), message.body.clone()))
.collect::>(),
&[
- ("nathansobo".into(), "y".into()),
+ ("user-5".into(), "y".into()),
("maxbrunsfeld".into(), "z".into())
]
);
diff --git a/crates/client/Cargo.toml b/crates/client/Cargo.toml
index dd97bd9ca4..365625b445 100644
--- a/crates/client/Cargo.toml
+++ b/crates/client/Cargo.toml
@@ -17,11 +17,11 @@ test-support = ["clock/test-support", "collections/test-support", "gpui/test-sup
[dependencies]
anyhow.workspace = true
-async-recursion = "0.3"
async-tungstenite = { workspace = true, features = ["tokio", "tokio-rustls-manual-roots"] }
base64.workspace = true
chrono = { workspace = true, features = ["serde"] }
clock.workspace = true
+cloud_api_client.workspace = true
cloud_llm_client.workspace = true
collections.workspace = true
credentials_provider.workspace = true
diff --git a/crates/client/src/client.rs b/crates/client/src/client.rs
index e0f4a70b15..b9b20aa4f2 100644
--- a/crates/client/src/client.rs
+++ b/crates/client/src/client.rs
@@ -6,22 +6,21 @@ pub mod telemetry;
pub mod user;
pub mod zed_urls;
-use anyhow::{Context as _, Result, anyhow, bail};
-use async_recursion::async_recursion;
+use anyhow::{Context as _, Result, anyhow};
use async_tungstenite::tungstenite::{
client::IntoClientRequest,
error::Error as WebsocketError,
http::{HeaderValue, Request, StatusCode},
};
-use chrono::{DateTime, Utc};
use clock::SystemClock;
+use cloud_api_client::CloudApiClient;
use credentials_provider::CredentialsProvider;
use futures::{
AsyncReadExt, FutureExt, SinkExt, Stream, StreamExt, TryFutureExt as _, TryStreamExt,
channel::oneshot, future::BoxFuture,
};
use gpui::{App, AsyncApp, Entity, Global, Task, WeakEntity, actions};
-use http_client::{AsyncBody, HttpClient, HttpClientWithUrl, http};
+use http_client::{HttpClient, HttpClientWithUrl, http};
use parking_lot::RwLock;
use postage::watch;
use proxy::connect_proxy_stream;
@@ -31,7 +30,6 @@ use rpc::proto::{AnyTypedEnvelope, EnvelopedMessage, PeerId, RequestMessage};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use settings::{Settings, SettingsSources};
-use std::pin::Pin;
use std::{
any::TypeId,
convert::TryFrom,
@@ -45,6 +43,7 @@ use std::{
},
time::{Duration, Instant},
};
+use std::{cmp, pin::Pin};
use telemetry::Telemetry;
use thiserror::Error;
use tokio::net::TcpStream;
@@ -78,7 +77,7 @@ pub static ZED_ALWAYS_ACTIVE: LazyLock =
LazyLock::new(|| std::env::var("ZED_ALWAYS_ACTIVE").map_or(false, |e| !e.is_empty()));
pub const INITIAL_RECONNECTION_DELAY: Duration = Duration::from_millis(500);
-pub const MAX_RECONNECTION_DELAY: Duration = Duration::from_secs(10);
+pub const MAX_RECONNECTION_DELAY: Duration = Duration::from_secs(30);
pub const CONNECTION_TIMEOUT: Duration = Duration::from_secs(20);
actions!(
@@ -162,20 +161,8 @@ pub fn init(client: &Arc, cx: &mut App) {
let client = client.clone();
move |_: &SignIn, cx| {
if let Some(client) = client.upgrade() {
- cx.spawn(
- async move |cx| match client.authenticate_and_connect(true, &cx).await {
- ConnectionResult::Timeout => {
- log::error!("Initial authentication timed out");
- }
- ConnectionResult::ConnectionReset => {
- log::error!("Initial authentication connection reset");
- }
- ConnectionResult::Result(r) => {
- r.log_err();
- }
- },
- )
- .detach();
+ cx.spawn(async move |cx| client.sign_in_with_optional_connect(true, &cx).await)
+ .detach_and_log_err(cx);
}
}
});
@@ -213,6 +200,7 @@ pub struct Client {
id: AtomicU64,
peer: Arc,
http: Arc,
+ cloud_client: Arc,
telemetry: Arc,
credentials_provider: ClientCredentialsProvider,
state: RwLock,
@@ -283,6 +271,8 @@ pub enum Status {
SignedOut,
UpgradeRequired,
Authenticating,
+ Authenticated,
+ AuthenticationError,
Connecting,
ConnectionError,
Connected {
@@ -586,6 +576,7 @@ impl Client {
id: AtomicU64::new(0),
peer: Peer::new(0),
telemetry: Telemetry::new(clock, http.clone(), cx),
+ cloud_client: Arc::new(CloudApiClient::new(http.clone())),
http,
credentials_provider: ClientCredentialsProvider::new(cx),
state: Default::default(),
@@ -618,6 +609,10 @@ impl Client {
self.http.clone()
}
+ pub fn cloud_client(&self) -> Arc {
+ self.cloud_client.clone()
+ }
+
pub fn set_id(&self, id: u64) -> &Self {
self.id.store(id, Ordering::SeqCst);
self
@@ -704,7 +699,7 @@ impl Client {
let mut delay = INITIAL_RECONNECTION_DELAY;
loop {
- match client.authenticate_and_connect(true, &cx).await {
+ match client.connect(true, &cx).await {
ConnectionResult::Timeout => {
log::error!("client connect attempt timed out")
}
@@ -727,11 +722,10 @@ impl Client {
},
&cx,
);
- cx.background_executor().timer(delay).await;
- delay = delay
- .mul_f32(rng.gen_range(0.5..=2.5))
- .max(INITIAL_RECONNECTION_DELAY)
- .min(MAX_RECONNECTION_DELAY);
+ let jitter =
+ Duration::from_millis(rng.gen_range(0..delay.as_millis() as u64));
+ cx.background_executor().timer(delay + jitter).await;
+ delay = cmp::min(delay * 2, MAX_RECONNECTION_DELAY);
} else {
break;
}
@@ -875,17 +869,122 @@ impl Client {
.is_some()
}
- #[async_recursion(?Send)]
- pub async fn authenticate_and_connect(
+ pub async fn sign_in(
+ self: &Arc,
+ try_provider: bool,
+ cx: &AsyncApp,
+ ) -> Result {
+ if self.status().borrow().is_signed_out() {
+ self.set_status(Status::Authenticating, cx);
+ } else {
+ self.set_status(Status::Reauthenticating, cx);
+ }
+
+ let mut credentials = None;
+
+ let old_credentials = self.state.read().credentials.clone();
+ if let Some(old_credentials) = old_credentials {
+ self.cloud_client.set_credentials(
+ old_credentials.user_id as u32,
+ old_credentials.access_token.clone(),
+ );
+
+ // Fetch the authenticated user with the old credentials, to ensure they are still valid.
+ if self.cloud_client.get_authenticated_user().await.is_ok() {
+ credentials = Some(old_credentials);
+ }
+ }
+
+ if credentials.is_none() && try_provider {
+ if let Some(stored_credentials) = self.credentials_provider.read_credentials(cx).await {
+ self.cloud_client.set_credentials(
+ stored_credentials.user_id as u32,
+ stored_credentials.access_token.clone(),
+ );
+
+ // Fetch the authenticated user with the stored credentials, and
+ // clear them from the credentials provider if that fails.
+ if self.cloud_client.get_authenticated_user().await.is_ok() {
+ credentials = Some(stored_credentials);
+ } else {
+ self.credentials_provider
+ .delete_credentials(cx)
+ .await
+ .log_err();
+ }
+ }
+ }
+
+ if credentials.is_none() {
+ let mut status_rx = self.status();
+ let _ = status_rx.next().await;
+ futures::select_biased! {
+ authenticate = self.authenticate(cx).fuse() => {
+ match authenticate {
+ Ok(creds) => {
+ if IMPERSONATE_LOGIN.is_none() {
+ self.credentials_provider
+ .write_credentials(creds.user_id, creds.access_token.clone(), cx)
+ .await
+ .log_err();
+ }
+
+ credentials = Some(creds);
+ },
+ Err(err) => {
+ self.set_status(Status::AuthenticationError, cx);
+ return Err(err);
+ }
+ }
+ }
+ _ = status_rx.next().fuse() => {
+ return Err(anyhow!("authentication canceled"));
+ }
+ }
+ }
+
+ let credentials = credentials.unwrap();
+ self.set_id(credentials.user_id);
+ self.cloud_client
+ .set_credentials(credentials.user_id as u32, credentials.access_token.clone());
+ self.state.write().credentials = Some(credentials.clone());
+ self.set_status(Status::Authenticated, cx);
+
+ Ok(credentials)
+ }
+
+ /// Performs a sign-in and also connects to Collab.
+ ///
+ /// This is called in places where we *don't* need to connect in the future. We will replace these calls with calls
+ /// to `sign_in` when we're ready to remove auto-connection to Collab.
+ pub async fn sign_in_with_optional_connect(
+ self: &Arc,
+ try_provider: bool,
+ cx: &AsyncApp,
+ ) -> Result<()> {
+ let credentials = self.sign_in(try_provider, cx).await?;
+
+ let connect_result = match self.connect_with_credentials(credentials, cx).await {
+ ConnectionResult::Timeout => Err(anyhow!("connection timed out")),
+ ConnectionResult::ConnectionReset => Err(anyhow!("connection reset")),
+ ConnectionResult::Result(result) => result.context("client auth and connect"),
+ };
+ connect_result.log_err();
+
+ Ok(())
+ }
+
+ pub async fn connect(
self: &Arc,
try_provider: bool,
cx: &AsyncApp,
) -> ConnectionResult<()> {
let was_disconnected = match *self.status().borrow() {
- Status::SignedOut => true,
+ Status::SignedOut | Status::Authenticated => true,
Status::ConnectionError
| Status::ConnectionLost
| Status::Authenticating { .. }
+ | Status::AuthenticationError
| Status::Reauthenticating { .. }
| Status::ReconnectionError { .. } => false,
Status::Connected { .. } | Status::Connecting { .. } | Status::Reconnecting { .. } => {
@@ -898,39 +997,10 @@ impl Client {
);
}
};
- if was_disconnected {
- self.set_status(Status::Authenticating, cx);
- } else {
- self.set_status(Status::Reauthenticating, cx)
- }
-
- let mut read_from_provider = false;
- let mut credentials = self.state.read().credentials.clone();
- if credentials.is_none() && try_provider {
- credentials = self.credentials_provider.read_credentials(cx).await;
- read_from_provider = credentials.is_some();
- }
-
- if credentials.is_none() {
- let mut status_rx = self.status();
- let _ = status_rx.next().await;
- futures::select_biased! {
- authenticate = self.authenticate(cx).fuse() => {
- match authenticate {
- Ok(creds) => credentials = Some(creds),
- Err(err) => {
- self.set_status(Status::ConnectionError, cx);
- return ConnectionResult::Result(Err(err));
- }
- }
- }
- _ = status_rx.next().fuse() => {
- return ConnectionResult::Result(Err(anyhow!("authentication canceled")));
- }
- }
- }
- let credentials = credentials.unwrap();
- self.set_id(credentials.user_id);
+ let credentials = match self.sign_in(try_provider, cx).await {
+ Ok(credentials) => credentials,
+ Err(err) => return ConnectionResult::Result(Err(err)),
+ };
if was_disconnected {
self.set_status(Status::Connecting, cx);
@@ -938,17 +1008,20 @@ impl Client {
self.set_status(Status::Reconnecting, cx);
}
+ self.connect_with_credentials(credentials, cx).await
+ }
+
+ async fn connect_with_credentials(
+ self: &Arc,
+ credentials: Credentials,
+ cx: &AsyncApp,
+ ) -> ConnectionResult<()> {
let mut timeout =
futures::FutureExt::fuse(cx.background_executor().timer(CONNECTION_TIMEOUT));
futures::select_biased! {
connection = self.establish_connection(&credentials, cx).fuse() => {
match connection {
Ok(conn) => {
- self.state.write().credentials = Some(credentials.clone());
- if !read_from_provider && IMPERSONATE_LOGIN.is_none() {
- self.credentials_provider.write_credentials(credentials.user_id, credentials.access_token, cx).await.log_err();
- }
-
futures::select_biased! {
result = self.set_connection(conn, cx).fuse() => {
match result.context("client auth and connect") {
@@ -966,15 +1039,8 @@ impl Client {
}
}
Err(EstablishConnectionError::Unauthorized) => {
- self.state.write().credentials.take();
- if read_from_provider {
- self.credentials_provider.delete_credentials(cx).await.log_err();
- self.set_status(Status::SignedOut, cx);
- self.authenticate_and_connect(false, cx).await
- } else {
- self.set_status(Status::ConnectionError, cx);
- ConnectionResult::Result(Err(EstablishConnectionError::Unauthorized).context("client auth and connect"))
- }
+ self.set_status(Status::ConnectionError, cx);
+ ConnectionResult::Result(Err(EstablishConnectionError::Unauthorized).context("client auth and connect"))
}
Err(EstablishConnectionError::UpgradeRequired) => {
self.set_status(Status::UpgradeRequired, cx);
@@ -1369,96 +1435,31 @@ impl Client {
self: &Arc,
http: Arc,
login: String,
- mut api_token: String,
+ api_token: String,
) -> Result {
- #[derive(Deserialize)]
- struct AuthenticatedUserResponse {
- user: User,
+ #[derive(Serialize)]
+ struct ImpersonateUserBody {
+ github_login: String,
}
#[derive(Deserialize)]
- struct User {
- id: u64,
+ struct ImpersonateUserResponse {
+ user_id: u64,
+ access_token: String,
}
- let github_user = {
- #[derive(Deserialize)]
- struct GithubUser {
- id: i32,
- login: String,
- created_at: DateTime,
- }
-
- let request = {
- let mut request_builder =
- Request::get(&format!("https://api.github.com/users/{login}"));
- if let Ok(github_token) = std::env::var("GITHUB_TOKEN") {
- request_builder =
- request_builder.header("Authorization", format!("Bearer {}", github_token));
- }
-
- request_builder.body(AsyncBody::empty())?
- };
-
- let mut response = http
- .send(request)
- .await
- .context("error fetching GitHub user")?;
-
- let mut body = Vec::new();
- response
- .body_mut()
- .read_to_end(&mut body)
- .await
- .context("error reading GitHub user")?;
-
- if !response.status().is_success() {
- let text = String::from_utf8_lossy(body.as_slice());
- bail!(
- "status error {}, response: {text:?}",
- response.status().as_u16()
- );
- }
-
- serde_json::from_slice::(body.as_slice()).map_err(|err| {
- log::error!("Error deserializing: {:?}", err);
- log::error!(
- "GitHub API response text: {:?}",
- String::from_utf8_lossy(body.as_slice())
- );
- anyhow!("error deserializing GitHub user")
- })?
- };
-
- let query_params = [
- ("github_login", &github_user.login),
- ("github_user_id", &github_user.id.to_string()),
- (
- "github_user_created_at",
- &github_user.created_at.to_rfc3339(),
- ),
- ];
-
- // Use the collab server's admin API to retrieve the ID
- // of the impersonated user.
- let mut url = self.rpc_url(http.clone(), None).await?;
- url.set_path("/user");
- url.set_query(Some(
- &query_params
- .iter()
- .map(|(key, value)| {
- format!(
- "{}={}",
- key,
- url::form_urlencoded::byte_serialize(value.as_bytes()).collect::()
- )
- })
- .collect::>()
- .join("&"),
- ));
- let request: http_client::Request = Request::get(url.as_str())
- .header("Authorization", format!("token {api_token}"))
- .body("".into())?;
+ let url = self
+ .http
+ .build_zed_cloud_url("/internal/users/impersonate", &[])?;
+ let request = Request::post(url.as_str())
+ .header("Content-Type", "application/json")
+ .header("Authorization", format!("Bearer {api_token}"))
+ .body(
+ serde_json::to_string(&ImpersonateUserBody {
+ github_login: login,
+ })?
+ .into(),
+ )?;
let mut response = http.send(request).await?;
let mut body = String::new();
@@ -1469,18 +1470,17 @@ impl Client {
response.status().as_u16(),
body,
);
- let response: AuthenticatedUserResponse = serde_json::from_str(&body)?;
+ let response: ImpersonateUserResponse = serde_json::from_str(&body)?;
- // Use the admin API token to authenticate as the impersonated user.
- api_token.insert_str(0, "ADMIN_TOKEN:");
Ok(Credentials {
- user_id: response.user.id,
- access_token: api_token,
+ user_id: response.user_id,
+ access_token: response.access_token,
})
}
pub async fn sign_out(self: &Arc, cx: &AsyncApp) {
self.state.write().credentials = None;
+ self.cloud_client.clear_credentials();
self.disconnect(cx);
if self.has_credentials(cx).await {
@@ -1790,7 +1790,7 @@ mod tests {
});
let auth_and_connect = cx.spawn({
let client = client.clone();
- |cx| async move { client.authenticate_and_connect(false, &cx).await }
+ |cx| async move { client.connect(false, &cx).await }
});
executor.run_until_parked();
assert!(matches!(status.next().await, Some(Status::Connecting)));
@@ -1867,7 +1867,7 @@ mod tests {
let _authenticate = cx.spawn({
let client = client.clone();
- move |cx| async move { client.authenticate_and_connect(false, &cx).await }
+ move |cx| async move { client.connect(false, &cx).await }
});
executor.run_until_parked();
assert_eq!(*auth_count.lock(), 1);
@@ -1875,7 +1875,7 @@ mod tests {
let _authenticate = cx.spawn({
let client = client.clone();
- |cx| async move { client.authenticate_and_connect(false, &cx).await }
+ |cx| async move { client.connect(false, &cx).await }
});
executor.run_until_parked();
assert_eq!(*auth_count.lock(), 2);
diff --git a/crates/client/src/test.rs b/crates/client/src/test.rs
index 6ce79fa9c5..439fb100d2 100644
--- a/crates/client/src/test.rs
+++ b/crates/client/src/test.rs
@@ -1,8 +1,11 @@
use crate::{Client, Connection, Credentials, EstablishConnectionError, UserStore};
use anyhow::{Context as _, Result, anyhow};
use chrono::Duration;
+use cloud_api_client::{AuthenticatedUser, GetAuthenticatedUserResponse, PlanInfo};
+use cloud_llm_client::{CurrentUsage, Plan, UsageData, UsageLimit};
use futures::{StreamExt, stream::BoxStream};
use gpui::{AppContext as _, BackgroundExecutor, Entity, TestAppContext};
+use http_client::{AsyncBody, Method, Request, http};
use parking_lot::Mutex;
use rpc::{
ConnectionId, Peer, Receipt, TypedEnvelope,
@@ -39,6 +42,44 @@ impl FakeServer {
executor: cx.executor(),
};
+ client.http_client().as_fake().replace_handler({
+ let state = server.state.clone();
+ move |old_handler, req| {
+ let state = state.clone();
+ let old_handler = old_handler.clone();
+ async move {
+ match (req.method(), req.uri().path()) {
+ (&Method::GET, "/client/users/me") => {
+ let credentials = parse_authorization_header(&req);
+ if credentials
+ != Some(Credentials {
+ user_id: client_user_id,
+ access_token: state.lock().access_token.to_string(),
+ })
+ {
+ return Ok(http_client::Response::builder()
+ .status(401)
+ .body("Unauthorized".into())
+ .unwrap());
+ }
+
+ Ok(http_client::Response::builder()
+ .status(200)
+ .body(
+ serde_json::to_string(&make_get_authenticated_user_response(
+ client_user_id as i32,
+ format!("user-{client_user_id}"),
+ ))
+ .unwrap()
+ .into(),
+ )
+ .unwrap())
+ }
+ _ => old_handler(req).await,
+ }
+ }
+ }
+ });
client
.override_authenticate({
let state = Arc::downgrade(&server.state);
@@ -105,7 +146,7 @@ impl FakeServer {
});
client
- .authenticate_and_connect(false, &cx.to_async())
+ .connect(false, &cx.to_async())
.await
.into_response()
.unwrap();
@@ -223,3 +264,54 @@ impl Drop for FakeServer {
self.disconnect();
}
}
+
+pub fn parse_authorization_header(req: &Request) -> Option {
+ let mut auth_header = req
+ .headers()
+ .get(http::header::AUTHORIZATION)?
+ .to_str()
+ .ok()?
+ .split_whitespace();
+ let user_id = auth_header.next()?.parse().ok()?;
+ let access_token = auth_header.next()?;
+ Some(Credentials {
+ user_id,
+ access_token: access_token.to_string(),
+ })
+}
+
+pub fn make_get_authenticated_user_response(
+ user_id: i32,
+ github_login: String,
+) -> GetAuthenticatedUserResponse {
+ GetAuthenticatedUserResponse {
+ user: AuthenticatedUser {
+ id: user_id,
+ metrics_id: format!("metrics-id-{user_id}"),
+ avatar_url: "".to_string(),
+ github_login,
+ name: None,
+ is_staff: false,
+ accepted_tos_at: None,
+ },
+ feature_flags: vec![],
+ plan: PlanInfo {
+ plan: Plan::ZedPro,
+ subscription_period: None,
+ usage: CurrentUsage {
+ model_requests: UsageData {
+ used: 0,
+ limit: UsageLimit::Limited(500),
+ },
+ edit_predictions: UsageData {
+ used: 250,
+ limit: UsageLimit::Unlimited,
+ },
+ },
+ trial_started_at: None,
+ is_usage_based_billing_enabled: false,
+ is_account_too_young: false,
+ has_overdue_invoices: false,
+ },
+ }
+}
diff --git a/crates/client/src/user.rs b/crates/client/src/user.rs
index a7dab2a8d3..3c125a0882 100644
--- a/crates/client/src/user.rs
+++ b/crates/client/src/user.rs
@@ -1,6 +1,7 @@
use super::{Client, Status, TypedEnvelope, proto};
use anyhow::{Context as _, Result, anyhow};
use chrono::{DateTime, Utc};
+use cloud_api_client::{GetAuthenticatedUserResponse, PlanInfo};
use cloud_llm_client::{
EDIT_PREDICTIONS_USAGE_AMOUNT_HEADER_NAME, EDIT_PREDICTIONS_USAGE_LIMIT_HEADER_NAME,
MODEL_REQUESTS_USAGE_AMOUNT_HEADER_NAME, MODEL_REQUESTS_USAGE_LIMIT_HEADER_NAME, UsageLimit,
@@ -20,7 +21,7 @@ use std::{
sync::{Arc, Weak},
};
use text::ReplicaId;
-use util::{TryFutureExt as _, maybe};
+use util::{ResultExt, TryFutureExt as _};
pub type UserId = u64;
@@ -55,7 +56,7 @@ pub struct ParticipantIndex(pub u32);
#[derive(Default, Debug)]
pub struct User {
pub id: UserId,
- pub github_login: String,
+ pub github_login: SharedString,
pub avatar_uri: SharedUri,
pub name: Option,
}
@@ -107,19 +108,14 @@ pub enum ContactRequestStatus {
pub struct UserStore {
users: HashMap>,
- by_github_login: HashMap,
+ by_github_login: HashMap,
participant_indices: HashMap,
update_contacts_tx: mpsc::UnboundedSender,
- current_plan: Option,
- subscription_period: Option<(DateTime, DateTime)>,
- trial_started_at: Option>,
model_request_usage: Option,
edit_prediction_usage: Option,
- is_usage_based_billing_enabled: Option,
- account_too_young: Option,
- has_overdue_invoices: Option,
+ plan_info: Option,
current_user: watch::Receiver